com.datastax.driver.core.EventDebouncer Maven / Gradle / Ivy
Show all versions of scylla-driver-core Show documentation
/*
* Copyright 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 static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
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.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A helper class to debounce events received by the Control Connection.
*
* This class accumulates received events, and delivers them when either: - no events have been
* received for delayMs - maxPendingEvents have been received
*/
abstract class EventDebouncer {
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 immediateDelivery =
new AtomicReference(null);
private final AtomicReference delayedDelivery =
new AtomicReference(null);
private final ScheduledExecutorService executor;
private final DeliveryCallback callback;
private final int maxQueuedEvents;
private final Queue> 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 callback) {
this(name, executor, callback, DEFAULT_MAX_QUEUED_EVENTS);
}
EventDebouncer(
String name,
ScheduledExecutorService executor,
DeliveryCallback callback,
int maxQueuedEvents) {
this.name = name;
this.executor = executor;
this.callback = callback;
this.maxQueuedEvents = maxQueuedEvents;
this.events = new ConcurrentLinkedQueue>();
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;
}
}
while (true) {
DeliveryAttempt previous = cancelImmediateDelivery();
if (immediateDelivery.compareAndSet(previous, null)) {
break;
}
}
completeAllPendingFutures();
logger.trace("{} debouncer stopped", name);
}
private void completeAllPendingFutures() {
Entry entry;
while ((entry = this.events.poll()) != null) {
entry.future.set(null);
}
}
/** @return a future that will complete once the event has been processed */
ListenableFuture 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 entry = new Entry(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() {
return cancelDelivery(delayedDelivery.get());
}
private DeliveryAttempt cancelImmediateDelivery() {
return cancelDelivery(immediateDelivery.get());
}
private DeliveryAttempt cancelDelivery(DeliveryAttempt previous) {
if (previous != null) {
previous.cancel();
}
return previous;
}
private void deliverEvents() {
if (state == State.STOPPED) {
completeAllPendingFutures();
return;
}
final List toDeliver = Lists.newArrayList();
final List> futures = Lists.newArrayList();
Entry 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);
GuavaCompatibility.INSTANCE.addCallback(
delivered,
new FutureCallback