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

io.axonif.queuebacca.Subscriber Maven / Gradle / Ivy

There is a newer version: 0.4.2
Show newest version
/*
 * Copyright 2019 The Queuebacca Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.axonif.queuebacca;

import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import io.axonif.queuebacca.events.TimingEventListener;
import io.axonif.queuebacca.events.TimingEventSupport;

/**
 * Subscribes to {@link MessageBin MessageBins} for the purposes of consuming {@link Message ViaMessages}. Maintains
 * a list of active subscriptions for the purposes of cancelling them.
 */
public final class Subscriber {

	private static final int SUBSCRIPTION_INITIAL_DELAY = 1000; // milliseconds
	private static final int SUBSCRIPTION_PERIODIC_DELAY = 1; // milliseconds

    private final List> activeSubscriptions = new ArrayList<>();
    private final TimingEventSupport timingEventSupport = new TimingEventSupport();

    private final Client client;
    private final WorkExecutorFactory workExecutorFactory;
    private final ExceptionResolver exceptionResolver;

	/**
	 * Creates a new instance of a {@link Subscriber} for a specific {@link Client}.
	 *
	 * @param client the client for the message broker
	 * @param workExecutorFactory factory for creating {@link WorkExecutor WorkExecutors}
	 * @param exceptionResolver determines resolution for exceptions thrown by messages
	 */
	public Subscriber(Client client, WorkExecutorFactory workExecutorFactory, ExceptionResolver exceptionResolver) {
        this.client = requireNonNull(client);
		this.workExecutorFactory = requireNonNull(workExecutorFactory);
		this.exceptionResolver = requireNonNull(exceptionResolver);
	}

	/**
	 * Subscribes to a {@link MessageBin} using the provided {@link SubscriptionConfiguration}. This will produce
	 * an active thread that will check for messages.
	 *
	 * @param configuration subscription configuration
	 */
	public void subscribe(SubscriptionConfiguration configuration) {
        requireNonNull(configuration);

        ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNameFormat(configuration.getMessageBin().getName() + "-processor-%d")
                .build();
        WorkExecutor workExecutor = workExecutorFactory.newWorkExecutor(configuration.getMessageCapacity(), threadFactory);

        ActiveSubscription subscription = ActiveSubscription.start(configuration, client, workExecutor, exceptionResolver, timingEventSupport);
        activeSubscriptions.add(subscription);
    }

	/**
	 * Cancels all currently active subscriptions.
	 */
	public void cancelAll() {
        activeSubscriptions.forEach(ActiveSubscription::cancel);
    }

	public void addTimingEventListener(TimingEventListener timingEventListener) {
		timingEventSupport.addListener(timingEventListener);
	}

	public void removeTimingEventListener(TimingEventListener timingEventListener) {
		timingEventSupport.removeListener(timingEventListener);
	}

	/**
	 * An active subscription that periodically checks for new messages, distributing the consumption using a {@link ThreadPoolWorkExecutor}.
	 *
	 * @param  the message type
	 */
	private static class ActiveSubscription {

		private final Logger logger;
		private final ScheduledExecutorService subscriptionScheduler;

		private final Client client;
		private final MessageBin messageBin;
		private final WorkExecutor workExecutor;
		private final RetryDelayGenerator retryDelayGenerator;
		private final MessageConsumer consumer;
		private final ExceptionResolver exceptionResolver;
		private final TimingEventSupport timingEventSupport;

		ActiveSubscription(
				ScheduledExecutorService subscriptionScheduler, Client client,
				MessageBin messageBin,
				WorkExecutor workExecutor,
				RetryDelayGenerator retryDelayGenerator,
				MessageConsumer consumer,
				ExceptionResolver exceptionResolver,
				TimingEventSupport timingEventSupport,
				Logger logger
		) {
			this.subscriptionScheduler = subscriptionScheduler;
			this.client = client;
			this.messageBin = messageBin;
			this.workExecutor = workExecutor;
			this.retryDelayGenerator = retryDelayGenerator;
			this.consumer = consumer;
			this.exceptionResolver = exceptionResolver;
			this.timingEventSupport = timingEventSupport;
			this.logger = logger;
		}

