/*
* Copyright (C) 2012-2015 DataStax Inc.
*
* Licensed 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.datastax.driver.core;
import com.datastax.driver.core.utils.MoreFutures;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* A helper class to debounce events received by the Control Connection.
* <p/>
* This class accumulates received events, and delivers them when either:
* - no events have been received for delayMs
* - maxPendingEvents have been received
*/
abstract class EventDebouncer<T> {
private static final Logger logger = LoggerFactory.getLogger(EventDebouncer.class);
private static final int DEFAULT_MAX_QUEUED_EVENTS = 10000;
private final String name;
private final AtomicReference<DeliveryAttempt> immediateDelivery = new AtomicReference<DeliveryAttempt>(null);
private final AtomicReference<DeliveryAttempt> delayedDelivery = new AtomicReference<DeliveryAttempt>(null);
private final ScheduledExecutorService executor;
private final DeliveryCallback<T> callback;
private final int maxQueuedEvents;
private final Queue<Entry<T>> events;
private final AtomicInteger eventCount;
private enum State {NEW, RUNNING, STOPPED}
private volatile State state;
private static final long OVERFLOW_WARNING_INTERVAL = NANOSECONDS.convert(5, SECONDS);
private volatile long lastOverflowWarning = Long.MIN_VALUE;
EventDebouncer(String name, ScheduledExecutorService executor, DeliveryCallback<T> callback) {
this(name, executor, callback, DEFAULT_MAX_QUEUED_EVENTS);
}
EventDebouncer(String name, ScheduledExecutorService executor, DeliveryCallback<T> callback, int maxQueuedEvents) {
this.name = name;
this.executor = executor;
this.callback = callback;
this.maxQueuedEvents = maxQueuedEvents;
this.events = new ConcurrentLinkedQueue<Entry<T>>();
this.eventCount = new AtomicInteger();
this.state = State.NEW;
}
abstract int maxPendingEvents();
abstract long delayMs();
void start() {
logger.trace("Starting {} debouncer...", name);
state = State.RUNNING;
if (!events.isEmpty()) {
logger.trace("{} debouncer: {} events were accumulated before the debouncer started: delivering now",
name, eventCount.get());
scheduleImmediateDelivery();
}
}
void stop() {
logger.trace("Stopping {} debouncer...", name);
state = State.STOPPED;
while (true) {
DeliveryAttempt previous = cancelDelayedDelivery();
if (delayedDelivery.compareAndSet(previous, null)) {
break;
}
}
completeAllPendingFutures();
logger.trace("{} debouncer stopped", name);
}
private void completeAllPendingFutures() {
Entry<T> entry;
while ((entry = this.events.poll()) != null) {
entry.future.set(null);
}
}
/**
* @return a future that will complete once the event has been processed
*/
ListenableFuture<Void> eventReceived(T event) {
if (state == State.STOPPED) {
logger.trace("{} debouncer is stopped, rejecting event: {}", name, event);
return MoreFutures.VOID_SUCCESS;
}
checkNotNull(event);
logger.trace("{} debouncer: event received {}", name, event);
// Safeguard against the queue filling up faster than we can process it
if (eventCount.incrementAndGet() > maxQueuedEvents) {
long now = System.nanoTime();
if (now > lastOverflowWarning + OVERFLOW_WARNING_INTERVAL) {
lastOverflowWarning = now;
logger.warn("{} debouncer enqueued more than {} events, rejecting new events. "
+ "This should not happen and is likely a sign that something is wrong.",
name, maxQueuedEvents);
}
eventCount.decrementAndGet();
return MoreFutures.VOID_SUCCESS;
}
Entry<T> entry = new Entry<T>(event);
try {
events.add(entry);
} catch (RuntimeException e) {
eventCount.decrementAndGet();
throw e;
}
if (state == State.RUNNING) {
int count = eventCount.get();
int maxPendingEvents = maxPendingEvents();
if (count < maxPendingEvents) {
scheduleDelayedDelivery();
} else if (count == maxPendingEvents) {
scheduleImmediateDelivery();
}
} else if (state == State.STOPPED) {
// If we race with stop() since the check at the beginning, ensure the future
// gets completed (no-op if the future was already set).
entry.future.set(null);
}
return entry.future;
}
void scheduleImmediateDelivery() {
cancelDelayedDelivery();
while (state == State.RUNNING) {
DeliveryAttempt previous = immediateDelivery.get();
if (previous != null)
previous.cancel();
DeliveryAttempt current = new DeliveryAttempt();
if (immediateDelivery.compareAndSet(previous, current)) {
current.executeNow();
return;
}
}
}
private void scheduleDelayedDelivery() {
while (state == State.RUNNING) {
DeliveryAttempt previous = cancelDelayedDelivery();
DeliveryAttempt next = new DeliveryAttempt();
if (delayedDelivery.compareAndSet(previous, next)) {
next.scheduleAfterDelay();
break;
}
}
}
private DeliveryAttempt cancelDelayedDelivery() {
DeliveryAttempt previous = delayedDelivery.get();
if (previous != null) {
previous.cancel();
}
return previous;
}
void deliverEvents() {
if (state == State.STOPPED) {
completeAllPendingFutures();
return;
}
final List<T> toDeliver = Lists.newArrayList();
final List<SettableFuture<Void>> futures = Lists.newArrayList();
Entry<T> entry;
// Limit the number of events we dequeue, to avoid an infinite loop if the queue starts filling faster than we can consume it.
int count = 0;
while (++count <= maxQueuedEvents && (entry = this.events.poll()) != null) {
toDeliver.add(entry.event);
futures.add(entry.future);
}
eventCount.addAndGet(-toDeliver.size());
if (toDeliver.isEmpty()) {
logger.trace("{} debouncer: no events to deliver", name);
} else {
logger.trace("{} debouncer: delivering {} events", name, toDeliver.size());
ListenableFuture<?> delivered = callback.deliver(toDeliver);
Futures.addCallback(delivered, new FutureCallback<Object>() {
@Override
public void onSuccess(Object result) {
for (SettableFuture<Void> future : futures)
future.set(null);
}
@Override
public void onFailure(Throwable t) {
for (SettableFuture<Void> future : futures)
future.setException(t);
}
});
}
// If we didn't dequeue all events (or new ones arrived since we did), make sure we eventually
// process the remaining events, because eventReceived might have skipped the delivery
if (eventCount.get() > 0)
scheduleDelayedDelivery();
}
class DeliveryAttempt extends ExceptionCatchingRunnable {
volatile Future<?> deliveryFuture;
boolean isDone() {
return deliveryFuture != null && deliveryFuture.isDone();
}
void cancel() {
if (deliveryFuture != null)
deliveryFuture.cancel(true);
}
void executeNow() {
if (state != State.STOPPED)
deliveryFuture = executor.submit(this);
}
void scheduleAfterDelay() {
if (state != State.STOPPED)
deliveryFuture = executor.schedule(this, delayMs(), TimeUnit.MILLISECONDS);
}
@Override
public void runMayThrow() throws Exception {
deliverEvents();
}
}
interface DeliveryCallback<T> {
/**
* Deliver the given list of events.
* The given list is a private copy and any modification made to it
* has no side-effect; it is also guaranteed not to be null nor empty.
*
* @param events the events to deliver
*/
ListenableFuture<?> deliver(List<T> events);
}
static class Entry<T> {
final T event;
final SettableFuture<Void> future;
Entry(T event) {
this.event = event;
this.future = SettableFuture.create();
}
}
}