All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } }); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy