/*
* Copyright 2015-2017 Groupon, Inc
* Copyright 2015-2017 The Billing Project, LLC
*
* The Billing Project 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 org.killbill.bus;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.awaitility.Awaitility;
import org.killbill.TestSetup;
import org.killbill.bus.api.BusEvent;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBusConfig;
import org.skife.config.ConfigurationObjectFactory;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.TransactionCallback;
import org.skife.jdbi.v2.TransactionStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
public class TestLoadDefaultPersistentBus extends TestSetup {
private static final Logger log = LoggerFactory.getLogger(TestLoadDefaultPersistentBus.class);
PersistentBus eventBus;
@BeforeClass(groups = "load")
public void beforeClass() throws Exception {
super.beforeClass();
final Properties properties = new Properties();
// See sleep time below in LoadHandler
properties.setProperty("org.killbill.persistent.bus.main.inflight.claimed", "500");
properties.setProperty("org.killbill.persistent.bus.external.inMemory", "false");
properties.setProperty("org.killbill.persistent.bus.main.nbThreads", "200");
properties.setProperty("org.killbill.persistent.bus.main.queue.capacity", "20000");
properties.setProperty("org.killbill.persistent.bus.main.sleep", "0");
properties.setProperty("org.killbill.persistent.bus.main.sticky", "true");
properties.setProperty("org.killbill.persistent.bus.main.useInflightQ", "true");
final PersistentBusConfig busConfig = new ConfigurationObjectFactory(properties).buildWithReplacements(PersistentBusConfig.class, ImmutableMap.<String, String>of("instanceName", "main"));
eventBus = new DefaultPersistentBus(dbi, clock, busConfig, metricRegistry, databaseTransactionNotificationApi);
}
@BeforeMethod(groups = "load")
public void beforeMethod() throws Exception {
super.beforeMethod();
eventBus.start();
}
@AfterMethod(groups = "load")
public void afterMethod() throws Exception {
eventBus.stop();
}
@Test(groups = "load")
public void testMultiThreadedLoad() throws Exception {
final LoadHandler consumer = new LoadHandler();
eventBus.register(consumer);
final Long targetEventsPerSecond = 700L;
final int testDurationMinutes = 2;
final Long nbEvents = targetEventsPerSecond * testDurationMinutes * 60;
final Producer producer = new Producer(nbEvents, targetEventsPerSecond);
try {
final Thread producerThread = new Thread(producer);
producerThread.start();
consumer.waitForCompletion(nbEvents, testDurationMinutes * 60 * 1000);
} finally {
producer.stop();
}
}
public static final class LoadHandler {
private final AtomicLong nbEvents = new AtomicLong(0);
private final AtomicLong nbEventsForLogging = new AtomicLong(0);
private final AtomicLong lastLogLineTime = new AtomicLong(System.currentTimeMillis());
@AllowConcurrentEvents
@Subscribe
public void processEvent(final LoadBusEvent event) {
try {
// Go to a Ruby plugin, database, etc.
Thread.sleep(200L);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
nbEvents.incrementAndGet();
nbEventsForLogging.incrementAndGet();
final long delayMillis = System.currentTimeMillis() - lastLogLineTime.get();
if (delayMillis > 10000) {
log.info("Consumer processed {} events in {}s", nbEventsForLogging, delayMillis / 1000.0);
nbEventsForLogging.set(0);
lastLogLineTime.set(System.currentTimeMillis());
}
}
public void waitForCompletion(final Long expectedEvents, final long timeoutMs) {
Awaitility.await()
.atMost(timeoutMs, TimeUnit.MILLISECONDS)
.until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return nbEvents.get() == expectedEvents;
}
});
}
}
private static final class LoadBusEvent implements BusEvent {
private final String payload;
private final Long searchKey1;
private final Long searchKey2;
private final UUID userToken;
public LoadBusEvent() {
// Generate 540 bytes of data
final StringBuilder payloadBuilder = new StringBuilder();
for (int i = 0; i < 15; i++) {
payloadBuilder.append(UUID.randomUUID().toString());
}
this.payload = payloadBuilder.toString();
this.searchKey2 = 12L;
this.searchKey1 = 42L;
this.userToken = UUID.randomUUID();
}
@JsonCreator
public LoadBusEvent(@JsonProperty("payload") final String payload,
@JsonProperty("searchKey1") final Long searchKey1,
@JsonProperty("searchKey2") final Long searchKey2,
@JsonProperty("userToken") final UUID userToken) {
this.payload = payload;
this.searchKey2 = searchKey2;
this.searchKey1 = searchKey1;
this.userToken = userToken;
}
// Note! Getter required to have the value serialized to disk
public String getPayload() {
return payload;
}
@Override
public Long getSearchKey1() {
return searchKey1;
}
@Override
public Long getSearchKey2() {
return searchKey2;
}
@Override
public UUID getUserToken() {
return userToken;
}
}
private final class Producer implements Runnable {
private final AtomicBoolean isStarted = new AtomicBoolean(true);
// Total number of events to send
private final Long nbEvents;
// Producer speed
private final Long targetEventsPerSecond;
public Producer(final Long nbEvents, final Long targetEventsPerSecond) {
this.nbEvents = nbEvents;
this.targetEventsPerSecond = targetEventsPerSecond;
}
public void stop() {
isStarted.set(false);
}
@Override
public void run() {
final int batchLengthSeconds = 10;
final long eventsPerBatch = batchLengthSeconds * targetEventsPerSecond;
Long nbEventsSent = 0L;
while (isStarted.get() && nbEventsSent < nbEvents) {
final Long t1 = System.currentTimeMillis();
for (int i = 0; i < eventsPerBatch; i++) {
postEvent();
}
final Long delayMillis = System.currentTimeMillis() - t1;
final int maxDelayMillis = batchLengthSeconds * 1000;
if (delayMillis > maxDelayMillis) {
log.warn("Generated {} entries in {}s - producer slow", eventsPerBatch, delayMillis / 1000.0);
} else {
log.info("Generated {} entries in {}s", eventsPerBatch, delayMillis / 1000.0);
try {
Thread.sleep(maxDelayMillis - delayMillis);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
nbEventsSent += eventsPerBatch;
}
log.info("Producer shutting down - {} events sent", nbEventsSent);
}
private void postEvent() {
dbi.inTransaction(new TransactionCallback<Void>() {
@Override
public Void inTransaction(final Handle conn, final TransactionStatus status) throws Exception {
Assert.assertEquals(conn.select("select now();").size(), 1);
final BusEvent event = new LoadBusEvent();
eventBus.postFromTransaction(event, conn.getConnection());
return null;
}
});
}
}
}