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