Configuration
Minimal configuration is needed to get extension up and running.
Static tenants configuration
If you know list of contexts that you want your application to connect in advanced configure them coma separated in application.properties
via following properties: axon.axonserver.contexts=tenant-context-1,tenant-context-2,tenant-context-3
Dynamic tenants configuration
If you don’t know tenants in advance and you plan to create them in runtime, you can define a predicate which will tell application to which contexts to connect to in runtime:
@Bean
public TenantConnectPredicate tenantFilterPredicate() {
return context -> context.tenantId().startsWith("tenant-");
}
Note that in this case you need to remove axon.axonserver.contexts property.
Route message to specific tenant
By default, to route message to specific tenant you need to tag initial message that enters your system with metadata . This is done with meta-data helper, and you need to add tenant name to metadata with key TenantConfiguration.TENANT_CORRELATION_KEY
.
message.andMetaData(Collections.singletonMap(TENANT_CORRELATION_KEY, "tenant-context-1")
Metadata needs to be added only to initial message that enters your system. Any message that is produced by consequence of initial message will have this metadata copied automatically using to CorrelationProvider
.
Custom resolver
If you wish to disable default meta-data based routing define following property:
axon.multi-tenancy.use-metadata-helper=false
And define custom tenant resolver bean. For example following imaginary bean can use message payload to route message to specific tenant:
@Bean
public TargetTenantResolver<Message<?>> customTargetTenantResolver() {
return (message, tenants) -> (1)
TenantDescriptor.tenantWithId(
message.getPayload().getField("tenantName")
);
}
1 | First lambda parameter message represents message to be routed, while second parameter tenants represents list of currently registered tenants, if you wish to use is to route only to one of connected tenants. |
Multi-tenant projections
If you wish to use distinct database to store projections and token store for each tenant, configure following bean:
@Bean
public Function<TenantDescriptor, DataSourceProperties> tenantDataSourceResolver() {
return tenant -> {
DataSourceProperties properties = new DataSourceProperties();
properties.setUrl("jdbc:postgresql://localhost:5432/"+tenant.tenantId());
properties.setDriverClassName("org.postgresql.Driver");
properties.setUsername("postgres");
properties.setPassword("postgres");
return properties;
};
}
Note that this works by using JPA multi-tenancy support, that means only SQL Databases are supported out of the box. If you wish to implement multi-tenancy for a different type of databases (for example, NoSQL) make sure that your projection database supports multi-tenancy. While in transaction you may find out which tenant owns transaction by calling: TenantWrappedTransactionManager.getCurrentTenant()
.
For more hints how to enable multi-tenancy for NoSQL databases check on how JPA SQL version is implemented
In this case Liquibase or Flyway will not be able to initialise schemas for dynamic data sources. Any datasource that you use needs to have pre-initialized schema. |
Query update emitter
In order to correctly resolve right query update emitter inject update emitter in following style:
@EventHandler
public void on(Event event, QueryUpdateEmitter queryUpdateEmitter) {
//queryUpdateEmitter will route updates to same tenant as event will be
...
}
Resetting projections
Resetting projections works a bit different because you have instanced of each event processor group for each tenant.
Reset specific tenant event processor group:
TrackingEventProcessor trackingEventProcessor =
configuration.eventProcessingConfiguration()
.eventProcessor("com.demo.query-ep@tenant-context-1",
TrackingEventProcessor.class)
.get();
Name of each event processor is: {even processor name}@{tenant name}
Access all tenant event processors by retrieving MultiTenantEventProcessor
only. MultiTenantEventProcessor
acts as a proxy Event Processor that references all tenant event processors.
Dead-letter queue
The configuration of a dead-letter queue is similar to a non-multi-tenant environment. The tenant will be resolved through the Message’s MetaData
and routed to the corresponding DLQ. If you wish to have different enqueuing policies per tenant, you can use the MetaData
from the dead letter message to determine to which tenant the message belongs to act accordingly.
Do note that processing dead letters from the queue is slightly different, as you need the specific tenant context to process dead-letter from.
To select the tenant for which you want to process a dead letter, you need to cast the SequencedDeadLetterProcessor
to a MultiTenantDeadLetterProcessor
. From the MultiTenantDeadLetterProcessor
, you need to use the forTenant
method to select the tenant-specific SequencedDeadLetterProcessor
.
public class DlqManagement {
private MultiTenantDeadLetterProcessor multiTenantDeadLetterProcessor;
// Axon Framework's org.axonframework.config.Configuration
public DlqManagement(Configuration configuration) {
SequencedDeadLetterProcessor deadLetterProcessor = configuration.sequencedDeadLetterProcessor();
this.multiTenantDeadLetterProcessor = (MultiTenantDeadLetterProcessor) deadLetterProcessor;
}
public void processDeadLetterSequenceForTenant(String tenantId,
Predicate<DeadLetter<? extends EventMessage<?>>> sequenceFilter) {
multiTenantDeadLetterProcessor.forTenant(tenantId)
.process(sequenceFilter);
}
}
Here is a full example of a REST endpoint to retry dead letters for a specific tenant:
public class DlqManagementController {
// Axon Framework's org.axonframework.config.Configuration
private Configuration configuration;
@PostMapping(path = "/retry-dlq")
public void retryDLQ(@RequestParam String tenant, @RequestParam String processingGroup) {
configuration.eventProcessingConfiguration()
.sequencedDeadLetterProcessor(processingGroup)
.map(p -> (MultiTenantDeadLetterProcessor) p)
.map(mp -> mp.forTenant(TenantDescriptor.tenantWithId(tenant)))
.ifPresent(SequencedDeadLetterProcessor::processAny);
}
}
Only JPA Dead letter queue and In-Memory queues are supported. |
Deadline manager
As of now, there is no plan to support deadline manager out of the box. None of deadline manager implementation support multi-tenancy. See Event scheduler section as alternative.
Event scheduler
You can use the MultiTenantEventScheduler
to schedule events for specific tenants. To do so, you can inject the EventScheduler
and use it to schedule events:
public class EventHandlingComponentSchedulingEvents {
private EventScheduler eventScheduler;
@EventHandler
public void eventHandler(Event event) {
// Schedules the given event to be published in 10 days.
ScheduledToken token = eventScheduler.schedule(Instant.now().plusDays(10), event);
// The token returned by EventScheduler#schedule can be used to, for example, cancel the scheduled task.
eventScheduler.cancelSchedule(token);
}
}
If you use the EventScheduler
from any message handling method, it will automatically pick up tenant from Message#metadata
. Hence, there is no need to specify the tenant you want to schedule an event for. If you wish to use the EventScheduler
outside of message handlers, you are inclined to wrap the execution into a so-called TenantWrappedTransactionManager
. Within this TenantWrappedTransactionManager
you can schedule the event:
public class EventSchedulingComponent {
private EventScheduler eventScheduler;
public void schedule(Event event) {
ScheduledToken token;
// Schedules the given event to be published in 10 days.
new TenantWrappedTransactionManager(
TenantDescriptor.tenantWithId(tenantName))
.executeInTransaction(
() -> token = eventScheduler.schedule(Instant.now().plusDays(10), event)
);
// The token returned by EventScheduler#schedule can be used to, for example, cancel the scheduled task.
new TenantWrappedTransactionManager(
TenantDescriptor.tenantWithId(tenantName))
.executeInTransaction(
() -> eventScheduler.cancelSchedule(token)
);
}
}
Advanced configuration
Overriding default message source
You can override the default message source for each tenant by defining the following bean:
@Bean
public MultiTenantStreamableMessageSourceProvider multiTenantStreamableMessageSourceProvider(AxonServerEventStore customSource) {
return (defaultTenantSource, processorName, tenantDescriptor, configuration) -> {
if (tenantDescriptor.tenantId().startsWith("tenant-custom")) {
return customSource;
}
return defaultTenantSource;
};
}
This bean should return a StreamableMessageSource
that will be used for specific tenants. This lambda will be called for each tenant and each event processor, so be sure to return a default tenant source if you don’t want to override it.
Disable multi-tenancy for specific event processor
In certain cases, you may want to disable multi-tenancy for specific Event Processor which does not have any tenants. For example, when you have an event processor that is consuming events from an external context. Per default, each event processor is scaled, and duplicated for each tenant. To disable this behavior for a specific processing, you can define following bean:
@Bean
public MultiTenantEventProcessorPredicate multiTenantEventProcessorPredicate() {
return (processorName) -> !processorName.equals("external-context");
}
This bean should return true
for each processor that you want to be multi-tenant, and false
for each processor that you want to be single tenant.
Tenant Segment Factories
This extension provides several factory interfaces that are used to create tenant-specific segments for various Axon components, such as Command Bus, Query Bus, Event Store, and Event Scheduler. These factories allow you to configure and customize the behavior of these components for each tenant.
The following tenant segment factories are available:
TenantCommandSegmentFactory
This factory is responsible for creating a CommandBus
instance for each tenant. By default, it creates an AxonServerCommandBus
that uses a SimpleCommandBus
as the local segment and connects to Axon Server. You can override this factory to provide a custom implementation of the CommandBus
for specific tenants.
TenantQuerySegmentFactory
This factory creates a QueryBus
instance for each tenant. By default, it creates an AxonServerQueryBus
that uses a SimpleQueryBus
as lhe local segment and connects to Axon Server. You can override this factory to provide a custom implementation of the QueryBus
for specific tenants.
TenantEventSegmentFactory
This factory is responsible for creating an EventStore
instance for each tenant. By default, it creates an AxonServerEventStore
that connects to Axon Server. You can override this factory to provide a custom implementation of the EventStore
for specific tenants.
TenantEventSchedulerSegmentFactory
This factory creates an EventScheduler
instance for each tenant. By default, it creates an AxonServerEventScheduler
that connects to Axon Server. You can override this factory to provide a custom implementation of the EventScheduler
for specific tenants.
TenantEventProcessorControlSegmentFactory
This factory creates a TenantDescriptor
for each event processor, which is used to identify the tenant associated with the event processor. By default, it uses the tenant identifier as the TenantDescriptor
. You can override this factory to provide a custom implementation of the TenantDescriptor
for specific event processors.