Class MultiTenantEventStorageEngine
- All Implemented Interfaces:
MultiTenantAwareComponent,DescribableComponent,EventStorageEngine
EventStorageEngine. Appends and sourcing are routed to the engine of the one tenant resolved
from the ProcessingContext, so each tenant's events live in its own store.
Like the other multi-tenant infrastructure components, this engine holds the tenant information, the registration of
tenants, and the routing to them. Snapshot writes are routed the same way by MultiTenantSnapshotStore, a
component of its own so that resolving an EventStorageEngine or a SnapshotStore by type stays
unambiguous.
The SourcingCondition is routed unchanged, so a snapshot sourcing strategy
reaches the tenant's own engine rather than being resolved above the fan-out, where no tenant is known yet. Keeping
it intact requires the application-wide snapshot composition to be switched off, which the configuration enhancer
registering this engine does.
Each tenant's engine is composed once with that tenant's snapshot store through
decorate, applying the same
rule the event sourcing defaults apply to a single-tenant engine. An engine that is its own snapshot store serves a
snapshot sourcing strategy within one call and is left untouched. Any other engine is decorated with that tenant's
snapshot store, resolving the snapshot first and sourcing the events following it. Both stay within one tenant.
A tenant-carrying processing context is required for appends and sourcing. When none is available, or the tenant cannot be resolved from it, the operation fails. An append without a context resolves its tenant from the events instead, which must then all belong to the same tenant.
As a MultiTenantAwareComponent this engine follows the
TenantProvider: a tenant added at runtime gets
its engine on first use, and a removed tenant's composed engine is evicted. Each such registration is announced to
the listeners subscribed through subscribe(TenantChangeListener), for a component that has to act on the
tenants this engine holds rather than on the tenants the provider knows. Re-registering an already registered tenant
is announced too, since that rebuilds the tenant's composed engine.
The read side (stream(org.axonframework.messaging.eventstreaming.StreamingCondition), firstToken(), latestToken(), tokenAt(java.time.Instant)) carries no context and
spans all current tenants. It merges the per-tenant streams, tags every event with its tenant, and positions the
merged stream with a MultiTenantTrackingToken holding one position per tenant. A tenant added while a
processor runs streams from its beginning once the processor re-opens the stream.
- Since:
- 5.3.0
- Author:
- Jakob Hatzl, Laura Devriendt
-
Nested Class Summary
Nested classes/interfaces inherited from interface org.axonframework.eventsourcing.eventstore.EventStorageEngine
EventStorageEngine.AppendTransaction<R> -
Constructor Summary
ConstructorsConstructorDescriptionMultiTenantEventStorageEngine(TenantEventStorageEngineFactory engineFactory, TenantSnapshotStoreFactory snapshotStoreFactory, TenantRouter tenantRouter) Constructs aMultiTenantEventStorageEngine. -
Method Summary
Modifier and TypeMethodDescriptionappendEvents(AppendCondition condition, @Nullable ProcessingContext context, List<TaggedEventMessage<?>> events) voiddescribeTo(ComponentDescriptor descriptor) Describe the properties ofthis DescribableComponentwith the givendescriptor.Returns a token holding no per-tenant position, so a processor resuming from it opens every tenant at the beginning of its own store.Creates aTrackingTokenthat is at the latest position of an event stream.registerAndStartTenant(TenantDescriptor tenantDescriptor) Registers the giventenantDescriptoras a known tenant with this multi-tenant aware component.registerTenant(TenantDescriptor tenantDescriptor) Registers the giventenantDescriptoras a known tenant with this multi-tenant aware component.source(SourcingCondition condition, @Nullable ProcessingContext context) Creates a finiteMessageStreamofeventsmatching the givencondition, carrying along the activeProcessingContext.stream(StreamingCondition condition) subscribe(TenantChangeListener listener) Subscribes the givenlistenerto the changes of the tenantsthisengine holds.tenants()Returns the tenants currently registered withthisengine.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface org.axonframework.eventsourcing.eventstore.EventStorageEngine
appendEvents, source
-
Constructor Details
-
MultiTenantEventStorageEngine
public MultiTenantEventStorageEngine(TenantEventStorageEngineFactory engineFactory, TenantSnapshotStoreFactory snapshotStoreFactory, TenantRouter tenantRouter) Constructs aMultiTenantEventStorageEngine.- Parameters:
engineFactory- the factory providing each tenant'sEventStorageEnginesnapshotStoreFactory- the factory providing each tenant'sSnapshotStoretenantRouter- the router deciding which tenant an operation is routed to
-
-
Method Details
-
appendEvents
public CompletableFuture<EventStorageEngine.AppendTransaction<?>> appendEvents(AppendCondition condition, @Nullable ProcessingContext context, List<TaggedEventMessage<?>> events) Description copied from interface:EventStorageEngineAppends aListofeventsto the underlying storage solution.Events will be appended in the order that they are offered in, validating the given
conditionbefore being stored. Note that all events should have a unique event identifier.Tagspaired with theeventswill be stored as well.Implementations may be able to detect conflicts during the append stage. In such case, the returned completable future will complete exceptionally, indicating such conflict. Other implementations may delay such checks until the
EventStorageEngine.AppendTransaction.commit()is called.Called during the
PREPARE_COMMITphase.- Specified by:
appendEventsin interfaceEventStorageEngine- Parameters:
condition- The condition describing the transactional requirements for the append transactioncontext- The currentProcessingContext, if any.events- TheListofeventsto append to the underlying storage solution.- Returns:
- A
transactioninstance that can be committed or rolled back.
-
source
public MessageStream<EventMessage> source(SourcingCondition condition, @Nullable ProcessingContext context) Description copied from interface:EventStorageEngineCreates a finiteMessageStreamofeventsmatching the givencondition, carrying along the activeProcessingContext.Behaves identically to
EventStorageEngine.source(SourcingCondition); thecontextis passed to decorators (for example snapshot-loading and tracing decorators) that need to correlate the sourcing operation with the surrounding unit of work.- Specified by:
sourcein interfaceEventStorageEngine- Parameters:
condition- theSourcingConditiondictating thestreamofeventsto sourcecontext- theProcessingContextactive while sourcing; may benull- Returns:
- a finite
MessageStreamofeventsmatching the givencondition
-
tenants
Returns the tenants currently registered withthisengine.- Returns:
- the tenants currently registered with
thisengine
-
subscribe
Subscribes the givenlistenerto the changes of the tenantsthisengine holds.A change is announced only once it is visible through
tenants(), which is what makes acting on one safe. A listener must therefore subscribe here rather than to theTenantProvider: the provider notifies its subscribers in turn, so a subscriber of the provider can observe this engine before it registered the tenant.Every call adds a listener of its own, so a listener subscribed twice is invoked twice for one change. Each returned registration removes what its own call added.
Internal, because the only listeners are the module's own components, and what they are told is a detail of how this engine holds its tenants.
Announcing happens on the thread applying the change, so a listener must return promptly and hand off any work of its own. A listener cancelled while an announcement is in flight may still be invoked for that announcement, so it has to tolerate running once more after its own cancellation.
- Parameters:
listener- the listener to invoke after every change to the tenantsthisengine holds- Returns:
- a registration whose cancellation stops the given
listenerfrom being invoked further - Throws:
NullPointerException- if the givenlistenerisnull
-
registerTenant
Description copied from interface:MultiTenantAwareComponentRegisters the giventenantDescriptoras a known tenant with this multi-tenant aware component.The caller must retain the returned
Registrationand cancel it when the tenant is removed, since releasing the component's per-tenant resources rides on that cancellation.- Specified by:
registerTenantin interfaceMultiTenantAwareComponent- Parameters:
tenantDescriptor- TheTenantDescriptorto register with this component.- Returns:
- A
Registrationused to deregister the giventenantDescriptor.
-
registerAndStartTenant
Description copied from interface:MultiTenantAwareComponentRegisters the giventenantDescriptoras a known tenant with this multi-tenant aware component. If applicable, this task will construct a tenant segment and start it.- Specified by:
registerAndStartTenantin interfaceMultiTenantAwareComponent- Parameters:
tenantDescriptor- TheTenantDescriptorto register with this component.- Returns:
- A
Registrationused to deregister the giventenantDescriptor.
-
stream
Description copied from interface:EventStorageEngineCreates an infiniteMessageStreamofeventsmatching the givencondition.The
conditionmay dictate theStreamingCondition.position()to start streaming from, as well as definefilter criteriafor the returnedMessageStream.- Specified by:
streamin interfaceEventStorageEngine- Parameters:
condition- TheStreamingConditiondictating theStreamingCondition.position()to start streaming from, as well as thefilter criteriaused for the returnedMessageStream.- Returns:
- An infinite
MessageStreamofeventsmatching the givencondition.
-
firstToken
Returns a token holding no per-tenant position, so a processor resuming from it opens every tenant at the beginning of its own store.The beginning of a tenant's store is not assumed to be the zero position.
Streamingfrom this token fills in each tenant's ownfirst tokenwhen it opens that tenant's stream, so a tenant whose early events were pruned still opens at its real first event. Naming those positions in this token instead would make it disagree with a token written before a tenant existed, and a streaming processor compares the two to recognize an event it already handled, so they are left out.- Specified by:
firstTokenin interfaceEventStorageEngine- Returns:
- A
CompletableFutureof aTrackingTokenat the first position of an event stream.
-
latestToken
Description copied from interface:EventStorageEngineCreates aTrackingTokenthat is at the latest position of an event stream.In other words, a token that tracks all new events from this point forward.
- Specified by:
latestTokenin interfaceEventStorageEngine- Returns:
- A
CompletableFutureof aTrackingTokenat the latest position of an event stream.
-
tokenAt
Description copied from interface:EventStorageEngineCreates aTrackingTokenthat tracks alleventsafter the givenat.If there is an event exactly at the given
at, it will be tracked too.- Specified by:
tokenAtin interfaceEventStorageEngine- Parameters:
at- TheInstantdetermining how theTrackingTokenshould be created. A tracking token should point to very first event before thisInstant.- Returns:
- A
CompletableFutureof aTrackingTokenat the givenat, if there aren't events matching this criterianullis returned
-
describeTo
Description copied from interface:DescribableComponentDescribe the properties ofthis DescribableComponentwith the givendescriptor.Components should call the appropriate
describePropertymethods on the descriptor to register their properties. The descriptor is responsible for determining how these properties are formatted and structured in the final output.Best Practices: As a general rule, all relevant fields of a
DescribableComponentimplementation should be described in this method. However, developers have discretion to include only the fields that make sense in the context. Not every field may be meaningful for description purposes, especially internal implementation details. Furthermore, components might want to expose different information based on their current state. The final decision on what properties to include lies with the person implementing thedescribeTomethod, who should focus on providing information that is useful for understanding the component's configuration and state.Example implementation:
public void describeTo(ComponentDescriptor descriptor) { descriptor.describeProperty("name", this.name); descriptor.describeProperty("enabled", this.enabled); descriptor.describeProperty("configuration", this.configuration); // A nested component descriptor.describeProperty("handlers", this.eventHandlers); // A collection }- Specified by:
describeToin interfaceDescribableComponent- Parameters:
descriptor- The component descriptor to describethis DescribableComponentn its properties in.
-