package com.netflix.schlep.producer; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.Observable.OnSubscribeFunc; import rx.Observer; import rx.Subscription; import rx.concurrency.Schedulers; import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; import rx.util.functions.Action1; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.netflix.schlep.Completion; public abstract class ConcurrentMessageProducer implements MessageProducer { private static final Logger LOG = LoggerFactory.getLogger(ConcurrentMessageProducer.class); // Defaults private static final int DEFAULT_THREAD_COUNT = 1; private static final int DEFAULT_BATCH_SIZE = 10; private static final int DEFAULT_BUFFER_DELAY = 1000; // Configuration private final String id; private final int threadCount; private final int batchSize; private final long bufferDelay; // State private final AtomicLong busyCount = new AtomicLong(0); private PublishSubject<ObservableCompletion> subject = PublishSubject.create(); private Subscription subscription; private ExecutorService executor; public static class ObservableCompletion extends Completion<OutgoingMessage> { private Observer<? super Completion<OutgoingMessage>> observer; public ObservableCompletion(OutgoingMessage message, Observer<? super Completion<OutgoingMessage>> observer) { super(message); this.observer = observer; } public void done() { this.observer.onNext(this); this.observer.onCompleted(); } } /** * Builder * @param <T> */ public static abstract class Builder<T extends Builder<T>> { private String id; private int threadCount = DEFAULT_THREAD_COUNT; private int batchSize = DEFAULT_BATCH_SIZE; private long bufferDelay = DEFAULT_BUFFER_DELAY; protected abstract T self(); public T withId(String id) { this.id = id; return self(); } public T withThreadCount(int threadCount) { this.threadCount = threadCount; return self(); } public T withBufferDelay(long amount) { this.bufferDelay = amount; return self(); } public T withBatchSize(int size) { this.batchSize = size; return self(); } } private final AtomicLong sendAttempt = new AtomicLong(); protected ConcurrentMessageProducer(Builder<?> init) { this.batchSize = init.batchSize; this.threadCount = init.threadCount; this.id = init.id; this.bufferDelay = init.bufferDelay; } @Override public synchronized void start() throws Exception { if (isStarted()) { LOG.warn(String.format("Producer '%s' already started", getId())); return; } LOG.info(String.format("Starting producer '%s'", getId())); subject = PublishSubject.create(); // Consider making this a cache thread pool so we can adjust the number of threads at runtime executor = Executors.newScheduledThreadPool(threadCount, new ThreadFactoryBuilder() .setNameFormat("Producer-" + getId() + "-%d") .setDaemon(true) .build()); subscription = subject .buffer(bufferDelay, TimeUnit.MILLISECONDS, batchSize) .observeOn(Schedulers.executor(executor)) .subscribe(new Action1<List<ObservableCompletion>>() { @Override public void call(List<ObservableCompletion> messages) { if (messages.size() > 0) sendMessages(messages); } }); } @Override public synchronized void stop() throws Exception { LOG.info(String.format("Stopping producer '%s'", getId())); if (executor != null) { executor.shutdownNow(); executor = null; } if (subscription != null) { subscription.unsubscribe(); subscription = null; } } @Override public void pause() throws Exception { // NOOP } @Override public void resume() throws Exception { // NOOP } @Override public String getId() { return id; } /** * Write a message and notify completion on the provided observer * * @param message * @param observer */ @Override public void send(OutgoingMessage message, Observer<Completion<OutgoingMessage>> observer) { sendAttempt.incrementAndGet(); subject.onNext(new ObservableCompletion(message, observer)); } /** * Write a message and return an observable on which a Completion will be emitted * once the message is written successfully * @param message * @return */ @Override public Observable<Completion<OutgoingMessage>> send(final OutgoingMessage message) { sendAttempt.incrementAndGet(); return Observable.create(new OnSubscribeFunc<Completion<OutgoingMessage>>() { @Override public Subscription onSubscribe(Observer<? super Completion<OutgoingMessage>> observer) { subject.onNext(new ObservableCompletion(message, observer)); return Subscriptions.empty(); } }); } protected abstract void sendMessages(List<ObservableCompletion> messages); @Override public boolean isStarted() { return executor != null; } }