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

dk.cloudcreate.essentials.components.eventsourced.aggregates.decider.CommandHandler Maven / Gradle / Ivy

There is a newer version: 0.40.19
Show newest version
/*
 * Copyright 2021-2024 the original author or 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
 *
 *      https://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 dk.cloudcreate.essentials.components.eventsourced.aggregates.decider;

import dk.cloudcreate.essentials.components.eventsourced.aggregates.snapshot.AggregateSnapshotRepository;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.eventstream.AggregateType;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.persistence.AggregateEventStreamConfiguration;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.types.EventOrder;
import dk.cloudcreate.essentials.components.foundation.transaction.*;
import dk.cloudcreate.essentials.shared.functional.tuple.Pair;
import dk.cloudcreate.essentials.types.LongRange;
import org.slf4j.*;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import static dk.cloudcreate.essentials.shared.FailFast.requireNonNull;
import static dk.cloudcreate.essentials.shared.MessageFormatter.msg;

/**
 * Command Handler which is responsible for loading any existing Aggregate STATE from the underlying {@link EventStore}
 * (see {@link #deciderBasedCommandHandler(ConfigurableEventStore, AggregateType, Class, AggregateIdResolver, AggregateIdResolver, AggregateSnapshotRepository, Class, Decider)})
 * and coordinate persisting any changes to the Aggregate, in the form of EVENT's, to the {@link EventStore} as part of an active {@link UnitOfWork} (if one exists)
* The actual logic is delegated to an instance of a {@link Decider} * * @param The type of Commands that the {@link Decider#handle(Object, Object)} can process * @param The type of Events that can be returned by {@link Decider#handle(Object, Object)} and applied in the {@link StateEvolver#applyEvent(Object, Object)} * @param The type of Error that can be returned by the {@link Decider#handle(Object, Object)} method */ @FunctionalInterface public interface CommandHandler { /** * The execute method is responsible for handling a COMMAND, which can either result * in an ERROR or a list of EVENT's.
* Note: This method is called decide in the decider pattern
* Idempotent handling of a COMMAND will result in an empty list of EVENT's * * @param cmd the command to handle * @return either an ERROR or a list of EVENT's.
* Idempotent handling of a COMMAND will result in an empty list of EVENT's */ HandlerResult handle(COMMAND cmd); /** * Create an instance of a {@link CommandHandler} that is responsible for loading any existing Aggregate STATE from the underlying {@link EventStore} * and coordinate persisting any changes to the Aggregate, in the form of EVENT's, to the {@link EventStore} as part of an active {@link UnitOfWork} (if one exists)
* The actual logic is delegated to an instance of a {@link Decider} * * @param eventStore the {@link EventStore} that provides persistence support for aggregate events * @param aggregateType the aggregate type that this command handler can support COMMAND's related to and which the {@link Decider} supports EVENT's related to * @param aggregateIdType the type of aggregate id that is associated with the {@link AggregateType} * @param aggregateIdFromCommandResolver resolver that can resolve the aggregate-id from a COMMAND object instance * @param aggregateIdFromEventResolver resolver that can resolve the aggregate-id from an EVENT object instance * @param aggregateSnapshotRepository optional {@link AggregateSnapshotRepository} for storing snapshots of the aggregate STATE for faster loading * @param stateType The type of aggregate STATE that the {@link Decider} instance works with * @param decider the {@link Decider} instance responsible for Aggregate logic * @param The type of {@link AggregateEventStreamConfiguration} that the {@link EventStore} provided supports * @param the type of aggregate id that is associated with the {@link AggregateType} * @param the type of COMMAND that the {@link Decider} instance supports * @param the type of EVENT that the {@link Decider} instance supports * @param the type of ERROR that the {@link Decider} instance supports * @param the type of Aggregate STATE that the {@link Decider} instance supports * @return a {@link CommandHandler} that is responsible for loading any existing Aggregate STATE from the underlying {@link EventStore} * and coordinate persisting any changes to the Aggregate, in the form of EVENT's, to the {@link EventStore} as part of an active {@link UnitOfWork} (if one exists)
* The actual logic is delegated to an instance of a {@link Decider} */ static CommandHandler deciderBasedCommandHandler(ConfigurableEventStore eventStore, AggregateType aggregateType, Class aggregateIdType, AggregateIdResolver aggregateIdFromCommandResolver, AggregateIdResolver aggregateIdFromEventResolver, AggregateSnapshotRepository aggregateSnapshotRepository, Class stateType, Decider decider) { requireNonNull(eventStore, "No eventStore provided"); requireNonNull(aggregateType, "No aggregateType provided"); requireNonNull(aggregateIdType, "No aggregateIdType provided"); requireNonNull(aggregateIdFromCommandResolver, "No aggregateIdFromCommandResolver provided"); requireNonNull(aggregateIdFromEventResolver, "No aggregateIdFromEventResolver provided"); requireNonNull(stateType, "No stateType provided"); requireNonNull(decider, "No decider provided"); if (eventStore.findAggregateEventStreamConfiguration(aggregateType).isEmpty()) { eventStore.addAggregateEventStreamConfiguration(aggregateType, aggregateIdType); } var optionalAggregateSnapshotRepository = Optional.ofNullable(aggregateSnapshotRepository); return new CommandHandler() { private static final Logger log = LoggerFactory.getLogger(CommandHandler.class); @Override public HandlerResult handle(COMMAND cmd) { var optionalAggregateId = aggregateIdFromCommandResolver.resolveFrom(cmd); var eventOrderOfLastRehydratedEvent = new AtomicReference(EventOrder.NO_EVENTS_PREVIOUSLY_PERSISTED); STATE finalState = optionalAggregateId.map(aggregateId -> { // Check for aggregate snapshot var possibleAggregateSnapshot = optionalAggregateSnapshotRepository.flatMap(repository -> repository.loadSnapshot(aggregateType, optionalAggregateId.get(), stateType)); var initialStateAndEventStream = possibleAggregateSnapshot.map(aggregateSnapshot -> { log.trace("[{}] Preparing to handle command '{}' with associated aggregateId '{}' using '{}' snapshot with eventOrderOfLastIncludedEvent {}", aggregateType, cmd.getClass().getName(), aggregateId, stateType.getName(), aggregateSnapshot.eventOrderOfLastIncludedEvent); var eventStream = eventStore.fetchStream(aggregateType, optionalAggregateId.get(), LongRange.from(aggregateSnapshot.eventOrderOfLastIncludedEvent.increment().longValue())); return Pair.of(aggregateSnapshot.aggregateSnapshot, eventStream); }).orElseGet(() -> { log.trace("[{}] Preparing to handle command '{}' with associated aggregateId '{}' and not using a snapshot", aggregateType, cmd.getClass().getName(), aggregateId); return Pair.of(decider.initialState(), eventStore.fetchStream(aggregateType, optionalAggregateId.get())); }); var state = initialStateAndEventStream._1; var applyEvents = initialStateAndEventStream._2.isPresent(); if (applyEvents) { log.trace("[{}] ApplyEvents: Preparing state '{}' to handle command '{}', associated aggregateId '{}'", aggregateType, stateType.getName(), cmd.getClass().getName(), aggregateId); state = initialStateAndEventStream._2.get() .events() .reduce(initialStateAndEventStream._1, (deltaState, event) -> { eventOrderOfLastRehydratedEvent.set(event.eventOrder()); return decider.applyEvent(event.event().deserialize(), deltaState); }, (deltaState, deltaState2) -> deltaState2); } return state; }).orElseGet(() -> { log.trace("[{}] No aggregate-id resolved. Preparing initial-state '{}' to handle command '{}'", aggregateType, stateType.getName(), cmd.getClass().getName()); return decider.initialState(); }); log.debug("[{}] Handling command '{}' using state '{}'", aggregateType, cmd.getClass().getName(), stateType.getName()); var result = decider.handle(cmd, finalState); if (result.isSuccess()) { var events = result.asSuccess().events(); if (!events.isEmpty()) { log.debug("[{}] Successfully handled command '{}' against state '{}' associated with aggregateId '{}'. Resulted in {} events of type {}", aggregateType, cmd.getClass().getName(), stateType.getName(), optionalAggregateId, events.size(), events.stream().map(event -> event.getClass().getSimpleName()).toList()); var firstEvent = events.get(0); var aggregateId = optionalAggregateId.orElseGet(() -> { var resolvesAggregateIdFromFirstEvent = aggregateIdFromEventResolver.resolveFrom(firstEvent) .orElseThrow(() -> new IllegalStateException(msg("First event didn't an aggregateId. First Event type: '{}'", firstEvent.getClass().getName()))); log.debug("[{}] Resolved aggregateId '{}' from first-event '{}'", aggregateType, resolvesAggregateIdFromFirstEvent, firstEvent.getClass().getName()); return resolvesAggregateIdFromFirstEvent; }); eventStore.getUnitOfWorkFactory().getCurrentUnitOfWork() .ifPresentOrElse(eventStoreUnitOfWork -> { log.debug("[{}] Registering UnitOfWorkCallback to persist {} events associated with '{}' with aggregateId '{}'", aggregateType, events.size(), stateType.getName(), aggregateId); eventStoreUnitOfWork.registerLifecycleCallbackForResource(new EventsToAppendToStream(finalState, aggregateType, aggregateId, eventOrderOfLastRehydratedEvent.get(), events), new DeciderUnitOfWorkLifecycleCallback()); }, () -> { log.debug("[{}] !!! No active UnitOfWork so will NOT persist {} events associated withfi '{}' with aggregateId '{}'", aggregateType, events.size(), stateType.getName(), aggregateId); }); } else { log.debug("[{}] Successfully handled command '{}' against state '{}' associated with aggregateId '{}', but it didn't result in any events", aggregateType, cmd.getClass().getName(), stateType.getName(), optionalAggregateId); } } else { log.debug("[{}] Failed to handle command '{}' against '{}' with associated aggregateId '{}'. Resulted in error: {}", aggregateType, cmd.getClass().getName(), stateType.getName(), optionalAggregateId, result.asError().error()); eventStore.getUnitOfWorkFactory().getCurrentUnitOfWork() .ifPresent(UnitOfWork::markAsRollbackOnly); } return result; } class DeciderUnitOfWorkLifecycleCallback implements UnitOfWorkLifecycleCallback> { @Override public void beforeCommit(UnitOfWork unitOfWork, List> associatedResources) { log.trace("[{}] beforeCommit processing {} '{}' registered with the UnitOfWork being committed", aggregateType, associatedResources.size(), stateType.getName()); associatedResources.forEach(eventsToAppendToStream -> { log.trace("[{}] beforeCommit processing '{}' with id '{}'", aggregateType, stateType.getName(), eventsToAppendToStream.aggregateId()); if (log.isTraceEnabled()) { log.trace("[{}] Persisting {} event(s) related to '{}' with id '{}': {}", aggregateType, eventsToAppendToStream.events().size(), stateType.getName(), eventsToAppendToStream.aggregateId(), eventsToAppendToStream.events().stream() .map(persistableEvent -> persistableEvent.getClass().getName()) .reduce((s, s2) -> s + ", " + s2)); } else { log.debug("[{}] Persisting {} event(s) related to '{}' with id '{}'", aggregateType, eventsToAppendToStream.events().size(), stateType.getName(), eventsToAppendToStream.aggregateId()); } var persistedEvents = eventStore.appendToStream(aggregateType, eventsToAppendToStream.aggregateId(), eventsToAppendToStream.eventOrderOfLastRehydratedEvent(), eventsToAppendToStream.events()); optionalAggregateSnapshotRepository.ifPresent(repository -> repository.aggregateUpdated(eventsToAppendToStream.state(), persistedEvents)); }); } @Override public void afterCommit(UnitOfWork unitOfWork, List> associatedResources) { } @Override public void beforeRollback(UnitOfWork unitOfWork, List> associatedResources, Exception causeOfTheRollback) { } @Override public void afterRollback(UnitOfWork unitOfWork, List> associatedResources, Exception causeOfTheRollback) { } } record EventsToAppendToStream( STATE state, AggregateType aggregateType, ID aggregateId, EventOrder eventOrderOfLastRehydratedEvent, List events) { } }; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy