Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/reference-guide/modules/tuning/pages/snapshotting.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,32 @@ Only the events that occurred after that position are applied to bring the entit

If a snapshot cannot be used, the full event stream is replayed and a new snapshot may be created after sourcing completes, depending on the configured `SnapshotPolicy`.
This new snapshot replaces the previous one according to the current storage behavior.

=== Snapshot sourcing composition

An `EventStorageEngine` reads snapshots in one of two ways:

* An engine that supports the snapshot sourcing strategy natively serves the snapshot and the events following it in a single call.
* Any other engine is decorated with a `SnapshotCapableEventStorageEngine`, which loads the snapshot from the configured `SnapshotStore` first and then sources the events that follow it.

That decoration is applied by the `SnapshotSourcingConfigurationEnhancer`, which the event sourcing defaults register for you.
The engine is left as is when no `SnapshotStore` is configured, and when the engine is the configured `SnapshotStore` itself.

A storage topology that composes snapshot sourcing itself disables that enhancer and takes over the responsibility:

[source,java]
----
configurer.componentRegistry(
registry -> registry.disableEnhancer(SnapshotSourcingConfigurationEnhancer.class)
);
----

An engine routing sourcing to one engine per shard, tenant, or region is the case this exists for.
The target is only known once a message is inspected, so decorating the routing engine would read snapshots before then, and the engines it routes to would never receive the snapshot sourcing strategy.
Such a topology composes each of its engines with that engine's own snapshot store instead, using `SnapshotCapableEventStorageEngine.decorate(engine, snapshotStore)`.

