com.mantledillusion.vaadin.metrics.AbstractMetricsQueue Maven / Gradle / Ivy
package com.mantledillusion.vaadin.metrics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import com.mantledillusion.vaadin.metrics.api.Metric;
abstract class AbstractMetricsQueue {
public static final long[] CONSUMER_DELIVERY_RETRY_INTERVALS = {
// 5 Seconds
5000,
// 1 Minute
60000,
// 5 Minutes
300000,
// 15 Minutes
900000,
// 30 Minutes
1800000 };
private class Counter {
private long value;
void add(long value) {
this.value += value;
}
long get() {
return this.value;
}
}
/**
* Represents a queue of events that are about to be delivered to a
* {@link MetricsConsumer}.
*/
public class ConsumerQueue {
private class ConsumerSessionMetricsGate {
private final String sessionId;
private final MetricsPredicate gate;
private final MetricsPredicate filter;
private final List accumulatedDeliveries = new ArrayList<>();
private ConsumerSessionMetricsGate(String sessionId) {
this.sessionId = sessionId;
this.gate = ConsumerQueue.this.gate != null ? ConsumerQueue.this.gate.functionalClone() : null;
this.filter = ConsumerQueue.this.filter != null ? ConsumerQueue.this.filter.functionalClone() : null;
}
private void pass(Metric metric) {
this.accumulatedDeliveries.add(metric);
if (this.gate == null || (metric != null && this.gate.test(metric))) {
flush();
}
}
private void flush() {
this.accumulatedDeliveries.stream()
.filter(accumulatedDelivery -> this.filter == null || this.filter.test(accumulatedDelivery))
.forEach(
accumulatedDelivery -> ConsumerQueue.this.deliver(this.sessionId, accumulatedDelivery));
this.accumulatedDeliveries.clear();
}
}
private final String consumerId;
private final ThreadPoolExecutor delivererService = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
private final MetricsConsumer consumer;
private final MetricsPredicate gate;
private final MetricsPredicate filter;
private final Map sessionGates = new ConcurrentHashMap<>();
private boolean doFlushOnSessionDestroy = true;
private boolean doFlushOnShutdown = true;
private ConsumerQueue(String consumerId, MetricsConsumer consumer, MetricsPredicate gate,
MetricsPredicate filter) {
this.consumerId = consumerId;
this.consumer = consumer;
this.gate = gate;
this.filter = filter;
}
private void enqueue(String sessionId, Metric metric) {
this.sessionGates.computeIfAbsent(sessionId, id -> new ConsumerSessionMetricsGate(sessionId)).pass(metric);
}
private void deliver(String sessionId, Metric metric) {
this.delivererService.execute(() -> {
int tries = 0;
try {
while (true) {
try {
ConsumerQueue.this.consumer.consume(this.consumerId, sessionId, metric);
break;
} catch (Exception e) {
/*
* If a consumer is not able to consume its delivery, we wait for the next time
* to try it.
*/
long retryIntervalMs = AbstractMetricsQueue.this.consumerRetryIntervals[tries];
try {
AbstractMetricsQueue.this.onRetry(consumer, e, retryIntervalMs);
} catch (Exception e2) {
// nop: this method is just called to inform.
}
Thread.sleep(retryIntervalMs);
tries = Math.min(tries + 1, AbstractMetricsQueue.this.consumerRetryIntervals.length - 1);
}
}
} catch (InterruptedException e) {
/*
* If we are not able to wait for a next try we cannot continue; we unregister
* the consumer to make sure not to create inconsistent data
*/
remove(false);
throw new RuntimeException("Delivering a metric to the " + MetricsConsumer.class.getSimpleName()
+ " '" + ConsumerQueue.this.consumer
+ "' failed, and triggering to wait for a retry failed as well.", e);
} catch (Throwable t) {
/*
* When something so destructive happens, we unregister the consumer to make
* sure not to create inconsistent data
*/
remove(false);
throw t;
}
});
}
/**
* Returns whether the {@link ConsumerQueue} should flush all of a session's
* gated events when the session is destroyed.
*
* @return True if a possibly set gate should be ignored when a session is
* destroyed, false otherwise.
*/
public boolean doFlushOnSessionDestroy() {
return doFlushOnSessionDestroy;
}
/**
* Sets whether the {@link ConsumerQueue} should flush all of a session's gated
* events when the session is destroyed.
*
* True by default.
*
* @param doFlushOnSessionDestroy True if a possibly set gate should be ignored
* when a session is destroyed, false otherwise.
*/
public void setDoFlushOnSessionDestroy(boolean doFlushOnSessionDestroy) {
this.doFlushOnSessionDestroy = doFlushOnSessionDestroy;
}
/**
* Returns whether the {@link ConsumerQueue} should flush all of a session's
* gated events when it is shut down using {@link #remove()}.
*
* @return True if a possibly set gate should be ignored when the queue is shut
* down, false otherwise.
*/
public boolean doFlushOnShutdown() {
return doFlushOnShutdown;
}
/**
* Sets whether the {@link ConsumerQueue} should flush all of a session's gated
* events when it is shut down using {@link #remove()}.
*
* True by default.
*
* @param doFlushOnShutdown True if a possibly set gate should be ignored when
* the queue is shut down, false otherwise.
*/
public void setDoFlushOnShutdown(boolean doFlushOnShutdown) {
this.doFlushOnShutdown = doFlushOnShutdown;
}
/**
* Returns the count of events that are enqueued and waiting for this consumer's
* gate to open so they can be delivered.
*
* @return The event count
*/
public long getGatedCount() {
Counter count = new Counter();
this.sessionGates.values().forEach(gate -> count.add(gate.accumulatedDeliveries.size()));
return count.get();
}
/**
* Returns the count of events that are currently being delivered to this
* consumer by asynchronous tasks.
*
* Also note that delivering is an asynchronous process, so the returned number
* is an approximation.
*
* @return the event count
*/
public long getDeliveringCount() {
return this.delivererService.getQueue().size() + (this.delivererService.getActiveCount() > 0 ? 1 : 0);
}
/**
* Removes this {@link ConsumerQueue} from its observer.
*/
public void remove() {
remove(ConsumerQueue.this.doFlushOnShutdown);
}
private void remove(boolean doFlush) {
synchronized (AbstractMetricsQueue.this) {
AbstractMetricsQueue.this.consumerQueues.remove(ConsumerQueue.this.consumerId);
endSessions(doFlush);
}
}
void endSession(String sessionId, boolean doFlush) {
if (this.sessionGates.containsKey(sessionId)) {
if (doFlush) {
this.sessionGates.get(sessionId).flush();
}
this.sessionGates.remove(sessionId);
}
}
void endSessions(boolean doFlush) {
Iterator> iter = this.sessionGates.entrySet().iterator();
while (iter.hasNext()) {
ConsumerSessionMetricsGate sessionGate = iter.next().getValue();
if (doFlush) {
sessionGate.flush();
}
iter.remove();
}
}
}
private final ThreadPoolExecutor enqueuerService = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
private final Map consumerQueues = new ConcurrentHashMap<>();
private long[] consumerRetryIntervals = CONSUMER_DELIVERY_RETRY_INTERVALS;
protected AbstractMetricsQueue() {
}
/**
* Info method that is called when delivering a metric to a
* {@link MetricsConsumer} has failed because of an {@link Exception}.
*
* @param consumer The consumer on which the delivery failed; might
* not be null.
* @param e The {@link Exception} that caused the delivery to fail;
* might not be null.
* @param nextIntervalMs The length of the next waiting interval before a retry
* will be performed.
*/
protected abstract void onRetry(MetricsConsumer consumer, Exception e, long nextIntervalMs);
protected Collection getConsumerQueues() {
return this.consumerQueues.values();
}
/**
* Sets the intervals in milliseconds the {@link AbstractMetricsQueue} waits
* until it tries to deliver an event to a {@link MetricsConsumer} again after
* the first delivery failed.
*
* If the delivery fails more times than there are intervals set, the last
* defined interval is used.
*
* For example, if the method is called with the arguments (0, 5000, 300000),
* the first retry will be done directly after the first failed, the second
* after 5 seconds and the third->nth after 5 minutes.
*
* The default intervals are {@link #CONSUMER_DELIVERY_RETRY_INTERVALS}.
*
* @param interval The first interval; might not be negative.
* @param intervals The additional intervals; might not be negative.
*/
public void setConsumerDeliveryRetryIntervals(long interval, long... intervals) {
long[] consumerRetryIntervals;
if (intervals == null) {
consumerRetryIntervals = new long[] { interval };
} else {
consumerRetryIntervals = new long[intervals.length + 1];
Arrays.setAll(consumerRetryIntervals, i -> i == 0 ? interval : intervals[i - 1]);
}
if (Arrays.stream(consumerRetryIntervals).anyMatch(i -> i < 0)) {
throw new IllegalArgumentException("Cannot set a retry interval < 0");
}
this.consumerRetryIntervals = consumerRetryIntervals;
}
/**
* Adds a {@link MetricsConsumer} to the {@link AbstractMetricsQueue} that will
* receive all events the observer gets aware of.
*
* @param consumerId The id to add the consumer under, which will be delivered
* to the consumer on each
* {@link MetricsConsumer#consume(String, String, Metric)}
* invocation. Allows the same consumer to be registered
* multiple times with differing configurations; might
* not be null.
* @param consumer The consumer to add; might not be null.
* @return The created {@link ConsumerQueue} that can be used to configure and
* unregister the consumer, never null
*/
public ConsumerQueue addConsumer(String consumerId, MetricsConsumer consumer) {
return addConsumer(consumerId, consumer, null, null);
}
/**
* Adds a {@link MetricsConsumer} to the {@link AbstractMetricsQueue} that will
* receive all events the observer gets aware of.
*
* @param consumerId The unique id to add the consumer under, which will be
* delivered to the consumer on each
* {@link MetricsConsumer#consume(String, String, Metric)}
* invocation. Allows the same consumer to be registered
* multiple times with differing configurations; might
* not be null.
* @param consumer The consumer to add; might not be null.
* @param gate The predicate that needs to
* {@link MetricsPredicate#test(Metric)} true to trigger
* flushing all of a session's accumulated
* {@link Metric}s; might be null.
* @param filter The predicate that needs to
* {@link MetricsPredicate#test(Metric)} true to allow an
* about-to-be-flushed event to be delivered to the consumer;
* might be null.
* @return The created {@link ConsumerQueue} that can be used to configure and
* unregister the consumer, never null
*/
public synchronized ConsumerQueue addConsumer(String consumerId, MetricsConsumer consumer, MetricsPredicate gate,
MetricsPredicate filter) {
if (consumerId == null || consumerId.isEmpty()) {
throw new IllegalArgumentException("Cannot register a consumer under a null or empty id");
} else if (this.consumerQueues.containsKey(consumerId)) {
throw new IllegalArgumentException(
"There is already a consumer registered under the id '" + consumerId + "'");
} else if (consumer == null) {
throw new IllegalArgumentException("Cannot register a null consumer");
}
ConsumerQueue queue = new ConsumerQueue(consumerId, consumer, gate, filter);
this.consumerQueues.put(consumerId, queue);
return queue;
}
/**
* Returns the count of events (over all consumers) that are currently being
* enqueued in asynchronous tasks.
*
* Also note that enqueueing is an asynchronous process, so the returned number
* is an approximation.
*
* @return The event count
*/
public long getEnqueueingCount() {
return this.enqueuerService.getQueue().size() + (this.enqueuerService.getActiveCount() > 0 ? 1 : 0);
}
/**
* Returns the count of events (over all consumers) that are enqueued and
* waiting for their consumer's gate to open so they can be delivered.
*
* @see ConsumerQueue#getGatedCount()
* @return The event count
*/
public long getGatedCount() {
Counter count = new Counter();
this.consumerQueues.values().forEach(queue -> count.add(queue.getGatedCount()));
return count.get();
}
/**
* Returns the count of events (over all consumers) that are currently being
* delivered by asynchronous tasks.
*
* Also note that delivering is an asynchronous process, so the returned number
* is an approximation.
*
* @see ConsumerQueue#getDeliveringCount()
* @return the event count
*/
public long getDeliveringCount() {
Counter count = new Counter();
this.consumerQueues.values().forEach(queue -> count.add(queue.getDeliveringCount()));
return count.get();
}
void enqueue(String sessionId, Metric metric) {
this.enqueuerService.execute(() -> {
synchronized (AbstractMetricsQueue.this) {
for (ConsumerQueue queue : AbstractMetricsQueue.this.consumerQueues.values()) {
queue.enqueue(sessionId, metric);
}
}
});
}
}