All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.datastax.driver.core.EventDebouncer Maven / Gradle / Ivy

Go to download

A driver for Scylla and Apache Cassandra 1.2+ that works exclusively with the Cassandra Query Language version 3 (CQL3) and Cassandra's binary protocol.

The newest version!
/*
 * 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() { @Override public void onSuccess(Object result) { for (SettableFuture future : futures) future.set(null); } @Override public void onFailure(Throwable t) { for (SettableFuture 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 { /** * 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 events); } static class Entry { final T event; final SettableFuture future; Entry(T event) { this.event = event; this.future = SettableFuture.create(); } } }