[NOTE]
====
Disabling the enhancer without composing snapshot sourcing elsewhere leaves snapshots being written while never being read.
Sourcing then replays an entity's full event history, which is correct but forfeits the optimization.
====
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.eventsourcing.eventstore.EventStore;
import org.axonframework.eventsourcing.eventstore.InterceptingEventStore;
import org.axonframework.eventsourcing.eventstore.SnapshotCapableEventStorageEngine;
import org.axonframework.eventsourcing.eventstore.StorageEngineBackedEventStore;
import org.axonframework.eventsourcing.eventstore.TagResolver;
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine;
import org.axonframework.eventsourcing.snapshot.store.SnapshotStore;
import org.axonframework.messaging.core.MessageDispatchInterceptor;
import org.axonframework.messaging.core.configuration.MessagingConfigurationDefaults;
import org.axonframework.messaging.core.interception.DispatchInterceptorRegistry;
Expand All @@ -52,13 +50,13 @@
* </ul>
* Furthermore, this enhancer will decorate the:
* <ul>
* <li>The {@link EventStorageEngine} in a {@link SnapshotCapableEventStorageEngine} <b>if</b> a
* {@link SnapshotStore} is present and it is not the same instance as the engine. When the engine and the
* snapshot store are the same instance, the engine handles snapshots natively (potentially in a single
* round-trip) and wrapping it would degrade performance.</li>
* <li>The {@link EventStore} in a {@link InterceptingEventStore} <b>if</b> there are any
* {@link MessageDispatchInterceptor MessageDispatchInterceptors} present in the {@link DispatchInterceptorRegistry}.</li>
* </ul>
* It also registers the {@link SnapshotSourcingConfigurationEnhancer}, which decorates the
* {@link EventStorageEngine} to support the {@link org.axonframework.eventsourcing.eventstore.SourcingStrategy.Snapshot
* snapshot sourcing strategy}. Registering it here rather than through the {@link java.util.ServiceLoader} means
* applications registering {@code this} enhancer manually receive it as well.
*
* @author Steven van Beelen
* @author John Hendrikx
Expand All @@ -85,16 +83,7 @@ public void enhance(ComponentRegistry registry) {
.registerIfNotPresent(EventStorageEngine.class,
EventSourcingConfigurationDefaults::defaultEventStorageEngine)
.registerIfNotPresent(EventStore.class, EventSourcingConfigurationDefaults::simpleEventStore);
registry.registerDecorator(
EventStorageEngine.class,
SnapshotCapableEventStorageEngine.DECORATION_ORDER,
(config, name, engine) -> {
SnapshotStore snapshotStore = config.getOptionalComponent(SnapshotStore.class).orElse(null);
return snapshotStore == null || snapshotStore == engine
? engine
: new SnapshotCapableEventStorageEngine(engine, snapshotStore);
}
);
registry.registerEnhancer(new SnapshotSourcingConfigurationEnhancer());
registry.registerDecorator(
EventStore.class,
InterceptingEventStore.DECORATION_ORDER,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2010-2026. Axon Framework
*
* 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 org.axonframework.eventsourcing.configuration;

import org.axonframework.common.annotation.RegistrationScope;
import org.axonframework.common.configuration.ComponentRegistry;
import org.axonframework.common.configuration.ConfigurationEnhancer;
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.eventsourcing.eventstore.SnapshotCapableEventStorageEngine;
import org.axonframework.eventsourcing.eventstore.SourcingStrategy;
import org.axonframework.eventsourcing.snapshot.store.SnapshotStore;

/**
* A {@link ConfigurationEnhancer} making the {@link EventStorageEngine} support the
* {@link SourcingStrategy.Snapshot snapshot sourcing strategy}, by decorating it with the configured
* {@link SnapshotStore} through {@link SnapshotCapableEventStorageEngine#decorate(EventStorageEngine, SnapshotStore)}.
* <p>
* The engine is left as is when no {@code SnapshotStore} is configured, and when it is the configured
* {@code SnapshotStore} itself. The latter resolves the snapshot within its own {@code source} call, in a single round
* trip, which decorating would undo.
* <p>
* Registered by {@link EventSourcingConfigurationDefaults}, so applications registering those defaults manually receive
* it as well. It is deliberately not contributed through the {@link java.util.ServiceLoader}, unlike its sibling
* enhancers, to keep that single registration channel. Registered for the {@link RegistrationScope.Scope#CURRENT
* current} registry only, since the defaults registering it are copied into every module registry and would otherwise
* register it there a second time.
* <p>
* A storage topology composing snapshot sourcing itself disables this enhancer, taking over that responsibility:
* <pre>{@code
* configurer.componentRegistry(
* registry -> registry.disableEnhancer(SnapshotSourcingConfigurationEnhancer.class)
* );
* }</pre>
* An engine routing {@code source} to one engine per shard, tenant, or region is the case this exists for: the shard is
* only known once a message is inspected, so decorating the routing engine would resolve snapshots before then, and the
* engines it routes to would never receive the snapshot sourcing strategy. Such a topology composes each of its engines
* with that engine's own snapshot store instead, through the same
* {@link SnapshotCapableEventStorageEngine#decorate(EventStorageEngine, SnapshotStore) decorate} operation.
* <p>
* Disabling this enhancer without composing snapshot sourcing elsewhere leaves snapshots being written while never
* being read: sourcing then replays an entity's full event history.
*
* @author Laura Devriendt
* @since 5.3.0
*/
@RegistrationScope(scope = RegistrationScope.Scope.CURRENT)
public class SnapshotSourcingConfigurationEnhancer implements ConfigurationEnhancer {

/**
* The order of {@code this} enhancer compared to others, equal to 10 positions after
* {@link EventSourcingConfigurationDefaults} (thus,
* {@link EventSourcingConfigurationDefaults#ENHANCER_ORDER} + 10).
* <p>
* What this order governs is the window in which {@code this} enhancer can still be disabled: a disable is only
* recorded while the enhancer has not been invoked yet, and is otherwise reported as having no effect. Running late
* therefore leaves every earlier-ordered enhancer free to take over snapshot composition.
*/
public static final int ENHANCER_ORDER = EventSourcingConfigurationDefaults.ENHANCER_ORDER + 10;

@Override
public int order() {
return ENHANCER_ORDER;
}

@Override
public void enhance(ComponentRegistry registry) {
registry.registerDecorator(
EventStorageEngine.class,
SnapshotCapableEventStorageEngine.DECORATION_ORDER,
(config, name, engine) -> config
.getOptionalComponent(SnapshotStore.class)
.map(snapshotStore -> SnapshotCapableEventStorageEngine.decorate(engine, snapshotStore))
.orElse(engine)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,36 @@ public SnapshotCapableEventStorageEngine(EventStorageEngine delegate, SnapshotSt
this.snapshotStore = Objects.requireNonNull(snapshotStore, "The snapshotStore parameter cannot be null.");
}

/**
* Returns an {@link EventStorageEngine} that supports the {@link SourcingStrategy.Snapshot} sourcing strategy,
* given the {@code engine} to source events from and the {@code snapshotStore} holding its snapshots.
* <p>
* The given {@code engine} is returned as is when it is the given {@code snapshotStore} itself. Such an engine
* resolves the snapshot within its own {@link #source(SourcingCondition, ProcessingContext) source} call, serving
* the snapshot and the events following it in a single round trip. Decorating it would resolve the snapshot
* separately and pass an {@link SourcingStrategy.Absolute absolute strategy} inward, disabling that optimization.
* <p>
* An {@code engine} that is already decorated is returned as is too, so composing twice is harmless. It keeps
* resolving snapshots from the store it was decorated with, and the given {@code snapshotStore} is ignored for it.
* Decorating again would put the given store in front of that one instead of adding anything.
* <p>
* Any other {@code engine} is decorated, resolving the snapshot from the {@code snapshotStore} before sourcing the
* events that follow it.
*
* @param engine the engine to source events from
* @param snapshotStore the store holding the snapshots of the given {@code engine}
* @return an event storage engine supporting the snapshot sourcing strategy
* @throws NullPointerException if the given {@code engine} or {@code snapshotStore} is {@code null}
* @since 5.3.0
*/
public static EventStorageEngine decorate(EventStorageEngine engine, SnapshotStore snapshotStore) {
Objects.requireNonNull(engine, "The engine parameter cannot be null.");
Objects.requireNonNull(snapshotStore, "The snapshotStore parameter cannot be null.");
return engine == snapshotStore || engine instanceof SnapshotCapableEventStorageEngine
? engine
: new SnapshotCapableEventStorageEngine(engine, snapshotStore);
}

@Override
public MessageStream<EventMessage> source(SourcingCondition condition, @Nullable ProcessingContext context) {
if (condition.strategy() instanceof SourcingStrategy.Snapshot s) {
Expand Down
Loading