		static  ActiveSubscription start(
				SubscriptionConfiguration configuration,
				Client client,
				WorkExecutor processingPool,
				ExceptionResolver exceptionResolver,
				TimingEventSupport timingEventSupport
		) {
			ScheduledExecutorService subscriptionScheduler = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder()
					.setNameFormat(configuration.getMessageBin().getName() + "-subscription-%d")
					.build());

			Logger logger = LoggerFactory.getLogger(configuration.getMessageBin().getName());
			ActiveSubscription subscription = new ActiveSubscription<>(
					subscriptionScheduler,
					client,
					configuration.getMessageBin(),
					processingPool,
					configuration.getRetryDelayGenerator(),
					configuration.getMessageConsumer(),
					exceptionResolver,
					timingEventSupport,
					logger
			);

			subscriptionScheduler.scheduleAtFixedRate(() -> {
				try {
					subscription.check();
				} catch (InterruptedException e) {
					// Do nothing
				} catch (Exception e) {
					logger.error("Exception occurred while checking a subscription", e);
				} catch (Error e) {
					logger.error("Error occurred while checking a subscription", e);
					throw e;
				}
			}, SUBSCRIPTION_INITIAL_DELAY, SUBSCRIPTION_PERIODIC_DELAY, TimeUnit.MILLISECONDS);
			return subscription;
		}

		void cancel() {
			logger.info("Cancelling subscription to '{}'", messageBin.getName());
			subscriptionScheduler.shutdownNow();
			workExecutor.shutdownNow();
		}

		private void check() throws InterruptedException {
			workExecutor.submitWorkOrders(capacity -> {
				Collection> envelopes = client.retrieveMessages(messageBin, capacity);
				return envelopes.stream()
						.map(this::newWorkOrder)
						.collect(Collectors.toList());
			});
		}

		private ThreadPoolWorkExecutor.WorkOrder newWorkOrder(IncomingEnvelope envelope) {
			return () -> {
				MessageContext messageContext = new MessageContext(envelope.getMessageId(), envelope.getReadCount(), envelope.getFirstReceived(), envelope.getRawMessage());
				MDC.put("queuebaccaMessageId", messageContext.getMessageId());
				MDC.put("queuebaccaMessageReadCount", String.valueOf(messageContext.getReadCount()));
				try {
					MessageResponse messageResponse;
					try {
						long start = System.currentTimeMillis();
						try {
							messageResponse = consumer.consume(envelope.getMessage(), messageContext);
						} finally {
							long duration = System.currentTimeMillis() - start;
							timingEventSupport.fireEvent(messageBin, envelope.getMessage().getClass(), envelope.getMessageId(), duration);
						}
					} catch (Exception e) {
						messageResponse = exceptionResolver.resolve(e, messageContext);
					}
					handleResponse(messageResponse, envelope);
				} catch (Error e) {
					logger.error("Error occurred while processing message: '{}' with body '{}'", new Object[]{ envelope.getMessageId(), envelope.getRawMessage(), e });
					throw e;
				} finally {
                    MDC.clear();
				}
			};
		}

		private void handleResponse(MessageResponse messageResponse, IncomingEnvelope envelope) {
			switch (messageResponse) {
				case CONSUMED:
					logger.info("Consumed '{}'; disposing", envelope.getMessageId());
					client.disposeMessage(messageBin, envelope);
					break;
				case RETRY:
					int retryDelay = retryDelayGenerator.nextRetryDelay(envelope.getReadCount());
					logger.warn("Retrying Message '{}' with body {} after {} seconds", new Object[]{ envelope.getMessageId(), envelope.getRawMessage(), retryDelay });
					client.returnMessage(messageBin, envelope, retryDelay);
					break;
				case TERMINATE:
					logger.warn("Terminated '{}' with body {}; disposing", envelope.getMessageId(), envelope.getRawMessage());
					client.disposeMessage(messageBin, envelope);
					break;
				default:
					logger.error("Unknown exception resolution, {}, for '{}'  with body {}; disposing", new Object[]{messageResponse, envelope.getMessageId(), envelope.getRawMessage() });
					client.disposeMessage(messageBin, envelope);
					break;
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy