/*
* 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.EventDebouncer.DeliveryCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public class EventDebouncerTest {
private ScheduledExecutorService executor;
private MockDeliveryCallback callback;
@BeforeMethod(groups = "unit")
public void setup() {
executor = Executors.newScheduledThreadPool(1);
callback = new MockDeliveryCallback();
}
@AfterMethod(groups = "unit")
public void tearDown() {
executor.shutdownNow();
}
@Test(groups = "unit")
public void should_deliver_single_event() throws InterruptedException {
EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 50;
}
};
debouncer.start();
MockEvent event = new MockEvent(0);
debouncer.eventReceived(event);
callback.awaitEvents(1);
assertThat(callback.getEvents()).containsOnly(event);
}
@Test(groups = "unit")
public void should_log_and_drop_events_on_overflow() throws InterruptedException {
MemoryAppender logs = new MemoryAppender();
Logger logger = Logger.getLogger(EventDebouncer.class);
Level originalLoggerLevel = logger.getLevel();
logger.setLevel(Level.WARN);
logger.addAppender(logs);
try {
EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback, 10) {
@Override
int maxPendingEvents() {
return 100;
}
@Override
long delayMs() {
return 15;
}
};
debouncer.start();
List<MockEvent> events = new ArrayList<MockEvent>();
for (int i = 0; i < 14; i++) {
MockEvent event = new MockEvent(i);
events.add(event);
debouncer.eventReceived(event);
}
// Only 10 events should have been handled.
callback.awaitEvents(10);
assertThat(callback.getEvents()).isEqualTo(events.subList(0, 10));
// Debouncer warning should have been logged, but only once.
assertThat(logs.get()).containsOnlyOnce("test debouncer enqueued more than 10 events, rejecting new events.");
} finally {
logger.removeAppender(logs);
logger.setLevel(originalLoggerLevel);
}
}
@Test(groups = "unit")
public void should_deliver_n_events_in_order() throws InterruptedException {
EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 50;
}
};
debouncer.start();
List<MockEvent> events = new ArrayList<MockEvent>();
for (int i = 0; i < 50; i++) {
MockEvent event = new MockEvent(i);
events.add(event);
debouncer.eventReceived(event);
}
callback.awaitEvents(50);
assertThat(callback.getEvents()).isEqualTo(events);
}
@Test(groups = "unit")
public void should_deliver_n_events_in_order_even_if_queue_full() throws InterruptedException {
EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 1;
}
};
debouncer.start();
List<MockEvent> events = new ArrayList<MockEvent>();
for (int i = 0; i < 50; i++) {
MockEvent event = new MockEvent(i);
events.add(event);
debouncer.eventReceived(event);
}
callback.awaitEvents(50);
assertThat(callback.getEvents()).isEqualTo(events);
}
@Test(groups = "unit")
public void should_accumulate_events_if_not_ready() throws InterruptedException {
EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 50;
}
};
List<MockEvent> events = new ArrayList<MockEvent>();
for (int i = 0; i < 50; i++) {
MockEvent event = new MockEvent(i);
events.add(event);
debouncer.eventReceived(event);
}
// simulate late start
debouncer.start();
callback.awaitEvents(50);
assertThat(callback.getEvents()).hasSize(50);
assertThat(callback.getEvents()).isEqualTo(events);
}
@Test(groups = "unit")
public void should_accumulate_all_events_until_start() throws InterruptedException {
final EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 25;
}
};
final List<MockEvent> events = new ArrayList<MockEvent>();
for (int i = 0; i < 50; i++) {
MockEvent event = new MockEvent(i);
events.add(event);
debouncer.eventReceived(event);
}
debouncer.start();
callback.awaitEvents(50);
assertThat(callback.getEvents()).isEqualTo(events);
}
@Test(groups = "unit")
public void should_reset_timer_if_n_events_received_within_same_window() throws InterruptedException {
final EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 50;
}
@Override
long delayMs() {
return 50;
}
};
debouncer.start();
final CountDownLatch latch = new CountDownLatch(50);
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (latch.getCount() > 0) {
MockEvent event = new MockEvent(0);
debouncer.eventReceived(event);
latch.countDown();
}
}
}, 0, 5, MILLISECONDS);
latch.await();
pool.shutdownNow();
callback.awaitEvents(50);
assertThat(callback.getEvents()).hasSize(50);
}
@Test(groups = "unit")
public void should_stop_receiving_events() throws InterruptedException {
final EventDebouncer<MockEvent> debouncer = new EventDebouncer<MockEvent>("test", executor, callback) {
@Override
int maxPendingEvents() {
return 10;
}
@Override
long delayMs() {
return 50;
}
};
debouncer.start();
for (int i = 0; i < 50; i++) {
MockEvent event = new MockEvent(i);
debouncer.eventReceived(event);
}
callback.awaitEvents(50);
debouncer.stop();
MockEvent event = new MockEvent(0);
debouncer.eventReceived(event);
assertThat(callback.getEvents()).hasSize(50);
}
private static class MockDeliveryCallback implements DeliveryCallback<MockEvent> {
final List<MockEvent> events = new CopyOnWriteArrayList<MockEvent>();
final Lock lock = new ReentrantLock();
final Condition cond = lock.newCondition();
@Override
public ListenableFuture<?> deliver(List<MockEvent> events) {
lock.lock();
try {
this.events.addAll(events);
cond.signal();
} finally {
lock.unlock();
}
return Futures.immediateFuture(null);
}
void awaitEvents(int expected) throws InterruptedException {
long nanos = MINUTES.toNanos(5);
lock.lock();
try {
while (events.size() < expected) {
if (nanos <= 0L)
fail("Timed out waiting for events");
nanos = cond.awaitNanos(nanos);
}
} finally {
lock.unlock();
}
}
public List<MockEvent> getEvents() {
return events;
}
}
private class MockEvent {
private final int i;
private MockEvent(int i) {
this.i = i;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MockEvent mockEvent = (MockEvent) o;
return i == mockEvent.i;
}
@Override
public int hashCode() {
return i;
}
@Override
public String toString() {
return "MockEvent" + i;
}
}
}