dk.cloudcreate.essentials.components.eventsourced.aggregates.decider.CommandHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eventsourced-aggregates Show documentation
Show all versions of eventsourced-aggregates Show documentation
This library focuses on providing different flavors of eventsourced aggregrates
/*
* 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