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

org.occurrent.subscription.blocking.competingconsumers.CompetingConsumerSubscriptionModel Maven / Gradle / Ivy

The newest version!
package org.occurrent.subscription.blocking.competingconsumers;

import io.cloudevents.CloudEvent;
import jakarta.annotation.PreDestroy;
import org.occurrent.subscription.StartAt;
import org.occurrent.subscription.StartAt.SubscriptionModelContext;
import org.occurrent.subscription.SubscriptionFilter;
import org.occurrent.subscription.api.blocking.*;
import org.occurrent.subscription.api.blocking.CompetingConsumerStrategy.CompetingConsumerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;
import static java.util.function.Predicate.not;

/**
 * A competing consumer subscription model wraps another subscription model to allow several subscribers to subscribe to the same subscription. One of the subscribes will get a lock of the subscription
 * and receive events from it. If a subscriber looses its lock, another subscriber will take over automatically. To achieve distributed locking, the subscription model uses a {@link CompetingConsumerStrategy} to
 * support different algorithms. You can write custom algorithms by implementing this interface yourself. Here's an example of how to create and use the {@link CompetingConsumerSubscriptionModel}. This example
 * uses the {@code NativeMongoLeaseCompetingConsumerStrategy} from module {@code org.occurrent:subscription-mongodb-native-blocking-competing-consumer-strategy}.
 * It also wraps the DurableSubscriptionModel which in turn wraps the
 * Native MongoDB subscription model.
 * 
*
*
 * MongoDatabase mongoDatabase = mongoClient.getDatabase("some-database");
 * SubscriptionPositionStorage positionStorage = NativeMongoSubscriptionPositionStorage(mongoDatabase, "position-storage");
 * SubscriptionModel wrappedSubscriptionModel = new DurableSubscriptionModel(new NativeMongoSubscriptionModel(mongoDatabase, "events", TimeRepresentation.DATE), positionStorage);
 *
 * // Create the CompetingConsumerSubscriptionModel
 * NativeMongoLeaseCompetingConsumerStrategy competingConsumerStrategy = NativeMongoLeaseCompetingConsumerStrategy.withDefaults(mongoDatabase);
 * CompetingConsumerSubscriptionModel competingConsumerSubscriptionModel = new CompetingConsumerSubscriptionModel(wrappedSubscriptionModel, competingConsumerStrategy);
 *
 * // Now subscribe!
 * competingConsumerSubscriptionModel.subscribe("subscriptionId", type("SomeEvent"));
 * 
*

* If the above code is executed on multiple nodes/processes, then only one subscriber will receive events. */ public class CompetingConsumerSubscriptionModel implements DelegatingSubscriptionModel, SubscriptionModel, SubscriptionModelLifeCycle, CompetingConsumerListener { private static final Logger log = LoggerFactory.getLogger(CompetingConsumerSubscriptionModel.class); private final SubscriptionModel delegate; private final CompetingConsumerStrategy competingConsumerStrategy; private final ConcurrentMap competingConsumers = new ConcurrentHashMap<>(); // A set that hold which subscriptions whose StartAt position have indicated that they should not use the competing consumer model private final Set nonCompetingConsumersSubscriptions = Collections.newSetFromMap(new ConcurrentHashMap<>()); public CompetingConsumerSubscriptionModel(SubscriptionModel subscriptionModel, CompetingConsumerStrategy strategy) { requireNonNull(subscriptionModel, "Subscription model cannot be null"); requireNonNull(subscriptionModel, CompetingConsumerStrategy.class.getSimpleName() + " cannot be null"); this.delegate = subscriptionModel; this.competingConsumerStrategy = strategy; this.competingConsumerStrategy.addListener(this); } /** * Start listening to cloud events persisted to the event store using the supplied start position and filter. * * @param subscriberId The unique if of the subscriber * @param subscriptionId The id of the subscription, must be unique! * @param filter The filter to use to limit which events that are of interest from the EventStore. * @param startAt The position to start the subscription from * @param action This action will be invoked for each cloud event that is stored in the EventStore. */ public Subscription subscribe(String subscriberId, String subscriptionId, SubscriptionFilter filter, StartAt startAt, Consumer action) { Objects.requireNonNull(subscriberId, "SubscriberId cannot be null"); Objects.requireNonNull(subscriptionId, "SubscriptionId cannot be null"); final Subscription subscription; if (startAt.get(new SubscriptionModelContext(CompetingConsumerSubscriptionModel.class)) == null) { nonCompetingConsumersSubscriptions.add(subscriptionId); // We're not allowed to start the competing consumer subscription, just delegate to parent. // One reason for this might be if we're starting a non-durable in-memory subscription on multiple nodes. // Then typically you want all nodes to receive all events, and thus there's no need for a CompetingConsumerSubscription. subscription = getDelegatedSubscriptionModel().subscribe(subscriptionId, filter, startAt, action); } else { subscription = startCompetingConsumerSubscription(subscriberId, subscriptionId, filter, startAt, action); } return subscription; } /** * @see SubscriptionModel#subscribe(String, SubscriptionFilter, StartAt, Consumer) */ @Override public Subscription subscribe(String subscriptionId, SubscriptionFilter filter, StartAt startAt, Consumer action) { return subscribe(UUID.randomUUID().toString(), subscriptionId, filter, startAt, action); } /** * @see SubscriptionModelLifeCycle#cancelSubscription(String) */ @Override public synchronized void cancelSubscription(String subscriptionId) { logDebug("Cancelling CompetingConsumer subscription (subscriptionId={})", subscriptionId); delegate.cancelSubscription(subscriptionId); findFirstCompetingConsumerMatching(cc -> cc.hasSubscriptionId(subscriptionId)) .ifPresent(cc -> unregisterCompetingConsumer(cc, __ -> competingConsumers.remove(cc.subscriptionIdAndSubscriberId))); } /** * @see SubscriptionModelLifeCycle#stop() */ @Override public synchronized void stop() { logDebug("Stopping CompetingConsumer subscription model"); if (!isRunning()) { return; } delegate.stop(); unregisterAllCompetingConsumers(cc -> { logDebug("Stopped CompetingConsumer subscription (subscriberId={}, subscriptionId={})", cc.getSubscriberId(), cc.getSubscriptionId()); competingConsumers.put(cc.subscriptionIdAndSubscriberId, cc.registerPaused()); }); } /** * @see SubscriptionModelLifeCycle#start() */ @Override public synchronized void start(boolean resumeSubscriptionsAutomatically) { logDebug("Starting CompetingConsumer subscription model"); if (isRunning()) { throw new IllegalStateException(CompetingConsumerSubscriptionModel.class.getSimpleName() + " is already started"); } if (!nonCompetingConsumersSubscriptions.isEmpty()) { delegate.start(false); // This will automatically start all paused subscriptions (including those in nonCompetingConsumersSubscriptions) nonCompetingConsumersSubscriptions.forEach(delegate::resumeSubscription); } if (!resumeSubscriptionsAutomatically) { // Note that we deliberately don't start the delegated subscription model here!! // This is because we're not sure that we have the lock. The underlying SM will be started // automatically if required (since it's instructed to do so in the Waiting state supplier). competingConsumers.values().stream() .filter(not(CompetingConsumer::isRunning)) .forEach(cc -> { logDebug("Starting CompetingConsumer subscription (subscriberId={}, subscriptionId={}, state={})", cc.getSubscriberId(), cc.getSubscriptionId(), cc.state.getClass().getSimpleName()); // Only change state if we have permission to consume if (cc.isWaiting()) { // Registering a competing consumer will start the subscription automatically if lock was acquired competingConsumerStrategy.registerCompetingConsumer(cc.getSubscriptionId(), cc.getSubscriberId()); } else if (cc.isPaused()) { resumeSubscription(cc.getSubscriptionId()); } } ); } } /** * @see SubscriptionModelLifeCycle#isRunning() */ @Override public boolean isRunning() { return delegate.isRunning(); } /** * @see SubscriptionModelLifeCycle#isRunning(String) */ @Override public boolean isRunning(String subscriptionId) { return delegate.isRunning(subscriptionId); } /** * @see SubscriptionModelLifeCycle#isPaused(String) */ @Override public boolean isPaused(String subscriptionId) { return delegate.isPaused(subscriptionId); } /** * @see SubscriptionModelLifeCycle#resumeSubscription(String) */ @Override public synchronized Subscription resumeSubscription(String subscriptionId) { logDebug("Trying to resume CompetingConsumer subscription (subscriptionId={})", subscriptionId); if (isRunning(subscriptionId)) { throw new IllegalArgumentException("Subscription " + subscriptionId + " is not paused"); } if (nonCompetingConsumersSubscriptions.contains(subscriptionId)) { return delegate.resumeSubscription(subscriptionId); } return findFirstCompetingConsumerMatching(competingConsumer -> competingConsumer.hasSubscriptionId(subscriptionId)) .map(competingConsumer -> { final Subscription subscription; String subscriberId = competingConsumer.getSubscriberId(); boolean hasLock = hasLock(subscriptionId, subscriberId); logDebug("Resuming CompetingConsumer (subscriberId={}, subscriptionId={}, state={}, hasLock={})", subscriberId, subscriptionId, competingConsumer.state.getClass().getSimpleName(), hasLock); if (hasLock) { if (competingConsumer.isWaiting()) { subscription = startWaitingConsumer(competingConsumer); } else { competingConsumers.put(competingConsumer.subscriptionIdAndSubscriberId, competingConsumer.registerRunning()); // This works because we've checked that it's already paused earlier subscription = delegate.resumeSubscription(subscriptionId); } } else if (registerCompetingConsumer(subscriptionId, subscriberId) && !competingConsumer.isWaiting()) { // This works because we've checked that it's already paused earlier competingConsumers.put(competingConsumer.subscriptionIdAndSubscriberId, competingConsumer.registerRunning()); subscription = delegate.resumeSubscription(subscriptionId); } else { // We're not allowed to resume since we don't have the lock. subscription = new CompetingConsumerSubscription(subscriptionId, subscriberId); } return subscription; }) .orElseThrow(() -> new IllegalStateException("Cannot resume subscription " + subscriptionId + " since another consumer currently subscribes to it.")); } /** * @see SubscriptionModelLifeCycle#pauseSubscription(String) */ @Override public synchronized void pauseSubscription(String subscriptionId) { pauseSubscription(subscriptionId, true); } private CompetingConsumerSubscription startCompetingConsumerSubscription(String subscriberId, String subscriptionId, SubscriptionFilter filter, StartAt startAt, Consumer action) { logDebug("Starting CompetingConsumer subscription (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); SubscriptionIdAndSubscriberId subscriptionIdAndSubscriberId = SubscriptionIdAndSubscriberId.from(subscriptionId, subscriberId); final CompetingConsumerSubscription competingConsumerSubscription; if (competingConsumerStrategy.registerCompetingConsumer(subscriptionId, subscriberId)) { logDebug("Successfully registered CompetingConsumer subscription (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); Subscription subscription = delegate.subscribe(subscriptionId, filter, startAt, action); competingConsumerSubscription = new CompetingConsumerSubscription(subscriptionId, subscriberId, subscription); competingConsumers.put(subscriptionIdAndSubscriberId, new CompetingConsumer(subscriptionIdAndSubscriberId, new CompetingConsumerState.Running())); } else { logDebug("CompetingConsumer already registered, overriding to Waiting (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); competingConsumers.put(subscriptionIdAndSubscriberId, new CompetingConsumer(subscriptionIdAndSubscriberId, new CompetingConsumerState.Waiting(() -> { logDebug("Starting delegated CompetingConsumer subscription after waiting (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); if (!delegate.isRunning()) { delegate.start(); } return delegate.subscribe(subscriptionId, filter, startAt, action); }))); competingConsumerSubscription = new CompetingConsumerSubscription(subscriptionId, subscriberId); } return competingConsumerSubscription; } private synchronized void pauseSubscription(String subscriptionId, boolean pausedByUser) { logDebug("Trying to pause CompetingConsumer subscription (subscriptionId={}, pausedByUser={})", subscriptionId, pausedByUser); if (isPaused(subscriptionId)) { throw new IllegalArgumentException("Subscription " + subscriptionId + " is already paused"); } if (nonCompetingConsumersSubscriptions.contains(subscriptionId)) { delegate.pauseSubscription(subscriptionId); } else { CompetingConsumer competingConsumer = findFirstCompetingConsumerMatching(cc -> cc.hasSubscriptionId(subscriptionId)).orElse(null); if (competingConsumer == null) { logDebug("Failed to find CompetingConsumer for subscription (subscriptionId={}, pausedByUser={})", subscriptionId, pausedByUser); } else if (competingConsumer.isWaiting()) { logDebug("CompetingConsumer in waiting state, will ignore (subscriptionId={}, subscriberId={}, pausedByUser={})", subscriptionId, competingConsumer.getSubscriberId(), pausedByUser); } else { delegate.pauseSubscription(subscriptionId); competingConsumers.put(SubscriptionIdAndSubscriberId.from(competingConsumer), competingConsumer.registerPaused(pausedByUser)); if (pausedByUser) { logDebug("Will unregister competing consumer because subscription was paused explicitly by user (subscriptionId={}, subscriberId={})", subscriptionId, competingConsumer.getSubscriberId()); // If subscription is paused by user explicitly, then the user needs to resume it again explicitly to start it. // In these cases, we unregister the competing consumer. This means that it cannot be leader again until the subscription is // resumed explicitly (it'll re-register the competing consumers). competingConsumerStrategy.unregisterCompetingConsumer(competingConsumer.getSubscriptionId(), competingConsumer.getSubscriberId()); } else { logDebug("Will release competing consumer because subscription was paused by system (subscriptionId={}, subscriberId={})", subscriptionId, competingConsumer.getSubscriberId()); // Subscription was not paused by the user, thus we just "release" the competing consumer so that it can re-gain leader status // later without explicitly resuming the subscription. competingConsumerStrategy.releaseCompetingConsumer(competingConsumer.getSubscriptionId(), competingConsumer.getSubscriberId()); } } } } /** * @see DelegatingSubscriptionModel#getDelegatedSubscriptionModel() */ @Override public SubscriptionModel getDelegatedSubscriptionModel() { return delegate; } /** * @see SubscriptionModelLifeCycle#shutdown() */ @PreDestroy @Override public synchronized void shutdown() { logDebug("Trying to shutdown CompetingConsumer subscription model"); delegate.shutdown(); nonCompetingConsumersSubscriptions.clear(); unregisterAllCompetingConsumers(cc -> competingConsumers.remove(cc.subscriptionIdAndSubscriberId)); competingConsumerStrategy.removeListener(this); competingConsumerStrategy.shutdown(); } @Override public synchronized void onConsumeGranted(String subscriptionId, String subscriberId) { logDebug("Consumption granted to CompetingConsumer (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); CompetingConsumer competingConsumer = competingConsumers.get(SubscriptionIdAndSubscriberId.from(subscriptionId, subscriberId)); if (competingConsumer == null) { logDebug("Failed to find CompetingConsumer, returning (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); return; } if (competingConsumer.isWaiting()) { startWaitingConsumer(competingConsumer); } else if (competingConsumer.isPaused()) { CompetingConsumerState.Paused state = (CompetingConsumerState.Paused) competingConsumer.state; if (state.pausedByUser) { logDebug("Won't resume CompetingConsumer, because it was paused by user (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); } else { resumeSubscription(subscriptionId); } } } @Override public synchronized void onConsumeProhibited(String subscriptionId, String subscriberId) { logDebug("Consumption prohibited for CompetingConsumer (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); SubscriptionIdAndSubscriberId subscriptionIdAndSubscriberId = SubscriptionIdAndSubscriberId.from(subscriptionId, subscriberId); CompetingConsumer competingConsumer = competingConsumers.get(subscriptionIdAndSubscriberId); if (competingConsumer == null) { logDebug("CompetingConsumer couldn't be found when calling onConsumeProhibited (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); return; } if (competingConsumer.isRunning()) { logDebug("CompetingConsumer is running, will pause subscription and consumers (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); pauseSubscription(subscriptionId, false); pauseConsumer(competingConsumer, false); } else if (competingConsumer.isPaused()) { logDebug("CompetingConsumer is paused (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); CompetingConsumerState.Paused paused = (CompetingConsumerState.Paused) competingConsumer.state; pauseConsumer(competingConsumer, paused.pausedByUser); } else { logDebug("CompetingConsumer is neither running nor paused, won't do anything (subscriberId={}, subscriptionId={}, state={})", subscriberId, subscriptionId, competingConsumer.state.getClass().getSimpleName()); } } private Subscription startWaitingConsumer(CompetingConsumer cc) { logDebug("Start CompetingConsumer that has previously been waiting (subscriberId={}, subscriptionId={})", cc.getSubscriberId(), cc.getSubscriptionId()); String subscriptionId = cc.getSubscriptionId(); competingConsumers.put(SubscriptionIdAndSubscriberId.from(subscriptionId, cc.getSubscriberId()), cc.registerRunning()); return ((CompetingConsumerState.Waiting) cc.state).startSubscription(); } private void pauseConsumer(CompetingConsumer cc, boolean pausedByUser) { logDebug("Pausing CompetingConsumer (subscriberId={}, subscriptionId={}, pausedByUser={})", cc.getSubscriberId(), cc.getSubscriptionId(), pausedByUser); SubscriptionIdAndSubscriberId subscriptionIdAndSubscriberId = SubscriptionIdAndSubscriberId.from(cc.getSubscriptionId(), cc.getSubscriberId()); competingConsumers.put(subscriptionIdAndSubscriberId, cc.registerPaused(pausedByUser)); } private record SubscriptionIdAndSubscriberId(String subscriptionId, String subscriberId) { private static SubscriptionIdAndSubscriberId from(String subscriptionId, String subscriberId) { return new SubscriptionIdAndSubscriberId(subscriptionId, subscriberId); } private static SubscriptionIdAndSubscriberId from(CompetingConsumer cc) { return from(cc.getSubscriptionId(), cc.getSubscriberId()); } } private record CompetingConsumer(SubscriptionIdAndSubscriberId subscriptionIdAndSubscriberId, CompetingConsumerState state) { boolean hasId(String subscriptionId, String subscriberId) { return hasSubscriptionId(subscriptionId) && Objects.equals(getSubscriberId(), subscriberId); } boolean hasSubscriptionId(String subscriptionId) { return Objects.equals(getSubscriptionId(), subscriptionId); } boolean isPaused() { return state instanceof CompetingConsumerState.Paused; } boolean isRunning() { return state instanceof CompetingConsumerState.Running; } boolean isWaiting() { return state instanceof CompetingConsumerState.Waiting; } boolean isPausedFor(String subscriptionId) { return isPaused() && hasSubscriptionId(subscriptionId); } String getSubscriptionId() { return subscriptionIdAndSubscriberId.subscriptionId; } String getSubscriberId() { return subscriptionIdAndSubscriberId.subscriberId; } CompetingConsumer registerRunning() { return new CompetingConsumer(subscriptionIdAndSubscriberId, new CompetingConsumerState.Running()); } CompetingConsumer registerPaused() { return registerPaused(state.hasPermissionToConsume()); } CompetingConsumer registerPaused(boolean pausedByUser) { return new CompetingConsumer(subscriptionIdAndSubscriberId, new CompetingConsumerState.Paused(pausedByUser)); } } private sealed interface CompetingConsumerState { boolean hasPermissionToConsume(); final class Running implements CompetingConsumerState { @Override public boolean hasPermissionToConsume() { return true; } } final class Waiting implements CompetingConsumerState { private final Supplier supplier; Waiting(Supplier supplier) { this.supplier = supplier; } @Override public boolean hasPermissionToConsume() { return false; } private Subscription startSubscription() { return supplier.get(); } } final class Paused implements CompetingConsumerState { private final boolean pausedByUser; Paused(boolean pausedByUser) { this.pausedByUser = pausedByUser; } @Override public boolean hasPermissionToConsume() { return pausedByUser; } } } private void unregisterAllCompetingConsumers(Consumer andDo) { logDebug("Unregistering all CompetingConsumer's"); unregisterCompetingConsumersMatching(CompetingConsumer::isRunning, andDo); } private void unregisterCompetingConsumersMatching(Predicate predicate, Consumer and) { competingConsumers.values().stream().filter(predicate).forEach(cc -> unregisterCompetingConsumer(cc, and)); } private synchronized void unregisterCompetingConsumer(CompetingConsumer cc, Consumer and) { logDebug("Unregistering CompetingConsumer (subscriberId={}, subscriptionId={})", cc.getSubscriberId(), cc.getSubscriptionId()); and.accept(cc); competingConsumerStrategy.unregisterCompetingConsumer(cc.getSubscriptionId(), cc.getSubscriberId()); } private boolean registerCompetingConsumer(String subscriptionId, String subscriberId) { logDebug("Registering CompetingConsumer (subscriberId={}, subscriptionId={})", subscriberId, subscriptionId); return competingConsumerStrategy.registerCompetingConsumer(subscriptionId, subscriberId); } private boolean hasLock(String subscriptionId, String subscriberId) { return competingConsumerStrategy.hasLock(subscriptionId, subscriberId); } private Optional findFirstCompetingConsumerMatching(Predicate predicate) { return findCompetingConsumersMatching(predicate).findFirst(); } private Stream findCompetingConsumersMatching(Predicate predicate) { return competingConsumers.values().stream().filter(predicate); } private static void logDebug(String message, Object... params) { if (log.isDebugEnabled()) { log.debug(message, params); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy