diff --git a/README.md b/README.md index 110e8674..69eb6438 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Code Samples -This repository contains several Axon-based sample projects to explain specific topics. -Axon usage extends itself from the repositories under the [Axon Framework project](https://github.com/AxonFramework/), -as well as the tools provided by AxonIQ like [Axon Server](https://www.axoniq.io/products/axon-server). +This repository contains several Axon Framework 5-based sample projects to explain specific topics. +Axon usage extends itself from the repositories under the [Axon Framework project](https://github.com/AxonIQ/AxonFramework), +as well as the tools provided by AxoniQ like [Axon Server](https://www.axoniq.io/products/axon-server). Each Maven module within this project represents a different sample you can use, each with its own `README.md` explaining the intent and usage. @@ -17,23 +17,31 @@ Down below is an exhaustive list of all the sample: and Spring-based application. 2. [Distributed Exceptions](distributed-exceptions/README.md) - Sample showing how to deal with exceptions in a distributed application. -3. [Multitenancy](multitenancy/README.md) - Sample showing 'a' approach to multitenancy. -4. [Reset Handler](reset-handler/README.md) - Sample showing how to reset a `StreamingEventProcessor`. -5. [Saga - No TOAST in PostgreSQL](saga/README.md) - Sample showing a basis Saga, that's stored in a PostgreSQL - without [TOAST](https://wiki.postgresql.org/wiki/TOAST). -6. [Serialization Avro](serialization-avro/README.md) - Sample showing usage of Apache Avro Commands/Events/Queries. -7. [Sequencing Policy](sequencing-policy/README.md) - Sample showing how to set up a custom `SequencingPolicy` to adjust - the event sequence for a `StreamingEventProcessor`. -8. [Set-Based Validation](set-based-validation/README.md) - Sample showing several approaches to implement set-based - validation. -9. [Set-Based Validation - Actor Model](set-based-validation-actor-model/README.md) - Sample showing how to implement - set-based validation through a dedicated aggregate instance. -10. [Snapshots](snapshots/README.md) - Sample showing how to configure aggregate snapshotting. -11. [Stateful Event Handler](stateful-event-handler/README.md) - Sample showing a stateful event handler that can be - used as a replacement for sagas. -12. [Subscription Query - REST](subscription-query-rest/README.md) - Sample showing how to use Axon's subscription query - cleanly in a REST-based controller. -13. [Subscription Query - Streaming](subscription-query-streaming/README.md) - Sample showing how to use Axon's +3. [Order Fulfillment Workflow](order-fulfillment-workflow/README.md) - Sample showing how to model an Order + Fulfillment process with the AxoniQ Workflow Engine. +4. [Reset Handler](reset-handler/README.md) - Sample showing how to reset a `PooledStreamingEventProcessor`. +5. [Serialization Avro](serialization-avro/README.md) - Sample showing usage of Apache Avro Commands/Events/Queries. +6. [Sequencing Policy](sequencing-policy/README.md) - Sample showing how to set up a custom `SequencingPolicy` to adjust + the event sequence for a `PooledStreamingEventProcessor`. +7. [Snapshots](snapshots/README.md) - Sample showing how to configure event-sourced entity snapshotting. +8. [Stateful Event Handler](stateful-event-handler/README.md) - Sample showing a stateful event handler that can be + used as a replacement for sagas. +9. [Subscription Query - REST](subscription-query-rest/README.md) - Sample showing how to use Axon's subscription query + cleanly in a REST-based controller. +10. [Subscription Query - Streaming](subscription-query-streaming/README.md) - Sample showing how to use Axon's subscription query cleanly in a streaming-based controller. -14. [Upcasters](upcaster/README.md) - Sample showing several implementations of upcasters. +11. [Workflow Saga](workflow-saga/README.md) - Sample showing a Saga-like process modelled with the AxoniQ Workflow + Engine. + +## Topics now covered by the Axon Framework project itself + +A few topics that used to live here as standalone samples now have more thorough, actively maintained examples in the +[AxonIQ/AxonFramework](https://github.com/AxonIQ/AxonFramework) repository's `examples/` directory. Rather than +maintaining a second, thinner copy, we point to those instead: + +- **Sagas** - see the [`extension-workflow`](https://github.com/AxonIQ/extension-workflow) project and the automation + patterns in [`examples/university-demo`](https://github.com/AxonIQ/AxonFramework/tree/main/examples/university-demo). +- **Multitenancy** - see [`examples/university-multi-tenancy-examples`](https://github.com/AxonIQ/AxonFramework/tree/main/examples/university-multi-tenancy-examples). +- **Set-based validation** - see `CourseUniqueNameSetValidation` in [`examples/university-demo`](https://github.com/AxonIQ/AxonFramework/tree/main/examples/university-demo/src/main/java/org/axonframework/examples/demo/university/faculty/write/createcourse). +- **Upcasting / event transformation** - see [`examples/university-message-transformation`](https://github.com/AxonIQ/AxonFramework/tree/main/examples/university-message-transformation). diff --git a/axon-spring-template/pom.xml b/axon-spring-template/pom.xml index c2c3e222..fd6e2c25 100644 --- a/axon-spring-template/pom.xml +++ b/axon-spring-template/pom.xml @@ -14,7 +14,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter diff --git a/distributed-exceptions/pom.xml b/distributed-exceptions/pom.xml index a84c8981..f3714305 100644 --- a/distributed-exceptions/pom.xml +++ b/distributed-exceptions/pom.xml @@ -29,7 +29,7 @@ axon-eventsourcing - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter @@ -56,7 +56,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter org.junit.jupiter diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/DistributedExceptionsApplication.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/DistributedExceptionsApplication.java index fbb4831b..4a0baada 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/DistributedExceptionsApplication.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/DistributedExceptionsApplication.java @@ -1,10 +1,7 @@ package io.axoniq.distributedexceptions; -import io.axoniq.distributedexceptions.command.ExceptionWrappingHandlerInterceptor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Profile; @SpringBootApplication public class DistributedExceptionsApplication { @@ -12,10 +9,4 @@ public class DistributedExceptionsApplication { public static void main(String[] args) { SpringApplication.run(DistributedExceptionsApplication.class, args); } - - @Bean - @Profile("command") - public ExceptionWrappingHandlerInterceptor exceptionWrappingHandlerInterceptor() { - return new ExceptionWrappingHandlerInterceptor(); - } } diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardIssuedEvent.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardIssuedEvent.java index c812eac4..eda51bff 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardIssuedEvent.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardIssuedEvent.java @@ -1,7 +1,9 @@ package io.axoniq.distributedexceptions.api; -import javax.annotation.Nonnull; +import org.axonframework.eventsourcing.annotation.EventTag; +import org.axonframework.messaging.eventhandling.annotation.Event; -public record CardIssuedEvent(@Nonnull String id, int amount) { +@Event +public record CardIssuedEvent(@EventTag(key = "GiftCard") String id, int amount) { } diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardRedeemedEvent.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardRedeemedEvent.java index 5c724482..a8c1626d 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardRedeemedEvent.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/CardRedeemedEvent.java @@ -1,7 +1,9 @@ package io.axoniq.distributedexceptions.api; -import javax.annotation.Nonnull; +import org.axonframework.eventsourcing.annotation.EventTag; +import org.axonframework.messaging.eventhandling.annotation.Event; -public record CardRedeemedEvent(@Nonnull String id, int amount) { +@Event +public record CardRedeemedEvent(@EventTag(key = "GiftCard") String id, int amount) { } diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/IssueCardCommand.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/IssueCardCommand.java index 3dcafbfe..c4a34e86 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/IssueCardCommand.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/IssueCardCommand.java @@ -1,9 +1,9 @@ package io.axoniq.distributedexceptions.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.messaging.commandhandling.annotation.Command; +import org.axonframework.modelling.annotation.TargetEntityId; -import javax.annotation.Nonnull; - -public record IssueCardCommand(@TargetAggregateIdentifier @Nonnull String id, int amount) { +@Command(routingKey = "id") +public record IssueCardCommand(@TargetEntityId String id, int amount) { } diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/RedeemCardCommand.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/RedeemCardCommand.java index 4b369ac1..c36e44aa 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/RedeemCardCommand.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/api/RedeemCardCommand.java @@ -1,9 +1,9 @@ package io.axoniq.distributedexceptions.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.messaging.commandhandling.annotation.Command; +import org.axonframework.modelling.annotation.TargetEntityId; -import javax.annotation.Nonnull; - -public record RedeemCardCommand(@TargetAggregateIdentifier @Nonnull String id, int amount) { +@Command(routingKey = "id") +public record RedeemCardCommand(@TargetEntityId String id, int amount) { } diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/ExceptionWrappingHandlerInterceptor.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/ExceptionWrappingHandlerInterceptor.java index 5cf88137..2de71cdd 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/ExceptionWrappingHandlerInterceptor.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/ExceptionWrappingHandlerInterceptor.java @@ -2,31 +2,37 @@ import io.axoniq.distributedexceptions.api.GiftCardBusinessError; import io.axoniq.distributedexceptions.api.GiftCardBusinessErrorCode; -import org.axonframework.commandhandling.CommandExecutionException; -import org.axonframework.commandhandling.CommandMessage; -import org.axonframework.messaging.InterceptorChain; -import org.axonframework.messaging.MessageHandlerInterceptor; -import org.axonframework.messaging.unitofwork.UnitOfWork; +import org.axonframework.messaging.commandhandling.CommandExecutionException; +import org.axonframework.messaging.commandhandling.CommandMessage; +import org.axonframework.messaging.core.MessageHandlerInterceptor; +import org.axonframework.messaging.core.MessageHandlerInterceptorChain; +import org.axonframework.messaging.core.MessageStream; +import org.axonframework.messaging.core.unitofwork.ProcessingContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; import java.lang.invoke.MethodHandles; -import javax.annotation.Nonnull; -public class ExceptionWrappingHandlerInterceptor implements MessageHandlerInterceptor> { +@Component +@Profile("command") +public class ExceptionWrappingHandlerInterceptor implements MessageHandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Override - public Object handle(@Nonnull UnitOfWork> unitOfWork, - @Nonnull InterceptorChain interceptorChain) { - try { - return interceptorChain.proceed(); - } catch (Throwable e) { - throw new CommandExecutionException( - "An exception has occurred during command execution", e, exceptionDetails(e) - ); - } + public MessageStream interceptOnHandle(CommandMessage message, + ProcessingContext context, + MessageHandlerInterceptorChain chain) { + return chain.proceed(message, context) + .onErrorContinue(throwable -> MessageStream.failed( + new CommandExecutionException( + "An exception has occurred during command execution", + throwable, + exceptionDetails(throwable) + ) + )); } // Domain specific details can be returned in a couple of forms. diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/GiftCard.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/GiftCard.java index 4b27cccc..3a9b283c 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/GiftCard.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/command/GiftCard.java @@ -4,39 +4,37 @@ import io.axoniq.distributedexceptions.api.CardIssuedEvent; import io.axoniq.distributedexceptions.api.RedeemCardCommand; import io.axoniq.distributedexceptions.api.CardRedeemedEvent; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; +import org.axonframework.eventsourcing.annotation.EventSourcingHandler; +import org.axonframework.eventsourcing.annotation.reflection.EntityCreator; +import org.axonframework.extension.spring.stereotype.EventSourced; +import org.axonframework.messaging.commandhandling.annotation.CommandHandler; +import org.axonframework.messaging.eventhandling.gateway.EventAppender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import java.lang.invoke.MethodHandles; -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@Aggregate +@EventSourced(tagKey = "GiftCard") @Profile("command") class GiftCard { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @AggregateIdentifier private String giftCardId; private int remainingValue; @CommandHandler - public GiftCard(IssueCardCommand command) { + public static void handle(IssueCardCommand command, EventAppender eventAppender) { logger.debug("handling {}", command); if (command.amount() <= 0) { throw new NegativeOrZeroAmount(command.amount(), "amount <= 0"); } - apply(new CardIssuedEvent(command.id(), command.amount())); + eventAppender.append(new CardIssuedEvent(command.id(), command.amount())); } @CommandHandler - public void handle(RedeemCardCommand command) { + public void handle(RedeemCardCommand command, EventAppender eventAppender) { logger.debug("handling {}", command); if (command.amount() <= 0) { throw new NegativeOrZeroAmount(command.amount(), "amount <= 0"); @@ -44,7 +42,7 @@ public void handle(RedeemCardCommand command) { if (command.amount() > remainingValue) { throw new InsufficientFunds("amount > remaining value"); } - apply(new CardRedeemedEvent(giftCardId, command.amount())); + eventAppender.append(new CardRedeemedEvent(giftCardId, command.amount())); } @EventSourcingHandler @@ -62,6 +60,7 @@ public void on(CardRedeemedEvent event) { logger.debug("new remaining value: {}", remainingValue); } + @EntityCreator public GiftCard() { // Required by Axon logger.debug("Empty constructor invoked"); diff --git a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/rest/GiftCardController.java b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/rest/GiftCardController.java index 77916dd2..77832c79 100644 --- a/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/rest/GiftCardController.java +++ b/distributed-exceptions/src/main/java/io/axoniq/distributedexceptions/rest/GiftCardController.java @@ -3,8 +3,8 @@ import io.axoniq.distributedexceptions.api.GiftCardBusinessError; import io.axoniq.distributedexceptions.api.IssueCardCommand; import io.axoniq.distributedexceptions.api.RedeemCardCommand; -import org.axonframework.commandhandling.CommandExecutionException; -import org.axonframework.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.commandhandling.CommandExecutionException; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; @@ -37,7 +37,8 @@ public GiftCardController(CommandGateway commandGateway) { public CompletableFuture> issueNewGiftCard(@RequestBody IssueCardDto request) { IssueCardCommand command = new IssueCardCommand(UUID.randomUUID().toString(), request.amount()); return commandGateway.send(command) - .thenApply(it -> ResponseEntity.ok(String.valueOf(it))) + .getResultMessage() + .thenApply(it -> ResponseEntity.ok(String.valueOf(it.payload()))) .exceptionally(e -> { logException(e); String errorResponse = getErrorResponseMessage(e.getCause()); @@ -49,6 +50,7 @@ public CompletableFuture> issueNewGiftCard(@RequestBody I @PutMapping("/{id}") public CompletableFuture> redeem(@PathVariable String id, @RequestBody RedeemCardDto dto) { return commandGateway.send(new RedeemCardCommand(id, dto.amount())) + .getResultMessage() .thenApply(it -> ResponseEntity.ok("")) .exceptionally(e -> { logException(e); @@ -68,9 +70,8 @@ private void logException(Throwable throwable) { private String getErrorResponseMessage(Throwable throwable) { if (throwable instanceof CommandExecutionException cee) { - return cee.getDetails() - .map((Object it) -> { - GiftCardBusinessError giftCardBusinessError = (GiftCardBusinessError) it; + return cee.getDetails(GiftCardBusinessError.class) + .map(giftCardBusinessError -> { logger.debug("Received BusinessError with data: " + giftCardBusinessError); logger.error("Unable to create GiftCard due to validation constrains. Reason: " + giftCardBusinessError); diff --git a/distributed-exceptions/src/main/resources/application.properties b/distributed-exceptions/src/main/resources/application.properties index c662bc56..f91296e5 100644 --- a/distributed-exceptions/src/main/resources/application.properties +++ b/distributed-exceptions/src/main/resources/application.properties @@ -1,2 +1,2 @@ spring.main.web-application-type=none -axon.serializer.general=jackson +axon.converter.general=jackson diff --git a/multitenancy/.gitignore b/multitenancy/.gitignore deleted file mode 100644 index 9f5ab22c..00000000 --- a/multitenancy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -src/main/frontend diff --git a/multitenancy/README.md b/multitenancy/README.md deleted file mode 100644 index 55c1384f..00000000 --- a/multitenancy/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Multi-tenant playground - -This simple app allows you to experiment with multi-tenancy feature in Axon Framework. -By using [Multitenancy extension](https://github.com/AxonFramework/extension-multitenancy) this app can connect to multiple contexts at once dynamicly. -Using Vaadin UI, explore how to create tenants in runtime and dispatch messages to them. - -## Requirements - - - Requires Axon Server 2024.1+ with a valid license - - Axon Framework 4.10.1+ - - [Multitenancy extension](https://github.com/AxonFramework/extension-multitenancy) - - Uses In-Memory H2 Database for projections - DB per tenant - -## How to start - -While in `src/main/docker` type in terminal `docker-compose up` to start Axon Server EE. - -Make sure to replace content of `axoniq.license` with a valid license. - -Start application with `mvn spring-boot:run` or manually via favorite IDE. - -After starting application visit and interact with UI at [localhost:8080](http://localhost:8080) - -You may start by registering a new tenant. - -### With UI you can: - - Login as existing tenant -![](login.png) - - Register new tenant at runtime - ![](register.png) - - Send commands, queries, events - ![](actions.png) - - Reset projections -![](reset.png) - - Explore H2 Database used for projections - per tenant - - ![](h2.png) - -## How it works - -### Multi-tenant configuration - -[MultiTenantConfig#tenantFilter](src/main/java/io/axoniq/multitenancy/MultiTenantConfig.java) configures application to connect to any context which name starts with `tenant-`. -If new context is created during runtime that matches the filter, application will automatically connect to that tenant. - -[MultiTenantConfig#tenantDataSourceResolver](src/main/java/io/axoniq/multitenancy/MultiTenantConfig.java) configures application to use DB for projections per tenant. It defines datasource properties for tenant. -Important: DB needs to be created and schema needs to be initialiesd before context is created, otherwise you will see exception such as DB/Table not found... - -[CreateTenantService#createTenant](src/main/java/io/axoniq/multitenancy/web/CreateTenantService.java) uses Admin API to interact with Axon Server and create tenant context. -Before creating context it creates embedded H2 database. As H2 database is in memory, it will not be persistent after application restart, while Axon Server contexts will. -Be sure to clean up contexts if you are planing to play with this application multiple times or you may expect to see some exceptions. - -[MessageService](src/main/java/io/axoniq/multitenancy/web/MessageService.java) routes messages to specific tenant by setting `TENANT_CORRELATION_KEY` MetaData to initial message. - -[ResetService](src/main/java/io/axoniq/multitenancy/web/ResetService.java) lists all event processors and resets one for specificity tenant. Convention is that Event Processor name contains original event processor name and tenant name. Such as `even_processor_name@tenant_name`. diff --git a/multitenancy/actions.png b/multitenancy/actions.png deleted file mode 100644 index e07959cc..00000000 Binary files a/multitenancy/actions.png and /dev/null differ diff --git a/multitenancy/h2.png b/multitenancy/h2.png deleted file mode 100644 index 54fae173..00000000 Binary files a/multitenancy/h2.png and /dev/null differ diff --git a/multitenancy/login.png b/multitenancy/login.png deleted file mode 100644 index db2a8d8e..00000000 Binary files a/multitenancy/login.png and /dev/null differ diff --git a/multitenancy/pom.xml b/multitenancy/pom.xml deleted file mode 100644 index 5c52c6c0..00000000 --- a/multitenancy/pom.xml +++ /dev/null @@ -1,161 +0,0 @@ - - - 4.0.0 - - code-samples - io.axoniq - 0.0.2-SNAPSHOT - - - multitenancy - - Multitenancy - Axon Framework application using the Multitenancy Extension - - - - 11.13.2 - 2.4.240 - - 24.9.2 - 21 - - - - - - com.vaadin - vaadin-bom - - ${vaadin.version} - pom - import - - - - - - - - org.axonframework - axon-spring-boot-starter - - - org.axonframework - axon-configuration - - - io.axoniq - axonserver-connector-java - - - org.axonframework - axon-test - - - org.axonframework.extensions.multitenancy - axon-multitenancy-spring-boot-starter - - - org.axonframework.extensions.multitenancy - axon-multitenancy-spring-boot-autoconfigure - - - org.axonframework.extensions.multitenancy - axon-multitenancy - - - - - org.springframework.data - spring-data-commons - - - org.springframework.data - spring-data-jpa - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-web - - - - org.flywaydb - flyway-core - ${flyway.version} - - - com.h2database - h2 - ${h2.version} - - - - io.projectreactor - reactor-core - - - com.vaadin - vaadin-spring-boot-starter - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 17 - 17 - 21 - - - - - com.vaadin - vaadin-maven-plugin - ${vaadin.version} - - - - prepare-frontend - build-frontend - - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - spring-boot - io.axoniq.multitenancy.MultitenancyExampleApplication - - - - - - - diff --git a/multitenancy/register.png b/multitenancy/register.png deleted file mode 100644 index 7a180ba2..00000000 Binary files a/multitenancy/register.png and /dev/null differ diff --git a/multitenancy/reset.png b/multitenancy/reset.png deleted file mode 100644 index f96efe3d..00000000 Binary files a/multitenancy/reset.png and /dev/null differ diff --git a/multitenancy/src/main/docker/axoniq.license b/multitenancy/src/main/docker/axoniq.license deleted file mode 100644 index 03a560a7..00000000 --- a/multitenancy/src/main/docker/axoniq.license +++ /dev/null @@ -1,14 +0,0 @@ -#AxonIQ License File generated on 2022-09-20 -#Tue Sep 20 08:26:45 UTC 2022 -license_key_id=dd722d00-dd7c-4c70-bf1a-2f5c78a3af6c -signature=POSseYXhv/Op+DaXT7UYjQudjzhkM3adU/gPGWuSEUPXTspHoF788iL0poZoycIVBsm8CGxUqeWdZyAaWQnvEuveqQCl9YlgmK6ocYp6uPL3ltxfAO93xW8X9paUqMMj1Y6FFky8ITSPRPWNA1zpTFyoPLs3Dmy+/kx/DjVJBiTo8QaDt08IWhr7f8Zezp9n6HgZKU/UnojSwBBtp/NxxMGu11VjjqrBI6NBBj+J3LtJMNkA5P8za4cQ2Huu+HufROz+6N2Ia1obdkpG2eqaoRsWf6MLKzYBX6eU9nV/b5SFCOX9/ehw3O0lHT5WZokY3ZKZUaXTAWNYgTopRkpeunNv7p+pVHnSAV969zWZ6Y1YbgDkL1TAmnqWu1+1w3C48xoyFvlSSNcUkhWljOlwF2X0gmYX6/r02UDPD5Q0nQcqC01QcOFOeYny0VgWe6XfYovKMmlqDCW5m2y/W7dEJffpl4I93p0OFzh4LRxqrqriNTVKs95YNh5XPvm4e8u2/0ieeo9so8gCz+xTeDB6rZw81YCyLDYkS4GP1pVt/SOPtvZHPncKWOdv19790zc6azByyXAxnqi/GXGI6dB9kaKDmBVPeCMtSdF6Z0Cq9K+aGAHoFhwZZJmG4Bo1MAutAAA6J+Qw60yilehKt+SgibwQFxIn1LEkRZu4Ga4e/mg\= -reference=Trial license for use by the participants to AxonIQCon 2022 -contexts=100 -expiry_date=2022-10-15 -contact=bert.laverman@axoniq.io -edition=Enterprise -issue_date=2022-09-20 -grace_date=2022-10-15 -product=AxonServer -licensee=AxonIQCon 2022 -clusterNodes=3 \ No newline at end of file diff --git a/multitenancy/src/main/docker/cluster-template.yml b/multitenancy/src/main/docker/cluster-template.yml deleted file mode 100644 index 19c609e1..00000000 --- a/multitenancy/src/main/docker/cluster-template.yml +++ /dev/null @@ -1,33 +0,0 @@ -axoniq: - axonserver: - cluster-template: - users: [] - replicationGroups: - - roles: - - role: PRIMARY - node: axonserver-enterprise-1 - - role: PRIMARY - node: axonserver-enterprise-2 - - role: PRIMARY - node: axonserver-enterprise-3 - name: _admin - contexts: - - name: _admin - metaData: - event.index-format: JUMP_SKIP_INDEX - snapshot.index-format: JUMP_SKIP_INDEX - - roles: - - role: PRIMARY - node: axonserver-enterprise-2 - - role: PRIMARY - node: axonserver-enterprise-1 - - role: PRIMARY - node: axonserver-enterprise-3 - name: default - contexts: - - name: tenant-one - metaData: - event.index-format: JUMP_SKIP_INDEX - snapshot.index-format: JUMP_SKIP_INDEX - applications: [] - first: axonserver-enterprise-1:8224 diff --git a/multitenancy/src/main/docker/docker-compose.yml b/multitenancy/src/main/docker/docker-compose.yml deleted file mode 100644 index 69d5e01e..00000000 --- a/multitenancy/src/main/docker/docker-compose.yml +++ /dev/null @@ -1,84 +0,0 @@ -version: '3.8' -services: - axonserver-enterprise-1: - image: axoniq/axonserver-enterprise:4.6.4-dev - hostname: axonserver-enterprise-1 - environment: - - AXONIQ_AXONSERVER_CLUSTERTEMPLATE_PATH=/axonserver/cluster-template.yml - - AXONIQ_AXONSERVER_ENTERPRISE_LICENSE-DIRECTORY=/axonserver - - SERVER_PORT=8024 - - AXONIQ_AXONSERVER_PORT=8124 - - AXONIQ_AXONSERVER_METRICS_GRPC_ENABLED=true - - AXONIQ_AXONSERVER_METRICS_GRPC_PROMETHEUS-ENABLED=true - volumes: - - axonserver-enterprise-1-log:/axonserver/log - - axonserver-enterprise-1-events:/axonserver/events - - axonserver-enterprise-1-data:/axonserver/data - - ./axoniq.license:/axonserver/axoniq.license - - ./cluster-template.yml:/axonserver/cluster-template.yml - ports: - - '8024:8024' - - '8124:8124' - - '8224:8224' - networks: - - axon-net - - axonserver-enterprise-2: - image: axoniq/axonserver-enterprise:4.6.4-dev - hostname: axonserver-enterprise-2 - environment: - - AXONIQ_AXONSERVER_CLUSTERTEMPLATE_PATH=/axonserver/cluster-template.yml - - AXONIQ_AXONSERVER_ENTERPRISE_LICENSE-DIRECTORY=/axonserver - - SERVER_PORT=8025 - - AXONIQ_AXONSERVER_PORT=8125 - - AXONIQ_AXONSERVER_METRICS_GRPC_ENABLED=true - - AXONIQ_AXONSERVER_METRICS_GRPC_PROMETHEUS-ENABLED=true - volumes: - - axonserver-enterprise-2-log:/axonserver/log - - axonserver-enterprise-2-events:/axonserver/events - - axonserver-enterprise-2-data:/axonserver/data - - ./axoniq.license:/axonserver/axoniq.license - - ./cluster-template.yml:/axonserver/cluster-template.yml - ports: - - '8025:8025' - - '8125:8125' - - '8225:8225' - networks: - - axon-net - - axonserver-enterprise-3: - image: axoniq/axonserver-enterprise:4.6.4-dev - hostname: axonserver-enterprise-3 - environment: - - AXONIQ_AXONSERVER_CLUSTERTEMPLATE_PATH=/axonserver/cluster-template.yml - - AXONIQ_AXONSERVER_ENTERPRISE_LICENSE-DIRECTORY=/axonserver - - SERVER_PORT=8026 - - AXONIQ_AXONSERVER_PORT=8126 - - AXONIQ_AXONSERVER_METRICS_GRPC_ENABLED=true - - AXONIQ_AXONSERVER_METRICS_GRPC_PROMETHEUS-ENABLED=true - volumes: - - axonserver-enterprise-3-log:/axonserver/log - - axonserver-enterprise-3-events:/axonserver/events - - axonserver-enterprise-3-data:/axonserver/data - - ./axoniq.license:/axonserver/axoniq.license - - ./cluster-template.yml:/axonserver/cluster-template.yml - ports: - - '8026:8026' - - '8126:8126' - - '8226:8226' - networks: - - axon-net - -volumes: - axonserver-enterprise-1-log: - axonserver-enterprise-1-events: - axonserver-enterprise-1-data: - axonserver-enterprise-2-log: - axonserver-enterprise-2-events: - axonserver-enterprise-2-data: - axonserver-enterprise-3-log: - axonserver-enterprise-3-events: - axonserver-enterprise-3-data: - -networks: - axon-net: \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/MultiTenantConfig.java b/multitenancy/src/main/java/io/axoniq/multitenancy/MultiTenantConfig.java deleted file mode 100644 index a539af43..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/MultiTenantConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.axoniq.multitenancy; - -import org.axonframework.axonserver.connector.AxonServerConfiguration; -import org.axonframework.axonserver.connector.AxonServerConnectionManager; -import org.axonframework.axonserver.connector.event.axon.AxonServerEventStore; -import org.axonframework.eventsourcing.snapshotting.SnapshotFilter; -import org.axonframework.extensions.multitenancy.components.TenantConnectPredicate; - -import org.axonframework.extensions.multitenancy.configuration.MultiTenantStreamableMessageSourceProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -@Configuration -public class MultiTenantConfig { - - @Bean - public TenantConnectPredicate tenantFilter() { - return context -> context.tenantId().startsWith("tenant-"); - } - - @Bean - public ScheduledExecutorService persistentStreamScheduler() { - return Executors.newScheduledThreadPool(10, Thread.ofVirtual() - .name("persistent-streams-", 0) - .factory()); - } - - // UNCOMMENT THIS BEAN TO ENABLE MULTITENANCY WITH MULTIPLE DATA SOURCES -// @Bean -// public Function tenantDataSourceResolver() { -// return tenant -> { -// DataSourceProperties properties = new DataSourceProperties(); -// properties.setUrl("jdbc:h2:mem:" + tenant.tenantId()); -// properties.setDriverClassName("org.h2.Driver"); -// properties.setUsername("sa"); -// return properties; -// }; -// } -} - diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/MultitenancyExampleApplication.java b/multitenancy/src/main/java/io/axoniq/multitenancy/MultitenancyExampleApplication.java deleted file mode 100644 index 3b7113f7..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/MultitenancyExampleApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.axoniq.multitenancy; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -@SpringBootApplication -@EnableJpaRepositories -public class MultitenancyExampleApplication { - - public static void main(String[] args) { - SpringApplication.run(MultitenancyExampleApplication.class, args); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/AddFundsCommand.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/AddFundsCommand.java deleted file mode 100644 index 6cb2e001..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/AddFundsCommand.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.axoniq.multitenancy.api; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import javax.annotation.Nonnull; - -public record AddFundsCommand(@TargetAggregateIdentifier @Nonnull String id, Integer amount) { - -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/CardIssuedEvent.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/CardIssuedEvent.java deleted file mode 100644 index 4c488ab3..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/CardIssuedEvent.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.multitenancy.api; - -public record CardIssuedEvent(String id, Integer amount) { - -} \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindAllCardsQuery.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindAllCardsQuery.java deleted file mode 100644 index 57219514..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindAllCardsQuery.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.multitenancy.api; - -public record FindAllCardsQuery() { - -} \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindCardQuery.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindCardQuery.java deleted file mode 100644 index 669a58c1..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FindCardQuery.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.multitenancy.api; - -public record FindCardQuery(String id) { - -} \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FundsAddedEvent.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/FundsAddedEvent.java deleted file mode 100644 index 31f815bf..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/FundsAddedEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.multitenancy.api; - -import javax.annotation.Nonnull; - -public record FundsAddedEvent(@Nonnull String id, int amount) { - -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/GiftCardRecord.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/GiftCardRecord.java deleted file mode 100644 index 7e75db64..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/GiftCardRecord.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.multitenancy.api; - -public record GiftCardRecord(String id, Integer initialValue, Integer remainingValue, String payload) { - -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/api/IssueCardCommand.java b/multitenancy/src/main/java/io/axoniq/multitenancy/api/IssueCardCommand.java deleted file mode 100644 index a1270c7e..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/api/IssueCardCommand.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.multitenancy.api; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public record IssueCardCommand(@TargetAggregateIdentifier String id, Integer amount) { - -} \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/domain/GiftCard.java b/multitenancy/src/main/java/io/axoniq/multitenancy/domain/GiftCard.java deleted file mode 100644 index 9513caf3..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/domain/GiftCard.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.axoniq.multitenancy.domain; - -import io.axoniq.multitenancy.api.AddFundsCommand; -import io.axoniq.multitenancy.api.FundsAddedEvent; -import io.axoniq.multitenancy.api.IssueCardCommand; -import io.axoniq.multitenancy.api.CardIssuedEvent; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; - -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@ProcessingGroup("giftcard") -@Aggregate -class GiftCard { - - private final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - @AggregateIdentifier - private String id; - private int remainingValue; - - public GiftCard() { - logger.debug("Empty constructor invoked"); - } - - @CommandHandler - GiftCard(IssueCardCommand command) throws InterruptedException { - logger.debug("handling {}", command); - Integer amount = command.amount(); - if (amount <= 0) { - throw new IllegalArgumentException("amount <= 0"); - } - - //simulate some work time - Thread.sleep(amount); - apply(new CardIssuedEvent(command.id(), amount)); - } - - @CommandHandler - void add(AddFundsCommand command) { - apply(new FundsAddedEvent(command.id(), command.amount())); - } - - @EventSourcingHandler - void on(FundsAddedEvent event) { - remainingValue += event.amount(); - logger.info("remaining amount is {} for account {}", remainingValue, id); - } - - @EventSourcingHandler - void on(CardIssuedEvent event) { - logger.debug("applying {}", event); - id = event.id(); - remainingValue = event.amount(); - logger.debug("new remaining value: {}", remainingValue); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardEntity.java b/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardEntity.java deleted file mode 100644 index fe68393a..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardEntity.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.axoniq.multitenancy.query; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -@Entity -class GiftCardEntity { - - @Id - private String id; - private Integer initialValue; - private Integer remainingValue; - - public GiftCardEntity(String id, Integer initialValue, Integer remainingValue) { - this.id = id; - this.initialValue = initialValue; - this.remainingValue = remainingValue; - } - - public GiftCardEntity() { - } - - public String getId() { - return id; - } - - public Integer getInitialValue() { - return initialValue; - } - - public Integer getRemainingValue() { - return remainingValue; - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardHandler.java b/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardHandler.java deleted file mode 100644 index b60f3803..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.axoniq.multitenancy.query; - -import io.axoniq.multitenancy.api.FindAllCardsQuery; -import io.axoniq.multitenancy.api.FindCardQuery; -import io.axoniq.multitenancy.api.GiftCardRecord; -import io.axoniq.multitenancy.api.CardIssuedEvent; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; -import org.axonframework.messaging.MetaData; -import org.axonframework.queryhandling.QueryHandler; -import org.axonframework.queryhandling.QueryUpdateEmitter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.lang.invoke.MethodHandles; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -@ProcessingGroup("giftcard") -@Component -class GiftCardHandler { - - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final GiftCardJpaRepository giftCardJpaRepository; - - public GiftCardHandler(GiftCardJpaRepository giftCardJpaRepository) { - this.giftCardJpaRepository = giftCardJpaRepository; - } - - @EventHandler - public void on(CardIssuedEvent event, QueryUpdateEmitter queryUpdateEmitter) { - /* - * Update our read model by inserting the new card. - */ - giftCardJpaRepository.save(new GiftCardEntity(event.id(), event.amount(), event.amount())); - - /* Send it to subscription queries of type FindGiftCardQry, but only if the card id matches. */ - queryUpdateEmitter.emit(FindCardQuery.class, - query -> Objects.equals(event.id(), query.id()), - new GiftCardRecord(event.id(), event.amount(), event.amount(), "payload") - ); - } - - @QueryHandler - public List handle(FindAllCardsQuery query, MetaData metaData) { - logger.debug("@" + metaData + " - " + "FindGiftCardQry: " + query); - return Collections.emptyList(); - } - - @QueryHandler - public Optional handle(FindCardQuery query, MetaData metaData) { - logger.debug("@" + metaData + " - " + "FindGiftCardQry: " + query); - return Optional.empty(); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardJpaRepository.java b/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardJpaRepository.java deleted file mode 100644 index f31115b7..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/query/GiftCardJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.axoniq.multitenancy.query; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface GiftCardJpaRepository extends CrudRepository { - -} \ No newline at end of file diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/ActionView.java b/multitenancy/src/main/java/io/axoniq/multitenancy/ui/ActionView.java deleted file mode 100644 index ee7973f4..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/ActionView.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.axoniq.multitenancy.ui; - -import com.vaadin.flow.component.Text; -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.details.Details; -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.html.H1; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.BeforeEvent; -import com.vaadin.flow.router.HasUrlParameter; -import com.vaadin.flow.router.Route; -import io.axoniq.multitenancy.web.MessageService; -import io.axoniq.multitenancy.web.ResetService; - -@Route("action") -public class ActionView extends VerticalLayout implements HasUrlParameter { - - private final MessageService messageService; - private final ResetService resetService; - - public ActionView(MessageService messageService, - ResetService resetService) { - this.messageService = messageService; - this.resetService = resetService; - } - - @Override - public void setParameter(BeforeEvent beforeEvent, String tenantName) { - VerticalLayout layout = new VerticalLayout(); - - H1 title = new H1("Actions (" + tenantName + ")"); - layout.add(title); - - layout.add(new Text("Do something:")); - - layout.add(new Button("Send commands...", evt -> { - messageService.sendCommands(tenantName); - showSuccess(tenantName); - })); - - - layout.add(new Button("Publish events...", evt -> { - messageService.sendEvents(tenantName); - showSuccess(tenantName); - })); - - layout.add(new Button("Schedule events...", evt -> { - messageService.scheduleEvents(tenantName); - showSuccess(tenantName); - })); - - layout.add(new Button("Send queries...", evt -> { - messageService.sendQueries(tenantName); - showSuccess(tenantName); - })); - - layout.add(new Button("Send subscription query...", evt -> { - messageService.subscriptionQuery(tenantName).block(); - showSuccess(tenantName); - })); - - layout.add(new Button("Reset projections...", evt -> { - Dialog dialog = new Dialog(); - - dialog.setHeaderTitle("Reset"); - VerticalLayout content = new VerticalLayout(); - content.setSpacing(false); - content.setPadding(false); - - resetService.listTenantEventProcessors(tenantName) - .forEach(ep -> { - Button resetEp = new Button(ep, e -> { - resetService.reset(ep); - showSuccess(tenantName); - }); - content.add(resetEp); - }); - - dialog.add(content); - Button cancelButton = new Button("Close", e -> dialog.close()); - dialog.getFooter().add(cancelButton); - dialog.open(); - })); - - layout.add(new Button("Explore projections...", evt -> { - Dialog dialog = new Dialog(); - - dialog.setHeaderTitle("Explore projections"); - - Span name = new Span("JDBC URL: jdbc:h2:mem:" + tenantName); - Span email = new Span("username: sa"); - - VerticalLayout content = new VerticalLayout(name, email); - content.setSpacing(false); - content.setPadding(false); - - Details details = new Details("Login details", content); - details.setOpened(true); - dialog.add(content); - Button goButton = new Button("Open DB", - e -> UI.getCurrent().getPage() - .open("http://localhost:8080/h2-console", "_blank")); - Button cancelButton = new Button("Close", e -> dialog.close()); - dialog.getFooter().add(goButton); - dialog.getFooter().add(cancelButton); - dialog.open(); - })); - - layout.setAlignItems(Alignment.CENTER); - - Button register = new Button("Or go back...", event -> UI.getCurrent().navigate("")); - register.addThemeVariants(ButtonVariant.LUMO_CONTRAST); - layout.add(register); - - add(layout); - } - - private void showSuccess(String tenantName) { - Notification notification = Notification.show( - "Action done! Check Axon Server dashboard and stats/events for context: " + tenantName - ); - notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/MainView.java b/multitenancy/src/main/java/io/axoniq/multitenancy/ui/MainView.java deleted file mode 100644 index 1b9f6e9a..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/MainView.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.axoniq.multitenancy.ui; - -import com.vaadin.flow.component.Text; -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.html.H1; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.Route; -import org.axonframework.extensions.multitenancy.components.TenantDescriptor; -import org.axonframework.extensions.multitenancy.components.TenantProvider; - -@Route -public class MainView extends VerticalLayout { - - public MainView(TenantProvider tenantProvider) { - VerticalLayout layout = new VerticalLayout(); - - H1 title = new H1("Login"); - layout.add(title); - - layout.add(new Text("Available tenants:")); - - tenantProvider - .getTenants() - .stream() - .map(TenantDescriptor::tenantId) - .map(name -> new Button(name, event -> UI.getCurrent().navigate("action/" + name))) - .forEach(layout::add); - - layout.setAlignItems(Alignment.CENTER); - - - Button register = new Button("Or register new...", event -> UI.getCurrent().navigate("register")); - register.addThemeVariants(ButtonVariant.LUMO_CONTRAST); - layout.add(register); - - add(layout); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/RegisterView.java b/multitenancy/src/main/java/io/axoniq/multitenancy/ui/RegisterView.java deleted file mode 100644 index 57b6caec..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/RegisterView.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.axoniq.multitenancy.ui; - -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.html.H1; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.Route; -import io.axoniq.multitenancy.web.CreateTenantService; - -@Route("/register") -public class RegisterView extends VerticalLayout { - - public RegisterView(CreateTenantService createTenantService) { - H1 title = new H1("Register tenant"); - - VerticalLayout layout = new VerticalLayout(); - layout.setAlignItems(Alignment.CENTER); - TextField tenantName = new TextField("Tenant name"); - - layout.add(title); - - layout.add(tenantName); - - Button submitButton = new Button("Register"); - submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - layout.add(submitButton); - - submitButton.getStyle().set("padding", "25px"); - - add(layout); - - - submitButton.addClickListener(e -> { - String tenantNameValue = tenantName.getValue(); - - createTenantService - .createTenant("tenant-" + tenantNameValue, "default", true) - .exceptionally(err -> { - showError(err.getMessage()); - return null; - }).join(); - - showSuccess(tenantNameValue); - - try { - Thread.sleep(2500); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - - new Button("redirect", event -> UI.getCurrent().navigate("")).click(); - }); - } - - private void showSuccess(String tenantName) { - Notification notification = Notification.show("Tenant created with username: tenant-" + tenantName); - notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS); - } - - private void showError(String error) { - Notification notification = Notification.show("Error has occurred: " + error); - notification.addThemeVariants(NotificationVariant.LUMO_ERROR); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/UIConfig.java b/multitenancy/src/main/java/io/axoniq/multitenancy/ui/UIConfig.java deleted file mode 100644 index e458fddf..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/ui/UIConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.axoniq.multitenancy.ui; - -import com.vaadin.flow.component.page.AppShellConfigurator; -import com.vaadin.flow.theme.Theme; -import com.vaadin.flow.theme.lumo.Lumo; - -@Theme(variant = Lumo.DARK) -class UIConfig implements AppShellConfigurator { - -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/web/CreateTenantService.java b/multitenancy/src/main/java/io/axoniq/multitenancy/web/CreateTenantService.java deleted file mode 100644 index 574f7d1f..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/web/CreateTenantService.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2020-2020. AxonIQ - * - * 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 io.axoniq.multitenancy.web; - -import io.axoniq.axonserver.connector.AxonServerConnection; -import io.axoniq.axonserver.grpc.admin.CreateContextRequest; -import org.axonframework.axonserver.connector.AxonServerConnectionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.stereotype.Service; - -import java.util.concurrent.CompletableFuture; - -@Service -public class CreateTenantService { - - private final AxonServerConnectionManager axonServerConnectionManager; - - - public CreateTenantService(AxonServerConnectionManager axonServerConnectionManager) { - this.axonServerConnectionManager = axonServerConnectionManager; - } - - public CompletableFuture createTenant(String tenantName, - String replicationGroup, - boolean initializeSchema) { - AxonServerConnection admin = axonServerConnectionManager.getConnection("_admin"); - CreateContextRequest createContextRequest = CreateContextRequest.newBuilder() - .setName(tenantName) - .setReplicationGroupName(replicationGroup) - .build(); - - return CompletableFuture.completedFuture(null) - .thenRun(() -> { - if (initializeSchema) { - new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .setName(tenantName + ";INIT=create " + - "schema if not exists " + - "schema_a\\;create schema if not exists schema_b;" + - "DB_CLOSE_DELAY=-1;") - .addScript("db/migration/V0.sql") - .build(); - } - }) - .thenCompose(s -> admin.adminChannel().createContext(createContextRequest)); - } -} diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/web/MessageService.java b/multitenancy/src/main/java/io/axoniq/multitenancy/web/MessageService.java deleted file mode 100644 index 22d18126..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/web/MessageService.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.axoniq.multitenancy.web; - -import io.axoniq.multitenancy.api.AddFundsCommand; -import io.axoniq.multitenancy.api.CardIssuedEvent; -import io.axoniq.multitenancy.api.FindAllCardsQuery; -import io.axoniq.multitenancy.api.FindCardQuery; -import io.axoniq.multitenancy.api.GiftCardRecord; -import io.axoniq.multitenancy.api.IssueCardCommand; -import org.axonframework.commandhandling.CommandMessage; -import org.axonframework.commandhandling.GenericCommandMessage; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.eventhandling.EventBus; -import org.axonframework.eventhandling.EventMessage; -import org.axonframework.eventhandling.GenericEventMessage; -import org.axonframework.eventhandling.scheduling.EventScheduler; -import org.axonframework.messaging.MetaData; -import org.axonframework.messaging.responsetypes.ResponseTypes; -import org.axonframework.queryhandling.GenericQueryMessage; -import org.axonframework.queryhandling.QueryGateway; -import org.axonframework.queryhandling.QueryMessage; -import org.axonframework.queryhandling.SubscriptionQueryResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -import java.lang.invoke.MethodHandles; -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static java.time.temporal.ChronoUnit.MINUTES; -import static org.axonframework.commandhandling.GenericCommandMessage.asCommandMessage; -import static org.axonframework.extensions.multitenancy.autoconfig.TenantConfiguration.TENANT_CORRELATION_KEY; - -@Service -public class MessageService { - - private final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final CommandGateway commandGateway; - private final QueryGateway queryGateway; - private final EventBus eventBus; - - private final EventScheduler eventScheduler; - - public MessageService(CommandGateway commandGateway, QueryGateway queryGateway, EventBus eventBus, EventScheduler eventScheduler) { - this.commandGateway = commandGateway; - this.queryGateway = queryGateway; - this.eventBus = eventBus; - this.eventScheduler = eventScheduler; - } - - public void sendQueries(String tenantName) { - for (int i = 0; i < 50; i++) { - logger.info("Sending {}/{} queries delayed by {}", i + 1, 50, 50); - - QueryMessage> query = - new GenericQueryMessage<>( - new FindAllCardsQuery(), - ResponseTypes.multipleInstancesOf(GiftCardRecord.class) - ).withMetaData(Collections.singletonMap(TENANT_CORRELATION_KEY, tenantName)); - - queryGateway.query(query, ResponseTypes.multipleInstancesOf(GiftCardRecord.class)); - } - } - - public Mono subscriptionQuery(String tenantName) { - final String giftCardId = UUID.randomUUID().toString(); - QueryMessage> query = - new GenericQueryMessage<>( - new FindCardQuery(giftCardId), - ResponseTypes.optionalInstanceOf(GiftCardRecord.class) - ).withMetaData(Collections.singletonMap(TENANT_CORRELATION_KEY, tenantName)); - - SubscriptionQueryResult, GiftCardRecord> queryResult = queryGateway.subscriptionQuery( - query, - ResponseTypes.optionalInstanceOf(GiftCardRecord.class), - ResponseTypes.instanceOf(GiftCardRecord.class)); - - return sendAndReturnUpdate(new IssueCardCommand(giftCardId, 50), queryResult, tenantName) - .map(GiftCardRecord::id); - } - - public Mono sendAndReturnUpdate(Object command, SubscriptionQueryResult result, String tenantName) { - MetaData tenantMetaData = MetaData.with(TENANT_CORRELATION_KEY, tenantName); - CommandMessage commandMessage = asCommandMessage(command).withMetaData(tenantMetaData); - - return Mono.when(result.initialResult()) - .then(Mono.fromCompletionStage(() -> commandGateway.send(commandMessage))) - .thenMany(result.updates()) - .timeout(Duration.ofSeconds(180)) - .next() - .doFinally(unused -> result.cancel()); - } - - - public void sendEvents(String tenantName) { - final String giftCardId = UUID.randomUUID().toString(); - MetaData tenantMetaData = MetaData.with(TENANT_CORRELATION_KEY, tenantName); - CommandMessage commandMessage = - GenericCommandMessage.asCommandMessage( - new IssueCardCommand(giftCardId, 1) - ).withMetaData(tenantMetaData); - - commandGateway.sendAndWait(commandMessage); - - for (int i = 0; i < 50; i++) { - logger.info("Sending {}/{} events delayed by {}", i + 1, 50, 50); - EventMessage eventMessage = GenericEventMessage.asEventMessage(new CardIssuedEvent(giftCardId, i)) - .withMetaData(tenantMetaData); - eventBus.publish(eventMessage); - } - } - - public void scheduleEvents(String tenantName) { - final String giftCardId = UUID.randomUUID().toString(); - MetaData tenantMetaData = MetaData.with(TENANT_CORRELATION_KEY, tenantName); - CommandMessage commandMessage = - GenericCommandMessage.asCommandMessage( - new IssueCardCommand(giftCardId, 1) - ).withMetaData(tenantMetaData); - - commandGateway.sendAndWait(commandMessage); - - for (int i = 0; i < 50; i++) { - logger.info("Sending {}/{} events delayed by {}", i + 1, 50, 50); - EventMessage eventMessage = GenericEventMessage.asEventMessage(new CardIssuedEvent(giftCardId, i)) - .withMetaData(tenantMetaData); - eventScheduler.schedule(Instant.now().plus(5, MINUTES), eventMessage); - } - } - - public void sendCommands(String tenantName) { - logger.info("Starting!"); - - final String giftCardId = UUID.randomUUID().toString(); - MetaData tenantMetaData = MetaData.with(TENANT_CORRELATION_KEY, tenantName); - - CommandMessage issueCmd = - GenericCommandMessage.asCommandMessage( - new IssueCardCommand(giftCardId, 1) - ).withMetaData(tenantMetaData); - commandGateway.sendAndWait(issueCmd); - - for (int i = 0; i < 50; i++) { - CommandMessage addCmd = GenericCommandMessage.asCommandMessage(new AddFundsCommand(giftCardId, 1)) - .withMetaData(tenantMetaData); - commandGateway.sendAndWait(addCmd); - } - - logger.info("Done!"); - } -} - - - - - - - - - - - - - - - - - - - - diff --git a/multitenancy/src/main/java/io/axoniq/multitenancy/web/ResetService.java b/multitenancy/src/main/java/io/axoniq/multitenancy/web/ResetService.java deleted file mode 100644 index f8cdb8a3..00000000 --- a/multitenancy/src/main/java/io/axoniq/multitenancy/web/ResetService.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020-2020. AxonIQ - * - * 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 io.axoniq.multitenancy.web; - -import org.axonframework.config.Configuration; -import org.axonframework.eventhandling.StreamingEventProcessor; -import org.axonframework.extensions.multitenancy.components.eventhandeling.MultiTenantEventProcessor; -import org.axonframework.messaging.StreamableMessageSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.PostMapping; - -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class ResetService { - - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final Configuration configuration; - - public ResetService(Configuration configuration) { - this.configuration = configuration; - } - - public List listTenantEventProcessors(String tenantName) { - return configuration.eventProcessingConfiguration() - .eventProcessors().keySet().stream() - .filter(eventProcessor -> eventProcessor.contains("@" + tenantName)) - .collect(Collectors.toList()); - } - - public void reset(String processingGroup) { - Assert.hasLength(processingGroup, "Processing Group is mandatory and can't be empty!"); - - configuration.eventProcessingConfiguration() - .eventProcessorByProcessingGroup(processingGroup, StreamingEventProcessor.class) - .ifPresent(streamingEventProcessor -> { - if (streamingEventProcessor.supportsReset()) { - logger.info("Trigger ResetTriggeredEvent for processingGroup {}", processingGroup); - streamingEventProcessor.shutDown(); - streamingEventProcessor.resetTokens(StreamableMessageSource::createTailToken); - streamingEventProcessor.start(); - } - }); - } - - @PostMapping(path = "/reset") - public void resetAll() { - configuration.eventProcessingConfiguration() - .eventProcessors().values().stream() - .filter(instance -> instance instanceof MultiTenantEventProcessor) - .findFirst() - .ifPresent(processor -> resetTenantSpecificsProcessorsFor((MultiTenantEventProcessor) processor)); - } - - private static void resetTenantSpecificsProcessorsFor(MultiTenantEventProcessor processor) { - processor.tenantSegments() - .values() - .stream() - .filter(i -> i instanceof StreamingEventProcessor) - .map(i -> (StreamingEventProcessor) i) - .forEach(eventProcessor -> { - if (eventProcessor.supportsReset()) { - eventProcessor.shutDown(); - eventProcessor.resetTokens(StreamableMessageSource::createTailToken); - eventProcessor.start(); - } - }); - } -} diff --git a/multitenancy/src/main/resources/application.properties b/multitenancy/src/main/resources/application.properties deleted file mode 100644 index 3a72e09c..00000000 --- a/multitenancy/src/main/resources/application.properties +++ /dev/null @@ -1,36 +0,0 @@ -# -# Copyright (c) 2020-2020. AxonIQ -# -# 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. -# - -spring.application.name=multitenant-example - -axon.serializer.general=jackson -axon.serializer.events=jackson -axon.serializer.messages=jackson - -# Datasource -spring.datasource.url=jdbc:h2:mem:tenant-one -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console - - -axon.eventhandling.processors.giftcard.mode=SUBSCRIBING -axon.eventhandling.processors.giftcard.source=giftcard-bean -axon.axonserver.persistent-streams[giftcard-bean].name=giftcard -axon.axonserver.persistent-streams[giftcard-bean].batch-size=100 -axon.axonserver.persistent-streams[giftcard-bean].initial-segment-count=2 -axon.axonserver.persistent-streams[giftcard-bean].initial-position=HEAD \ No newline at end of file diff --git a/multitenancy/src/main/resources/db/migration/V0.sql b/multitenancy/src/main/resources/db/migration/V0.sql deleted file mode 100644 index 013fc873..00000000 --- a/multitenancy/src/main/resources/db/migration/V0.sql +++ /dev/null @@ -1,100 +0,0 @@ -; -CREATE USER IF NOT EXISTS "SA" SALT '4ebb585a10373988' HASH '39a4dfa89574866f177d898b9f7c4749c526efc4ae92df425e000470068ff594' ADMIN; -CREATE SEQUENCE "PUBLIC"."HIBERNATE_SEQUENCE" START WITH 1; -CREATE MEMORY TABLE "PUBLIC"."ASSOCIATION_VALUE_ENTRY"( - "ID" BIGINT NOT NULL, - "ASSOCIATION_KEY" VARCHAR(255) NOT NULL, - "ASSOCIATION_VALUE" VARCHAR(255), - "SAGA_ID" VARCHAR(255) NOT NULL, - "SAGA_TYPE" VARCHAR(255) -); -ALTER TABLE "PUBLIC"."ASSOCIATION_VALUE_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_B" PRIMARY KEY("ID"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.ASSOCIATION_VALUE_ENTRY; -CREATE INDEX "PUBLIC"."IDXK45EQNXKGD8HPDN6XIXN8SGFT" ON "PUBLIC"."ASSOCIATION_VALUE_ENTRY"("SAGA_TYPE", "ASSOCIATION_KEY", "ASSOCIATION_VALUE"); -CREATE INDEX "PUBLIC"."IDXGV5K1V2MH6FRXUY5C0HGBAU94" ON "PUBLIC"."ASSOCIATION_VALUE_ENTRY"("SAGA_ID", "SAGA_TYPE"); -CREATE MEMORY TABLE "PUBLIC"."DEAD_LETTER_ENTRY"( - "DEAD_LETTER_ID" VARCHAR(255) NOT NULL, - "CAUSE_MESSAGE" VARCHAR(255), - "CAUSE_TYPE" VARCHAR(255), - "DIAGNOSTICS" BLOB, - "ENQUEUED_AT" TIMESTAMP NOT NULL, - "INDEX" BIGINT NOT NULL, - "LAST_TOUCHED" TIMESTAMP, - "AGGREGATE_IDENTIFIER" VARCHAR(255), - "EVENT_IDENTIFIER" VARCHAR(255) NOT NULL, - "MESSAGE_TYPE" VARCHAR(255) NOT NULL, - "META_DATA" BLOB, - "PAYLOAD" BLOB NOT NULL, - "PAYLOAD_REVISION" VARCHAR(255), - "PAYLOAD_TYPE" VARCHAR(255) NOT NULL, - "SEQUENCE_NUMBER" BIGINT, - "TIME_STAMP" VARCHAR(255) NOT NULL, - "TOKEN" BLOB, - "TOKEN_TYPE" VARCHAR(255), - "TYPE" VARCHAR(255), - "PROCESSING_GROUP" VARCHAR(255) NOT NULL, - "PROCESSING_STARTED" TIMESTAMP, - "SEQUENCE_IDENTIFIER" VARCHAR(255) NOT NULL -); -ALTER TABLE "PUBLIC"."DEAD_LETTER_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_5" PRIMARY KEY("DEAD_LETTER_ID"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.DEAD_LETTER_ENTRY; -CREATE INDEX "PUBLIC"."IDXE67WCX5FIQ9HL4Y4QKHLCJ9CG" ON "PUBLIC"."DEAD_LETTER_ENTRY"("PROCESSING_GROUP"); -CREATE INDEX "PUBLIC"."IDXRWUCPGS6SN93LDGOEH2Q9K6BN" ON "PUBLIC"."DEAD_LETTER_ENTRY"("PROCESSING_GROUP", "SEQUENCE_IDENTIFIER"); -CREATE MEMORY TABLE "PUBLIC"."DOMAIN_EVENT_ENTRY"( - "GLOBAL_INDEX" BIGINT NOT NULL, - "EVENT_IDENTIFIER" VARCHAR(255) NOT NULL, - "META_DATA" BLOB, - "PAYLOAD" BLOB NOT NULL, - "PAYLOAD_REVISION" VARCHAR(255), - "PAYLOAD_TYPE" VARCHAR(255) NOT NULL, - "TIME_STAMP" VARCHAR(255) NOT NULL, - "AGGREGATE_IDENTIFIER" VARCHAR(255) NOT NULL, - "SEQUENCE_NUMBER" BIGINT NOT NULL, - "TYPE" VARCHAR(255) -); -ALTER TABLE "PUBLIC"."DOMAIN_EVENT_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_8" PRIMARY KEY("GLOBAL_INDEX"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.DOMAIN_EVENT_ENTRY; -CREATE MEMORY TABLE "PUBLIC"."GIFT_CARD_ENTITY"( - "ID" VARCHAR(255) NOT NULL, - "INITIAL_VALUE" INTEGER, - "REMAINING_VALUE" INTEGER -); -ALTER TABLE "PUBLIC"."GIFT_CARD_ENTITY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_3" PRIMARY KEY("ID"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.GIFT_CARD_ENTITY; -CREATE MEMORY TABLE "PUBLIC"."SAGA_ENTRY"( - "SAGA_ID" VARCHAR(255) NOT NULL, - "REVISION" VARCHAR(255), - "SAGA_TYPE" VARCHAR(255), - "SERIALIZED_SAGA" BLOB -); -ALTER TABLE "PUBLIC"."SAGA_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_BD" PRIMARY KEY("SAGA_ID"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.SAGA_ENTRY; -CREATE MEMORY TABLE "PUBLIC"."SNAPSHOT_EVENT_ENTRY"( - "AGGREGATE_IDENTIFIER" VARCHAR(255) NOT NULL, - "SEQUENCE_NUMBER" BIGINT NOT NULL, - "TYPE" VARCHAR(255) NOT NULL, - "EVENT_IDENTIFIER" VARCHAR(255) NOT NULL, - "META_DATA" BLOB, - "PAYLOAD" BLOB NOT NULL, - "PAYLOAD_REVISION" VARCHAR(255), - "PAYLOAD_TYPE" VARCHAR(255) NOT NULL, - "TIME_STAMP" VARCHAR(255) NOT NULL -); -ALTER TABLE "PUBLIC"."SNAPSHOT_EVENT_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_E" PRIMARY KEY("AGGREGATE_IDENTIFIER", "SEQUENCE_NUMBER", "TYPE"); --- 0 +/- SELECT COUNT(*) FROM PUBLIC.SNAPSHOT_EVENT_ENTRY; -CREATE MEMORY TABLE "PUBLIC"."TOKEN_ENTRY"( - "PROCESSOR_NAME" VARCHAR(255) NOT NULL, - "SEGMENT" INTEGER NOT NULL, - "OWNER" VARCHAR(255), - "TIMESTAMP" VARCHAR(255) NOT NULL, - "TOKEN" BLOB, - "TOKEN_TYPE" VARCHAR(255) -); -ALTER TABLE "PUBLIC"."TOKEN_ENTRY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_7" PRIMARY KEY("PROCESSOR_NAME", "SEGMENT"); --- 1 +/- SELECT COUNT(*) FROM PUBLIC.TOKEN_ENTRY; -INSERT INTO "PUBLIC"."TOKEN_ENTRY" VALUES - ('io.axoniq.multitenancy.query', 0, '49420@stefan', '2022-09-26T15:17:14.914Z', NULL, NULL); -ALTER TABLE "PUBLIC"."DOMAIN_EVENT_ENTRY" ADD CONSTRAINT "PUBLIC"."UK8S1F994P4LA2IPB13ME2XQM1W" UNIQUE("AGGREGATE_IDENTIFIER", "SEQUENCE_NUMBER"); -ALTER TABLE "PUBLIC"."DOMAIN_EVENT_ENTRY" ADD CONSTRAINT "PUBLIC"."UK_FWE6LSA8BFO6HYAS6UD3M8C7X" UNIQUE("EVENT_IDENTIFIER"); -ALTER TABLE "PUBLIC"."DEAD_LETTER_ENTRY" ADD CONSTRAINT "PUBLIC"."UK5O90GYVTPSXM46GIIRSKCL486" UNIQUE("PROCESSING_GROUP", "SEQUENCE_IDENTIFIER", "INDEX"); -ALTER TABLE "PUBLIC"."SNAPSHOT_EVENT_ENTRY" ADD CONSTRAINT "PUBLIC"."UK_E1UUCJSEO68GOPMND0VGDL44H" UNIQUE("EVENT_IDENTIFIER"); diff --git a/order-fulfillment-workflow/pom.xml b/order-fulfillment-workflow/pom.xml index 713365d7..ea5f44ac 100644 --- a/order-fulfillment-workflow/pom.xml +++ b/order-fulfillment-workflow/pom.xml @@ -16,10 +16,10 @@ 21 UTF-8 - 5.1.0 - 5.1.0 - 0.1.0 - 4.0.6 + 5.3.1 + 5.3.1 + 0.2.0 + 4.1.0 2.0.5 @@ -59,8 +59,8 @@ - io.axoniq.framework.workflow - axon-workflow-spring-boot + io.axoniq.framework + axoniq-workflow-spring-boot ${axon-workflow.version} @@ -86,8 +86,8 @@ - io.axoniq.framework.workflow - axon-workflow-test + io.axoniq.framework + axoniq-workflow-test ${axon-workflow.version} test diff --git a/order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/workflow/OrderFulfillmentWorkflow.java b/order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/workflow/OrderFulfillmentWorkflow.java index 685989e6..fff9ac24 100644 --- a/order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/workflow/OrderFulfillmentWorkflow.java +++ b/order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/workflow/OrderFulfillmentWorkflow.java @@ -1,5 +1,6 @@ package io.axoniq.demo.orderfulfillment.workflow; +import io.axoniq.demo.orderfulfillment.api.OrderPlaced; import io.axoniq.demo.orderfulfillment.api.PaymentConfirmed; import io.axoniq.demo.orderfulfillment.service.FailureRecorder; import io.axoniq.demo.orderfulfillment.service.InventoryService; @@ -17,9 +18,9 @@ import java.util.Map; import java.util.UUID; -import static io.axoniq.workflow.dsl.api.AssociationsUtils.associate; -import static io.axoniq.workflow.dsl.simple.SimpleWorkflowContext.equalsTo; -import static io.axoniq.workflow.runtime.association.PayloadPropertyValueRetriever.payloadProperty; +import static io.axoniq.workflow.dsl.api.EventAssociationsUtils.equalsTo; +import static io.axoniq.workflow.dsl.api.EventAssociationsUtils.payloadProperty; +import static io.axoniq.workflow.runtime.association.Associations.associate; import static io.axoniq.workflow.runtime.execution.DefaultEventNameCustomizer.Builder.baseName; import static io.axoniq.workflow.runtime.execution.DefaultEventNameCustomizer.Builder.namespace; @@ -48,7 +49,7 @@ public OrderFulfillmentWorkflow(InventoryService inventory, @Workflow( idProperty = "orderId", - startOnEvent = "io.axoniq.demo.orderfulfillment.api.OrderPlaced", + startOnEventClass = OrderPlaced.class, workflowName = "OrderFulfillmentWorkflow" ) public void execute(SimpleWorkflowContext ctx) { @@ -73,22 +74,20 @@ public void execute(SimpleWorkflowContext ctx) { "awaitPayment", PaymentConfirmed.class, associate(payloadProperty("orderId"), equalsTo(orderId)), - paymentTimeout + step -> step.timeout(paymentTimeout) ); - var reserved = ctx.awaitExecute( + var reserved = (Boolean) ctx.awaitExecute( "reserveStock", Map.of("customerId", customerId, "amount", amount, "scenario", scenario), - Boolean.class, - inventory::reserveStock - ); + (pc, p) -> Map.of("result", inventory.reserveStock(p)) + ).get("result"); if (!reserved) { paymentConfirmation.cancel("Stock unavailable"); ctx.awaitExecute( "recordOutOfStock", Map.of("orderId", orderId, "reason", "Out of stock"), - Boolean.class, - failures::record + (pc, p) -> Map.of("result", failures.record(p)) ); ctx.fail(new RuntimeException("Stock unavailable for order " + orderId)); return; @@ -101,18 +100,18 @@ public void execute(SimpleWorkflowContext ctx) { payment.initiatePayment(p); return Map.of(); }, - Duration.ofSeconds(30), - baseName("InitiatingPaymentForCustomer").namespace("io.axoniq.demo.orderfulfillment.api") + def -> def.timeout(Duration.ofSeconds(30)) + .eventNameCustomizer(baseName("InitiatingPaymentForCustomer") + .namespace("io.axoniq.demo.orderfulfillment.api")) ); paymentConfirmation.await(); - var confirmation = paymentConfirmation.>result(); + var confirmation = paymentConfirmation.result(); if (confirmation.isEmpty()) { ctx.awaitExecute( "recordPaymentTimeout", Map.of("orderId", orderId, "reason", "Payment timed out"), - Boolean.class, - failures::record + (pc, p) -> Map.of("result", failures.record(p)) ); ctx.fail(new RuntimeException("Payment timed out for order " + orderId)); return; @@ -135,8 +134,8 @@ public void execute(SimpleWorkflowContext ctx) { "shipOrder", shipPayload, (pc, p) -> shipping.shipOrder(p), - Duration.ofSeconds(30), - namespace("io.axoniq.demo.orderfulfillment.api") + def -> def.timeout(Duration.ofSeconds(30)) + .eventNameCustomizer(namespace("io.axoniq.demo.orderfulfillment.api")) ); ctx.awaitExecute("notifyCustomer", Boolean.class, () -> { diff --git a/pom.xml b/pom.xml index f2cc339b..c844ef6f 100644 --- a/pom.xml +++ b/pom.xml @@ -16,19 +16,14 @@ axon-spring-template distributed-exceptions - multitenancy order-fulfillment-workflow reset-handler - saga serialization-avro sequencing-policy - set-based-validation - set-based-validation-actor-model snapshots stateful-event-handler subscription-query-rest subscription-query-streaming - upcaster workflow-saga @@ -36,81 +31,50 @@ UTF-8 UTF-8 - 17 + 21 2.2.20 2.2.20 - 17 - 17 + 21 + 21 - 4.12.1 + 5.3.1 - 3.5.6 + 4.1.0 - 6.2.6.Final + 7.4.5.Final 3.0.2 - 3.7.11 + 3.8.6 1.3 - 6.0.0 - 5.20.0 - 1.21.3 + 6.1.3 + 5.23.0 + 2.0.5 3.14.1 - 3.5.4 + 3.5.6 - + - org.axonframework - axon-bom - ${axon-bom.version} + io.axoniq.framework + axoniq-framework-bom + ${axoniq-framework.version} pom import - - io.axoniq - axonserver-connector-java - 2025.2.0-EAP - - - org.axonframework - axon-server-connector - 4.12.1 - - - org.axonframework - axon-messaging - 4.12.1 - - - org.axonframework - axon-spring-boot-autoconfigure - 4.12.1 - org.springframework.boot - spring-boot-starter-parent + spring-boot-dependencies ${spring-boot.version} pom import - - org.springframework.boot - spring-boot-starter-webflux - ${spring-boot.version} - - - - org.yaml - snakeyaml - - - io.projectreactor @@ -143,9 +107,10 @@ org.testcontainers - junit-jupiter + testcontainers-bom ${testcontainers.version} - test + pom + import org.mockito @@ -184,8 +149,8 @@ org.apache.maven.plugins maven-compiler-plugin - 17 - 17 + 21 + 21 true diff --git a/reset-handler/README.md b/reset-handler/README.md index 8ef63aa5..2e876581 100644 --- a/reset-handler/README.md +++ b/reset-handler/README.md @@ -18,8 +18,17 @@ Processor Name to be able to reset it and start it back again. ### Using Axon Framework Axon Framework provides another easy way to do it using the `StreamingEventProcessor` methods, -namely `shutDown`, `resetTokens` and `start`. When doing it through Axon Framework, the application instance doing the -operation should be the one having the claim of the token. +namely `shutdown`, `resetTokens` and `start`. Since Axon Framework 5, `TrackingEventProcessor` has been removed in +favor of `PooledStreamingEventProcessor`, which is the only implementation of `StreamingEventProcessor` you'll +encounter, and these methods are now asynchronous, returning a `CompletableFuture` instead of blocking. When doing +it through Axon Framework, the application instance doing the operation should be the one having the claim of the +token. + +> Note: as of Axon Framework 5, tokens carry a `mask` used to support (un)claiming individual segments. This sample +> has no meaningful data to preserve across a reset, so it always performs a full reset to the start of the event +> stream (`resetTokens()`) rather than migrating an existing token's mask. If you're adapting this sample to a real +> application with an existing token store, consult the Axon Framework 5 migration guide for the token store schema +> changes before resetting tokens that must retain their claim/mask state. > We recommend checking > the [FrameworkEventProcessorRestController.java](https://github.com/AxonIQ/code-samples/blob/master/reset-handler/src/main/java/io/axoniq/framework/FrameworkEventProcessorRestController.java) @@ -44,5 +53,5 @@ the [EventProcessorService.java](https://github.com/AxonIQ/code-samples/blob/mas class and the added javadoc should be enough to explain what it does. For the Axon Framework version, we recommend checking the -official [StreamingEventProcessor.java](https://github.com/AxonFramework/AxonFramework/blob/master/messaging/src/main/java/org/axonframework/eventhandling/StreamingEventProcessor.java) +official [StreamingEventProcessor.java](https://github.com/AxonFramework/AxonFramework/blob/main/messaging/src/main/java/org/axonframework/messaging/eventhandling/processing/streaming/StreamingEventProcessor.java) documentation. diff --git a/reset-handler/pom.xml b/reset-handler/pom.xml index b138164e..b8f777c1 100644 --- a/reset-handler/pom.xml +++ b/reset-handler/pom.xml @@ -19,7 +19,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter diff --git a/reset-handler/src/main/java/io/axoniq/EventRestController.java b/reset-handler/src/main/java/io/axoniq/EventRestController.java index fbafd642..56bd4bc3 100644 --- a/reset-handler/src/main/java/io/axoniq/EventRestController.java +++ b/reset-handler/src/main/java/io/axoniq/EventRestController.java @@ -1,6 +1,6 @@ package io.axoniq; -import org.axonframework.eventhandling.gateway.EventGateway; +import org.axonframework.messaging.eventhandling.gateway.EventGateway; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,6 +15,8 @@ public EventRestController(EventGateway eventGateway) { @GetMapping("event") public void event() { - eventGateway.publish(new Object()); + // AF5's EventGateway#publish requires an (optional) ProcessingContext as the first argument. + // There's none available here, so we pass null. + eventGateway.publish(null, new Object()); } } diff --git a/reset-handler/src/main/java/io/axoniq/MyFakeProjection.java b/reset-handler/src/main/java/io/axoniq/MyFakeProjection.java index ddc9f7bc..0762c43f 100644 --- a/reset-handler/src/main/java/io/axoniq/MyFakeProjection.java +++ b/reset-handler/src/main/java/io/axoniq/MyFakeProjection.java @@ -1,6 +1,6 @@ package io.axoniq; -import org.axonframework.eventhandling.EventHandler; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; import org.springframework.stereotype.Component; @Component diff --git a/reset-handler/src/main/java/io/axoniq/ResetHandlerSampleApplication.java b/reset-handler/src/main/java/io/axoniq/ResetHandlerSampleApplication.java index 4b5ebe79..d34e46e2 100644 --- a/reset-handler/src/main/java/io/axoniq/ResetHandlerSampleApplication.java +++ b/reset-handler/src/main/java/io/axoniq/ResetHandlerSampleApplication.java @@ -1,7 +1,7 @@ package io.axoniq; -import org.axonframework.eventhandling.tokenstore.TokenStore; -import org.axonframework.eventhandling.tokenstore.inmemory.InMemoryTokenStore; +import org.axonframework.messaging.eventhandling.processing.streaming.token.store.TokenStore; +import org.axonframework.messaging.eventhandling.processing.streaming.token.store.inmemory.InMemoryTokenStore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; diff --git a/reset-handler/src/main/java/io/axoniq/framework/FrameworkEventProcessorRestController.java b/reset-handler/src/main/java/io/axoniq/framework/FrameworkEventProcessorRestController.java index bcdb5a49..eb150761 100644 --- a/reset-handler/src/main/java/io/axoniq/framework/FrameworkEventProcessorRestController.java +++ b/reset-handler/src/main/java/io/axoniq/framework/FrameworkEventProcessorRestController.java @@ -1,7 +1,7 @@ package io.axoniq.framework; -import org.axonframework.config.Configuration; -import org.axonframework.eventhandling.StreamingEventProcessor; +import org.axonframework.common.configuration.Configuration; +import org.axonframework.messaging.eventhandling.processing.streaming.StreamingEventProcessor; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; @@ -23,16 +23,23 @@ public FrameworkEventProcessorRestController(Configuration configuration) { public ResponseEntity reset(@PathVariable String processorName) { Assert.hasLength(processorName, "Processing Group is mandatory and can't be empty!"); - configuration.eventProcessingConfiguration() - .eventProcessorByProcessingGroup(processorName, - StreamingEventProcessor.class) - .ifPresent(streamingEventProcessor -> { - if (streamingEventProcessor.supportsReset()) { - streamingEventProcessor.shutDown(); - streamingEventProcessor.resetTokens(); - streamingEventProcessor.start(); - } - }); + // AF5 removed the (Tracking)EventProcessor-by-processing-group lookup API. Every StreamingEventProcessor + // (nowadays always a PooledStreamingEventProcessor, since TrackingEventProcessor has been removed) is + // available as a named component instead, keyed by its processing group / processor name. + StreamingEventProcessor streamingEventProcessor = + configuration.getComponents(StreamingEventProcessor.class).get(processorName); + if (streamingEventProcessor != null && streamingEventProcessor.supportsReset()) { + // shutdown/resetTokens/start are asynchronous in AF5 (they return a CompletableFuture instead of + // blocking void calls). We chain and join them here to keep this endpoint's original blocking + // behavior: the response is only returned once the reset has fully completed. + // This sample has no data worth preserving, so we simply reset all tokens to the start of the + // stream (the "reset to latest"/full-replay strategy) rather than migrating a stored token's + // (now mandatory) mask column programmatically. + streamingEventProcessor.shutdown() + .thenCompose(ignored -> streamingEventProcessor.resetTokens()) + .thenCompose(ignored -> streamingEventProcessor.start()) + .join(); + } return ResponseEntity.ok().build(); } diff --git a/reset-handler/src/main/java/io/axoniq/server/EventProcessorService.java b/reset-handler/src/main/java/io/axoniq/server/EventProcessorService.java index 821bd40a..a5c58e4f 100644 --- a/reset-handler/src/main/java/io/axoniq/server/EventProcessorService.java +++ b/reset-handler/src/main/java/io/axoniq/server/EventProcessorService.java @@ -1,6 +1,6 @@ package io.axoniq.server; -import org.axonframework.eventhandling.tokenstore.TokenStore; +import org.axonframework.messaging.eventhandling.processing.streaming.token.store.TokenStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -31,7 +31,10 @@ public EventProcessorService(@Value("${axon.axonserver.context}") String context this.webClient = WebClient.create("http://localhost:8024"); this.contextSupplier = () -> context; this.componentSupplier = () -> component; - this.tokenStoreIdSupplier = () -> tokenStore.retrieveStorageIdentifier().orElseThrow(); + // AF5's TokenStore#retrieveStorageIdentifier now takes a (nullable) ProcessingContext and returns a + // CompletableFuture instead of an Optional. There is no processing context available + // here, so we pass null and block for the result, mirroring the previous synchronous lookup. + this.tokenStoreIdSupplier = () -> tokenStore.retrieveStorageIdentifier(null).join(); } /** diff --git a/reset-handler/src/main/java/io/axoniq/server/ServerEventProcessorRestController.java b/reset-handler/src/main/java/io/axoniq/server/ServerEventProcessorRestController.java index a50b7e33..6278e85d 100644 --- a/reset-handler/src/main/java/io/axoniq/server/ServerEventProcessorRestController.java +++ b/reset-handler/src/main/java/io/axoniq/server/ServerEventProcessorRestController.java @@ -1,7 +1,7 @@ package io.axoniq.server; -import org.axonframework.config.Configuration; -import org.axonframework.eventhandling.StreamingEventProcessor; +import org.axonframework.common.configuration.Configuration; +import org.axonframework.messaging.eventhandling.processing.streaming.StreamingEventProcessor; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -36,15 +36,23 @@ public Mono pause(@PathVariable String processorName) { @GetMapping("reset/{processorName}") public Mono reset(@PathVariable String processorName) { Assert.hasLength(processorName, "Processor Name is mandatory and can't be empty!"); - StreamingEventProcessor eventProcessor = configuration.eventProcessingConfiguration() - .eventProcessorByProcessingGroup( - processorName, - StreamingEventProcessor.class) - .orElseThrow(IllegalArgumentException::new); - + // AF5 removed the (Tracking)EventProcessor-by-processing-group lookup API. Every StreamingEventProcessor + // (nowadays always a PooledStreamingEventProcessor) is available as a named component instead, keyed by + // its processing group / processor name. + StreamingEventProcessor eventProcessor = configuration.getComponents(StreamingEventProcessor.class) + .get(processorName); + if (eventProcessor == null) { + throw new IllegalArgumentException("Unknown processor: " + processorName); + } + + // resetTokens() now returns a CompletableFuture instead of blocking, so we bridge it into the + // reactive chain with Mono.fromFuture instead of Mono.fromRunnable. + // This sample has no data worth preserving, so we simply reset all tokens to the start of the stream + // (the "reset to latest"/full-replay strategy) rather than migrating a stored token's (now mandatory) + // mask column programmatically. return eventProcessorService.pause(processorName) .then(eventProcessorService.awaitTermination(processorName)) - .then(Mono.fromRunnable(eventProcessor::resetTokens)) + .then(Mono.fromFuture(eventProcessor::resetTokens)) .then(eventProcessorService.start(processorName)); } } diff --git a/saga/README.md b/saga/README.md deleted file mode 100644 index 81dbda61..00000000 --- a/saga/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Saga - No Toast in PostgreSQL - -This sample project shows a simple Saga use case (as explained [here](#saga-implementation-example)), stored in -PostgreSQL replacing the default [TOAST](https://wiki.postgresql.org/wiki/TOAST) behavior (as -explained [here](#serialized-saga-in-postgresql-without-toast)). - -To run this application, you should first start the services described in the `docker-compose.yml`. With that in hand, -you can run the Spring Boot application, either through a terminal (e.g. `mvn spring-boot:run`) or through your IDE of -choice. - -## Saga Implementation example - -This is an example on how to implement a Saga. This use case is implemented in -the [ProcessOrderSaga](src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSaga.java) - -A webshop has different bounded contexts, for instance, a checkout (where the customer can add items to the card), an -order, a shipment, a payment context. The checkout context will produce events based on actions that the customer does -in the checkout. Some of these events are the so-called domain events. These events could trigger actions within other -contexts. An example could be -the [OrderConfirmedEvent](src/main/java/io/axoniq/dev/samples/order/api/OrderConfirmedEvent.java). When an order is -confirmed, the order processing starts: the customer should pay, and the ordered articles need shipping. This process -needs to be managed. When the order is not paid you need to take some compensating actions like cancelling the shipment. - -A Saga is responsible for the process between multiple bounded contexts. The Saga starts on -the [OrderConfirmedEvent](src/main/java/io/axoniq/dev/samples/order/api/OrderConfirmedEvent.java) which marks the start -of the order process and contains the `orderId`. - -An essential aspect of a Saga that is it needs to have an end. An everyday use case is to end a Saga after a certain -amount of time. This is why a deadline is set in the first event handler of this Saga. This deadline is handled in the -method marked with `@DeadlineHandler`. - -Sagas are serialized and stored into the database, but the CommandGateway should not be serialized the CommandGateway -because of the large dependency tree. You can prevent the CommandGateway from being serialized by adding the transient -keyword to de definition. If you forget to do so Axon will throw an exception. - -The event-handling methods are annotated with `@SagaEventHandler`. The `@StartSaga` annotation will instantiate a new -Saga on every `OrderConfirmedEvent` with a unique orderId. The association property on the `SagaEventHandler` annotation -is mandatory and associates the Saga with the `orderId` on the OrderConfirmedEvent. Other ids like the `shipmentId` and -the `paymentId` are added using the SagaLifeCycle.associateWith() method. The event handlers will be invoked for events -with the same id. - -## Saga test example - -For testing a test fixture ([ProcessOrderSagaTest](src/test/java/io/axoniq/dev/samples/saga/ProcessOrderSagaTest.java)) -can be used. This is a convenient method to implement the tests. -Just define the test subject: -`private SagaTestFixture testFixture = new SagaTestFixture<>(ProcessOrderSaga.class);` - -Inject the resources needed for your Saga (Don't inject Axon resources since they are already injected) : -`testFixture.registerResource(uuidProviderMock);` - -And implement any given-when-then style test. - -## Saga configuration example - -Sagas are backed by an event processor. A saga's event processor will (by default) start its token at the head of the -stream. It is possible to change this behavior and let the processor take all historical events into account. The -configuration example can be found [here](src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSagaConfig.java) - -## Serialized saga in PostgreSQL without TOAST - -If you are using PostgreSQL the `serialized_saga` column in the saga_entry table will be compressed to OID. If you don't -want that, you can overwrite the dialect -like [this](src/main/java/io/axoniq/dev/samples/dialect/NoToastPostgreSQLDialect.java). And add a Hibernate mapping with -an [orm file](src/main/resources/orm.xml). If needed you can migrate the existing Saga entries with -a [sql script](src/main/resources/migrateToBytea.sql). \ No newline at end of file diff --git a/saga/docker-compose.yml b/saga/docker-compose.yml deleted file mode 100644 index 87d24c03..00000000 --- a/saga/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: '3.3' - -services: - axon-server: - image: axoniq/axonserver:latest-jdk-17-dev - ports: - - "8024:8024" - - "8124:8124" - environment: - axoniq_axonserver_hostname: axon-server - axoniq_axonserver_autocluster_first: axon-server - axoniq_axonserver_autocluster_contexts: _admin,default - - volumes: - - data:/axonserver/data - - events:/axonserver/events - db: - image: postgres:14.1-alpine - restart: always - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - ports: - - '5432:5432' - volumes: - - db:/var/lib/postgresql/data -volumes: - db: - driver: local - data: - driver: local - events: - driver: local \ No newline at end of file diff --git a/saga/pom.xml b/saga/pom.xml deleted file mode 100644 index 941842c1..00000000 --- a/saga/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - code-samples - io.axoniq - 0.0.2-SNAPSHOT - - 4.0.0 - - saga - - Saga - No TOAST in PostgreSQL - - Sample application showing what a basic Saga implementation may look like, - combined with switching PostgreSQLs TOAST behavior - - - - - - org.axonframework - axon-spring-boot-starter - - - org.axonframework - axon-test - test - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - - org.hibernate - hibernate-core - - - org.postgresql - postgresql - runtime - - - - org.hamcrest - hamcrest-core - test - - - org.junit.jupiter - junit-jupiter - - - org.mockito - mockito-core - test - - - \ No newline at end of file diff --git a/saga/src/main/java/io/axoniq/dev/samples/SagaNoToastApplication.java b/saga/src/main/java/io/axoniq/dev/samples/SagaNoToastApplication.java deleted file mode 100644 index 6897ad59..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/SagaNoToastApplication.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.axoniq.dev.samples; - -import org.axonframework.config.Configuration; -import org.axonframework.deadline.DeadlineManager; -import org.axonframework.deadline.SimpleDeadlineManager; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class SagaNoToastApplication { - - public static void main(String[] args) { - SpringApplication.run(SagaNoToastApplication.class); - } - - /** - * We're using a {@link SimpleDeadlineManager} to fulfill the requirements of the - * {@link io.axoniq.dev.samples.saga.ProcessOrderSaga}. Note though that you should not regard this - * {@link DeadlineManager} implementation as production-ready, as it does not persist scheduled deadlines! - */ - @Bean - public DeadlineManager deadlineManager(Configuration configuration) { - return SimpleDeadlineManager.builder() - .scopeAwareProvider(configuration.scopeAwareProvider()) - .build(); - } -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/dialect/NoToastPostgreSQLDialect.java b/saga/src/main/java/io/axoniq/dev/samples/dialect/NoToastPostgreSQLDialect.java deleted file mode 100644 index 3a0ceef8..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/dialect/NoToastPostgreSQLDialect.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.axoniq.dev.samples.dialect; - -import org.hibernate.boot.model.TypeContributions; -import org.hibernate.dialect.PostgreSQLDialect; -import org.hibernate.service.ServiceRegistry; -import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.jdbc.BinaryJdbcType; -import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; - -import java.sql.Types; - -/** - * Hibernate 6 based {@link PostgreSQLDialect}, used to enforce the {@link SqlTypes#BLOB} format to {@code BYTEA}. - * Without this dialect, PostgreSQL defaults to the so-called TOAST - * behavior, causing LOB formats to move to a dedicated private table and replacing the LOB column for the OID (Object - * Identifier) column. - */ -@SuppressWarnings("unused") // Used through the application.properties file -public class NoToastPostgreSQLDialect extends PostgreSQLDialect { - - public NoToastPostgreSQLDialect() { - super(); - } - - @Override - protected String columnType(int sqlTypeCode) { - return sqlTypeCode == SqlTypes.BLOB ? "BYTEA" : super.columnType(sqlTypeCode); - } - - @Override - protected String castType(int sqlTypeCode) { - return sqlTypeCode == SqlTypes.BLOB ? "BYTEA" : super.castType(sqlTypeCode); - } - - @Override - public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { - super.contributeTypes(typeContributions, serviceRegistry); - JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); - jdbcTypeRegistry.addDescriptor(Types.BLOB, BinaryJdbcType.INSTANCE); - } -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java b/saga/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java deleted file mode 100644 index 8498496f..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.axoniq.dev.samples.order.api; - -import io.axoniq.dev.samples.uuid.OrderId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public record CompleteOrderProcessCommand(@TargetAggregateIdentifier OrderId orderId, - boolean isPaid, - boolean orderIsDelivered) { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/order/api/OrderConfirmedEvent.java b/saga/src/main/java/io/axoniq/dev/samples/order/api/OrderConfirmedEvent.java deleted file mode 100644 index 9e5316c2..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/order/api/OrderConfirmedEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.order.api; - -import io.axoniq.dev.samples.uuid.OrderId; - -public record OrderConfirmedEvent(OrderId orderId) { - -} - diff --git a/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaidEvent.java b/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaidEvent.java deleted file mode 100644 index 1b315218..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaidEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.dev.samples.payment.api; - -import io.axoniq.dev.samples.uuid.PaymentId; - -public record OrderPaidEvent(PaymentId paymentId) { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaymentCancelledEvent.java b/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaymentCancelledEvent.java deleted file mode 100644 index 63d568cb..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/payment/api/OrderPaymentCancelledEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.dev.samples.payment.api; - -import io.axoniq.dev.samples.uuid.PaymentId; - -public record OrderPaymentCancelledEvent(PaymentId paymentId) { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java b/saga/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java deleted file mode 100644 index 0a26612a..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.payment.api; - -import io.axoniq.dev.samples.uuid.PaymentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public record PayOrderCommand(@TargetAggregateIdentifier PaymentId paymentId) { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSaga.java b/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSaga.java deleted file mode 100644 index 96ec6718..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSaga.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.axoniq.dev.samples.saga; - -import io.axoniq.dev.samples.order.api.CompleteOrderProcessCommand; -import io.axoniq.dev.samples.order.api.OrderConfirmedEvent; -import io.axoniq.dev.samples.payment.api.OrderPaidEvent; -import io.axoniq.dev.samples.payment.api.OrderPaymentCancelledEvent; -import io.axoniq.dev.samples.payment.api.PayOrderCommand; -import io.axoniq.dev.samples.shipment.api.CancelShipmentCommand; -import io.axoniq.dev.samples.shipment.api.ShipOrderCommand; -import io.axoniq.dev.samples.shipment.api.ShipmentStatus; -import io.axoniq.dev.samples.shipment.api.ShipmentStatusUpdatedEvent; -import io.axoniq.dev.samples.uuid.OrderId; -import io.axoniq.dev.samples.uuid.PaymentId; -import io.axoniq.dev.samples.uuid.ShipmentId; -import io.axoniq.dev.samples.uuid.UUIDProvider; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.deadline.DeadlineManager; -import org.axonframework.deadline.annotation.DeadlineHandler; -import org.axonframework.modelling.saga.EndSaga; -import org.axonframework.modelling.saga.SagaEventHandler; -import org.axonframework.modelling.saga.SagaLifecycle; -import org.axonframework.modelling.saga.StartSaga; -import org.axonframework.spring.stereotype.Saga; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -@Saga -public class ProcessOrderSaga { - - private static final String PAYMENT_ID_ASSOCIATION = "paymentId"; - private static final String SHIPMENT_ID_ASSOCIATION = "shipmentId"; - private static final String ORDER_ID_ASSOCIATION = "orderId"; - private static final String ORDER_COMPLETE_DEADLINE = "OrderCompleteDeadline"; - - private OrderId orderId; - private ShipmentId shipmentId; - private String orderDeadlineId; - private boolean orderIsPaid; - private boolean orderIsDelivered; - - @StartSaga - @SagaEventHandler(associationProperty = ORDER_ID_ASSOCIATION) - public void on(OrderConfirmedEvent event, - UUIDProvider uuidProvider, - CommandGateway commandGateway, - DeadlineManager deadlineManager) { - this.orderId = event.orderId(); - - //Send a command to paid to get the order paid. Associate this Saga with the payment identifier used. - PaymentId paymentId = uuidProvider.generatePaymentId(); - SagaLifecycle.associateWith(PAYMENT_ID_ASSOCIATION, paymentId.toString()); - commandGateway.send(new PayOrderCommand(paymentId)); - - //Send a command to logistics to ship the order. Associate this Saga with the shipment identifier used. - ShipmentId shipmentId = uuidProvider.generateShipmentId(); - SagaLifecycle.associateWith(SHIPMENT_ID_ASSOCIATION, shipmentId.toString()); - commandGateway.send(new ShipOrderCommand(shipmentId)); - this.shipmentId = shipmentId; - - //This order should be completed in 5 days - this.orderDeadlineId = deadlineManager.schedule(Duration.of(5, ChronoUnit.DAYS), ORDER_COMPLETE_DEADLINE); - } - - @SagaEventHandler(associationProperty = PAYMENT_ID_ASSOCIATION) - public void on(OrderPaidEvent event, - CommandGateway commandGateway, - DeadlineManager deadlineManager) { - this.orderIsPaid = true; - if (orderIsDelivered) { - completeOrderProcess(commandGateway, deadlineManager); - } - } - - @SagaEventHandler(associationProperty = PAYMENT_ID_ASSOCIATION) - public void on(OrderPaymentCancelledEvent event, - CommandGateway commandGateway, - DeadlineManager deadlineManager) { - // Cancel the shipment and update the Order - commandGateway.send(new CancelShipmentCommand(this.shipmentId)); - completeOrderProcess(commandGateway, deadlineManager); - } - - @SagaEventHandler(associationProperty = SHIPMENT_ID_ASSOCIATION) - public void on(ShipmentStatusUpdatedEvent event, - CommandGateway commandGateway, - DeadlineManager deadlineManager) { - this.orderIsDelivered = ShipmentStatus.DELIVERED.equals(event.shipmentStatus()); - if (orderIsPaid && orderIsDelivered) { - completeOrderProcess(commandGateway, deadlineManager); - } - } - - @DeadlineHandler(deadlineName = ORDER_COMPLETE_DEADLINE) - @EndSaga - public void on(CommandGateway commandGateway) { - commandGateway.send(new CompleteOrderProcessCommand(this.orderId, this.orderIsPaid, this.orderIsDelivered)); - } - - private void completeOrderProcess(CommandGateway commandGateway, - DeadlineManager deadlineManager) { - commandGateway.send(new CompleteOrderProcessCommand(this.orderId, this.orderIsPaid, this.orderIsDelivered)); - deadlineManager.cancelSchedule(ORDER_COMPLETE_DEADLINE, orderDeadlineId); - SagaLifecycle.end(); - } -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSagaConfig.java b/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSagaConfig.java deleted file mode 100644 index a7f5c538..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/saga/ProcessOrderSagaConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.axoniq.dev.samples.saga; - -import org.axonframework.config.EventProcessingConfigurer; -import org.axonframework.messaging.StreamableMessageSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ProcessOrderSagaConfig { - - @Autowired - public void configure(EventProcessingConfigurer configurer) { - configurer.registerPooledStreamingEventProcessor( - "ProcessOrderSagaProcessor", - org.axonframework.config.Configuration::eventStore, - (configuration, builder) -> builder.initialToken(StreamableMessageSource::createTailToken) - ); - } -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java b/saga/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java deleted file mode 100644 index 124c6c96..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.shipment.api; - -import io.axoniq.dev.samples.uuid.ShipmentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public record CancelShipmentCommand(@TargetAggregateIdentifier ShipmentId shipmentId) { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java b/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java deleted file mode 100644 index 75011c49..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.shipment.api; - -import io.axoniq.dev.samples.uuid.ShipmentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public record ShipOrderCommand(@TargetAggregateIdentifier ShipmentId shipmentId) { - -} \ No newline at end of file diff --git a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatus.java b/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatus.java deleted file mode 100644 index 87688115..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.shipment.api; - -public enum ShipmentStatus { - NEW, - SHIPPED, - DELIVERY_EXCEPTION, - DELIVERED -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatusUpdatedEvent.java b/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatusUpdatedEvent.java deleted file mode 100644 index 7ab78f6f..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/shipment/api/ShipmentStatusUpdatedEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.dev.samples.shipment.api; - -import io.axoniq.dev.samples.uuid.ShipmentId; - -public record ShipmentStatusUpdatedEvent(ShipmentId shipmentId, ShipmentStatus shipmentStatus) { - -} \ No newline at end of file diff --git a/saga/src/main/java/io/axoniq/dev/samples/uuid/AbstractId.java b/saga/src/main/java/io/axoniq/dev/samples/uuid/AbstractId.java deleted file mode 100644 index 465855fd..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/uuid/AbstractId.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.uuid; - -import java.util.UUID; - -abstract class AbstractId { - - UUID id; - - public AbstractId() { - this.id = UUID.randomUUID(); - } -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/uuid/OrderId.java b/saga/src/main/java/io/axoniq/dev/samples/uuid/OrderId.java deleted file mode 100644 index b005b0ca..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/uuid/OrderId.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.dev.samples.uuid; - -public class OrderId extends AbstractId { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/uuid/PaymentId.java b/saga/src/main/java/io/axoniq/dev/samples/uuid/PaymentId.java deleted file mode 100644 index a76ad4ba..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/uuid/PaymentId.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.dev.samples.uuid; - -public class PaymentId extends AbstractId { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/uuid/ShipmentId.java b/saga/src/main/java/io/axoniq/dev/samples/uuid/ShipmentId.java deleted file mode 100644 index 9c0d9c53..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/uuid/ShipmentId.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.axoniq.dev.samples.uuid; - -public class ShipmentId extends AbstractId { - -} diff --git a/saga/src/main/java/io/axoniq/dev/samples/uuid/UUIDProvider.java b/saga/src/main/java/io/axoniq/dev/samples/uuid/UUIDProvider.java deleted file mode 100644 index 4afeb8bc..00000000 --- a/saga/src/main/java/io/axoniq/dev/samples/uuid/UUIDProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.axoniq.dev.samples.uuid; - -import org.springframework.stereotype.Component; - -@Component -public class UUIDProvider { - - public OrderId generateOrderId() { - return new OrderId(); - } - - public PaymentId generatePaymentId() { - return new PaymentId(); - } - - public ShipmentId generateShipmentId() { - return new ShipmentId(); - } -} diff --git a/saga/src/main/resources/application.properties b/saga/src/main/resources/application.properties deleted file mode 100644 index 43454fd3..00000000 --- a/saga/src/main/resources/application.properties +++ /dev/null @@ -1,12 +0,0 @@ -spring.application.name=saga-no-toast - -axon.axonserver.enabled=true -axon.axonserver.context=default -axon.axonserver.component-name=saga-no-toast -axon.axonserver.servers=localhost:8124 - -spring.datasource.url=jdbc:postgresql://localhost:5432/postgres -spring.datasource.username=postgres -spring.datasource.password=postgres -spring.jpa.hibernate.ddl-auto=create-drop -spring.jpa.database-platform=io.axoniq.dev.samples.dialect.NoToastPostgreSQLDialect diff --git a/saga/src/main/resources/migrateToBytea.sql b/saga/src/main/resources/migrateToBytea.sql deleted file mode 100644 index c9c5f752..00000000 --- a/saga/src/main/resources/migrateToBytea.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE saga_entry ADD COLUMN serialized_saga_bytea BYTEA; -UPDATE saga_entry SET serialized_saga_bytea = lo_get(serialized_saga); -ALTER TABLE saga_entry DROP COLUMN serialized_saga; -ALTER TABLE saga_entry RENAME COLUMN serialized_saga_bytea to serialized_saga; \ No newline at end of file diff --git a/saga/src/main/resources/orm.xml b/saga/src/main/resources/orm.xml deleted file mode 100644 index 98e3eddc..00000000 --- a/saga/src/main/resources/orm.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/saga/src/test/java/io/axoniq/dev/samples/saga/ProcessOrderSagaTest.java b/saga/src/test/java/io/axoniq/dev/samples/saga/ProcessOrderSagaTest.java deleted file mode 100644 index 42a47530..00000000 --- a/saga/src/test/java/io/axoniq/dev/samples/saga/ProcessOrderSagaTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package io.axoniq.dev.samples.saga; - -import io.axoniq.dev.samples.order.api.CompleteOrderProcessCommand; -import io.axoniq.dev.samples.order.api.OrderConfirmedEvent; -import io.axoniq.dev.samples.payment.api.OrderPaidEvent; -import io.axoniq.dev.samples.payment.api.OrderPaymentCancelledEvent; -import io.axoniq.dev.samples.payment.api.PayOrderCommand; -import io.axoniq.dev.samples.shipment.api.CancelShipmentCommand; -import io.axoniq.dev.samples.shipment.api.ShipOrderCommand; -import io.axoniq.dev.samples.shipment.api.ShipmentStatus; -import io.axoniq.dev.samples.shipment.api.ShipmentStatusUpdatedEvent; -import io.axoniq.dev.samples.uuid.OrderId; -import io.axoniq.dev.samples.uuid.PaymentId; -import io.axoniq.dev.samples.uuid.ShipmentId; -import io.axoniq.dev.samples.uuid.UUIDProvider; -import org.axonframework.deadline.DeadlineMessage; -import org.axonframework.test.saga.SagaTestFixture; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; -import org.junit.jupiter.api.*; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.List; - -import static org.mockito.Mockito.*; - -public class ProcessOrderSagaTest { - - private final UUIDProvider uuidProviderMock = mock(UUIDProvider.class); - private final OrderId orderId = new OrderId(); - private final PaymentId paymentId = new PaymentId(); - private final ShipmentId shipmentId = new ShipmentId(); - - - private SagaTestFixture testFixture; - - @BeforeEach - void setUp() { - testFixture = new SagaTestFixture<>(ProcessOrderSaga.class); - testFixture.registerResource(uuidProviderMock); - when(uuidProviderMock.generateOrderId()).thenReturn(orderId); - when(uuidProviderMock.generatePaymentId()).thenReturn(paymentId); - when(uuidProviderMock.generateShipmentId()).thenReturn(shipmentId); - } - - @Test - void onOrderConfirmedTest() { - testFixture.givenNoPriorActivity() - .whenPublishingA(new OrderConfirmedEvent(orderId)) - .expectDispatchedCommands(new PayOrderCommand(paymentId), new ShipOrderCommand(shipmentId)) - .expectScheduledDeadlineWithName(Duration.of(5, ChronoUnit.DAYS), "OrderCompleteDeadline") - .expectActiveSagas(1); - } - - @Test - void onOrderPaidAndNotDeliveredTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .whenPublishingA(new OrderPaidEvent(paymentId)) - .expectActiveSagas(1); - } - - @Test - void onOrderPaidAndDeliveredTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .andThenAPublished(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.DELIVERED)) - .whenPublishingA(new OrderPaidEvent(paymentId)) - .expectDispatchedCommands(new CompleteOrderProcessCommand(orderId, true, true)) - .expectNoScheduledDeadlineWithName(Duration.of(5, ChronoUnit.DAYS), "OrderCompleteDeadline") - .expectActiveSagas(0); - } - - @Test - void onOrderPaymentCancelledTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .whenPublishingA(new OrderPaymentCancelledEvent(paymentId)) - .expectDispatchedCommands(new CancelShipmentCommand(shipmentId), - new CompleteOrderProcessCommand(orderId, false, false)) - .expectNoScheduledDeadlineWithName(Duration.of(5, ChronoUnit.DAYS), "OrderCompleteDeadline") - .expectActiveSagas(0); - } - - @Test - void onShipmentDeliveredAndPaidTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .andThenAPublished(new OrderPaidEvent(paymentId)) - .andThenAPublished(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.SHIPPED)) - .whenPublishingA(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.DELIVERED)) - .expectDispatchedCommands(new CompleteOrderProcessCommand(orderId, true, true)) - .expectNoScheduledDeadlineWithName(Duration.of(5, ChronoUnit.DAYS), "OrderCompleteDeadline") - .expectActiveSagas(0); - } - - @Test - void onShipmentDeliveredAndNotPaidTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .andThenAPublished(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.SHIPPED)) - .whenPublishingA(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.DELIVERED)) - .expectActiveSagas(1); - } - - @Test - void onShipmentInTransitTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .andThenAPublished(new OrderPaidEvent(paymentId)) - .whenPublishingA(new ShipmentStatusUpdatedEvent(shipmentId, ShipmentStatus.SHIPPED)) - .expectActiveSagas(1); - } - - @Test - void onOrderCompleteDeadlineTest() { - testFixture.givenAPublished(new OrderConfirmedEvent(orderId)) - .whenTimeElapses(Duration.of(5, ChronoUnit.DAYS).plus(Duration.ofSeconds(60))) - .expectNoScheduledDeadlines() - .expectTriggeredDeadlinesMatching(orderCompleteDeadline()); - } - - protected static Matcher>> orderCompleteDeadline() { - return new OrderCompleteDeadline(); - } - - static class OrderCompleteDeadline extends TypeSafeMatcher>> { - - @Override - protected boolean matchesSafely(List> deadlineMessages) { - return deadlineMessages.stream().allMatch(OrderCompleteDeadline::matches); - } - - private static boolean matches(DeadlineMessage deadlineMessage) { - return deadlineMessage.getDeadlineName().equals("OrderCompleteDeadline") - && deadlineMessage.getPayload() == null; - } - - @Override - public void describeTo(Description description) { - description.appendText("no matching deadline found"); - } - } -} diff --git a/sequencing-policy/README.md b/sequencing-policy/README.md index 5c6a9ee9..1b6d6709 100644 --- a/sequencing-policy/README.md +++ b/sequencing-policy/README.md @@ -13,8 +13,14 @@ statement containing all flight events to achieve the same. As a bonus, this sample shows how we can achieve the same result with the `PropertySequencingPolicy`. -For more information on sequencing policies (like other implementations) -check [this](https://docs.axoniq.io/reference-guide/axon-framework/events/event-processors/streaming#sequential-processing) +The `policy` property (`custom` or `property`) picks which of the two is active. Since Axon Framework 5 declares the +`SequencingPolicy` directly on the event handling component through the `@SequencingPolicy` annotation - rather than +through a Spring property pointing at a named bean - this sample models the two variants as separate +`@ConditionalOnProperty`-guarded components: `FlightTimeProjector` (custom policy) and `PropertyFlightTimeProjector` +(`PropertySequencingPolicy`). Only one of the two is registered at a time, based on the `policy` property. + +For more information on sequencing policies (like other implementations) check +[this](https://docs.axoniq.io/reference-guide/axon-framework/events/event-processors/streaming#sequencing_policies) page. ## Running the application diff --git a/sequencing-policy/pom.xml b/sequencing-policy/pom.xml index c80cc733..c9e6a3cb 100644 --- a/sequencing-policy/pom.xml +++ b/sequencing-policy/pom.xml @@ -16,7 +16,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter diff --git a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/AxonConfig.java b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/AxonConfig.java index 523f9b75..4f2ccbb3 100644 --- a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/AxonConfig.java +++ b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/AxonConfig.java @@ -1,14 +1,7 @@ package io.axoniq.dev.samples.sequencingpolicy; -import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightEvent; -import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightId; -import org.axonframework.eventhandling.EventMessage; -import org.axonframework.eventhandling.async.PropertySequencingPolicy; -import org.axonframework.eventhandling.async.SequencingPolicy; -import org.axonframework.eventhandling.tokenstore.TokenStore; -import org.axonframework.eventhandling.tokenstore.inmemory.InMemoryTokenStore; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.axonframework.messaging.eventhandling.processing.streaming.token.store.TokenStore; +import org.axonframework.messaging.eventhandling.processing.streaming.token.store.inmemory.InMemoryTokenStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,6 +11,14 @@ public class AxonConfig { /** * By default, the InMemoryTokenStore is not autowired since it's not 'production ready'. So, we need to explicitly * declare it ourselves. + *

+ * Note that the {@code SequencingPolicy} beans that used to live here + * ({@code flightIdSequencingPolicy}/{@code propertySequencingPolicy}, selected through the + * {@code axon.eventhandling.processors.flight-time.sequencing-policy} property) have been removed: that + * property-driven bean-lookup mechanism has no effect in Axon Framework 5. The sequencing policy is now declared + * directly on the event handling component through the {@code @SequencingPolicy} annotation - see + * {@link io.axoniq.dev.samples.sequencingpolicy.querymodel.FlightTimeProjector} and + * {@link io.axoniq.dev.samples.sequencingpolicy.querymodel.PropertyFlightTimeProjector}. * * @return an InMemoryTokenStore */ @@ -25,34 +26,4 @@ public class AxonConfig { TokenStore tokenStore() { return new InMemoryTokenStore(); } - - /** - * Constructs the custom {@link FlightIdSequencingPolicy}. - * - * @return the custom {@link FlightIdSequencingPolicy} - */ - @Bean - @Qualifier("flightIdSequencingPolicy") - @ConditionalOnProperty(value = "policy", havingValue = "custom") - public SequencingPolicy> flightIdSequencingPolicy() { - return new FlightIdSequencingPolicy(); - } - - /** - * Constructs a {@link PropertySequencingPolicy} instance. - *

- * Added to show that the {@code PropertySequencingPolicy} in essence works identical as the custom - * {@link FlightIdSequencingPolicy}. - * - * @return a {@link PropertySequencingPolicy} using the {@link FlightId} to sequence on - */ - @SuppressWarnings("rawtypes") - @Bean - @Qualifier("propertySequencingPolicy") - @ConditionalOnProperty(value = "policy", havingValue = "property") - public SequencingPolicy propertySequencingPolicy() { - return PropertySequencingPolicy.builder(FlightEvent.class, FlightId.class) - .propertyName("flightId") - .build(); - } } diff --git a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicy.java b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicy.java index 2b72443d..81d71f2e 100644 --- a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicy.java +++ b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicy.java @@ -1,8 +1,11 @@ package io.axoniq.dev.samples.sequencingpolicy; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightEvent; -import org.axonframework.eventhandling.EventMessage; -import org.axonframework.eventhandling.async.SequencingPolicy; +import org.axonframework.messaging.core.sequencing.SequencingPolicy; +import org.axonframework.messaging.core.unitofwork.ProcessingContext; +import org.axonframework.messaging.eventhandling.EventMessage; + +import java.util.Optional; /** * Custom implementation of the {@link SequencingPolicy} that returns the @@ -10,19 +13,19 @@ *

* This ensures a given segment of the event stream will handle all the {@code FlightEvents} with the same * {@code FlightId} in the correct order. If the event being handled isn't an implementation of {@code FlightEvent}, - * {@code null} is returned. + * an empty {@link Optional} is returned. *

* Key in simplifying this implementation, is the {@code FlightEvent} interface that is implemented by all flight * related events. Without this, every implementation should be validated separately. * * @author Steven van Beelen */ -public class FlightIdSequencingPolicy implements SequencingPolicy> { +public class FlightIdSequencingPolicy implements SequencingPolicy { @Override - public Object getSequenceIdentifierFor(EventMessage event) { - return FlightEvent.class.isAssignableFrom(event.getPayloadType()) - ? ((FlightEvent) event.getPayload()).flightId() - : null; // when returning null, Axon will default to EventMessage#getIdentifier + public Optional sequenceIdentifierFor(EventMessage event, ProcessingContext context) { + return FlightEvent.class.isAssignableFrom(event.payloadType()) + ? Optional.of(((FlightEvent) event.payload()).flightId()) + : Optional.empty(); // when returning an empty Optional, there are no sequencing requirements } } diff --git a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/controller/TestController.java b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/controller/TestController.java index a5ecc4e8..0a41f708 100644 --- a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/controller/TestController.java +++ b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/controller/TestController.java @@ -5,7 +5,7 @@ import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightDelayedEvent; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightId; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightScheduledEvent; -import org.axonframework.eventhandling.gateway.EventGateway; +import org.axonframework.messaging.eventhandling.gateway.EventGateway; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; @@ -45,6 +45,7 @@ public ResponseEntity bulkEvents(@PathVariable Integer amount) { String origin = fetchDestination(); String destination = fetchDestination(); eventGateway.publish( + null, new FlightScheduledEvent(flightId, origin, destination, scheduledArrival), new FlightDelayedEvent(flightId, Duration.ofHours(1)), new ArrivalTimeChangedEvent( diff --git a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/FlightTimeProjector.java b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/FlightTimeProjector.java index a964aabf..6cc7e012 100644 --- a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/FlightTimeProjector.java +++ b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/FlightTimeProjector.java @@ -1,13 +1,16 @@ package io.axoniq.dev.samples.sequencingpolicy.querymodel; +import io.axoniq.dev.samples.sequencingpolicy.FlightIdSequencingPolicy; import io.axoniq.dev.samples.sequencingpolicy.coreapi.ArrivalTimeChangedEvent; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightCanceledEvent; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightDelayedEvent; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightScheduledEvent; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; +import org.axonframework.messaging.core.annotation.Namespace; +import org.axonframework.messaging.core.annotation.SequencingPolicy; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.lang.invoke.MethodHandles; @@ -17,11 +20,17 @@ * {@link Thread#getId()} for every event it handles. *

* Doing so shows in the logs that the same thread is in charge of all events for a given {@code FlightId}. This further - * shows that the {@link org.axonframework.eventhandling.async.SequencingPolicy} based on the {@code FlightId} is in - * effect. + * shows that the {@link org.axonframework.messaging.core.sequencing.SequencingPolicy} based on the {@code FlightId} is + * in effect. + *

+ * The {@link FlightIdSequencingPolicy} is configured through the {@link SequencingPolicy @SequencingPolicy} annotation + * on this class. This projector is only registered when the {@code policy} property equals {@code custom}; see + * {@link PropertyFlightTimeProjector} for the equivalent setup using the built-in {@code PropertySequencingPolicy}. */ @Component -@ProcessingGroup("flight-time") +@Namespace("flight-time") +@ConditionalOnProperty(value = "policy", havingValue = "custom") +@SequencingPolicy(type = FlightIdSequencingPolicy.class) public class FlightTimeProjector { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/PropertyFlightTimeProjector.java b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/PropertyFlightTimeProjector.java new file mode 100644 index 00000000..8a4064cd --- /dev/null +++ b/sequencing-policy/src/main/java/io/axoniq/dev/samples/sequencingpolicy/querymodel/PropertyFlightTimeProjector.java @@ -0,0 +1,61 @@ +package io.axoniq.dev.samples.sequencingpolicy.querymodel; + +import io.axoniq.dev.samples.sequencingpolicy.coreapi.ArrivalTimeChangedEvent; +import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightCanceledEvent; +import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightDelayedEvent; +import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightScheduledEvent; +import org.axonframework.messaging.core.annotation.Namespace; +import org.axonframework.messaging.core.annotation.SequencingPolicy; +import org.axonframework.messaging.core.sequencing.PropertySequencingPolicy; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.lang.invoke.MethodHandles; + +/** + * Identical to {@link FlightTimeProjector}, but configured with the built-in {@link PropertySequencingPolicy} instead + * of the custom {@link io.axoniq.dev.samples.sequencingpolicy.FlightIdSequencingPolicy}. + *

+ * Added to show that the {@code PropertySequencingPolicy} works identically to the custom + * {@code FlightIdSequencingPolicy}: since every flight event exposes a {@code flightId} property (through the common + * {@link io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightEvent} interface), the {@code SequencingPolicy} + * annotation's constructor-parameter resolution can inject each handled event's concrete payload type automatically, + * requiring only the property name to be provided. + *

+ * This projector is only registered when the {@code policy} property equals {@code property}. + */ +@Component +@Namespace("flight-time") +@ConditionalOnProperty(value = "policy", havingValue = "property") +@SequencingPolicy(type = PropertySequencingPolicy.class, parameters = {"flightId"}) +public class PropertyFlightTimeProjector { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @EventHandler + public void on(FlightScheduledEvent event) { + logger.info("Thread[{}] handling FlightScheduledEvent with Flight Id [{}]", + Thread.currentThread().getId(), event.flightId()); + } + + @EventHandler + public void on(FlightDelayedEvent event) { + logger.info("Thread[{}] handling FlightDelayedEvent with Flight Id [{}]", + Thread.currentThread().getId(), event.flightId()); + } + + @EventHandler + public void on(FlightCanceledEvent event) { + logger.info("Thread[{}] handling FlightCanceledEvent with Flight Id [{}]", + Thread.currentThread().getId(), event.flightId()); + } + + @EventHandler + public void on(ArrivalTimeChangedEvent event) { + logger.info("Thread[{}] handling ArrivalTimeChangedEvent with Flight Id [{}]", + Thread.currentThread().getId(), event.flightId()); + } +} diff --git a/sequencing-policy/src/main/resources/application-property.properties b/sequencing-policy/src/main/resources/application-property.properties index 9bb3501f..459ea670 100644 --- a/sequencing-policy/src/main/resources/application-property.properties +++ b/sequencing-policy/src/main/resources/application-property.properties @@ -1,2 +1 @@ policy=property -axon.eventhandling.processors.flight-time.sequencing-policy=propertySequencingPolicy \ No newline at end of file diff --git a/sequencing-policy/src/main/resources/application.properties b/sequencing-policy/src/main/resources/application.properties index 1c53dd5e..459ee392 100644 --- a/sequencing-policy/src/main/resources/application.properties +++ b/sequencing-policy/src/main/resources/application.properties @@ -1,4 +1,3 @@ policy=custom axon.eventhandling.processors.flight-time.thread-count=4 axon.eventhandling.processors.flight-time.initial-segment-count=4 -axon.eventhandling.processors.flight-time.sequencing-policy=flightIdSequencingPolicy \ No newline at end of file diff --git a/sequencing-policy/src/test/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicyTest.java b/sequencing-policy/src/test/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicyTest.java index c8e9e5a7..53767e47 100644 --- a/sequencing-policy/src/test/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicyTest.java +++ b/sequencing-policy/src/test/java/io/axoniq/dev/samples/sequencingpolicy/FlightIdSequencingPolicyTest.java @@ -2,10 +2,12 @@ import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightCanceledEvent; import io.axoniq.dev.samples.sequencingpolicy.coreapi.FlightId; -import org.axonframework.eventhandling.EventMessage; -import org.axonframework.eventhandling.GenericEventMessage; +import org.axonframework.messaging.core.MessageType; +import org.axonframework.messaging.eventhandling.EventMessage; +import org.axonframework.messaging.eventhandling.GenericEventMessage; import org.junit.jupiter.api.*; +import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -15,19 +17,19 @@ class FlightIdSequencingPolicyTest { private final FlightIdSequencingPolicy testSubject = new FlightIdSequencingPolicy(); @Test - void testReturnsNullForNoneFlightEvent() { - EventMessage testEvent = GenericEventMessage.asEventMessage("some-event"); + void returnsEmptyOptionalForNoneFlightEvent() { + EventMessage testEvent = new GenericEventMessage(new MessageType(String.class), "some-event"); - assertNull(testSubject.getSequenceIdentifierFor(testEvent)); + assertEquals(Optional.empty(), testSubject.sequenceIdentifierFor(testEvent, null)); } @Test - void testReturnsFlightIdForFlightEventImplementations() { + void returnsFlightIdForFlightEventImplementations() { FlightId expectedResult = new FlightId(UUID.randomUUID().toString()); FlightCanceledEvent testEvent = new FlightCanceledEvent(expectedResult); - EventMessage testEventMessage = GenericEventMessage.asEventMessage(testEvent); + EventMessage testEventMessage = new GenericEventMessage(new MessageType(testEvent.getClass()), testEvent); - assertEquals(expectedResult, testSubject.getSequenceIdentifierFor(testEventMessage)); + assertEquals(Optional.of(expectedResult), testSubject.sequenceIdentifierFor(testEventMessage, null)); } -} \ No newline at end of file +} diff --git a/serialization-avro/README.md b/serialization-avro/README.md index 19eae2fb..dc1c5275 100644 --- a/serialization-avro/README.md +++ b/serialization-avro/README.md @@ -15,11 +15,12 @@ Here is an example schema for the `CardIssuedEvent` event: "name": "CardIssuedEvent", "doc": "A new gift card is issued.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.eventhandling.annotation.Event(version = \"1\")", "fields": [ { "name": "id", - "type": "string" + "type": "string", + "javaAnnotation": "org.axonframework.eventsourcing.annotation.EventTag(key = \"GiftCard\")" }, { "name": "amount", @@ -33,12 +34,12 @@ Using the `avro-maven-plugin`, the Java classes are generated into `target/gener during the `generate-sources` build phase. The remaining part of the application is a standard implementation of a gift card application, that uses the -`AvroSerializer` by applying configuration of the serializer using application properties. +`AvroConverter` by applying configuration of the converter using application properties. ```yaml axon: - serializer: + converter: events: avro messages: avro general: jackson diff --git a/serialization-avro/pom.xml b/serialization-avro/pom.xml index 4720575a..32bbe610 100644 --- a/serialization-avro/pom.xml +++ b/serialization-avro/pom.xml @@ -21,11 +21,11 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter - org.axonframework + io.axoniq.framework axon-server-connector @@ -85,7 +85,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test diff --git a/serialization-avro/src/main/avro/giftcard.command.IssueCard.avsc b/serialization-avro/src/main/avro/giftcard.command.IssueCard.avsc index 7a8a8a6e..c53b9fd8 100644 --- a/serialization-avro/src/main/avro/giftcard.command.IssueCard.avsc +++ b/serialization-avro/src/main/avro/giftcard.command.IssueCard.avsc @@ -4,12 +4,11 @@ "name": "IssueCardCommand", "doc": "Issues a new gift card.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.commandhandling.annotation.Command(routingKey = \"id\", version = \"1\")", "fields": [ { "name": "id", - "type": "string", - "javaAnnotation": "org.axonframework.modelling.command.TargetAggregateIdentifier" + "type": "string" }, { "name": "amount", diff --git a/serialization-avro/src/main/avro/giftcard.command.RedeemCard.avsc b/serialization-avro/src/main/avro/giftcard.command.RedeemCard.avsc index 14284b66..80a4d353 100644 --- a/serialization-avro/src/main/avro/giftcard.command.RedeemCard.avsc +++ b/serialization-avro/src/main/avro/giftcard.command.RedeemCard.avsc @@ -4,12 +4,12 @@ "name": "RedeemCardCommand", "doc": "Redeems existing gift card.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.commandhandling.annotation.Command(routingKey = \"id\", version = \"1\")", "fields": [ { "name": "id", "type": "string", - "javaAnnotation": "org.axonframework.modelling.command.TargetAggregateIdentifier" + "javaAnnotation": "org.axonframework.modelling.annotation.TargetEntityId" }, { "name": "amount", diff --git a/serialization-avro/src/main/avro/giftcard.event.CardIssued.avsc b/serialization-avro/src/main/avro/giftcard.event.CardIssued.avsc index 84f6134f..e4a64b82 100644 --- a/serialization-avro/src/main/avro/giftcard.event.CardIssued.avsc +++ b/serialization-avro/src/main/avro/giftcard.event.CardIssued.avsc @@ -4,11 +4,12 @@ "name": "CardIssuedEvent", "doc": "A new gift card is issued.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.eventhandling.annotation.Event(version = \"1\")", "fields": [ { "name": "id", - "type": "string" + "type": "string", + "javaAnnotation": "org.axonframework.eventsourcing.annotation.EventTag(key = \"GiftCard\")" }, { "name": "amount", diff --git a/serialization-avro/src/main/avro/giftcard.event.CardRedeemed.avsc b/serialization-avro/src/main/avro/giftcard.event.CardRedeemed.avsc index 56f912a6..3ec99fc4 100644 --- a/serialization-avro/src/main/avro/giftcard.event.CardRedeemed.avsc +++ b/serialization-avro/src/main/avro/giftcard.event.CardRedeemed.avsc @@ -4,11 +4,12 @@ "name": "CardRedeemedEvent", "doc": "Existing gift card is redeemed.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.eventhandling.annotation.Event(version = \"1\")", "fields": [ { "name": "id", - "type": "string" + "type": "string", + "javaAnnotation": "org.axonframework.eventsourcing.annotation.EventTag(key = \"GiftCard\")" }, { "name": "amount", diff --git a/serialization-avro/src/main/avro/giftcard.query.CardList.avsc b/serialization-avro/src/main/avro/giftcard.query.CardList.avsc index 037c0266..452c69b5 100644 --- a/serialization-avro/src/main/avro/giftcard.query.CardList.avsc +++ b/serialization-avro/src/main/avro/giftcard.query.CardList.avsc @@ -4,7 +4,6 @@ "name": "CardList", "doc": "Represents a list of gift cards.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", "fields": [ { "name": "elements", diff --git a/serialization-avro/src/main/avro/giftcard.query.GetAllCards.avsc b/serialization-avro/src/main/avro/giftcard.query.GetAllCards.avsc index 9fec00c1..29aba327 100644 --- a/serialization-avro/src/main/avro/giftcard.query.GetAllCards.avsc +++ b/serialization-avro/src/main/avro/giftcard.query.GetAllCards.avsc @@ -4,7 +4,7 @@ "name": "GetAllCardsQuery", "doc": "Query for all gift cards.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.queryhandling.annotation.Query(version = \"1\")", "fields": [ ] } diff --git a/serialization-avro/src/main/avro/giftcard.query.GetCardById.avsc b/serialization-avro/src/main/avro/giftcard.query.GetCardById.avsc index 11a3760c..2eb52e21 100644 --- a/serialization-avro/src/main/avro/giftcard.query.GetCardById.avsc +++ b/serialization-avro/src/main/avro/giftcard.query.GetCardById.avsc @@ -4,7 +4,7 @@ "name": "GetCardByIdQuery", "doc": "Query for git card by given id.", "revision": "1", - "javaAnnotation": "org.axonframework.serialization.Revision(\"1\")", + "javaAnnotation": "org.axonframework.messaging.queryhandling.annotation.Query(version = \"1\")", "fields": [ { "name": "id", diff --git a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/SerializationAvroApplication.java b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/SerializationAvroApplication.java index 41d6a086..0fd63576 100644 --- a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/SerializationAvroApplication.java +++ b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/SerializationAvroApplication.java @@ -9,7 +9,7 @@ import io.axoniq.dev.samples.serializationavro.api.IssueCardCommand; import io.axoniq.dev.samples.serializationavro.api.RedeemCardCommand; import org.apache.avro.message.SchemaStore; -import org.axonframework.spring.serialization.avro.AvroSchemaScan; +import org.axonframework.extension.spring.conversion.avro.AvroSchemaScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; diff --git a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/command/GiftCard.java b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/command/GiftCard.java index 2918b34b..256ae53b 100644 --- a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/command/GiftCard.java +++ b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/command/GiftCard.java @@ -1,42 +1,41 @@ package io.axoniq.dev.samples.serializationavro.command; import io.axoniq.dev.samples.serializationavro.api.*; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; +import org.axonframework.eventsourcing.annotation.EventSourcingHandler; +import org.axonframework.eventsourcing.annotation.reflection.EntityCreator; +import org.axonframework.extension.spring.stereotype.EventSourced; +import org.axonframework.messaging.commandhandling.annotation.CommandHandler; +import org.axonframework.messaging.eventhandling.gateway.EventAppender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@Aggregate +@EventSourced class GiftCard { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @AggregateIdentifier private String giftCardId; private int remainingValue; + @EntityCreator public GiftCard() { // Required by Axon logger.debug("Empty constructor invoked"); } @CommandHandler - public GiftCard(IssueCardCommand command) { + public static void handle(IssueCardCommand command, EventAppender eventAppender) { logger.debug("handling {}", command); if (command.getAmount() <= 0) { throw new NegativeOrZeroAmount(command.getAmount(), "amount <= 0"); } - apply(new CardIssuedEvent(command.getId(), command.getAmount())); + eventAppender.append(new CardIssuedEvent(command.getId(), command.getAmount())); } @CommandHandler - public void handle(RedeemCardCommand command) { + public void handle(RedeemCardCommand command, EventAppender eventAppender) { logger.debug("handling {}", command); if (command.getAmount() <= 0) { throw new NegativeOrZeroAmount(command.getAmount(), "amount <= 0"); @@ -44,7 +43,7 @@ public void handle(RedeemCardCommand command) { if (command.getAmount() > remainingValue) { throw new InsufficientFunds("amount > remaining value"); } - apply(new CardRedeemedEvent(giftCardId, command.getAmount())); + eventAppender.append(new CardRedeemedEvent(giftCardId, command.getAmount())); } @EventSourcingHandler diff --git a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/query/GiftCardProjection.java b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/query/GiftCardProjection.java index bfb79697..21e37088 100644 --- a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/query/GiftCardProjection.java +++ b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/query/GiftCardProjection.java @@ -6,8 +6,8 @@ import io.axoniq.dev.samples.serializationavro.api.CardRedeemedEvent; import io.axoniq.dev.samples.serializationavro.api.GetAllCardsQuery; import io.axoniq.dev.samples.serializationavro.api.GetCardByIdQuery; -import org.axonframework.eventhandling.EventHandler; -import org.axonframework.queryhandling.QueryHandler; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; +import org.axonframework.messaging.queryhandling.annotation.QueryHandler; import org.springframework.stereotype.Component; import java.util.Optional; diff --git a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardController.java b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardController.java index 3eac7314..fa935f59 100644 --- a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardController.java +++ b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardController.java @@ -2,8 +2,7 @@ import io.axoniq.dev.samples.serializationavro.api.IssueCardCommand; import io.axoniq.dev.samples.serializationavro.api.RedeemCardCommand; -import org.axonframework.commandhandling.CommandExecutionException; -import org.axonframework.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -34,7 +33,7 @@ public GiftCardController(CommandGateway commandGateway) { @PostMapping public CompletableFuture> issueNewGiftCard(@RequestBody IssueCardDto request) { IssueCardCommand command = new IssueCardCommand(UUID.randomUUID().toString(), request.amount()); - return commandGateway.send(command) + return commandGateway.send(command, Object.class) .thenApply(it -> ResponseEntity.ok(String.valueOf(it))) .exceptionally(e -> { logException(e); @@ -45,7 +44,7 @@ public CompletableFuture> issueNewGiftCard(@RequestBody I @PutMapping("/{id}") public CompletableFuture> redeem(@PathVariable String id, @RequestBody RedeemCardDto dto) { - return commandGateway.send(new RedeemCardCommand(id, dto.amount())) + return commandGateway.send(new RedeemCardCommand(id, dto.amount()), Object.class) .thenApply(it -> ResponseEntity.ok("")) .exceptionally(e -> { logException(e); diff --git a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardInventoryController.java b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardInventoryController.java index 55c1f00b..b69bb36a 100644 --- a/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardInventoryController.java +++ b/serialization-avro/src/main/java/io/axoniq/dev/samples/serializationavro/rest/GiftCardInventoryController.java @@ -4,8 +4,7 @@ import io.axoniq.dev.samples.serializationavro.api.CardList; import io.axoniq.dev.samples.serializationavro.api.GetAllCardsQuery; import io.axoniq.dev.samples.serializationavro.api.GetCardByIdQuery; -import org.axonframework.messaging.responsetypes.ResponseTypes; -import org.axonframework.queryhandling.QueryGateway; +import org.axonframework.messaging.queryhandling.gateway.QueryGateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -29,7 +28,7 @@ public GiftCardInventoryController(QueryGateway queryGateway) { @GetMapping public CompletableFuture>> getAllGiftCards() { - return queryGateway.query(new GetAllCardsQuery(), ResponseTypes.instanceOf(CardList.class)) + return queryGateway.query(new GetAllCardsQuery(), CardList.class) .thenApply(it -> ResponseEntity.ok( it .getElements() @@ -46,7 +45,7 @@ public CompletableFuture>> getAllGiftCards() { @GetMapping("/{id}") public CompletableFuture> getById(@PathVariable String id) { - return queryGateway.query(new GetCardByIdQuery(id), ResponseTypes.instanceOf(Card.class)) + return queryGateway.query(new GetCardByIdQuery(id), Card.class) .thenApply(it -> ResponseEntity.ok(new CardDto(it.getId(), it.getAmount()))) .exceptionally(e -> { logException(e); diff --git a/serialization-avro/src/main/resources/application.yaml b/serialization-avro/src/main/resources/application.yaml index d1f6939d..2cd5980e 100644 --- a/serialization-avro/src/main/resources/application.yaml +++ b/serialization-avro/src/main/resources/application.yaml @@ -8,7 +8,7 @@ server: port: 8282 axon: - serializer: + converter: events: avro messages: avro general: jackson \ No newline at end of file diff --git a/set-based-validation-actor-model/README.md b/set-based-validation-actor-model/README.md deleted file mode 100644 index 08dc1f7d..00000000 --- a/set-based-validation-actor-model/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Set Based Consistency Validations using Actor Model - -This is an example how use the Actor Model to validate the consistency of the set with Axon Framework. - -## How to claim an email address - -Let’s assume we got this constraint: `A User can only be registered when the email address is unique.` -We need to create two aggregate types in this case. - -The first one is -the [EmailUniquenessCheck Aggregate](src/main/java/io/axoniq/dev/samples/command/aggregate/EmailUniquenessCheck.java), -and the second one is the [User aggregate](src/main/java/io/axoniq/dev/samples/command/aggregate/User.java). The -EmailUniquenessCheck - -The `EmailUniquenessCheck` aggregate has the `emailAddress` as the aggregate identifier. Every time a user should be -registered, a command must be sent to the `EmailUniquenessCheck` aggregate to validate the uniqueness. - -A command handler should handle this command with a `create_if_missing` creation policy. A boolean property on the -aggregate that keeps the state whether the email -was claimed or not. If the email has not been claimed the email address is approved. - -The moment the email address is approved, the user should be created. As you can see, this is done in -the `EmailUniquenessCheck` using the `Aggregatelifecycle.createNew()` function. - -If the user registration fails, the email address is not added to the claimed email addresses because the -`EmailAddressApproved` event will not be stored in the event store. And when the email address is not unique, the User -will not be registered. Doing it this way, consistency is guaranteed. - -## How to remove release a claim on an email address - -Now that we have the creation part covered, we can also try to solve the issue of updating the email address. If someone -wants to change the email address, the former email address should be removed from the existing claimed email addresses. -The good news is that this can be done in two separate transactions. - -First, create the user with the new email address. If that command succeeds, the old email address can be claimed again. -The initial value of the claimed boolean is false. When event sourcing the creation event, this value must be set to -true. In the command handler with the `create_if_missing creation` policy, we need to check if the email address has -been claimed. If that is not the case, the email address is allowed and added to the set. - -## Composite keys - -In this example, I used a String as a unique key, but a custom object can also be used to check a more sophisticated -constraint. An elegant way is to use the object's hash code or concatenate the different values as the aggregate -identifier. - -## Testing the behaviour - -This [requests-file](src/main/resources/requests.http) contains example requests that you can use to see how this works, -you can start the [DemoApplication](src/main/java/io/axoniq/dev/samples/DemoApplication.java) and see for yourself. - -Created with :heart: by [AxonIQ](http://www.axoniq.io) - -[axon]: https://axoniq.io/ - diff --git a/set-based-validation-actor-model/pom.xml b/set-based-validation-actor-model/pom.xml deleted file mode 100644 index d0bec430..00000000 --- a/set-based-validation-actor-model/pom.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - code-samples - io.axoniq - 0.0.2-SNAPSHOT - - 4.0.0 - - set-based-validation-actor-model - - Set-Based Validation - Actor Model - Module explaining an approach to deal with Set-Based Validation by using the Actor Model - - - - - org.axonframework - axon-spring-boot-starter - - - - org.springframework.boot - spring-boot-starter-web - - - \ No newline at end of file diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/SetBasedValidationActorModelApplication.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/SetBasedValidationActorModelApplication.java deleted file mode 100644 index 5f4a99b8..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/SetBasedValidationActorModelApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SetBasedValidationActorModelApplication { - - public static void main(String[] args) { - SpringApplication.run(SetBasedValidationActorModelApplication.class, args); - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RegisterUserWithUniqueEmailAddressCommand.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RegisterUserWithUniqueEmailAddressCommand.java deleted file mode 100644 index 66fdb400..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RegisterUserWithUniqueEmailAddressCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api.commands; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record RegisterUserWithUniqueEmailAddressCommand( - @TargetAggregateIdentifier String email, - UUID userId -) { - -} \ No newline at end of file diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserCommand.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserCommand.java deleted file mode 100644 index 3acf4e93..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserCommand.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.axoniq.dev.samples.api.commands; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record RemoveUserCommand( - @TargetAggregateIdentifier UUID userId -) { - -} \ No newline at end of file diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserWithUniqueEmailAddressCommand.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserWithUniqueEmailAddressCommand.java deleted file mode 100644 index f1bfd0fa..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/commands/RemoveUserWithUniqueEmailAddressCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api.commands; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record RemoveUserWithUniqueEmailAddressCommand( - @TargetAggregateIdentifier String email, - UUID userId -) { - -} \ No newline at end of file diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/RegisterUserDto.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/RegisterUserDto.java deleted file mode 100644 index 9c5700cb..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/RegisterUserDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.axoniq.dev.samples.api.dto; - -public class RegisterUserDto { - - private String emailAddress; - - public RegisterUserDto(String emailAddress) { - this.emailAddress = emailAddress; - } - - public RegisterUserDto() { - } - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress = emailAddress; - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/UpdateEmailFromUserDto.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/UpdateEmailFromUserDto.java deleted file mode 100644 index 766d283c..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/dto/UpdateEmailFromUserDto.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.axoniq.dev.samples.api.dto; - -import java.util.UUID; - -public class UpdateEmailFromUserDto { - - private String emailAddress; - private String formerEmailAddress; - private UUID userId; - - public UpdateEmailFromUserDto(String emailAddress, String formerEmailAddress) { - this.emailAddress = emailAddress; - this.formerEmailAddress = formerEmailAddress; - } - - public UpdateEmailFromUserDto() { - } - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress = emailAddress; - } - - public String getFormerEmailAddress() { - return formerEmailAddress; - } - - public void setFormerEmailAddress(String formerEmailAddress) { - this.formerEmailAddress = formerEmailAddress; - } - - public UUID getUserId() { - return userId; - } - - public void setUserId(UUID userId) { - this.userId = userId; - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressApprovedEvent.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressApprovedEvent.java deleted file mode 100644 index b23e02df..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressApprovedEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.axoniq.dev.samples.api.events; - -import java.util.UUID; - -public record EmailAddressApprovedEvent( - String emailAddress, - UUID userId -) { -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressRemovedEvent.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressRemovedEvent.java deleted file mode 100644 index bb7cb882..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/EmailAddressRemovedEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.axoniq.dev.samples.api.events; - -import java.util.UUID; - -public record EmailAddressRemovedEvent( - String emailAddress, - UUID userId -) { - -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRegisteredEvent.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRegisteredEvent.java deleted file mode 100644 index be2060b7..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRegisteredEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.axoniq.dev.samples.api.events; - -import java.util.UUID; - -public record UserRegisteredEvent( - UUID userId, - String email -) { - -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRemovedEvent.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRemovedEvent.java deleted file mode 100644 index c54e20e9..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/api/events/UserRemovedEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.axoniq.dev.samples.api.events; - -import java.util.UUID; - -public record UserRemovedEvent( - UUID userId -) { - -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/EmailUniquenessCheck.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/EmailUniquenessCheck.java deleted file mode 100644 index ea1ee41e..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/EmailUniquenessCheck.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.axoniq.dev.samples.command.aggregate; - -import io.axoniq.dev.samples.api.commands.RegisterUserWithUniqueEmailAddressCommand; -import io.axoniq.dev.samples.api.commands.RemoveUserWithUniqueEmailAddressCommand; -import io.axoniq.dev.samples.api.events.EmailAddressApprovedEvent; -import io.axoniq.dev.samples.api.events.EmailAddressRemovedEvent; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateCreationPolicy; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.modelling.command.AggregateLifecycle; -import org.axonframework.modelling.command.CreationPolicy; -import org.axonframework.spring.stereotype.Aggregate; -import org.springframework.util.Assert; - -import java.util.UUID; - -@Aggregate -class EmailUniquenessCheck { - - @AggregateIdentifier - private String emailAddress; - private UUID userId; - private boolean claimed = false; - - @CommandHandler - @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) - public void handle(RegisterUserWithUniqueEmailAddressCommand command) throws Exception { - // A user should only be registered when the email address has not been claimed - Assert.isTrue(!claimed, () -> "A user with this email address already exists"); - AggregateLifecycle.createNew(User.class, () -> new User(command.userId(), command.email())); - AggregateLifecycle.apply(new EmailAddressApprovedEvent(command.email(), command.userId())); - } - - @CommandHandler - public void handle(RemoveUserWithUniqueEmailAddressCommand command) { - Assert.isTrue(claimed, () -> "This email address has not been claimed"); - AggregateLifecycle.apply(new EmailAddressRemovedEvent(command.email(), userId)); - } - - @EventSourcingHandler - public void on(EmailAddressApprovedEvent event) { - this.emailAddress = event.emailAddress(); - this.claimed = true; - this.userId = event.userId(); - } - - @EventSourcingHandler - public void on(EmailAddressRemovedEvent event) { - this.claimed = false; - } - - public EmailUniquenessCheck() { - // needed by Axon - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/User.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/User.java deleted file mode 100644 index 6ba3b338..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/aggregate/User.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.axoniq.dev.samples.command.aggregate; - -import io.axoniq.dev.samples.api.commands.RemoveUserCommand; -import io.axoniq.dev.samples.api.events.UserRegisteredEvent; -import io.axoniq.dev.samples.api.events.UserRemovedEvent; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.modelling.command.AggregateLifecycle; -import org.axonframework.spring.stereotype.Aggregate; - -import java.util.UUID; - -@Aggregate -class User { - - @AggregateIdentifier - public UUID userId; - - public User(UUID userId, String emailAddress) { - AggregateLifecycle.apply(new UserRegisteredEvent(userId, emailAddress)); - } - - @CommandHandler - public void handle(RemoveUserCommand command) { - AggregateLifecycle.apply(new UserRemovedEvent(command.userId())); - } - - @EventSourcingHandler - public void handle(UserRegisteredEvent event) { - this.userId = event.userId(); - } - - @EventSourcingHandler - public void handle(UserRemovedEvent event) { - AggregateLifecycle.markDeleted(); - } - - public User() { - //needed by Axon - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/controller/UserController.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/controller/UserController.java deleted file mode 100644 index 5aaf1a1b..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/command/controller/UserController.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.axoniq.dev.samples.command.controller; - -import io.axoniq.dev.samples.api.commands.RegisterUserWithUniqueEmailAddressCommand; -import io.axoniq.dev.samples.api.commands.RemoveUserWithUniqueEmailAddressCommand; -import io.axoniq.dev.samples.api.dto.RegisterUserDto; -import io.axoniq.dev.samples.api.dto.UpdateEmailFromUserDto; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import java.lang.invoke.MethodHandles; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@RestController -public class UserController { - - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final CommandGateway commandGateway; - - public UserController(CommandGateway commandGateway) { - this.commandGateway = commandGateway; - } - - @PostMapping(path = "/users") - public CompletableFuture register(@RequestBody RegisterUserDto registerUserDto) { - UUID generatedUserId = UUID.randomUUID(); - logger.info("UUID [{}]", generatedUserId); - return commandGateway.send(new RegisterUserWithUniqueEmailAddressCommand( - registerUserDto.getEmailAddress(), generatedUserId - )); - } - - @PutMapping(path = "/users") - public CompletableFuture changeEmailAddress(@RequestBody UpdateEmailFromUserDto updateEmailFromUserDto) { - UUID generatedUserId = UUID.randomUUID(); - commandGateway.sendAndWait(new RegisterUserWithUniqueEmailAddressCommand( - updateEmailFromUserDto.getEmailAddress(), generatedUserId - )); - return commandGateway.send(new RemoveUserWithUniqueEmailAddressCommand( - updateEmailFromUserDto.getFormerEmailAddress(), updateEmailFromUserDto.getUserId() - )); - } -} diff --git a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/query/eventhandler/UserEventHandler.java b/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/query/eventhandler/UserEventHandler.java deleted file mode 100644 index ae4cc005..00000000 --- a/set-based-validation-actor-model/src/main/java/io/axoniq/dev/samples/query/eventhandler/UserEventHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.axoniq.dev.samples.query.eventhandler; - -import io.axoniq.dev.samples.api.commands.RemoveUserCommand; -import io.axoniq.dev.samples.api.events.EmailAddressRemovedEvent; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.eventhandling.EventHandler; -import org.springframework.stereotype.Component; - -@Component -public class UserEventHandler { - - private final CommandGateway commandGateway; - - public UserEventHandler(CommandGateway commandGateway) { - this.commandGateway = commandGateway; - } - - @EventHandler - public void handle(EmailAddressRemovedEvent event) { - commandGateway.send(new RemoveUserCommand(event.userId())); - } -} diff --git a/set-based-validation-actor-model/src/main/resources/requests.http b/set-based-validation-actor-model/src/main/resources/requests.http deleted file mode 100644 index d326fa25..00000000 --- a/set-based-validation-actor-model/src/main/resources/requests.http +++ /dev/null @@ -1,19 +0,0 @@ -### Create a User and check if the email address exists - -POST http://localhost:8080/users -Content-Type: application/json - -{ - "emailAddress": "bob@gmail.com" -} - -### Change the email address and release the claim on the email address. Take the userId from the create user. - -PUT http://localhost:8080/users -Content-Type: application/json - -{ - "emailAddress": "robert@gmail.com", - "formerEmailAddress": "bob@gmail.com", - "userId": "cd1f8637-d41c-4a37-bc75-cc052d2d262e" -} \ No newline at end of file diff --git a/set-based-validation/README.md b/set-based-validation/README.md deleted file mode 100644 index ff5d12a6..00000000 --- a/set-based-validation/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Set Based Consistency Validation - -A type of question that occurs a lot is `How can I check if an account with a particular email address already exists?`. -In essence this signals the requirement to be able to validate a set, which is a more intricate process when it comes to -applications which follow the CQRS paradigm. Without going in full detail here why this is a problem, a straightforward -solution is to enhance the command model by adding a small look-up table which can be queried during such a validation -step. - -There are several ways to invoke this set validation when using Axon, several of which are shared in this repository. - -Prior to checking if the email address already exists you need to save all the email addresses that are used. You can do -this by using an event handling component in this case -an [AccountEventHandler](https://github.com/AxonIQ/code-samples/blob/master/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountEventHandler.java) - -You’ll need to make the `AccountEventHandler`'s processing group subscribing to be able to update the repository in the -same thread that has applied the event; thus ensuring consistent updates of the set. You can achieve that by adding the -following property to your `application.properties` file: `axon.eventhandling.processors.emailEntity.mode=subscribing` - -Subscribing processors will create and maintain a projection immediately after an event has been applied and thus are -immediately consistent. Moreover, lookup tables like this should always be owned by the command side _only_ and as such -should not be exposed through a Query API on top of it. - -Now that you have a lookup table you can check if the email address exists before applying the event. We implemented -this in three different ways: - -1. By using a command message dispatcher which does a check on the CreateAccountCommand - e.g. [AccountCreationDispatchInterceptor](https://github.com/AxonIQ/code-samples/blob/master/set-based-validation/src/main/java/io/axoniq/dev/samples/command/interceptor/AccountCreationDispatchInterceptor.java) -2. By using an external commandHandler that does this check for the RequestEmailChangeCommand - e.g. [AccountCommandHandler](https://github.com/AxonIQ/code-samples/blob/master/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountCommandHandler.java) -3. By using a ParameterResolver that returns a Boolean value that returns true if the email already exists - e.g. [EmailAlreadyExistsResolverFactory](https://github.com/AxonIQ/code-samples/blob/master/set-based-validation/src/main/java/io/axoniq/dev/samples/resolver/EmailAlreadyExistsResolverFactory.java) - This way you can add the boolean emailAlreadyExists to the ChangeEmailAddressCommand command handler done - in [Account](https://github.com/AxonIQ/code-samples/blob/master/set-based-validation/src/main/java/io/axoniq/dev/samples/command/aggregate/Account.java) - -You can test the endpoints using [Swagger](http://localhost:8080/swagger-ui/#) - -# References - -This implementation is based on https://danielwhittaker.me/2017/10/09/handle-set-based-consistency-validation-cqrs/ diff --git a/set-based-validation/docker-compose.yml b/set-based-validation/docker-compose.yml deleted file mode 100644 index 2c9a3859..00000000 --- a/set-based-validation/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3.3' -services: - axonserver: - image: axoniq/axonserver - hostname: axonserver - ports: - - '8024:8024' - - '8124:8124' diff --git a/set-based-validation/pom.xml b/set-based-validation/pom.xml deleted file mode 100644 index 76d70d8f..00000000 --- a/set-based-validation/pom.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - code-samples - io.axoniq - 0.0.2-SNAPSHOT - - 4.0.0 - - set-based-validation - - Set-Based Validation - Module explaining an approach to deal with Set-Based Validation - - - - 2.20 - - 2.8.13 - - - - - - org.axonframework - axon-spring-boot-starter - - - org.axonframework - axon-test - test - - - - org.springframework.boot - spring-boot-starter-data-jpa - ${spring-boot.version} - compile - - - org.springframework.boot - spring-boot-starter-web - - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson-core.version} - compile - - - - com.h2database - h2 - runtime - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${spring-doc.version} - - - - org.yaml - snakeyaml - - - - - - org.hamcrest - hamcrest-core - test - - - org.junit.jupiter - junit-jupiter - - - org.mockito - mockito-core - test - - - \ No newline at end of file diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/SetBasedValidationApplication.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/SetBasedValidationApplication.java deleted file mode 100644 index 805fb286..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/SetBasedValidationApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SetBasedValidationApplication { - - public static void main(String[] args) { - SpringApplication.run(SetBasedValidationApplication.class, args); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountCreatedEvent.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountCreatedEvent.java deleted file mode 100644 index 268ceb42..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountCreatedEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.UUID; - -public record AccountCreatedEvent( - @JsonProperty("accountId") UUID accountId, - @JsonProperty("emailAddress") String emailAddress -) { - -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountRequestData.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountRequestData.java deleted file mode 100644 index 7caf89a2..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AccountRequestData.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.axoniq.dev.samples.api; - -import java.util.Objects; - -public class AccountRequestData { - - private String emailAddress; - - public AccountRequestData(String emailAddress) { - this.emailAddress = emailAddress; - } - - public AccountRequestData() { - } - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress = emailAddress; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AccountRequestData that = (AccountRequestData) o; - return Objects.equals(emailAddress, that.emailAddress); - } - - @Override - public int hashCode() { - return Objects.hash(emailAddress); - } - - @Override - public String toString() { - return "AccountRequestData{" + - "emailAddress='" + emailAddress + '\'' + - '}'; - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AlterEmailAddressCommand.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AlterEmailAddressCommand.java deleted file mode 100644 index 1bf19383..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/AlterEmailAddressCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record AlterEmailAddressCommand( - @TargetAggregateIdentifier UUID accountId, - String updatedEmailAddress -) { - -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/ChangeEmailAddressCommand.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/ChangeEmailAddressCommand.java deleted file mode 100644 index 03f2e534..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/ChangeEmailAddressCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record ChangeEmailAddressCommand( - @TargetAggregateIdentifier UUID accountId, - String updatedEmailAddress -) { - -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/CreateAccountCommand.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/CreateAccountCommand.java deleted file mode 100644 index d27a7e7f..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/CreateAccountCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -import java.util.UUID; - -public record CreateAccountCommand( - @TargetAggregateIdentifier UUID accountId, - String emailAddress -) { - -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/EmailAddressChangedEvent.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/EmailAddressChangedEvent.java deleted file mode 100644 index 00a0d2da..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/EmailAddressChangedEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.axoniq.dev.samples.api; - -import java.util.UUID; - -public record EmailAddressChangedEvent(UUID accountId, String emailAddress) { - -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/RequestEmailChangeCommand.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/api/RequestEmailChangeCommand.java deleted file mode 100644 index 3ccab8f2..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/api/RequestEmailChangeCommand.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.axoniq.dev.samples.api; - -import org.axonframework.commandhandling.RoutingKey; - -import java.util.UUID; - -/** - * Since this command is routed to a command handling component, the RoutingKey annotation is used. The - * TargetAggregateIdentifier could also be used because that has the RoutingKey meta annotated - */ -public record RequestEmailChangeCommand( - @RoutingKey UUID accountId, - String updatedEmailAddress -) { - -} \ No newline at end of file diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/aggregate/Account.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/aggregate/Account.java deleted file mode 100644 index fde65161..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/aggregate/Account.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.axoniq.dev.samples.command.aggregate; - -import io.axoniq.dev.samples.api.AccountCreatedEvent; -import io.axoniq.dev.samples.api.AlterEmailAddressCommand; -import io.axoniq.dev.samples.api.ChangeEmailAddressCommand; -import io.axoniq.dev.samples.api.CreateAccountCommand; -import io.axoniq.dev.samples.api.EmailAddressChangedEvent; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; - -import java.util.UUID; - -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@Aggregate -public class Account { - - @AggregateIdentifier - private UUID accountId; - private String emailAddress; - - @CommandHandler - public Account(CreateAccountCommand command) { - apply(new AccountCreatedEvent(command.accountId(), command.emailAddress())); - } - - /** - * Uses the parameter resolver for email address exists. Links to "Validation using a parameter resolver" in the - * blog - */ - @CommandHandler - public void handle(ChangeEmailAddressCommand command, Boolean emailAddressExists) { - if (emailAddressExists) { - throw new IllegalStateException(String.format("Account with email address %s already exists", - command.updatedEmailAddress())); - } - if (emailAddress.equals(command.updatedEmailAddress())) { - throw new IllegalStateException(String.format("Email address %s is already used for account with id %s ", - command.updatedEmailAddress(), accountId)); - } - apply(new EmailAddressChangedEvent(command.accountId(), command.updatedEmailAddress())); - } - - @CommandHandler - public void handle(AlterEmailAddressCommand command, EmailRepository emailRepository) { - if (emailRepository.existsById(command.updatedEmailAddress())) { - throw new IllegalStateException(String.format("Account with email address %s already exists", - command.updatedEmailAddress())); - } - if (emailAddress.equals(command.updatedEmailAddress())) { - throw new IllegalStateException(String.format("Email address %s is already used for account with id %s ", - command.updatedEmailAddress(), accountId)); - } - - apply(new EmailAddressChangedEvent(command.accountId(), command.updatedEmailAddress())); - } - - @EventSourcingHandler - public void on(AccountCreatedEvent event) { - this.accountId = event.accountId(); - this.emailAddress = event.emailAddress(); - } - - @EventSourcingHandler - public void on(EmailAddressChangedEvent event) { - this.emailAddress = event.emailAddress(); - } - - public Account() { - // Required by Axon - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountCommandHandler.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountCommandHandler.java deleted file mode 100644 index 61b0e4fc..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountCommandHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.axoniq.dev.samples.command.handler; - -import io.axoniq.dev.samples.api.ChangeEmailAddressCommand; -import io.axoniq.dev.samples.api.RequestEmailChangeCommand; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.springframework.stereotype.Component; - -/** - * Command handling component that validates if the email address already exists. Links to the `Validation in an - * External Command Handler` section in this set based - * validation blog. - * - * @author Yvonne Ceelie - */ -@Component -public class AccountCommandHandler { - - @CommandHandler - public void handle(RequestEmailChangeCommand command, CommandGateway commandGateway, - EmailRepository emailRepository) { - if (emailRepository.existsById(command.updatedEmailAddress())) { - throw new IllegalStateException(String.format("Account with email address %s already exists", - command.updatedEmailAddress())); - } - commandGateway.send(new ChangeEmailAddressCommand(command.accountId(), command.updatedEmailAddress())); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountEventHandler.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountEventHandler.java deleted file mode 100644 index 19b4c28e..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/handler/AccountEventHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.axoniq.dev.samples.command.handler; - -import io.axoniq.dev.samples.api.AccountCreatedEvent; -import io.axoniq.dev.samples.api.EmailAddressChangedEvent; -import io.axoniq.dev.samples.command.persistence.EmailJpaEntity; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -/** - * Subscribing processor that updates lookup table with email addresses used in the Account. Links to the `Update the - * look-up table` section in this set based validation - * blog. - * - * @author Yvonne Ceelie - */ -@Component -@ProcessingGroup("emailEntity") -public class AccountEventHandler { - - @EventHandler - public void on(AccountCreatedEvent event, EmailRepository emailRepository) { - emailRepository.save(new EmailJpaEntity(event.emailAddress(), event.accountId())); - } - - @EventHandler - public void on(EmailAddressChangedEvent event, EmailRepository emailRepository) { - Optional emailEntityOptional = emailRepository - .findEmailJpaEntityByAccountId(event.accountId()); - emailEntityOptional.ifPresent(emailRepository::delete); - emailRepository.save(new EmailJpaEntity(event.emailAddress(), event.accountId())); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/interceptor/AccountCreationDispatchInterceptor.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/interceptor/AccountCreationDispatchInterceptor.java deleted file mode 100644 index 91ff0c96..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/interceptor/AccountCreationDispatchInterceptor.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.axoniq.dev.samples.command.interceptor; - -import io.axoniq.dev.samples.api.CreateAccountCommand; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.axonframework.commandhandling.CommandMessage; -import org.axonframework.messaging.MessageDispatchInterceptor; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.function.BiFunction; - -/** - * Intercepts the create account command message and throws IllegalStateException when already account aggregate with - * email address already exists. Links to "Validation through a Dispatch Interceptor" section in this set based validation blog. - * - * @author Yvonne Ceelie - */ -@Component -public class AccountCreationDispatchInterceptor implements MessageDispatchInterceptor> { - - private final EmailRepository emailRepository; - - public AccountCreationDispatchInterceptor(EmailRepository emailRepository) { - this.emailRepository = emailRepository; - } - - @Override - public BiFunction, CommandMessage> handle(List> list) { - return (i, m) -> { - if (CreateAccountCommand.class.equals(m.getPayloadType())) { - final CreateAccountCommand createAccountCommand = (CreateAccountCommand) m.getPayload(); - if (emailRepository.existsById(createAccountCommand.emailAddress())) { - throw new IllegalStateException(String.format("Account with email address %s already exists", - createAccountCommand.emailAddress())); - } - } - return m; - }; - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailJpaEntity.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailJpaEntity.java deleted file mode 100644 index a0e4ddcb..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailJpaEntity.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.axoniq.dev.samples.command.persistence; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import java.util.Objects; -import java.util.UUID; - -@Entity -public class EmailJpaEntity { - - @Id - public String emailAddress; - - public UUID accountId; - - public EmailJpaEntity() { - } - - public EmailJpaEntity(String emailAddress, UUID accountId) { - this.accountId = accountId; - this.emailAddress = emailAddress; - } - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress = emailAddress; - } - - public UUID getAccountId() { - return accountId; - } - - public void setAccountId(UUID accountId) { - this.accountId = accountId; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - EmailJpaEntity that = (EmailJpaEntity) o; - return Objects.equals(emailAddress, that.emailAddress) && - Objects.equals(accountId, that.accountId); - } - - @Override - public int hashCode() { - return Objects.hash(emailAddress, accountId); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailRepository.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailRepository.java deleted file mode 100644 index 84c056fb..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/command/persistence/EmailRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.axoniq.dev.samples.command.persistence; - -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; -import java.util.UUID; - -public interface EmailRepository extends JpaRepository { - - Optional findEmailJpaEntityByAccountId(UUID accountId); -} \ No newline at end of file diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/config/AxonConfig.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/config/AxonConfig.java deleted file mode 100644 index 6f15f2d5..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/config/AxonConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.axoniq.dev.samples.config; - -import io.axoniq.dev.samples.command.interceptor.AccountCreationDispatchInterceptor; -import org.axonframework.commandhandling.CommandBus; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.commandhandling.gateway.DefaultCommandGateway; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AxonConfig { - - @Bean - public CommandGateway commandGateway(CommandBus commandBus, - AccountCreationDispatchInterceptor accountCreationDispatchInterceptor) { - return DefaultCommandGateway.builder() - .commandBus(commandBus) - .dispatchInterceptors(accountCreationDispatchInterceptor) - .build(); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/controller/AccountController.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/controller/AccountController.java deleted file mode 100644 index f85b655b..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/controller/AccountController.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.axoniq.dev.samples.controller; - -import io.axoniq.dev.samples.api.AccountRequestData; -import io.axoniq.dev.samples.api.ChangeEmailAddressCommand; -import io.axoniq.dev.samples.api.CreateAccountCommand; -import io.axoniq.dev.samples.api.RequestEmailChangeCommand; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@RestController -public class AccountController { - - private final CommandGateway commandGateway; - - public AccountController(CommandGateway commandGateway) { - this.commandGateway = commandGateway; - } - - @PostMapping(path = "/accounts") - public CompletableFuture register(@RequestBody AccountRequestData accountRequestData) { - UUID accountId = UUID.randomUUID(); - return commandGateway.send(new CreateAccountCommand(accountId, accountRequestData.getEmailAddress())); - } - - @PutMapping(path = "/accounts/{accountId}/requestUpdateEmailAddress") - public CompletableFuture requestUpdateEmailAddress(@RequestBody AccountRequestData accountRequestData, - @PathVariable("accountId") UUID accountId) { - return commandGateway.send(new RequestEmailChangeCommand(accountId, accountRequestData.getEmailAddress())); - } - - @PutMapping(path = "/accounts/{accountId}/updateEmailAddress") - public CompletableFuture updateEmailAddress(@RequestBody AccountRequestData accountRequestData, - @PathVariable("accountId") UUID accountId) { - return commandGateway.send(new ChangeEmailAddressCommand(accountId, accountRequestData.getEmailAddress())); - } -} diff --git a/set-based-validation/src/main/java/io/axoniq/dev/samples/resolver/EmailAlreadyExistsResolverFactory.java b/set-based-validation/src/main/java/io/axoniq/dev/samples/resolver/EmailAlreadyExistsResolverFactory.java deleted file mode 100644 index c3872df9..00000000 --- a/set-based-validation/src/main/java/io/axoniq/dev/samples/resolver/EmailAlreadyExistsResolverFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.axoniq.dev.samples.resolver; - -import io.axoniq.dev.samples.api.ChangeEmailAddressCommand; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.axonframework.messaging.Message; -import org.axonframework.messaging.annotation.ParameterResolver; -import org.axonframework.messaging.annotation.ParameterResolverFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Executable; -import java.lang.reflect.Parameter; - -/** - * This parameter resolver resolves to true if an account aggregate with email address already exists. Links to - * "Validation using a Parameter Resolver" section in this set based validation blog. - * - * @author Yvonne Ceelie - */ -@Component -public class EmailAlreadyExistsResolverFactory implements ParameterResolver, ParameterResolverFactory { - - private final EmailRepository emailRepository; - - @Autowired - public EmailAlreadyExistsResolverFactory(EmailRepository emailRepository) { - this.emailRepository = emailRepository; - } - - @Override - public ParameterResolver createInstance(Executable executable, - Parameter[] parameters, - int parameterIndex) { - return Boolean.class.equals(parameters[parameterIndex].getType()) ? this : null; - } - - @Override - public Boolean resolveParameterValue(Message message) { - if (matches(message)) { - String emailAddress = ((ChangeEmailAddressCommand) message.getPayload()).updatedEmailAddress(); - return emailRepository.findById(emailAddress).isPresent(); - } - throw new IllegalArgumentException("Message payload not of type ChangeEmailAddressCommand"); - } - - @Override - public boolean matches(Message message) { - return ChangeEmailAddressCommand.class.isAssignableFrom(message.getPayloadType()); - } - - @Override - public Class supportedPayloadType() { - return ChangeEmailAddressCommand.class; - } -} diff --git a/set-based-validation/src/main/resources/application.properties b/set-based-validation/src/main/resources/application.properties deleted file mode 100644 index 14e2b724..00000000 --- a/set-based-validation/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -axon.eventhandling.processors.emailEntity.mode=subscribing \ No newline at end of file diff --git a/set-based-validation/src/test/java/io/axoniq/dev/samples/aggregate/AccountTest.java b/set-based-validation/src/test/java/io/axoniq/dev/samples/aggregate/AccountTest.java deleted file mode 100644 index 9c2936b1..00000000 --- a/set-based-validation/src/test/java/io/axoniq/dev/samples/aggregate/AccountTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.axoniq.dev.samples.aggregate; - -import io.axoniq.dev.samples.api.AccountCreatedEvent; -import io.axoniq.dev.samples.api.AlterEmailAddressCommand; -import io.axoniq.dev.samples.api.ChangeEmailAddressCommand; -import io.axoniq.dev.samples.api.CreateAccountCommand; -import io.axoniq.dev.samples.api.EmailAddressChangedEvent; -import io.axoniq.dev.samples.command.aggregate.Account; -import io.axoniq.dev.samples.command.persistence.EmailJpaEntity; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import io.axoniq.dev.samples.resolver.EmailAlreadyExistsResolverFactory; -import org.axonframework.test.aggregate.AggregateTestFixture; -import org.axonframework.test.aggregate.FixtureConfiguration; -import org.junit.jupiter.api.*; - -import java.util.Optional; -import java.util.UUID; - -import static org.mockito.Mockito.*; - -class AccountTest { - - private static final UUID UUID = java.util.UUID.randomUUID(); - private static final String EMAIL_ADDRESS = "bob@gmail.com"; - private static final String EMAIL_ADDRESS_CHANGED = "bob2@gmail.com"; - - private EmailRepository emailRepository; - - private FixtureConfiguration fixture; - - @BeforeEach - void setup() { - emailRepository = mock(EmailRepository.class); - EmailAlreadyExistsResolverFactory emailAlreadyExistsResolverFactory = - new EmailAlreadyExistsResolverFactory(emailRepository); - - fixture = new AggregateTestFixture<>(Account.class); - fixture.registerInjectableResource(new EmailJpaEntity(EMAIL_ADDRESS, UUID)); - fixture.registerInjectableResource(emailRepository); - fixture.registerParameterResolverFactory(emailAlreadyExistsResolverFactory); - } - - @Test - void shouldCreateAccount() { - fixture.givenNoPriorActivity() - .when(new CreateAccountCommand(UUID, EMAIL_ADDRESS)) - .expectEvents(new AccountCreatedEvent(UUID, EMAIL_ADDRESS)); - } - - @Test - void shouldUpdateEmailAddress() { - when(emailRepository.findById(EMAIL_ADDRESS_CHANGED)).thenReturn(Optional.empty()); - - fixture.given(new AccountCreatedEvent(UUID, EMAIL_ADDRESS)) - .when(new ChangeEmailAddressCommand(UUID, EMAIL_ADDRESS_CHANGED)) - .expectEvents(new EmailAddressChangedEvent(UUID, EMAIL_ADDRESS_CHANGED)); - } - - @Test - void shouldNotUpdateDuplicateEmailAddress() { - when(emailRepository.findById(EMAIL_ADDRESS)).thenReturn(Optional.of(new EmailJpaEntity(EMAIL_ADDRESS, UUID))); - fixture.given(new AccountCreatedEvent(UUID, EMAIL_ADDRESS)) - .when(new ChangeEmailAddressCommand(UUID, EMAIL_ADDRESS)) - .expectNoEvents(); - } - - @Test - void shouldAlterEmailAddress() { - fixture.given(new AccountCreatedEvent(UUID, EMAIL_ADDRESS)) - .when(new AlterEmailAddressCommand(UUID, EMAIL_ADDRESS_CHANGED)) - .expectEvents(new EmailAddressChangedEvent(UUID, EMAIL_ADDRESS_CHANGED)); - } - - @Test - void shouldNotAlterDuplicateEmailAddress() { - when(emailRepository.existsById(EMAIL_ADDRESS)).thenReturn(true); - fixture.given(new AccountCreatedEvent(UUID, EMAIL_ADDRESS)) - .when(new AlterEmailAddressCommand(UUID, EMAIL_ADDRESS)) - .expectNoEvents(); - } -} \ No newline at end of file diff --git a/set-based-validation/src/test/java/io/axoniq/dev/samples/handler/AccountEventHandlerTest.java b/set-based-validation/src/test/java/io/axoniq/dev/samples/handler/AccountEventHandlerTest.java deleted file mode 100644 index 1bdc75db..00000000 --- a/set-based-validation/src/test/java/io/axoniq/dev/samples/handler/AccountEventHandlerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.axoniq.dev.samples.handler; - -import io.axoniq.dev.samples.api.AccountCreatedEvent; -import io.axoniq.dev.samples.api.EmailAddressChangedEvent; -import io.axoniq.dev.samples.command.handler.AccountEventHandler; -import io.axoniq.dev.samples.command.persistence.EmailJpaEntity; -import io.axoniq.dev.samples.command.persistence.EmailRepository; -import org.junit.jupiter.api.*; - -import java.util.Optional; - -import static org.mockito.Mockito.*; - -class AccountEventHandlerTest { - - private static final java.util.UUID UUID = java.util.UUID.randomUUID(); - private static final String EMAIL_ADDRESS = "bob@gmail.com"; - - private final EmailRepository emailRepository = mock(EmailRepository.class); - - private AccountEventHandler testSubject; - - @BeforeEach - void setup() { - testSubject = new AccountEventHandler(); - } - - @Test - void accountCreatedTest() { - testSubject.on(new AccountCreatedEvent(UUID, EMAIL_ADDRESS), emailRepository); - EmailJpaEntity emailJpaEntity = new EmailJpaEntity(EMAIL_ADDRESS, UUID); - verify(emailRepository).save(emailJpaEntity); - } - - @Test - void emailAddressChangedTest() { - when(emailRepository.findEmailJpaEntityByAccountId(UUID)).thenReturn(Optional.of(new EmailJpaEntity( - EMAIL_ADDRESS, - UUID))); - testSubject.on(new EmailAddressChangedEvent(UUID, EMAIL_ADDRESS), emailRepository); - EmailJpaEntity emailJpaEntity = new EmailJpaEntity(EMAIL_ADDRESS, UUID); - verify(emailRepository).delete(emailJpaEntity); - verify(emailRepository).save(emailJpaEntity); - } -} diff --git a/snapshots/README.md b/snapshots/README.md index 1dc7a63d..7ba489f3 100644 --- a/snapshots/README.md +++ b/snapshots/README.md @@ -1,63 +1,66 @@ # Snapshot -In this demo we will show how to define and configure a Snapshot Trigger for a given Aggregate. +In this demo we will show how to define and configure Snapshotting for a given event-sourced entity. -Eventually you will need to speed up the Event Sourcing part of your Aggregate because the number of events exceeds your -defined aggregate loading time. That is when a Snapshot is desirable. Axon Framework helps you on that by simply -configuring a `SnapshotTriggerDefinition` and instructing your `Aggregate` to use that. +Eventually you will need to speed up the Event Sourcing part of your entity because the number of events exceeds your +defined loading time. That is when a Snapshot is desirable. Axon Framework helps you on that by simply configuring a +`SnapshotPolicy` and instructing your entity to use it. -To do it, as said before, you have to configure one or more `SnapshotTriggerDefinition` instances. Axon Framework -already provides some implementations, but you are free to implement your own: +Snapshotting is enabled by providing a `SnapshotPolicy` for an event-sourced entity. Axon Framework already provides +some standard policies, but you are free to implement your own: -1. [NoSnapshotTriggerDefinition](https://github.com/AxonFramework/AxonFramework/blob/master/eventsourcing/src/main/java/org/axonframework/eventsourcing/NoSnapshotTriggerDefinition.java): - Implementation that doesn't trigger snapshots at all. -2. [EventCountSnapshotTriggerDefinition](https://github.com/AxonFramework/AxonFramework/blob/master/eventsourcing/src/main/java/org/axonframework/eventsourcing/EventCountSnapshotTriggerDefinition.java): - Snapshotter trigger mechanism that counts the number of events to decide when to create a snapshot. -3. [AggregateLoadTimeSnapshotTriggerDefinition](https://github.com/AxonFramework/AxonFramework/blob/master/eventsourcing/src/main/java/org/axonframework/eventsourcing/AggregateLoadTimeSnapshotTriggerDefinition.java): - A Snapshotter trigger mechanism which based on the loading time of an Aggregate decides when to trigger the creation - of a snapshot. +1. `SnapshotPolicy.afterEvents(int)`: Triggers a snapshot once the entity has evolved through a configurable number + of events. This is the entity-conversion of the former `EventCountSnapshotTriggerDefinition`. +2. `SnapshotPolicy.afterSourcingTime(Duration)`: Triggers a snapshot when the time to source an entity exceeds a + configured threshold. This is the entity-conversion of the former `AggregateLoadTimeSnapshotTriggerDefinition`. +3. `SnapshotPolicy.whenEventMatches(Predicate)`: Triggers a snapshot when a specific event is encountered during + sourcing, allowing snapshots to be forced for domain-specific events. -To make it available for the application to use, you have 2 options. When using Spring Boot AutoConfiguration, just -provide it as a `@Bean` like we did in this -example [here](./src/main/java/io/axoniq/dev/samples/SnapshottingApplication.java). +These policies can be combined (e.g. `policyA.or(policyB)`) to form more advanced strategies. -``` -@Bean -public SnapshotTriggerDefinition mySnapshotTriggerDefinition(Snapshotter snapshotter) { - return new EventCountSnapshotTriggerDefinition(snapshotter, 5); -} -``` +To make it available for the application to use, you have 2 options. When using Spring Boot AutoConfiguration, the +simplest option is to add the `@Snapshotting` annotation directly to your event-sourced entity, alongside +`@EventSourced`, like done [here](./src/main/java/io/axoniq/dev/samples/command/MyEntity.java). -and instruct your aggregate to use it like -done [here](./src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java). - -``` -@Aggregate(snapshotTriggerDefinition = "mySnapshotTriggerDefinition") -public class MyEntityAggregate { - // ... +```java +@EventSourced(tagKey = "MyEntity") +@Snapshotting(afterEvents = 5) +class MyEntity { + // ... } ``` -Or when using Axon Configuration API, you can configure it to your Aggregate like this: +Or when using the Axon Configuration API directly, you can configure the `SnapshotPolicy` on the +`EventSourcedEntityModule` like this: -``` -AggregateConfigurer myEntityAggregateConfigurer = - AggregateConfigurer.defaultConfiguration(MyEntityAggregate.class) - .configureSnapshotTrigger(config -> new EventCountSnapshotTriggerDefinition( - config.snapshotter(), 5 - )); -Configurer configurer = DefaultConfigurer.defaultConfiguration() - .configureAggregate(myEntityAggregateConfigurer); +```java +EventSourcedEntityModule module = + EventSourcedEntityModule.declarative(String.class, MyEntity.class) + // ... + .snapshotPolicy(config -> SnapshotPolicy.afterEvents(5)); ``` -In both cases, after every 5 events*, a Snapshot will be created. You can check if it was created or not by looking at -your logs for the following line: -`o.a.a.c.event.axon.AxonServerEventStore : Snapshot created`. +In both cases, after every 5 events, a Snapshot will be created. You can check if it was created or not by looking at +your logs, or by inspecting the configured `SnapshotStore`. > 5 is fine for testing, but in real life you would likely use a threshold between 100 and 250. Important to note is -> that it will always depend on your Aggregate implementation, so measuring how long it takes to load an Aggregate and +> that it will always depend on your entity implementation, so measuring how long it takes to load an entity and > basing the count on that will end you up with the most optimal solution. +Snapshots are persisted through a `SnapshotStore`. When using Axon Server as your event store (the default for this +sample), an `AxonServerSnapshotStore` is automatically provided, so no additional configuration is required. For +tests or simple setups you can register an `InMemorySnapshotStore` instead. + +Because a snapshot captures the state of the entity itself, that state needs to be (de)serializable by the +application's `Converter`. This sample relies on the default `JacksonConverter`, which is why +`MyEntity` still exposes its state through package-private `@JsonGetter`/`@JsonSetter` accessors - the +same requirement that existed under Axon Framework 4's `Serializer` model. + +> Snapshots created under Axon Framework 4 cannot be reused after migrating to Axon Framework 5: the event +> numbering scheme used to determine a snapshot's position in the stream has changed. This is expected, and of no +> concern for a sample application without persisted data - a fresh snapshot is simply created again after the +> configured number of events. + To test the project, we provide a [requests.http](./requests.http). But you can do the same using curl, like this: ``` diff --git a/snapshots/pom.xml b/snapshots/pom.xml index 2cf675b1..e35f4c46 100644 --- a/snapshots/pom.xml +++ b/snapshots/pom.xml @@ -17,7 +17,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter @@ -38,7 +38,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter test diff --git a/snapshots/src/main/java/io/axoniq/dev/samples/SnapshottingApplication.java b/snapshots/src/main/java/io/axoniq/dev/samples/SnapshottingApplication.java index 8e778a87..a20c21ef 100644 --- a/snapshots/src/main/java/io/axoniq/dev/samples/SnapshottingApplication.java +++ b/snapshots/src/main/java/io/axoniq/dev/samples/SnapshottingApplication.java @@ -1,14 +1,7 @@ package io.axoniq.dev.samples; -import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; -import org.axonframework.eventsourcing.SnapshotTriggerDefinition; -import org.axonframework.eventsourcing.Snapshotter; -import org.axonframework.serialization.Serializer; -import org.axonframework.serialization.json.JacksonSerializer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; @SpringBootApplication public class SnapshottingApplication { @@ -16,39 +9,4 @@ public class SnapshottingApplication { public static void main(String[] args) { SpringApplication.run(SnapshottingApplication.class, args); } - - /** - * This {@link SnapshotTriggerDefinition} is responsible to trigger the Snapshot. In this implementation, it creates - * a Snapshot every 5 events, including the Snapshot itself. eg: when we hit the 5th event, a snapshot will be - * created (5 events). When we hit the 9th event, another snapshot will be created (1 snapshot event + 4 events) and - * so on. - * - * @param snapshotter The default {@link Snapshotter} provided by Axon. - * @return the configured {@link SnapshotTriggerDefinition}. - */ - @Bean - public SnapshotTriggerDefinition mySnapshotTriggerDefinition(Snapshotter snapshotter) { - return new EventCountSnapshotTriggerDefinition(snapshotter, 5); - } - - /** - * Construct the main {@link Serializer} used by this sample application to be a {@link JacksonSerializer}. - *

- * By making this the {@link Primary} instance, Axon Framework will use this {@code Serializer} for commands, - * events, queries, but more importantly, also snapshots. Since (by default) Axon Framework serializes the state of - * the aggregate as the snapshot, this means the {@code io.axoniq.dev.samples.command.MyEntityAggregate} needs to be - * serializable by Jackson. - *

- * To support Jackson de-/serialization for this projects aggregate, we added the - * {@link com.fasterxml.jackson.annotation.JsonGetter} and {@link com.fasterxml.jackson.annotation.JsonSetter} - * annotation to getter and setter methods. Note that the getters/setters are made package-private on purpose, as - * they should only be used by the serializer. - * - * @return The main {@link Serializer} used by this sample application to be a {@link JacksonSerializer}. - */ - @Bean - @Primary - public Serializer serializer() { - return JacksonSerializer.defaultSerializer(); - } } diff --git a/snapshots/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java b/snapshots/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java index 64ed424a..a85d7318 100644 --- a/snapshots/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java +++ b/snapshots/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java @@ -1,9 +1,9 @@ package io.axoniq.dev.samples.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record CreateMyEntityCommand( - @TargetAggregateIdentifier String entityId, + @TargetEntityId String entityId, String name ) { diff --git a/snapshots/src/main/java/io/axoniq/dev/samples/api/RenameMyEntityCommand.java b/snapshots/src/main/java/io/axoniq/dev/samples/api/RenameMyEntityCommand.java index 274e6a62..2e464ea9 100644 --- a/snapshots/src/main/java/io/axoniq/dev/samples/api/RenameMyEntityCommand.java +++ b/snapshots/src/main/java/io/axoniq/dev/samples/api/RenameMyEntityCommand.java @@ -1,9 +1,9 @@ package io.axoniq.dev.samples.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record RenameMyEntityCommand( - @TargetAggregateIdentifier String entityId, + @TargetEntityId String entityId, String name ) { diff --git a/snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java b/snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntity.java similarity index 65% rename from snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java rename to snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntity.java index 1acf15ee..2234bd6e 100644 --- a/snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java +++ b/snapshots/src/main/java/io/axoniq/dev/samples/command/MyEntity.java @@ -6,43 +6,43 @@ import io.axoniq.dev.samples.api.MyEntityCreatedEvent; import io.axoniq.dev.samples.api.MyEntityRenamedEvent; import io.axoniq.dev.samples.api.RenameMyEntityCommand; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; +import org.axonframework.eventsourcing.annotation.EventSourcingHandler; +import org.axonframework.eventsourcing.annotation.Snapshotting; +import org.axonframework.eventsourcing.annotation.reflection.EntityCreator; +import org.axonframework.extension.spring.stereotype.EventSourced; +import org.axonframework.messaging.commandhandling.annotation.CommandHandler; +import org.axonframework.messaging.eventhandling.gateway.EventAppender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@Aggregate(snapshotTriggerDefinition = "mySnapshotTriggerDefinition") -class MyEntityAggregate { +@EventSourced(tagKey = "MyEntity") +@Snapshotting(afterEvents = 5) +class MyEntity { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @AggregateIdentifier private String entityId; private String name; @CommandHandler - public MyEntityAggregate(CreateMyEntityCommand command) { + public static void handle(CreateMyEntityCommand command, EventAppender appender) { logger.info("[CreateMyEntityCommand] Entity with id [{}] and name [{}] created.", command.entityId(), command.name()); - apply(new MyEntityCreatedEvent(command.entityId(), command.name())); + appender.append(new MyEntityCreatedEvent(command.entityId(), command.name())); } @CommandHandler - public void on(RenameMyEntityCommand command) { + public void on(RenameMyEntityCommand command, EventAppender appender) { logger.info("[RenameMyEntityCommand] Entity with id [{}] and name [{}] updated.", command.entityId(), command.name()); if (name.equals(command.name())) { throw new IllegalArgumentException("New name can not be the same as current name."); } - apply(new MyEntityRenamedEvent(command.entityId(), command.name())); + appender.append(new MyEntityRenamedEvent(command.entityId(), command.name())); } @EventSourcingHandler @@ -60,7 +60,8 @@ public void on(MyEntityRenamedEvent event) { logger.info("[MyEntityRenamedEvent] Entity with id [{}] being event sourced.", event.entityId()); } - // Since the main Serializer is a JacksonSerializer, the constructed Snapshot will also be serialized through Jackson + // The general Converter defaults to a JacksonConverter, so the constructed Snapshot will also be + // (de)serialized through Jackson. @JsonGetter String getEntityId() { return entityId; @@ -81,7 +82,8 @@ void setName(String name) { this.name = name; } - public MyEntityAggregate() { + @EntityCreator + public MyEntity() { // Required by Axon Framework } } diff --git a/snapshots/src/main/java/io/axoniq/dev/samples/rest/CommandController.java b/snapshots/src/main/java/io/axoniq/dev/samples/rest/CommandController.java index 4c315c73..936ba8dd 100644 --- a/snapshots/src/main/java/io/axoniq/dev/samples/rest/CommandController.java +++ b/snapshots/src/main/java/io/axoniq/dev/samples/rest/CommandController.java @@ -2,7 +2,7 @@ import io.axoniq.dev.samples.api.CreateMyEntityCommand; import io.axoniq.dev.samples.api.RenameMyEntityCommand; -import org.axonframework.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -25,13 +25,13 @@ public CommandController(CommandGateway commandGateway) { @PostMapping("/{id}") public CompletableFuture createMyEntity(@PathVariable("id") String entityId, @RequestParam("name") String name) { - return commandGateway.send(new CreateMyEntityCommand(entityId, name)); + return commandGateway.send(new CreateMyEntityCommand(entityId, name), Void.class); } @PatchMapping("/{id}") public CompletableFuture renameMyEntity(@PathVariable("id") String entityId, @RequestParam("name") String name) { - return commandGateway.send(new RenameMyEntityCommand(entityId, name)); + return commandGateway.send(new RenameMyEntityCommand(entityId, name), Void.class); } } diff --git a/stateful-event-handler/pom.xml b/stateful-event-handler/pom.xml index 4175c3f6..0b3081bc 100644 --- a/stateful-event-handler/pom.xml +++ b/stateful-event-handler/pom.xml @@ -21,7 +21,7 @@ axon-messaging - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/config/OrderProcessorConfig.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/config/OrderProcessorConfig.java index 26276954..00f78022 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/config/OrderProcessorConfig.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/config/OrderProcessorConfig.java @@ -1,8 +1,6 @@ package io.axoniq.dev.samples.config; -import org.axonframework.config.ConfigurerModule; -import org.axonframework.eventhandling.TrackingEventProcessorConfiguration; -import org.axonframework.messaging.StreamableMessageSource; +import org.axonframework.extension.spring.config.EventProcessorDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,11 +8,19 @@ public class OrderProcessorConfig { @Bean - public ConfigurerModule configureOrderProcessor() { - TrackingEventProcessorConfiguration tepConfig = - TrackingEventProcessorConfiguration.forSingleThreadedProcessing() - .andInitialTrackingToken(StreamableMessageSource::createHeadToken); - return configurer -> configurer.eventProcessing() - .registerTrackingEventProcessorConfiguration("OrderProcessor", c -> tepConfig); + public EventProcessorDefinition orderProcessorDefinition() { + // A PooledStreamingEventProcessor is inherently multi-segment/multi-threaded (16 segments by + // default). Restricting it to a single initial segment means there is only ever one segment + // to claim, so only one worker thread will ever be processing for this processor - reproducing + // AF4's TrackingEventProcessorConfiguration#forSingleThreadedProcessing() semantics. + // + // AF4's StreamableMessageSource#createHeadToken() maps directly onto AF5's + // TrackingTokenSource#latestToken(...): both create a token pointing at the current end of the + // stream, so only events published after start-up are processed and earlier events (which could + // otherwise trigger unwanted side effects, such as re-sending commands) are skipped. + return EventProcessorDefinition.pooledStreamingMatching("OrderProcessor") + .customized(config -> config + .initialSegmentCount(1) + .initialToken(source -> source.latestToken(null))); } } diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java index 64c307dd..ddcbfdb4 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/order/api/CompleteOrderProcessCommand.java @@ -1,10 +1,10 @@ package io.axoniq.dev.samples.order.api; import io.axoniq.dev.samples.uuid.OrderId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record CompleteOrderProcessCommand( - @TargetAggregateIdentifier OrderId orderId, + @TargetEntityId OrderId orderId, boolean isPaid, boolean orderIsDelivered ) { diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/orderprocessor/OrderProcessor.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/orderprocessor/OrderProcessor.java index 9599856b..8535c850 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/orderprocessor/OrderProcessor.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/orderprocessor/OrderProcessor.java @@ -12,14 +12,14 @@ import io.axoniq.dev.samples.uuid.PaymentId; import io.axoniq.dev.samples.uuid.ShipmentId; import io.axoniq.dev.samples.uuid.UUIDProvider; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.core.annotation.Namespace; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component -@ProcessingGroup("OrderProcessor") +@Namespace("OrderProcessor") class OrderProcessor { private final CommandGateway commandGateway; diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java index f66d2c4e..60ae146a 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/payment/api/PayOrderCommand.java @@ -1,10 +1,10 @@ package io.axoniq.dev.samples.payment.api; import io.axoniq.dev.samples.uuid.PaymentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record PayOrderCommand( - @TargetAggregateIdentifier PaymentId paymentId + @TargetEntityId PaymentId paymentId ) { } diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java index fc582578..c937ce72 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/CancelShipmentCommand.java @@ -1,10 +1,10 @@ package io.axoniq.dev.samples.shipment.api; import io.axoniq.dev.samples.uuid.ShipmentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record CancelShipmentCommand( - @TargetAggregateIdentifier ShipmentId shipmentId + @TargetEntityId ShipmentId shipmentId ) { } diff --git a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java index 50bf62f1..f2ec4a8a 100644 --- a/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java +++ b/stateful-event-handler/src/main/java/io/axoniq/dev/samples/shipment/api/ShipOrderCommand.java @@ -1,10 +1,10 @@ package io.axoniq.dev.samples.shipment.api; import io.axoniq.dev.samples.uuid.ShipmentId; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record ShipOrderCommand( - @TargetAggregateIdentifier ShipmentId shipmentId + @TargetEntityId ShipmentId shipmentId ) { } diff --git a/subscription-query-rest/README.md b/subscription-query-rest/README.md index dfa9d14f..79ae139d 100644 --- a/subscription-query-rest/README.md +++ b/subscription-query-rest/README.md @@ -6,12 +6,13 @@ projection right away, instead of listening for updates on a different endpoint. There are two issues that needs to be address in this case: 1. We need to subscribe for updates before we send a command, that’s the only way to be sure we will not miss any - updates. Sending commands first and then subscribing for updates will result in race conditions! The simple trick is - to subscribe for the initial result first (even if we don’t need it). Let’s call it virtual initial result. This will - open the Subscription query, which will buffer all updates that arrive at this point on. Since we now have a buffer - for updates, we can send a command and after the command has sent we can subscribe to updates flux. If an update - arrives after sending a command and before we are subscribed for updates, we will read it automatically from the - buffer, therefore we are sure we will not miss any updates. + updates. Sending commands first and then subscribing for updates will result in race conditions! Axon Framework's + `QueryGateway#subscriptionQuery(...)` returns a single reactive-streams `Publisher` that combines the (here virtual, + empty) initial result and every subsequent update: the query is only sent, and the update buffer only opened, once + that `Publisher` is subscribed to. The simple trick is therefore to subscribe to it first, and only dispatch the + command once that subscription is established (e.g. from a `doOnSubscribe` callback). If an update arrives right + after sending the command and before we start consuming from the `Publisher`, we will still read it from the + buffer that was already open, therefore we are sure we will not miss any updates. 2. We need to read our own writes, multiple updates/events could be dispatch at the same time, we can’t guarantee order and which one will arrive first. Without some kind of correlation, we will easily get into trouble and get someone diff --git a/subscription-query-rest/pom.xml b/subscription-query-rest/pom.xml index 65528137..8dc7be4e 100644 --- a/subscription-query-rest/pom.xml +++ b/subscription-query-rest/pom.xml @@ -19,7 +19,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter @@ -49,7 +49,7 @@ org.testcontainers - junit-jupiter + testcontainers-junit-jupiter \ No newline at end of file diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/CommandController.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/CommandController.java index e4afbf6f..c9ac804e 100644 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/CommandController.java +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/CommandController.java @@ -3,14 +3,16 @@ import io.axoniq.dev.samples.api.CreateMyEntityCommand; import io.axoniq.dev.samples.api.GetMyEntityByCorrelationIdQuery; import io.axoniq.dev.samples.query.MyEntity; -import org.axonframework.commandhandling.CommandMessage; -import org.axonframework.commandhandling.GenericCommandMessage; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.queryhandling.QueryGateway; -import org.axonframework.queryhandling.SubscriptionQueryResult; +import org.axonframework.messaging.commandhandling.CommandMessage; +import org.axonframework.messaging.commandhandling.GenericCommandMessage; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.core.MessageType; +import org.axonframework.messaging.queryhandling.gateway.QueryGateway; +import org.reactivestreams.Publisher; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; @@ -29,31 +31,30 @@ public CommandController(CommandGateway commandGateway, QueryGateway queryGatewa @PostMapping("/entities/{id}") public Mono myApi(@PathVariable("id") String entityId) { - /* We are wrapping command into GenericCommandMessage, so we can get its identifier (correlation id) */ - CommandMessage command = GenericCommandMessage.asCommandMessage(new CreateMyEntityCommand(entityId)); + /* We are wrapping the command into a GenericCommandMessage, so we can get its identifier (correlation id) */ + CommandMessage command = new GenericCommandMessage(new MessageType(CreateMyEntityCommand.class), + new CreateMyEntityCommand(entityId)); - /* With command identifier we can now subscribe for updates that this command produced */ - GetMyEntityByCorrelationIdQuery query = new GetMyEntityByCorrelationIdQuery(command.getIdentifier()); + /* With the command identifier we can now subscribe for updates that this command produced */ + GetMyEntityByCorrelationIdQuery query = new GetMyEntityByCorrelationIdQuery(command.identifier()); - /* since we don't care about initial result, we mark it as Void.class */ - SubscriptionQueryResult response = queryGateway.subscriptionQuery(query, - Void.class, - MyEntity.class); - return sendAndReturnUpdate(command, response) + /* Axon Framework 5 merges the "virtual" initial result and the updates into a single Publisher, + so there is only one responseType now instead of separate initial/update types */ + Publisher updates = queryGateway.subscriptionQuery(query, MyEntity.class); + + return sendAndReturnUpdate(command, updates) .map(MyEntity::id); } - public Mono sendAndReturnUpdate(Object command, SubscriptionQueryResult result) { - /* The trick here is to subscribe to initial results first, even it does not return any result - Subscribing to initialResult creates a buffer for updates, even that we didn't subscribe for updates yet - they will wait for us in buffer, after this we can safely send command, and then subscribe to updates */ - return Mono.when(result.initialResult()) - .then(Mono.fromCompletionStage(() -> commandGateway.send(command))) - .thenMany(result.updates()) + public Mono sendAndReturnUpdate(Object command, Publisher updates) { + /* The trick here is to subscribe to the subscription query's Publisher first: subscribing is what sends + the query and opens the buffer for updates. We hook the command dispatch into doOnSubscribe so it only + fires once that buffer is open, guaranteeing we cannot miss the update it produces. Cancelling (closing) + the subscription query is handled automatically once next() receives its element or the timeout fires. */ + return Flux.from(updates) + .doOnSubscribe(subscription -> commandGateway.send(command)) .timeout(Duration.ofSeconds(5)) - .next() - .doFinally(unused -> result.cancel()); - /* dont forget to close subscription query on the end and add a timeout */ + .next(); } } diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java index d891bb9b..356998d4 100644 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/CreateMyEntityCommand.java @@ -1,9 +1,9 @@ package io.axoniq.dev.samples.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record CreateMyEntityCommand( - @TargetAggregateIdentifier String entityId + @TargetEntityId String entityId ) { } diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/MyEntityCreatedEvent.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/MyEntityCreatedEvent.java index be9b1090..72625a31 100644 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/MyEntityCreatedEvent.java +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/MyEntityCreatedEvent.java @@ -1,7 +1,9 @@ package io.axoniq.dev.samples.api; +import org.axonframework.eventsourcing.annotation.EventTag; + public record MyEntityCreatedEvent( - String entityId + @EventTag(key = "MyEntity") String entityId ) { } diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/ValidateMyEntityCommand.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/ValidateMyEntityCommand.java index 9ec1f288..5b927fd9 100644 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/ValidateMyEntityCommand.java +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/api/ValidateMyEntityCommand.java @@ -1,9 +1,9 @@ package io.axoniq.dev.samples.api; -import org.axonframework.modelling.command.TargetAggregateIdentifier; +import org.axonframework.modelling.annotation.TargetEntityId; public record ValidateMyEntityCommand( - @TargetAggregateIdentifier String entityId, String email + @TargetEntityId String entityId, String email ) { } diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntity.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntity.java new file mode 100644 index 00000000..3a44a8b8 --- /dev/null +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntity.java @@ -0,0 +1,30 @@ +package io.axoniq.dev.samples.command; + +import io.axoniq.dev.samples.api.CreateMyEntityCommand; +import io.axoniq.dev.samples.api.MyEntityCreatedEvent; +import org.axonframework.eventsourcing.annotation.EventSourcingHandler; +import org.axonframework.eventsourcing.annotation.reflection.EntityCreator; +import org.axonframework.extension.spring.stereotype.EventSourced; +import org.axonframework.messaging.commandhandling.annotation.CommandHandler; +import org.axonframework.messaging.eventhandling.gateway.EventAppender; + +@EventSourced(tagKey = "MyEntity") +class MyEntity { + + private String entityId; + + @EntityCreator + public MyEntity() { + // Required by Axon Framework + } + + @CommandHandler + public static void handle(CreateMyEntityCommand command, EventAppender eventAppender) { + eventAppender.append(new MyEntityCreatedEvent(command.entityId())); + } + + @EventSourcingHandler + public void on(MyEntityCreatedEvent event) { + entityId = event.entityId(); + } +} diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java deleted file mode 100644 index 9d76a264..00000000 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/command/MyEntityAggregate.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.axoniq.dev.samples.command; - -import io.axoniq.dev.samples.api.CreateMyEntityCommand; -import io.axoniq.dev.samples.api.MyEntityCreatedEvent; -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; - -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -@Aggregate -class MyEntityAggregate { - - @AggregateIdentifier - private String entityId; - - public MyEntityAggregate() { - // Required by Axon Framework - } - - @CommandHandler - public MyEntityAggregate(CreateMyEntityCommand command) { - apply(new MyEntityCreatedEvent(command.entityId())); - } - - @EventSourcingHandler - public void on(MyEntityCreatedEvent event) { - entityId = event.entityId(); - } -} diff --git a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/query/MyEntityProjection.java b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/query/MyEntityProjection.java index 02faad27..766c4f46 100644 --- a/subscription-query-rest/src/main/java/io/axoniq/dev/samples/query/MyEntityProjection.java +++ b/subscription-query-rest/src/main/java/io/axoniq/dev/samples/query/MyEntityProjection.java @@ -2,10 +2,10 @@ import io.axoniq.dev.samples.api.GetMyEntityByCorrelationIdQuery; import io.axoniq.dev.samples.api.MyEntityCreatedEvent; -import org.axonframework.eventhandling.EventHandler; -import org.axonframework.messaging.annotation.MetaDataValue; -import org.axonframework.queryhandling.QueryHandler; -import org.axonframework.queryhandling.QueryUpdateEmitter; +import org.axonframework.messaging.core.annotation.MetadataValue; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; +import org.axonframework.messaging.queryhandling.QueryUpdateEmitter; +import org.axonframework.messaging.queryhandling.annotation.QueryHandler; import org.springframework.stereotype.Component; import java.util.Optional; @@ -13,12 +13,6 @@ @Component class MyEntityProjection { - private final QueryUpdateEmitter emitter; - - public MyEntityProjection(QueryUpdateEmitter emitter) { - this.emitter = emitter; - } - @QueryHandler /* We are creating virtual initial result, doesn't need to return anything, but also do not return null */ public Optional on(GetMyEntityByCorrelationIdQuery query) { @@ -26,7 +20,8 @@ public Optional on(GetMyEntityByCorrelationIdQuery query) { } @EventHandler - public void on(MyEntityCreatedEvent event, @MetaDataValue("correlationId") String correlationId) { + public void on(MyEntityCreatedEvent event, @MetadataValue("correlationId") String correlationId, + QueryUpdateEmitter emitter) { MyEntity entity = new MyEntity(event.entityId()); /* save your entity in your repository here */ diff --git a/subscription-query-rest/src/main/resources/application.properties b/subscription-query-rest/src/main/resources/application.properties index 7ed4f46c..04a7a8ef 100644 --- a/subscription-query-rest/src/main/resources/application.properties +++ b/subscription-query-rest/src/main/resources/application.properties @@ -1 +1 @@ -axon.serializer.general=jackson \ No newline at end of file +axon.converter.general=jackson \ No newline at end of file diff --git a/subscription-query-streaming/README.md b/subscription-query-streaming/README.md index 8ac29c88..97353ca9 100644 --- a/subscription-query-streaming/README.md +++ b/subscription-query-streaming/README.md @@ -19,9 +19,10 @@ First of these is the `EventPublisher` in the `commandmodel` package. The `Event a `StreamUpdatedEvent` to spoof an active applications. It fills the `StreamUpdatedEvent` with `UUIDs`. Secondly, the `ModelProjector` in the `querymodel` package is in charge of handling the `StreamUpdatedEvent`. It adds th -e contents to a `List` of strings, and emits an update through the `QueryUpdateEmitter`. Next to that, a `@QueryHandler` -annotated method is present for the `ModelQuery`. This query handler returns the entire list of updates. The combination -of this query handler, and the update emission on the event handler provide an entry point for a subscription query. +e contents to a `List` of strings, and emits an update through the `QueryUpdateEmitter`, which is injected as a +parameter of the `@EventHandler` method. Next to that, a `@QueryHandler` annotated method is present for the +`ModelQuery`. This query handler returns the entire list of updates. The combination of this query handler, and the +update emission on the event handler provide an entry point for a subscription query. Thirdly, the `QueryController` in the `ui` package provides an endpoint on `/app/updates`. This endpoint returns a `Flux` of `ServerSentEvents`. The`ServerSentEvents` are filled with the result of a subscription query on diff --git a/subscription-query-streaming/pom.xml b/subscription-query-streaming/pom.xml index a5aa94b3..6527b7cc 100644 --- a/subscription-query-streaming/pom.xml +++ b/subscription-query-streaming/pom.xml @@ -17,7 +17,7 @@ - org.axonframework + org.axonframework.extensions.spring axon-spring-boot-starter diff --git a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/AxonConfig.java b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/AxonConfig.java index e9af7066..2a4403ee 100644 --- a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/AxonConfig.java +++ b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/AxonConfig.java @@ -1,10 +1,10 @@ package io.axoniq.dev.samples; -import org.axonframework.commandhandling.gateway.CommandGateway; -import org.axonframework.eventhandling.gateway.EventGateway; -import org.axonframework.messaging.Message; -import org.axonframework.messaging.interceptors.LoggingInterceptor; -import org.axonframework.queryhandling.QueryGateway; +import org.axonframework.messaging.commandhandling.gateway.CommandGateway; +import org.axonframework.messaging.core.Message; +import org.axonframework.messaging.core.interception.LoggingInterceptor; +import org.axonframework.messaging.eventhandling.gateway.EventGateway; +import org.axonframework.messaging.queryhandling.gateway.QueryGateway; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,7 +18,7 @@ public class AxonConfig { @Bean - public LoggingInterceptor> loggingInterceptor() { + public LoggingInterceptor loggingInterceptor() { return new LoggingInterceptor<>(); } diff --git a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/commandmodel/EventPublisher.java b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/commandmodel/EventPublisher.java index 5d626ef8..4296a5ff 100644 --- a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/commandmodel/EventPublisher.java +++ b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/commandmodel/EventPublisher.java @@ -1,7 +1,7 @@ package io.axoniq.dev.samples.commandmodel; import io.axoniq.dev.samples.api.StreamUpdatedEvent; -import org.axonframework.eventhandling.gateway.EventGateway; +import org.axonframework.messaging.eventhandling.gateway.EventGateway; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -24,6 +24,8 @@ public EventPublisher(EventGateway eventGateway) { @Scheduled(initialDelay = 1_000, fixedDelay = 6_000) public void publishEvent() { - eventGateway.publish(new StreamUpdatedEvent(UUID.randomUUID().toString())); + // AF5's EventGateway no longer exposes a plain publish(Object...) — the varargs overload now + // requires a ProcessingContext, which we don't have from a @Scheduled method, hence null. + eventGateway.publish(null, new StreamUpdatedEvent(UUID.randomUUID().toString())); } } diff --git a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/querymodel/ModelProjector.java b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/querymodel/ModelProjector.java index bb822aff..424d57d2 100644 --- a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/querymodel/ModelProjector.java +++ b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/querymodel/ModelProjector.java @@ -2,10 +2,10 @@ import io.axoniq.dev.samples.api.ModelQuery; import io.axoniq.dev.samples.api.StreamUpdatedEvent; -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; -import org.axonframework.queryhandling.QueryHandler; -import org.axonframework.queryhandling.QueryUpdateEmitter; +import org.axonframework.messaging.core.annotation.Namespace; +import org.axonframework.messaging.eventhandling.annotation.EventHandler; +import org.axonframework.messaging.queryhandling.QueryUpdateEmitter; +import org.axonframework.messaging.queryhandling.annotation.QueryHandler; import org.springframework.stereotype.Component; import java.util.ArrayList; @@ -18,19 +18,17 @@ * @author Steven van Beelen */ @Component -@ProcessingGroup("model-projector") +@Namespace("model-projector") public class ModelProjector { - private final QueryUpdateEmitter updateEmitter; private final List updates; - public ModelProjector(QueryUpdateEmitter updateEmitter) { - this.updateEmitter = updateEmitter; + public ModelProjector() { this.updates = new ArrayList<>(); } @EventHandler - public void on(StreamUpdatedEvent event) { + public void on(StreamUpdatedEvent event, QueryUpdateEmitter updateEmitter) { updates.add(event.update()); updateEmitter.emit(ModelQuery.class, query -> true, event.update()); } diff --git a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/ui/QueryController.java b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/ui/QueryController.java index ecac856d..807cb727 100644 --- a/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/ui/QueryController.java +++ b/subscription-query-streaming/src/main/java/io/axoniq/dev/samples/ui/QueryController.java @@ -1,9 +1,9 @@ package io.axoniq.dev.samples.ui; import io.axoniq.dev.samples.api.ModelQuery; -import org.axonframework.messaging.responsetypes.ResponseTypes; -import org.axonframework.queryhandling.QueryGateway; -import org.axonframework.queryhandling.SubscriptionQueryResult; +import org.axonframework.messaging.queryhandling.QueryResponseMessage; +import org.axonframework.messaging.queryhandling.SubscriptionQueryUpdateMessage; +import org.axonframework.messaging.queryhandling.gateway.QueryGateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; @@ -39,21 +39,23 @@ public QueryController(QueryGateway queryGateway) { @CrossOrigin(exposedHeaders = "Access-Control-Allow-Origin") @GetMapping(path = "/updates", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> updates() { - //noinspection resource - SubscriptionQueryResult, String> result = - queryGateway.subscriptionQuery(new ModelQuery(), - ResponseTypes.multipleInstancesOf(String.class), - ResponseTypes.instanceOf(String.class)); - - Flux> sseStream = result.initialResult() - .flatMapMany(Flux::fromIterable) - .concatWith(result.updates()) - .doOnError(throwable -> logger.warn("something failed")) - .map(update -> ServerSentEvent.builder() - .event("update") - .data(update) - .build()) - .doFinally(signal -> result.close()); + // Axon Framework 5's QueryGateway#subscriptionQuery combines the initial result and the updates into a + // single Publisher of one responseType, and there is no more SubscriptionQueryResult to close explicitly: + // the underlying subscription is closed automatically once the returned Flux is cancelled/disposed. + // The ModelQuery's initial result is a List, while every emitted update is a single String. To + // keep that shape, we fall back to the mapper-based subscriptionQuery overload, which lets us + // distinguish the initial result from an update via the message type, and flatten the initial + // List into individual elements ourselves (mirroring the old initialResult().flatMapMany(...)). + Flux> sseStream = + Flux.from(queryGateway.subscriptionQuery(new ModelQuery(), Object.class, QueryController::mapResponse)) + .flatMap(response -> response instanceof List initialResult + ? Flux.fromIterable(initialResult).cast(String.class) + : Flux.just((String) response)) + .doOnError(throwable -> logger.warn("something failed")) + .map(update -> ServerSentEvent.builder() + .event("update") + .data(update) + .build()); // For Server Sent Events, the server doesn't get a close signal when the client closes the connection. // Hence, we are left with a hanging stream in that case. @@ -65,4 +67,10 @@ public Flux> updates() { .build()); return Flux.merge(sseStream, heartbeatStream); } + + private static Object mapResponse(QueryResponseMessage response) { + return response instanceof SubscriptionQueryUpdateMessage + ? response.payloadAs(String.class) + : response.payloadAs(List.class); + } } diff --git a/subscription-query-streaming/src/main/resources/application.properties b/subscription-query-streaming/src/main/resources/application.properties index 7f69874d..ec13b4aa 100644 --- a/subscription-query-streaming/src/main/resources/application.properties +++ b/subscription-query-streaming/src/main/resources/application.properties @@ -1,2 +1,2 @@ spring.application.name=Subscription Query Streaming -axon.serializer.general=jackson \ No newline at end of file +axon.converter.general=jackson \ No newline at end of file diff --git a/upcaster/README.md b/upcaster/README.md deleted file mode 100644 index 7606c831..00000000 --- a/upcaster/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Upcasters - -This module's intention is to give insights on how to create an upcaster and how to test it. This example uses a Json -serializer. - -The FlightDelayedEvent is used as example. The original (null) revision looks like this: - -```json -{ - "arrivalTime": "2021-05-27T15:06:10.629267", - "flightId": "KL123", - "origin": "LAX", - "destination": "LON" -} - -``` - -Then a new requirement popped up to put the origin and destination into a separate object called `leg`. Please note, -that this is just an example, and we do recommend keeping your event structure as flat as possible. The upcasted event -should look like this: - -```json -{ - "arrivalTime": "2021-05-27T15:06:10.629267", - "flightId": "KL123", - "leg": { - "origin": "LAX", - "destination": "LON" - } -} -``` - -You can find the implementation of the upcaster in -the [FlightDelayedEventUpcaster](src/main/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_to_1Upcaster.java). -The implementation of the test can be -found [here](src/test/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_To_1UpcasterTest.java) - -To get this upcaster invoked on the event handler it should be added to -the [EventUpcasterChainFactory](src/main/java/io/axoniq/dev/samples/upcaster/json/EventUpcasterChainFactory.java) or -annotate it as a Spring component together with an Order annotation. diff --git a/upcaster/pom.xml b/upcaster/pom.xml deleted file mode 100644 index 28900fcd..00000000 --- a/upcaster/pom.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - code-samples - io.axoniq - 0.0.2-SNAPSHOT - - 4.0.0 - - upcaster - - Upcaster - Module showing several Upcaster implementations - - - 1.5.3 - - - - - - org.axonframework - axon-spring-boot-starter - - - - org.springframework.boot - spring-boot-starter-web - - - - org.skyscreamer - jsonassert - ${jsonassert.version} - test - - - org.junit.jupiter - junit-jupiter - - - \ No newline at end of file diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/api/AirportCode.java b/upcaster/src/main/java/io/axoniq/dev/samples/api/AirportCode.java deleted file mode 100644 index 8b45460f..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/api/AirportCode.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.axoniq.dev.samples.api; - -public enum AirportCode { - AMS("Amsterdam"), - LON("London"), - PAR("Paris"), - NYC("New York City"), - LAX("Los Angeles"); - - AirportCode(@SuppressWarnings("unused") String airportName) { - } -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/api/FlightDelayedEvent.java b/upcaster/src/main/java/io/axoniq/dev/samples/api/FlightDelayedEvent.java deleted file mode 100644 index 845eaaf0..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/api/FlightDelayedEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.axoniq.dev.samples.api; - -import org.axonframework.serialization.Revision; - -import java.time.LocalDateTime; -import java.util.Objects; - -@Revision("1.0") -public record FlightDelayedEvent( - String flightId, - Leg leg, - LocalDateTime arrivalTime -) { - -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/api/Leg.java b/upcaster/src/main/java/io/axoniq/dev/samples/api/Leg.java deleted file mode 100644 index abf1d1b4..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/api/Leg.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.axoniq.dev.samples.api; - -public record Leg( - AirportCode origin, - AirportCode destination -) { - -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatAdjustedEvent.java b/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatAdjustedEvent.java deleted file mode 100644 index 169a7c09..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatAdjustedEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.axoniq.dev.samples.api; - -public record PassengerSeatAdjustedEvent( - String flightId, - String passengerId, - int seatNumber -) { - -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatsAdjustedEvent.java b/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatsAdjustedEvent.java deleted file mode 100644 index 2aa2154f..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/api/PassengerSeatsAdjustedEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.axoniq.dev.samples.api; - -import java.util.Map; - -/** - * An old event of the flight domain that's now been deprecated. - *

- * This event contained a complete list of all the passenger seat adjustments of a flight in one go. As time passed, the - * application developers noticed they required an event for every separate change. Hence, they introduced that - * {@link PassengerSeatAdjustedEvent} and added a one-to-many upcaster to make this adjustment. - * - * @author Steven van Beelen - * @see io.axoniq.dev.samples.upcaster.json.PassengerSeatsToPassengerSeatAdjustedEventUpcaster - * @deprecated in favor of singular {@link PassengerSeatAdjustedEvent}s - */ -@Deprecated -public record PassengerSeatsAdjustedEvent( - String flightId, - Map passengerSeats -) { - -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/EventUpcasterChainFactory.java b/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/EventUpcasterChainFactory.java deleted file mode 100644 index 8099101d..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/EventUpcasterChainFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import org.axonframework.config.Configurer; -import org.axonframework.serialization.upcasting.event.EventUpcasterChain; - -import java.util.function.Function; - -/** - * Utility class constructing the {@link EventUpcasterChain} to configure on the - * {@link org.axonframework.eventsourcing.eventstore.EventStore}. - *

- * In a Spring Boot environment exposing an {@code EventUpcasterChain} bean is sufficient for the framework to pick it - * up correctly. To that end we can use the {@link #buildEventUpcasterChain()} method. - *

- * When using Axon's {@link org.axonframework.config.Configurer} directly, you should configure all upcasters separately - * by invoking the {@link org.axonframework.config.Configurer#registerEventUpcaster(Function)} method. The - * {@link #configureUpcasters(Configurer)} shows how we should implement this. - * - * @author Yvonne Ceelie - */ -@SuppressWarnings("unused") -public abstract class EventUpcasterChainFactory { - - /** - * Constructs an {@link EventUpcasterChain} combining all the upcasters of this application. - *

- * Can be used to expose an {@code EventUpcasterChain} bean in a Spring environment. - * - * @return an {@link EventUpcasterChain} - */ - public static EventUpcasterChain buildEventUpcasterChain() { - return new EventUpcasterChain( - new FlightDelayedEvent0_to_1Upcaster(), - new PassengerSeatsToPassengerSeatAdjustedEventUpcaster() - ); - } - - /** - * Configures all the upcasters of this application with the given {@code configurer}. - *

- * Can be utilized to configure upcasters if the application code uses Axon's {@link Configurer} directly. - * - * @param configurer the {@link Configurer} to register all upcasters with - */ - public static void configureUpcasters(Configurer configurer) { - configurer.registerEventUpcaster(config -> new FlightDelayedEvent0_to_1Upcaster()) - .registerEventUpcaster(config -> new PassengerSeatsToPassengerSeatAdjustedEventUpcaster()); - } - - private EventUpcasterChainFactory() { - // Utility class - } -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_to_1Upcaster.java b/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_to_1Upcaster.java deleted file mode 100644 index 6ba041e8..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_to_1Upcaster.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.axoniq.dev.samples.api.FlightDelayedEvent; -import org.axonframework.serialization.SimpleSerializedType; -import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation; -import org.axonframework.serialization.upcasting.event.SingleEventUpcaster; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; - -/** - * Upcaster upcasting the {@code FlightDelayedEvent} from revision {@code 0} to revision {@code 1}. - *

- * This allows us to adjust the {@link FlightDelayedEvent} implementation to the new format. Part of the adjustment is - * adding the {@link org.axonframework.serialization.Revision} annotation to the {@code FlightDelayedEvent}. The - * annotation reflects the new version, defined as revision {@code 1}. - *

- * The new format of the {@code FlightDelayedEvent} uses a {@link io.axoniq.dev.samples.api.Leg} object to contain the - * {@code "origin"} and {@code "destination"} fields. - * - * @author Yvonne Ceelie - */ -public class FlightDelayedEvent0_to_1Upcaster extends SingleEventUpcaster { - - private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private static final String ORIGIN = "origin"; - private static final String DESTINATION = "destination"; - private static final String LEG = "leg"; - - private final SimpleSerializedType sourceType = - new SimpleSerializedType(FlightDelayedEvent.class.getTypeName(), null); - private final SimpleSerializedType targetType = - new SimpleSerializedType(FlightDelayedEvent.class.getTypeName(), "1.0"); - - @Override - protected boolean canUpcast(IntermediateEventRepresentation intermediateEventRepresentation) { - return intermediateEventRepresentation.getType().equals(sourceType); - } - - @Override - protected IntermediateEventRepresentation doUpcast( - IntermediateEventRepresentation intermediateEventRepresentation - ) { - logger.info("Upcast event: {}", intermediateEventRepresentation.getType()); - return intermediateEventRepresentation.upcastPayload(targetType, JsonNode.class, this::upcastEvent); - } - - private JsonNode upcastEvent(JsonNode jsonNode) { - if (!jsonNode.isObject()) { - return jsonNode; - } - final ObjectNode root = (ObjectNode) jsonNode; - // Create the leg node and set origin and destination in it - ObjectNode leg = JsonNodeFactory.instance.objectNode(); - leg.set(ORIGIN, root.get(ORIGIN)); - leg.set(DESTINATION, root.get(DESTINATION)); - root.set(LEG, leg); - // Remove origin and destination from the root - root.remove(ORIGIN); - root.remove(DESTINATION); - return jsonNode; - } -} diff --git a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcaster.java b/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcaster.java deleted file mode 100644 index ec9bf9cd..00000000 --- a/upcaster/src/main/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcaster.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import org.axonframework.serialization.SimpleSerializedType; -import org.axonframework.serialization.upcasting.event.EventMultiUpcaster; -import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation; - -import java.util.Map; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * An upcaster implementation that, instead of returning a single entry, returns a {@link Stream} of - * {@link IntermediateEventRepresentation}s for the {@link io.axoniq.dev.samples.api.PassengerSeatsAdjustedEvent}. - *

- * This {@link EventMultiUpcaster} implementation retrieves the {@code "passengerSeats"} collection from the deprecated - * event. Doing os it is able to upcast the single event to the right amount of - * {@link io.axoniq.dev.samples.api.PassengerSeatAdjustedEvent}s. - *

- * Note that the original event is removed from the result, as it is not part of the returned {@code Stream} by the - * {@link #doUpcast(IntermediateEventRepresentation)} implementation. - * - * @author Steven van Beelen - */ -public class PassengerSeatsToPassengerSeatAdjustedEventUpcaster extends EventMultiUpcaster { - - private static final SimpleSerializedType FROM = - new SimpleSerializedType("io.axoniq.dev.samples.api.PassengerSeatsAdjustedEvent", null); - - private static final SimpleSerializedType TO = - new SimpleSerializedType("io.axoniq.dev.samples.api.PassengerSeatAdjustedEvent", null); - - private static final String PASSENGER_SEATS_FIELD = "passengerSeats"; - private static final String PASSENGER_ID_FIELD = "passengerId"; - private static final String SEAT_NUMBER_FIELD = "seatNumber"; - - @Override - protected boolean canUpcast(IntermediateEventRepresentation intermediateRepresentation) { - return intermediateRepresentation.getType().equals(FROM); - } - - @Override - protected Stream doUpcast(IntermediateEventRepresentation intermediateRep) { - JsonNode eventJsonNode = intermediateRep.getData(JsonNode.class).getData(); - if (!eventJsonNode.isObject()) { - return Stream.of(intermediateRep); - } - - ObjectNode eventData = (ObjectNode) eventJsonNode; - Spliterator> passengerSpliterator = Spliterators.spliteratorUnknownSize( - eventData.get(PASSENGER_SEATS_FIELD).fields(), Spliterator.ORDERED - ); - - return StreamSupport.stream(passengerSpliterator, false) - .map(passengerEntry -> intermediateRep.upcastPayload( - TO, JsonNode.class, eventJson -> upcastToSeatAdjustedJson( - eventJson, passengerEntry.getKey(), passengerEntry.getValue() - ) - )); - } - - private JsonNode upcastToSeatAdjustedJson(JsonNode eventJson, String passengerId, JsonNode passengerSeat) { - if (!eventJson.isObject()) { - return eventJson; - } - ObjectNode eventData = (ObjectNode) eventJson; - - // Remove the map of passengerId to seatNumber - eventData.remove(PASSENGER_SEATS_FIELD); - - // And replace for a separate passengerId and seatNumber field - eventData.set(PASSENGER_ID_FIELD, new TextNode(passengerId)); - eventData.set(SEAT_NUMBER_FIELD, passengerSeat); - - return eventData; - } -} diff --git a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_To_1UpcasterTest.java b/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_To_1UpcasterTest.java deleted file mode 100644 index 4b4ccd3e..00000000 --- a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/FlightDelayedEvent0_To_1UpcasterTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import org.axonframework.eventhandling.AbstractEventEntry; -import org.axonframework.eventhandling.EventData; -import org.axonframework.serialization.SerializedType; -import org.axonframework.serialization.Serializer; -import org.axonframework.serialization.json.JacksonSerializer; -import org.axonframework.serialization.upcasting.event.InitialEventRepresentation; -import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation; -import org.json.JSONException; -import org.junit.jupiter.api.*; -import org.skyscreamer.jsonassert.JSONAssert; - -import java.time.Instant; -import java.util.UUID; - -import static io.axoniq.dev.samples.upcaster.json.UpcasterTestingUtils.extractFileContentsToString; -import static org.junit.jupiter.api.Assertions.*; - -@SuppressWarnings("NewClassNamingConvention") -class FlightDelayedEvent0_To_1UpcasterTest { - - private static final String PAYLOAD_TYPE = "io.axoniq.dev.samples.api.FlightDelayedEvent"; - private static final String INCORRECT_PAYLOAD_TYPE = "io.axoniq.dev.samples.api.FlightCreatedEvent"; - - private static final String PRE_UPCASTER_REVISION = null; - private static final String UPCASTED_REVISION = "1.0"; - private static final String FLIGHT_CREATED_EVENT_REVISION = "3.0"; - - private static final String FLIGHT_DELAYED_EVENT_REV_NULL_JSON_FILE_NAME = "/FlightDelayedEventRev_null.json"; - private static final String FLIGHT_DELAYED_EVENT_REV_1_JSON_FILE_NAME = "/FlightDelayedEventRev_1.json"; - private static final String PAYLOAD_OF_FLIGHT_CREATED_EVENT = "Payload of FlightCreatedEvent"; - - private String flightDelayedEventVersion1Payload; - - private final FlightDelayedEvent0_to_1Upcaster testSubject = new FlightDelayedEvent0_to_1Upcaster(); - - private EventData flightDelayedEventWithRevisionNull; - private EventData flightDelayedEventWithRevision1; - private EventData flightDelayedEventWithIncorrectPayloadType; - - private final Serializer serializer = JacksonSerializer.defaultSerializer(); - - @BeforeEach - void setUp() { - String flightDelayedEventVersionNullPayload = - extractFileContentsToString(FLIGHT_DELAYED_EVENT_REV_NULL_JSON_FILE_NAME); - flightDelayedEventVersion1Payload = extractFileContentsToString(FLIGHT_DELAYED_EVENT_REV_1_JSON_FILE_NAME); - - flightDelayedEventWithRevisionNull = - new TestEventEntry(PAYLOAD_TYPE, PRE_UPCASTER_REVISION, flightDelayedEventVersionNullPayload); - flightDelayedEventWithRevision1 = - new TestEventEntry(PAYLOAD_TYPE, UPCASTED_REVISION, flightDelayedEventVersion1Payload); - flightDelayedEventWithIncorrectPayloadType = new TestEventEntry( - INCORRECT_PAYLOAD_TYPE, FLIGHT_CREATED_EVENT_REVISION, PAYLOAD_OF_FLIGHT_CREATED_EVENT - ); - } - - @Test - void testCanUpcastReturnsTrueForMatchingPayloadTypeAndRevision() { - IntermediateEventRepresentation testRepresentation = - new InitialEventRepresentation(flightDelayedEventWithRevisionNull, serializer); - - assertTrue(testSubject.canUpcast(testRepresentation)); - } - - @Test - void testCanUpcastReturnsFalseForIncorrectPayloadType() { - IntermediateEventRepresentation testRepresentation = - new InitialEventRepresentation(flightDelayedEventWithIncorrectPayloadType, serializer); - - assertFalse(testSubject.canUpcast(testRepresentation)); - } - - @Test - void testCanUpcastReturnsFalseForIncorrectRevision() { - IntermediateEventRepresentation testRepresentation = - new InitialEventRepresentation(flightDelayedEventWithRevision1, serializer); - - assertFalse(testSubject.canUpcast(testRepresentation)); - } - - @Test - void testDoUpcast() throws JSONException { - InitialEventRepresentation testRepresentation = - new InitialEventRepresentation(flightDelayedEventWithRevisionNull, serializer); - - IntermediateEventRepresentation result = testSubject.doUpcast(testRepresentation); - SerializedType resultType = result.getType(); - assertEquals(PAYLOAD_TYPE, resultType.getName()); - assertEquals(UPCASTED_REVISION, resultType.getRevision()); - JSONAssert.assertEquals(flightDelayedEventVersion1Payload, result.getData().getData().toString(), true); - } - - private static class TestEventEntry extends AbstractEventEntry { - - public TestEventEntry(String payloadType, String payloadRevision, String payload) { - super(UUID.randomUUID().toString(), Instant.now(), payloadType, payloadRevision, payload, "metaData"); - } - } -} diff --git a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcasterTest.java b/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcasterTest.java deleted file mode 100644 index 28bcffb8..00000000 --- a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/PassengerSeatsToPassengerSeatAdjustedEventUpcasterTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import org.axonframework.eventhandling.EventData; -import org.axonframework.serialization.Serializer; -import org.axonframework.serialization.json.JacksonSerializer; -import org.axonframework.serialization.upcasting.event.InitialEventRepresentation; -import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation; -import org.json.JSONException; -import org.junit.jupiter.api.*; -import org.skyscreamer.jsonassert.JSONAssert; - -import java.util.List; -import java.util.stream.Collectors; - -import static io.axoniq.dev.samples.upcaster.json.UpcasterTestingUtils.extractFileContentsToString; -import static io.axoniq.dev.samples.upcaster.json.UpcasterTestingUtils.generateDomainEventData; -import static org.junit.jupiter.api.Assertions.*; - -class PassengerSeatsToPassengerSeatAdjustedEventUpcasterTest { - - private static final String FROM_PAYLOAD_TYPE = "io.axoniq.dev.samples.api.PassengerSeatsAdjustedEvent"; - private static final String TO_PAYLOAD_TYPE = "io.axoniq.dev.samples.api.PassengerSeatAdjustedEvent"; - private static final String PAYLOAD_REVISION = null; - - private String passengerSeatsAdjustedEvent; - - private Serializer testSerializer; - - private PassengerSeatsToPassengerSeatAdjustedEventUpcaster testSubject; - - @BeforeEach - void setUp() { - passengerSeatsAdjustedEvent = extractFileContentsToString("/PassengerSeatsAdjustedEvent.json"); - - testSerializer = JacksonSerializer.defaultSerializer(); - - testSubject = new PassengerSeatsToPassengerSeatAdjustedEventUpcaster(); - } - - @Test - void testCanUpcastReturnsTrueForPassengerSeatsAdjustedEventRevisionNull() { - EventData testEventData = - generateDomainEventData(FROM_PAYLOAD_TYPE, PAYLOAD_REVISION, passengerSeatsAdjustedEvent); - - assertTrue(testSubject.canUpcast(new InitialEventRepresentation(testEventData, testSerializer))); - } - - @Test - void testCanUpcastReturnsFalseForWrongPayloadType() { - EventData testEventData = - generateDomainEventData("incorrect-payload-type", PAYLOAD_REVISION, passengerSeatsAdjustedEvent); - - assertFalse(testSubject.canUpcast(new InitialEventRepresentation(testEventData, testSerializer))); - } - - @Test - void testCanUpcastReturnsFalseForWrongRevision() { - EventData testEventData = - generateDomainEventData(FROM_PAYLOAD_TYPE, "incorrect-revision", passengerSeatsAdjustedEvent); - - assertFalse(testSubject.canUpcast(new InitialEventRepresentation(testEventData, testSerializer))); - } - - @Test - void testDoUpcast() throws JSONException { - String expectedFirstEventJson = extractFileContentsToString("/PassengerSeatAdjustedEventOne.json"); - String expectedSecondEventJson = extractFileContentsToString("/PassengerSeatAdjustedEventTwo.json"); - String expectedThirdEventJson = extractFileContentsToString("/PassengerSeatAdjustedEventThree.json"); - EventData testEventData = - generateDomainEventData(FROM_PAYLOAD_TYPE, PAYLOAD_REVISION, passengerSeatsAdjustedEvent); - - List result = - testSubject.doUpcast(new InitialEventRepresentation(testEventData, testSerializer)) - .collect(Collectors.toList()); - - assertEquals(3, result.size()); - IntermediateEventRepresentation firstResult = result.get(2); - assertEquals(TO_PAYLOAD_TYPE, firstResult.getType().getName()); - assertEquals(PAYLOAD_REVISION, firstResult.getType().getRevision()); - JSONAssert.assertEquals(expectedFirstEventJson, firstResult.getData().getData().toString(), true); - - IntermediateEventRepresentation secondResult = result.get(1); - assertEquals(TO_PAYLOAD_TYPE, secondResult.getType().getName()); - assertEquals(PAYLOAD_REVISION, secondResult.getType().getRevision()); - JSONAssert.assertEquals(expectedSecondEventJson, secondResult.getData().getData().toString(), true); - - IntermediateEventRepresentation thirdResult = result.get(0); - assertEquals(TO_PAYLOAD_TYPE, thirdResult.getType().getName()); - assertEquals(PAYLOAD_REVISION, thirdResult.getType().getRevision()); - JSONAssert.assertEquals(expectedThirdEventJson, thirdResult.getData().getData().toString(), true); - } -} \ No newline at end of file diff --git a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/UpcasterTestingUtils.java b/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/UpcasterTestingUtils.java deleted file mode 100644 index 2926b72b..00000000 --- a/upcaster/src/test/java/io/axoniq/dev/samples/upcaster/json/UpcasterTestingUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.axoniq.dev.samples.upcaster.json; - -import org.axonframework.eventhandling.EventData; -import org.axonframework.eventhandling.GenericDomainEventEntry; -import org.axonframework.messaging.MetaData; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.UUID; -import java.util.stream.Collectors; - -public abstract class UpcasterTestingUtils { - - public static String extractFileContentsToString(String fileName) { - InputStream fileInputStream = UpcasterTestingUtils.class.getResourceAsStream(fileName); - if (fileInputStream == null) { - throw new IllegalArgumentException("File name [" + fileName + "] is unknown"); - } - InputStreamReader fileStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8); - return new BufferedReader(fileStreamReader) - .lines() - .collect(Collectors.joining("\n")); - } - - public static EventData generateDomainEventData(String payloadType, - String payloadRevision, - Object payload) { - return generateDomainEventData(UUID.randomUUID().toString(), payloadType, payloadRevision, payload); - } - - public static EventData generateDomainEventData(String aggregateIdentifier, - String payloadType, - String payloadRevision, - Object payload) { - return new GenericDomainEventEntry<>("aggregateType", - aggregateIdentifier, - 0, - UUID.randomUUID().toString(), - Instant.now(), - payloadType, - payloadRevision, - payload, - MetaData.emptyInstance()); - } - - private UpcasterTestingUtils() { - // Utility class - } -} diff --git a/upcaster/src/test/resources/FlightDelayedEventRev_1.json b/upcaster/src/test/resources/FlightDelayedEventRev_1.json deleted file mode 100644 index b9de7cff..00000000 --- a/upcaster/src/test/resources/FlightDelayedEventRev_1.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "arrivalTime": "2021-05-27T15:06:10.629267", - "flightId": "KL123", - "leg": { - "origin": "LAX", - "destination": "LON" - } -} diff --git a/upcaster/src/test/resources/FlightDelayedEventRev_null.json b/upcaster/src/test/resources/FlightDelayedEventRev_null.json deleted file mode 100644 index 1ddf7c4b..00000000 --- a/upcaster/src/test/resources/FlightDelayedEventRev_null.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "arrivalTime": "2021-05-27T15:06:10.629267", - "flightId": "KL123", - "origin": "LAX", - "destination": "LON" -} diff --git a/upcaster/src/test/resources/PassengerSeatAdjustedEventOne.json b/upcaster/src/test/resources/PassengerSeatAdjustedEventOne.json deleted file mode 100644 index 122acac3..00000000 --- a/upcaster/src/test/resources/PassengerSeatAdjustedEventOne.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "flightId": "98d74c7b-5bf2-4224-a525-1839c5e94199", - "passengerId": "e7b4e728-aa0f-40bd-a6c7-6db9c7f3aae3", - "seatNumber": 1 -} \ No newline at end of file diff --git a/upcaster/src/test/resources/PassengerSeatAdjustedEventThree.json b/upcaster/src/test/resources/PassengerSeatAdjustedEventThree.json deleted file mode 100644 index 2e02489b..00000000 --- a/upcaster/src/test/resources/PassengerSeatAdjustedEventThree.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "flightId": "98d74c7b-5bf2-4224-a525-1839c5e94199", - "passengerId": "c7d630cc-def8-463d-87db-061ec549efb2", - "seatNumber": 3 -} \ No newline at end of file diff --git a/upcaster/src/test/resources/PassengerSeatAdjustedEventTwo.json b/upcaster/src/test/resources/PassengerSeatAdjustedEventTwo.json deleted file mode 100644 index 666f608e..00000000 --- a/upcaster/src/test/resources/PassengerSeatAdjustedEventTwo.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "flightId": "98d74c7b-5bf2-4224-a525-1839c5e94199", - "passengerId": "8576e25b-eb30-422d-b522-85135eecc1f4", - "seatNumber": 2 -} \ No newline at end of file diff --git a/upcaster/src/test/resources/PassengerSeatsAdjustedEvent.json b/upcaster/src/test/resources/PassengerSeatsAdjustedEvent.json deleted file mode 100644 index 6744ce24..00000000 --- a/upcaster/src/test/resources/PassengerSeatsAdjustedEvent.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "flightId": "98d74c7b-5bf2-4224-a525-1839c5e94199", - "passengerSeats": { - "c7d630cc-def8-463d-87db-061ec549efb2": 3, - "8576e25b-eb30-422d-b522-85135eecc1f4": 2, - "e7b4e728-aa0f-40bd-a6c7-6db9c7f3aae3": 1 - } -} \ No newline at end of file diff --git a/workflow-saga/pom.xml b/workflow-saga/pom.xml index 638cf2c3..15343302 100644 --- a/workflow-saga/pom.xml +++ b/workflow-saga/pom.xml @@ -16,10 +16,10 @@ 21 UTF-8 - 5.1.0 - 5.1.0 - 0.1.0 - 4.0.6 + 5.3.1 + 5.3.1 + 0.2.0 + 4.1.0 2.0.5 @@ -58,8 +58,8 @@ - io.axoniq.framework.workflow - axon-workflow-spring-boot + io.axoniq.framework + axoniq-workflow-spring-boot ${axon-workflow.version} @@ -82,8 +82,8 @@ - io.axoniq.framework.workflow - axon-workflow-test + io.axoniq.framework + axoniq-workflow-test ${axon-workflow.version} test diff --git a/workflow-saga/src/main/java/io/axoniq/demo/workflowsaga/workflow/ProcessOrderWorkflow.java b/workflow-saga/src/main/java/io/axoniq/demo/workflowsaga/workflow/ProcessOrderWorkflow.java index d53ea3e8..864a3a76 100644 --- a/workflow-saga/src/main/java/io/axoniq/demo/workflowsaga/workflow/ProcessOrderWorkflow.java +++ b/workflow-saga/src/main/java/io/axoniq/demo/workflowsaga/workflow/ProcessOrderWorkflow.java @@ -1,5 +1,6 @@ package io.axoniq.demo.workflowsaga.workflow; +import io.axoniq.demo.workflowsaga.api.OrderConfirmedEvent; import io.axoniq.demo.workflowsaga.api.OrderPaidEvent; import io.axoniq.demo.workflowsaga.api.OrderPaymentCancelledEvent; import io.axoniq.demo.workflowsaga.api.ShipmentStatus; @@ -16,10 +17,10 @@ import java.util.Map; import java.util.UUID; -import static io.axoniq.workflow.dsl.api.AssociationsUtils.associate; +import static io.axoniq.workflow.dsl.api.EventAssociationsUtils.equalsTo; +import static io.axoniq.workflow.dsl.api.EventAssociationsUtils.payloadProperty; import static io.axoniq.workflow.dsl.api.Payload.payload; -import static io.axoniq.workflow.dsl.simple.SimpleWorkflowContext.equalsTo; -import static io.axoniq.workflow.runtime.association.PayloadPropertyValueRetriever.payloadProperty; +import static io.axoniq.workflow.runtime.association.Associations.associate; import static io.axoniq.workflow.runtime.execution.DefaultEventNameCustomizer.Builder.namespace; /** @@ -44,7 +45,7 @@ private static EventNameCustomizer apiNamespace() { @Workflow( idProperty = "orderId", - startOnEvent = "io.axoniq.demo.workflowsaga.api.OrderConfirmedEvent", + startOnEventClass = OrderConfirmedEvent.class, workflowName = "ProcessOrderWorkflow" ) public void execute(SimpleWorkflowContext ctx) { @@ -59,20 +60,20 @@ public void execute(SimpleWorkflowContext ctx) { "paid", OrderPaidEvent.class, associate(payloadProperty("paymentId"), equalsTo(paymentId)), - ORDER_DEADLINE + step -> step.timeout(ORDER_DEADLINE) ); var paymentCancelled = ctx.waitForEvent( "paymentCancelled", OrderPaymentCancelledEvent.class, associate(payloadProperty("paymentId"), equalsTo(paymentId)), - ORDER_DEADLINE + step -> step.timeout(ORDER_DEADLINE) ); var delivered = ctx.waitForEvent( "delivered", ShipmentStatusUpdatedEvent.class, associate(payloadProperty("shipmentId"), equalsTo(shipmentId)) .and(payloadProperty("shipmentStatus"), "=", ShipmentStatus.DELIVERED.name()), - ORDER_DEADLINE + step -> step.timeout(ORDER_DEADLINE) ); // The Started events of these steps — `RequestPaymentStarted` and `RequestShipmentStarted` @@ -81,15 +82,13 @@ public void execute(SimpleWorkflowContext ctx) { "requestPayment", Map.of("orderId", orderId, "paymentId", paymentId), (pc, p) -> Map.of(), - STEP_TIMEOUT, - apiNamespace() + def -> def.timeout(STEP_TIMEOUT).eventNameCustomizer(apiNamespace()) ); ctx.awaitExecute( "requestShipment", Map.of("orderId", orderId, "shipmentId", shipmentId), (pc, p) -> Map.of(), - STEP_TIMEOUT, - apiNamespace() + def -> def.timeout(STEP_TIMEOUT).eventNameCustomizer(apiNamespace()) ); var paymentOutcome = ctx.anyMatch(WorkflowStepResult::success, paid, paymentCancelled); @@ -102,16 +101,14 @@ public void execute(SimpleWorkflowContext ctx) { "cancelShipment", Map.of("orderId", orderId, "shipmentId", shipmentId), (pc, p) -> Map.of(), - STEP_TIMEOUT, - apiNamespace() + def -> def.timeout(STEP_TIMEOUT).eventNameCustomizer(apiNamespace()) ); delivered.cancel("payment cancelled"); ctx.awaitExecute( "completeOrder", Map.of("orderId", orderId, "paid", false, "delivered", false), (pc, p) -> Map.of(), - STEP_TIMEOUT, - apiNamespace() + def -> def.timeout(STEP_TIMEOUT).eventNameCustomizer(apiNamespace()) ); return; } @@ -123,8 +120,7 @@ public void execute(SimpleWorkflowContext ctx) { "completeOrder", Map.of("orderId", orderId, "paid", true, "delivered", isDelivered), (pc, p) -> Map.of(), - STEP_TIMEOUT, - apiNamespace() + def -> def.timeout(STEP_TIMEOUT).eventNameCustomizer(apiNamespace()) ); } }