diff --git a/SUMMARY.md b/SUMMARY.md index f01a3361..c1ae9b1a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -169,6 +169,7 @@ * [Reactor Gateways](extensions/reactor/reactive-gateways/reactive-gateways.md) * [Spring Cloud](extensions/spring-cloud.md) * [Tracing](extensions/tracing.md) +* [Multitenancy](extensions/multitenancy.md) ## Appendices diff --git a/axon-server/administration/admin-configuration/command-line-interface.md b/axon-server/administration/admin-configuration/command-line-interface.md index ecebe6cd..da0e7a7e 100644 --- a/axon-server/administration/admin-configuration/command-line-interface.md +++ b/axon-server/administration/admin-configuration/command-line-interface.md @@ -10,7 +10,7 @@ A quick summary of the various commands is depicted below. Each command has a sp Area (Server Edition) - Command-Line Options + Command name Description @@ -254,6 +254,43 @@ axonserver-cli.jar -S - The option -S with the url to the Axon Server is optional, if it is omitted it defaults to [http://localhost:8024](http://localhost:8024/).‌ While for Axon Server SE, the URL for the Axon Server SE will be the single running node, for Axon Server EE, the URL should be pointing to any node serving the _\_admin_ context within an Axon Server EE cluster. +The `` valid for all commands, are: `-S`, `-s`, `-i`, `-o`. +Their effect is described in the table below. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Option - ShortOption - LongDescription
`-S``server`Server to send command to (default http://localhost:8024)
`-s``https`Use HTTPS (SSL,TLS) to connect to the server, rather than HTTP.
`-i``insecure-ssl`Do not check the certificate when connecting using HTTPS.
`-o``output`Output format (txt,json)
+ +For options specific to individual commands, see the descriptions of the commands below. + ### Access control When running Axon Server with access control enabled, executing commands remotely requires an access token. This needs to be provided with the -t option. When you run a command on the Axon Server node itself from the directory where Axon Server was started, you don't have to provide a token.‌ diff --git a/extensions/kafka.md b/extensions/kafka.md index cbbf9f87..278760ef 100644 --- a/extensions/kafka.md +++ b/extensions/kafka.md @@ -60,7 +60,7 @@ public class KafkaEventPublicationConfiguration { } ``` -The second infrastructure component to introduce is the `KafkaPublisher`, which has a hard requirement on the `ProducerFactory`. Additionally, this would be the place to define the Kafka topic upon which Axon event messages will be published. Note that the `KafkaPublisher` needs to be `shutDown` properly, to ensure all `Producer` instances are properly closed. +The second infrastructure component to introduce is the `KafkaPublisher`, which has a hard requirement on the `ProducerFactory`. Additionally, this would be the place to define the Kafka topics upon which Axon event messages will be published. You can set a function from event to `Optional`. You can use this to only publish certain events, or put different events to different topics. Its not uncommon for Kafka topics to only contain one type of message. Note that the `KafkaPublisher` needs to be `shutDown` properly, to ensure all `Producer` instances are properly closed. ```java public class KafkaEventPublicationConfiguration { @@ -71,7 +71,7 @@ public class KafkaEventPublicationConfiguration { KafkaMessageConverter kafkaMessageConverter, int publisherAckTimeout) { return KafkaPublisher.builder() - .topic(topic) // Defaults to "Axon.Events" + .topicResolver(m -> Optional.of(topic)) // Defaults to "Axon.Events" for all events .producerFactory(producerFactory) // Hard requirement .messageConverter(kafkaMessageConverter) // Defaults to a "DefaultKafkaMessageConverter" .publisherAckTimeout(publisherAckTimeout) // Defaults to "1000" milliseconds; only used for "WAIT_FOR_ACK" mode @@ -250,7 +250,7 @@ public class KafkaEventConsumptionConfiguration { } ``` -Note that as with any tracking event processor, the progress on the event stream is stored in a `TrackingToken`. Using the `StreamableKafkaMessageSource` means a `KafkaTrackingToken` containing topic-partition to offset pairs is stored in the `TokenStore`. +Note that as with any tracking event processor, the progress on the event stream is stored in a `TrackingToken`. Using the `StreamableKafkaMessageSource` means a `KafkaTrackingToken` containing topic-partition to offset pairs is stored in the `TokenStore`. If no other `TokenStore` is provided, and auto-configuration is used, a `KafkaTokenStore` will be set instead of an `InMemoryTokenStore`. The `KafkaTokenStore` by default uses the `__axon_token_store_updates` topic. This should be a compacted topic, which should be created and configured automatically. ## Customizing event message format @@ -278,9 +278,9 @@ public class KafkaMessageConversationConfiguration { BiFunction headerValueMapper, EventUpcasterChain upcasterChain) { return DefaultKafkaMessageConverter.builder() - .serializer(serializer) // Hard requirement - .sequencingPolicy(sequencingPolicy) // Defaults to a "SequentialPerAggregatePolicy" - .upcasterChain(upcasterChain) // Defaults to empty upcaster chain + .serializer(serializer) // Hard requirement + .sequencingPolicy(sequencingPolicy) // Defaults to a "SequentialPerAggregatePolicy" + .upcasterChain(upcasterChain) // Defaults to empty upcaster chain .headerValueMapper(headerValueMapper) // Defaults to "HeaderUtils#byteMapper()" .build(); } @@ -288,7 +288,7 @@ public class KafkaMessageConversationConfiguration { } ``` -Make sure to use an identical `KafkaMessageConverter` on both the producing and consuming end, as otherwise exception upon deserialization should be expected. +Make sure to use an identical `KafkaMessageConverter` on both the producing and consuming end, as otherwise exception upon deserialization should be expected. A `CloudEventKafkaMessageConverter` is also available using the [Cloud Events](https://cloudevents.io/) spec. ## Configuration in Spring Boot @@ -296,9 +296,13 @@ This extension can be added as a Spring Boot starter dependency to your project **Generic Components:** -* A `DefaultKafkaMessageConverter` using the configured `eventSerializer` \(which defaults to `XStreamSerializer`\). +* A `DefaultKafkaMessageConverter` using the configured `eventSerializer` \(which defaults to `XStreamSerializer`\), which is used by default to convert between Axon Event messages and Kafka records. - Uses a `String` for the keys and a `byte[]` for the record's values + Uses a `String` for the keys and a `byte[]` for the record's values. + + When the property `axon.kafka.message-converter-mode` is set to `cloud_event` a `CloudEventKafkaMessageConverter` will be used instead. This will use `String` for the keys and `CloudEvent`. + + For each the matching Kafka (de)serializers will also be set as default. **Producer Components:** diff --git a/extensions/multitenancy.md b/extensions/multitenancy.md new file mode 100644 index 00000000..3029c7a6 --- /dev/null +++ b/extensions/multitenancy.md @@ -0,0 +1,148 @@ +# Multitenancy Extension + +The Axon Framework Multitenancy Extension provides your application with the ability to serve multiple tenants (event-stores) at once. +Multi-tenancy is important in cloud computing, as this extension provides the ability to connect tenants dynamically, physical separate tenant-data, and scale tenants independently. + +### Requirements + +- Currently, It's possible to configure extension using **Axon Framework 4.6+** together with **Spring Framework**. +- Minimal configuration and out-of-the box solution is available only for **Axon Server EE 4.6+** or Axon Cloud (*). +- Any other custom user solutions should implement [own factory beans for components and tenant provider](https://github.com/AxonFramework/extension-multitenancy/blob/main/multitenancy-spring-boot-autoconfigure/src/main/java/org/axonframework/extensions/multitenancy/autoconfig/MultiTenancyAxonServerAutoConfiguration.java) +- If you wish to enable multi-tenancy for your projections and token store, note that only JPA is supported out-of-the box. + +> **Axon Cloud and the Multi-Tenancy extension** +> +> Currently, Axon Cloud works only with static tenant configuration. + +### Configuration + +A minimal configuration is needed to get this extension up and running. +Please choose **either** the static or the dynamic tenant configuration. + +#### Static tenants configuration + +If you have a predefined list of tenants that your application should connect to, set following property: +`axon.axonserver.contexts=tenant-context-1,tenant-context-2,tenant-context-3` + +#### Dynamic tenants configuration + +If you plan to create tenants during runtime, you can define a predicate that will tell the application to which tenant-contexts to connect to once they appear: + +```java +@Bean +public TenantConnectPredicate tenantFilterPredicate() { + return context -> context.tenantId().startsWith("tenant-"); +} +``` + +### Route Messages to specific tenants + +Backbone of multitenancy is ability to route message to specific tenant. +This extension offers you meta-data based routing which is ready to be used with minimal configuration. +Also, one may wish to define stronger contract and include tenant information in message payload, which is also possible by defining custom tenant resolver. + +#### Using meta-data + +By default, to route any `Message` to a specific tenant, you need to tag the initial message that enters your system with metadata. +This is done with a meta-data helper function, which should add the tenant name with key `TenantConfiguration.TENANT_CORRELATION_KEY`. + +```java +message.andMetaData(Collections.singletonMap(TENANT_CORRELATION_KEY, "tenant-context-1") +``` + +Note that you only need to add metadata to the initial message entering your system. +Any message produced as a consequence of the initial message will have this metadata copied automatically using a `CorrelationDataProvider`. + +#### Custom tenant resolver + +If you wish to define a custom tenant resolver, set following property: + +`axon.multi-tenancy.use-metadata-helper=false` + +Then define the custom tenant resolver bean. +The following example can use the message payload to route a message to specific tenant: + +```java +@Bean +public TargetTenantResolver> customTargetTenantResolver() { + return (message, tenants) -> + TenantDescriptor.tenantWithId( + ((TenantAwareMessage) message.getPayload()).getTenantName() + ); +} +``` + +In example above, all messages should implement custom `TenantAwareMessage` interface that exposes tenant name. +Then we can use this interface to extract tenant name from the payload and define our tenant resolver. + +### Multi-tenant projections + +If you wish to use distinct tenant-databases to store projections and tokens, please configure the following: + +```java +@Bean +public Function 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 the JPA multi-tenancy support provided in this extension. +That means that currently only SQL Databases are supported out of the box. + +If you wish to implement multi-tenancy for a different type of databases (e.g. NoSQL) make sure that your projection database supports multi-tenancy, too. +When doing so, you can find it which tenants own the transaction by invoking `TenantWrappedTransactionManager.getCurrentTenant()`. + +> **Pre-initialized schema** +> +> Schema migration tools like Liquibase or Flyway usually won't be able to initialize schemas for dynamically created data sources. +> Hence, any data source that you use needs to have a the schema pre-initialized. + +#### Resetting projections + +Resetting projections works a bit different, because there are multiple instances of the "same" event processor. +Namely, one per tenant. + +Regard the following sample to reset an Event Processor for a specific tenant: + +```java +TrackingEventProcessor trackingEventProcessor = configuration.eventProcessingConfiguration() + .eventProcessor("com.demo.query-ep@tenant-context-1", TrackingEventProcessor.class) + .get(); +``` + +Note that the convention for naming tenant-specific event processor is `{even processor name}@{tenant name}`. + +If you need to access all tenant event processors in one go, you can retrieve the `MultiTenantEventProcessor` for a specific processing name. +The `MultiTenantEventProcessor` acts as a proxy event processor referencing all tenant-specific event processors. + +### Supported multi-tenant components + +Currently, the following infrastructure components support multi-tenancy: + +- MultiTenantCommandBus +- MultiTenantEventProcessor +- MultiTenantEventStore +- MultiTenantQueryBus +- MultiTenantQueryUpdateEmitter +- MultiTenantEventProcessorControlService +- MultiTenantDataSourceManager + +The following components are not yet supported: + +- MultitenantDeadlineManager +- MultitenantEventScheduler + + +### Disabling this Extension + +By default, this extension is enabled if found on class path when utilizing Spring Boot. +If you wish to disable the extension without removing the dependency, you can set the following property to `false`: + +`axon.multi-tenancy.enabled=false`