/************************************************************************* * (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( ); } } }