package com.github.triceo.splitlog;
import com.github.triceo.splitlog.api.*;
import com.github.triceo.splitlog.logging.SplitlogLoggerFactory;
import com.github.triceo.splitlog.util.LogUtil;
import com.github.triceo.splitlog.util.LogUtil.Level;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.slf4j.Logger;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;
class ConsumerManager<P extends MessageProducer<P>> implements MessageProducer<P>, MessageConsumer<P>,
ConsumerRegistrar<P> {
private static final Logger LOGGER = SplitlogLoggerFactory.getLogger(ConsumerManager.class);
private final Collection<MessageConsumer<P>> consumers = new CopyOnWriteArraySet<>();
private final AtomicBoolean isStopped = new AtomicBoolean(false);
private final BidiMap<String, DefaultMessageMetric<? extends Number, P>> metrics = new DualHashBidiMap<>();
private final P producer;
public ConsumerManager(final P producer) {
this.producer = producer;
}
@Override
public int countConsumers() {
return this.consumers.size();
}
@Override
public synchronized int countMetrics() {
return this.metrics.size();
}
@Override
public synchronized MessageMetric<? extends Number, P> getMetric(final String id) {
return this.metrics.get(id);
}
@Override
public synchronized String getMetricId(final MessageMetric<? extends Number, P> measure) {
return this.metrics.getKey(measure);
}
public P getProducer() {
return this.producer;
}
@Override
public boolean isConsuming(final MessageConsumer<P> consumer) {
return this.consumers.contains(consumer);
}
@Override
public synchronized boolean isMeasuring(final MessageMetric<? extends Number, P> metric) {
return this.metrics.containsValue(metric);
}
@Override
public synchronized boolean isMeasuring(final String id) {
return this.metrics.containsKey(id);
}
@Override
public boolean isStopped() {
return this.isStopped.get();
}
@Override
public void messageReceived(final Message message, final MessageDeliveryStatus status, final P producer) {
if (this.isStopped()) {
throw new IllegalStateException("Consumer manager already stopped.");
}
LogUtil.newMessage(ConsumerManager.LOGGER, Level.INFO, "New message received:", message, status, producer, this);
for (final MessageConsumer<P> consumer : this.consumers) {
try {
consumer.messageReceived(message, status, producer);
} catch (final Throwable t) {
// calling user code; we need to be prepared for anything
ConsumerManager.LOGGER.warn("Failed notifying {} of '{}' with status {}. Stack trace on DEBUG.",
consumer, message, status, t.getMessage());
ConsumerManager.LOGGER.debug("Failed notifying {} of '{}' with status {}.", consumer, message, status,
t);
}
}
}
@Override
public boolean registerConsumer(final MessageConsumer<P> consumer) {
if (this.isStopped()) {
throw new IllegalStateException("Consumer manager already stopped.");
} else if (this.consumers.add(consumer)) {
ConsumerManager.LOGGER.info("Registered consumer {} for {}.", consumer, this.producer);
return true;
} else {
return false;
}
}
@Override
public MessageConsumer<P> startConsuming(final MessageListener<P> listener) {
if (listener instanceof MessageConsumer<?>) {
throw new IllegalArgumentException("Cannot consume consumers.");
}
final MessageConsumer<P> consumer = new DefaultMessageConsumer<>(listener, this.producer);
if (this.registerConsumer(consumer)) {
ConsumerManager.LOGGER.info("Registered new consumer {} for {}.", consumer, listener);
return consumer;
}
/*
* we know that there is a consumer with the same properties; disregard
* the new and return the old instead.
*/
for (final MessageConsumer<P> existing : this.consumers) {
if (existing.equals(consumer)) {
ConsumerManager.LOGGER.info("Retrieve pre-existing consumer {} for {}.", consumer, listener);
return existing;
}
}
throw new IllegalStateException("Unreachable code.");
}
@Override
public synchronized <T extends Number> MessageMetric<T, P> startMeasuring(final MessageMeasure<T, P> measure,
final String id) {
if (this.isStopped()) {
throw new IllegalStateException("Measuring consumer manager already stopped.");
} else if (measure == null) {
throw new IllegalArgumentException("Measure may not be null.");
} else if (id == null) {
throw new IllegalArgumentException("ID may not be null.");
} else if (this.metrics.containsKey(id)) {
throw new IllegalArgumentException("Duplicate ID:" + id);
}
ConsumerManager.LOGGER.info("Starting measuring {} in {}.", id, this.getProducer());
final DefaultMessageMetric<T, P> metric = new DefaultMessageMetric<>(this.getProducer(), measure);
this.metrics.put(id, metric);
this.registerConsumer(metric);
return metric;
}
@Override
public boolean stop() {
if (!this.isStopped.compareAndSet(false, true)) {
return false;
}
ConsumerManager.LOGGER.info("Stopping consumer manager for {}.", this.producer);
this.consumers.forEach(this::stopConsuming);
new HashSet<>(this.metrics.keySet()).forEach(this::stopMeasuring);
ConsumerManager.LOGGER.info("Stopped metrics consumer manager for {}.", this.getProducer());
return true;
}
@Override
public boolean stopConsuming(final MessageConsumer<P> consumer) {
if (this.consumers.remove(consumer)) {
ConsumerManager.LOGGER.info("Unregistered consumer {} for {}.", consumer, this.producer);
return true;
} else {
return false;
}
}
@Override
public synchronized boolean stopMeasuring(final MessageMetric<? extends Number, P> measure) {
if (!this.isMeasuring(measure)) {
return false;
}
return this.stopMeasuring(this.metrics.getKey(measure));
}
@Override
public synchronized boolean stopMeasuring(final String id) {
if (!this.isMeasuring(id)) {
return false;
}
final DefaultMessageMetric<? extends Number, P> removed = this.metrics.remove(id);
this.stopConsuming(removed);
ConsumerManager.LOGGER.info("Stopped measuring {} in {}.", id, this.getProducer());
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("ConsumerManager [");
if (this.getProducer() != null) {
builder.append("getProducer()=").append(this.getProducer()).append(", ");
}
builder.append("isStopped()=").append(this.isStopped()).append("]");
return builder.toString();
}
}