package de.rwth.idsg.bikeman.utils;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
/**
* Processes items with a producer/consumer pattern.
*
* The clients of this class are producers which put the items in the queue in their current thread
* {@link #add(T item)}. A consumer runs in the background thread, watches the queue and processes the items.
*
* @author Sevket Goekay <goekay@dbis.rwth-aachen.de>
* @since 12.08.2016
*/
@Slf4j
public class QueueProcessor<T> implements Runnable {
private final ExecutorService executorService;
private final LinkedBlockingQueue<T> queue;
private final Consumer<T> consumer;
private final String queueName;
private Consumer<T> queueAdder = this::addInternal;
private Future task;
// These locks are only relevant for interrupt case. During the consumer impl change,
// since this is not that immediate, incoming items are still accepted and added to
// the queue. We do want to prevent that.
//
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
/**
* @param executorService Used to spawn background thread for the consumer.
* @param consumer Actual logic to process the items in the queue.
* @param queueName The name for this processor instance. Currently only used for logging and is helpful to
* distinguish between the behaviors of multiple instances of QueueProcessor (if there are
* any). This should better be something unique, but it's not a hard rule.
*/
public QueueProcessor(ExecutorService executorService, Consumer<T> consumer, String queueName) {
Objects.requireNonNull(executorService, "executorService may not be null!");
Objects.requireNonNull(consumer, "consumer may not be null!");
Preconditions.checkArgument(!Strings.isNullOrEmpty(queueName), "queueName may not be null!");
this.executorService = executorService;
this.consumer = consumer;
this.queueName = "name=" + queueName;
this.queue = new LinkedBlockingQueue<>();
}
public void start() {
task = executorService.submit(this);
}
public void stop() {
if (task != null) {
task.cancel(true);
}
}
// -------------------------------------------------------------------------
// Producer stuff
// -------------------------------------------------------------------------
public void add(final T item) {
readLock.lock();
try {
queueAdder.accept(item);
} finally {
readLock.unlock();
}
}
private void addInternal(final T item) {
log.debug("[{}] Adding {}", queueName, item);
boolean success = queue.offer(item);
if (!success) {
// Should not happen because we do NOT use a "capacity-restricted" LinkedBlockingQueue.
log.warn("[{}] Failed to put the item {} in queue", queueName, item);
}
}
private void addAfterInterrupt(final T item) {
log.warn("[{}] Consumer is stopped. Will not add the item to queue", queueName);
}
// -------------------------------------------------------------------------
// Consumer stuff
// -------------------------------------------------------------------------
@Override
public void run() {
while (true) {
try {
consumeInternal();
} catch (InterruptedException e) {
handleInterrupt();
return;
}
}
}
private void consumeInternal() throws InterruptedException {
// If there are no items, blocks the thread and waits until there is an element to take
T item = queue.take();
// Delegate
consumer.accept(item);
}
private void consumeRemaining() {
int counter = 0;
while (queue.size() != 0) {
try {
consumeInternal();
counter++;
} catch (InterruptedException e) {
log.error("[{}] Error occurred", queueName, e);
}
}
log.info("[{}] Finished processing of all {} remaining item(s)", queueName, counter);
}
// -------------------------------------------------------------------------
// Other helpers
// -------------------------------------------------------------------------
private void handleInterrupt() {
writeLock.lock();
try {
// Stop accepting/adding new items to queue
queueAdder = this::addAfterInterrupt;
} finally {
writeLock.unlock();
}
if (queue.isEmpty()) {
log.info("[{}] Interrupted. Queue is empty. Stopping consumer", queueName);
} else {
log.warn("[{}] Interrupted. There are still {} item(s) in queue. Will process these and THEN stop the consumer", queueName, queue.size());
// Consume the items that are already in the queue
consumeRemaining();
}
}
}