/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.client.circuitbreaker;
import static java.util.Objects.requireNonNull;
import java.time.Duration;
import java.util.Iterator;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import com.google.common.base.Ticker;
/**
* An {@link EventCounter} that accumulates the count of events within a time window.
*/
final class SlidingWindowCounter implements EventCounter {
private final Ticker ticker;
private final long slidingWindowNanos;
private final long updateIntervalNanos;
/**
* The reference to the latest {@link Bucket}.
*/
private final AtomicReference<Bucket> current;
/**
* The reference to the latest accumulated {@link EventCount}.
*/
private final AtomicReference<EventCount> snapshot = new AtomicReference<>(EventCount.ZERO);
/**
* The queue that stores {@link Bucket}s within the time window.
*/
private final Queue<Bucket> reservoir = new ConcurrentLinkedQueue<>();
SlidingWindowCounter(Ticker ticker, Duration slidingWindow, Duration updateInterval) {
this.ticker = requireNonNull(ticker, "ticker");
slidingWindowNanos = requireNonNull(slidingWindow, "slidingWindow").toNanos();
updateIntervalNanos = requireNonNull(updateInterval, "updateInterval").toNanos();
current = new AtomicReference<>(new Bucket(ticker.read()));
}
@Override
public EventCount count() {
return snapshot.get();
}
@Override
public Optional<EventCount> onSuccess() {
return onEvent(Event.SUCCESS);
}
@Override
public Optional<EventCount> onFailure() {
return onEvent(Event.FAILURE);
}
private Optional<EventCount> onEvent(Event event) {
final long tickerNanos = ticker.read();
final Bucket currentBucket = current.get();
if (tickerNanos < currentBucket.timestamp()) {
// if current timestamp is older than bucket's timestamp (maybe race or GC pause?),
// then creates an instant bucket and puts it to the reservoir not to lose event.
final Bucket bucket = new Bucket(tickerNanos);
event.increment(bucket);
reservoir.offer(bucket);
return Optional.empty();
}
if (tickerNanos < currentBucket.timestamp() + updateIntervalNanos) {
// increments the current bucket since it is exactly latest
event.increment(currentBucket);
return Optional.empty();
}
// the current bucket is old
// it's time to create new one
final Bucket nextBucket = new Bucket(tickerNanos);
event.increment(nextBucket);
// replaces the bucket
if (current.compareAndSet(currentBucket, nextBucket)) {
// puts old one to the reservoir
reservoir.offer(currentBucket);
// and then updates count
final EventCount eventCount = trimAndSum(tickerNanos);
snapshot.set(eventCount);
return Optional.of(eventCount);
} else {
// the bucket has been replaced already
// puts new one as an instant bucket to the reservoir not to lose event
reservoir.offer(nextBucket);
return Optional.empty();
}
}
/**
* Sums up buckets within the time window, and removes all the others.
*/
private EventCount trimAndSum(long tickerNanos) {
final long oldLimit = tickerNanos - slidingWindowNanos;
final Iterator<Bucket> iterator = reservoir.iterator();
long success = 0;
long failure = 0;
while (iterator.hasNext()) {
final Bucket bucket = iterator.next();
if (bucket.timestamp < oldLimit) {
// removes old bucket
iterator.remove();
} else {
success += bucket.success();
failure += bucket.failure();
}
}
return new EventCount(success, failure);
}
private enum Event {
SUCCESS {
@Override
void increment(Bucket bucket) {
bucket.success.increment();
}
},
FAILURE {
@Override
void increment(Bucket bucket) {
bucket.failure.increment();
}
};
abstract void increment(Bucket bucket);
}
/**
* Holds the count of events within {@code updateInterval}.
*/
private static final class Bucket {
private final long timestamp;
private final LongAdder success = new LongAdder();
private final LongAdder failure = new LongAdder();
private Bucket(long timestamp) {
this.timestamp = timestamp;
}
private long timestamp() {
return timestamp;
}
private long success() {
return success.sum();
}
private long failure() {
return failure.sum();
}
@Override
public String toString() {
return "Bucket{" +
"timestamp=" + timestamp +
", success=" + success +
", failure=" + failure +
'}';
}
}
}