Java tutorial
/************************************************************************* * (c) Copyright 2017 Hewlett Packard Enterprise Development Company LP * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. ************************************************************************/ package com.eucalyptus.reporting; import java.time.Clock; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.LongPredicate; import java.util.stream.Collectors; import javax.annotation.Nonnull; import com.eucalyptus.util.Assert; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import javaslang.Tuple; import javaslang.Tuple2; /** * */ public class Counter<T, C extends Counter.Counted> { private final int periodLength; // how long is each window private final int periodCount; // how many windows private final Function<? super T, C> countedFunction; // build a counted item from an input private final Function<? super T, Integer> countFunction; // build the count for the input (perhaps X per item) private final Clock clock; // clock for current time private final AtomicReference<List<CountPeriod<C>>> periods = new AtomicReference<>(Collections.emptyList()); public Counter(final int periodLength, final int periodCount, @Nonnull final Function<? super T, C> countedFunction) { this(Clock.systemUTC(), periodLength, periodCount, countedFunction, c -> 1); } public Counter(final int periodLength, final int periodCount, @Nonnull final Function<? super T, C> countedFunction, @Nonnull final Function<? super T, Integer> countFunction) { this(Clock.systemUTC(), periodLength, periodCount, countedFunction, countFunction); } public Counter(@Nonnull final Clock clock, final int periodLength, final int periodCount, @Nonnull final Function<? super T, C> countedFunction, @Nonnull final Function<? super T, Integer> countFunction) { this.clock = Assert.notNull(clock, "clock"); this.periodLength = periodLength; this.periodCount = periodCount; this.countedFunction = Assert.notNull(countedFunction, "countedFunction"); this.countFunction = Assert.notNull(countFunction, "countFunction"); } /** * Count the given item at the current time. */ public void count(final T t) { count(clock.millis(), t); } /** * Count the given item at the given time. */ public void count(final long time, final T t) { if (t != null) { final C counted = countedFunction.apply(t); final int count = countFunction.apply(t); if (counted != null && count > 0) { period(time).count(counted, count); } } } public long lastPeriodEnd() { return lastPeriodEnd(clock.millis()); } public long lastPeriodEnd(final long time) { final long periodLengthLong = periodLength; return (time / periodLengthLong) * periodLengthLong; } public CounterSnapshot<C> snapshot() { return snapshot(clock.millis()); } public CounterSnapshot<C> snapshot(final long time) { final List<CountPeriod<C>> periodList = periods.get().stream().filter(p -> p.key.end <= time) .collect(Collectors.toList()); if (periodList.isEmpty()) { periodList.add(newPeriod(time, started(periodList), time)); } return new CounterSnapshot<>(periodList.stream().map(CountPeriod::snapshot).collect(Collectors.toList())); } public Tuple2<Long, Integer> total() { final List<CountPeriod<C>> periodList = periods.get(); final int totalCount = periodList.stream().<Number>flatMap(p -> p.counts.values().stream()) .reduce(0, (a, b) -> a.intValue() + b.intValue()).intValue(); return Tuple.of(created(periodList), totalCount); } public Tuple2<Long, Integer> accountTotal(final String account) { Assert.notNull(account, "account"); final List<CountPeriod<C>> periodList = periods.get(); final int totalCount = periodList.stream() .<Number>flatMap(p -> p.counts.entrySet().stream() .filter(entry -> account.equals(entry.getKey().getAccount())).map(Entry::getValue)) .reduce(0, (a, b) -> a.intValue() + b.intValue()).intValue(); return Tuple.of(created(periodList), totalCount); } private long created(final List<CountPeriod<C>> periodList) { return periodList.isEmpty() ? clock.millis() : Iterables.getLast(periodList).key.created; } private long started(final List<CountPeriod<C>> periodList) { final long periodLengthLong = periodLength; return periodList.isEmpty() ? (clock.millis() / periodLengthLong) * periodLengthLong : Iterables.getLast(periodList).key.start; } private CountPeriod<C> period(final long time) { Optional<CountPeriod<C>> period = Optional.empty(); while (!period.isPresent()) { period = periods.get().stream().filter(p -> p.test(time)).findFirst(); if (!period.isPresent()) { final List<CountPeriod<C>> periodList = periods.get(); final List<CountPeriod<C>> newPeriodList = Lists.newArrayList(); newPeriodList.add(newPeriod(time)); while (newPeriodList.size() < periodCount && !periodList.isEmpty()) { if (newPeriodList.get(newPeriodList.size() - 1).key.start != periodList.get(0).key.end) { newPeriodList.add( newPeriod((newPeriodList.get(newPeriodList.size() - 1).key.start - periodLength))); } else { break; } } if (newPeriodList.size() < periodCount) { Iterables.addAll(newPeriodList, Iterables.limit(periodList, periodCount - newPeriodList.size())); } periods.compareAndSet(periodList, ImmutableList.copyOf(newPeriodList)); } } return period.get(); } public String toString() { final Tuple2<Long, Integer> totals = total(); return MoreObjects.toStringHelper(this).add("totalCount", totals._2()).add("since", totals._1()).toString(); } private CountPeriod<C> newPeriod(long time) { final long now = clock.millis(); final long periodLengthLong = periodLength; final long start = (time / periodLengthLong) * periodLengthLong; final long end = start + periodLength; if (start < (-(periodLength * 2))) { throw new IllegalArgumentException("Not creating expired period " + time); } return newPeriod(now, start, end); } static <C extends Counted> CountPeriod<C> newPeriod(final long created, final long start, final long end) { return new CountPeriod<>(newKey(created, start, end)); } static CountPeriodKey newKey(final long created, final long start, final long end) { return new CountPeriodKey(created, start, end); } static <C extends Counted> List<CounterPeriodSnapshot<C>> since(final List<CounterPeriodSnapshot<C>> snapshots, final List<CounterPeriodSnapshot<C>> oldSnapshosts) { return snapshots.stream().map(s -> s.subtractMatching(oldSnapshosts)).collect(Collectors.toList()); } static final class CountPeriodKey implements LongPredicate { private final long start; private final long end; private final long created; CountPeriodKey(final long created, final long start, final long end) { this.created = created; this.start = start; this.end = end; } @Override public boolean test(final long time) { return time >= start && time < end; } static CountPeriodKey combine(final CountPeriodKey key1, final CountPeriodKey key2) { return new CountPeriodKey(Math.max(key1.created, key2.created), Math.min(key1.start, key2.start), Math.max(key1.end, key2.end)); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("start", start).add("end", end).add("created", created) .toString(); } } public static final class CounterSnapshot<C extends Counter.Counted> { private final CounterPeriodSnapshot<C> aggregate; private final List<CounterPeriodSnapshot<C>> periodSnapshots; public CounterSnapshot(final List<CounterPeriodSnapshot<C>> periodSnapshots) { this.aggregate = new CountPeriod<>(periodSnapshots).snapshot(); this.periodSnapshots = periodSnapshots; } public long getPeriodStart() { return aggregate.key.start; } public long getPeriodEnd() { return aggregate.key.end; } public Iterable<C> counted() { return aggregate.counts.keySet(); } public int total() { return aggregate.counts.values().stream().reduce(0, Integer::sum); } public Iterable<Tuple2<C, Integer>> counts() { return () -> aggregate.counts.entrySet().stream().map(e -> Tuple.of(e.getKey(), e.getValue())) .iterator(); } public CounterSnapshot<C> since(final CounterSnapshot<C> old) { if (Assert.notNull(old, "old").aggregate.key.end > aggregate.key.end) { throw new IllegalArgumentException("Old snapshot is newer than current"); } return new CounterSnapshot<>(Counter.since(periodSnapshots, old.periodSnapshots)); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("key", aggregate.key) .add("countSize", aggregate.counts.size()).toString(); } } static final class CounterPeriodSnapshot<C extends Counter.Counted> { private final CountPeriodKey key; private final Map<C, Integer> counts; CounterPeriodSnapshot(final CountPeriod<C> period) { this(period.key, period.counts.entrySet().stream() .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().intValue()))); } CounterPeriodSnapshot(final CountPeriodKey key, final Map<C, Integer> counts) { this.key = key; this.counts = ImmutableMap.copyOf(counts); } CounterPeriodSnapshot<C> subtractMatching(final List<CounterPeriodSnapshot<C>> oldSnapshosts) { final Optional<CounterPeriodSnapshot<C>> matching = oldSnapshosts.stream() .filter(s -> s.key.equals(key)).findFirst(); if (matching.isPresent()) { final CounterPeriodSnapshot<C> oldSnapshot = matching.get(); return new CounterPeriodSnapshot<>(key, counts.entrySet().stream().map(entry -> { final Integer oldCount = oldSnapshot.counts.get(entry.getKey()); return Tuple.of(entry.getKey(), oldCount == null ? entry.getValue() : entry.getValue() - oldCount); }).collect(Collectors.toMap(Tuple2::_1, Tuple2::_2))); } else { return this; } } @Override public String toString() { return MoreObjects.toStringHelper(this).add("key", key).add("countSize", counts.size()).toString(); } } static final class CountPeriod<C extends Counter.Counted> implements LongPredicate { private final CountPeriodKey key; private final ConcurrentMap<C, AtomicInteger> counts = Maps.newConcurrentMap(); CountPeriod(final CountPeriodKey key) { this.key = key; } /** * Create a period from a non empty list of snapshots */ CountPeriod(final List<CounterPeriodSnapshot<C>> snapshots) { this.key = snapshots.stream().map(s -> s.key).reduce(CountPeriodKey::combine).get(); snapshots.forEach(s -> s.counts.entrySet().forEach(entry -> count(entry.getKey(), entry.getValue()))); } int count(C counted, int count) { return counts.computeIfAbsent(counted, c -> new AtomicInteger()).addAndGet(count); } CounterPeriodSnapshot<C> snapshot() { return new CounterPeriodSnapshot<>(this); } @Override public boolean test(final long time) { return key.test(time); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("key", key).add("countSize", counts.size()).toString(); } } public static class Counted { private final String account; private final String item; public Counted(final String account, final String item) { this.account = account; this.item = item; } public String getAccount() { return account; } public String getItem() { return item; } @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final Counted counted = (Counted) o; return Objects.equals(account, counted.account) && Objects.equals(item, counted.item); } @Override public int hashCode() { return Objects.hash(account, item); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("account", account).add("item", item).toString(); } } }