Skip to content

Commit

Permalink
Merge branch 'release/0.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
stoerti committed Mar 15, 2024
2 parents 2578310 + 4c6ea26 commit 56ba316
Show file tree
Hide file tree
Showing 60 changed files with 2,066 additions and 172 deletions.
147 changes: 122 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
[![sponsored](https://img.shields.io/badge/sponsoredBy-Holisticon-RED.svg)](https://holisticon.de/)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.holixon.axon/axon-adhoc-projection-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.holixon.axon/axon-adhoc-projection-core)

This library provides a stateless model repository for Axon Framework using selective event retrieval executed during query.
The advantage is, that one does not need a `TrackingEventProcessor`, `TokenStore` or other kind of persistence other than the Axon Server itself.
The `ModelRepository` directly accesses the event store and constructs the model on the fly by applying the events directly to the model.
This library provides a stateless model repository for a single aggregate for the Axon Framework using selective event
retrieval that is performed during the query.
The advantage is, that you do not need a `TrackingEventProcessor`, `TokenStore` or any other kind of persistence other
than the Axon Server itself.
The `ModelRepository` accesses the event store directly and constructs the model on the fly by applying the events
directly to the model.

> **_NOTE:_** This library is still under development. API changes may occur while using versions 0.x
## Usage

To use the extension, simply include the artifact in your POM:

```xml
<dependency>
<groupId>io.holixon.axon</groupId>
<artifactId>axon-adhoc-projection-core</artifactId>
<version>0.0.2</version>
</dependency>

<dependency>
<groupId>io.holixon.axon</groupId>
<artifactId>axon-adhoc-projection-core</artifactId>
<version>0.1.0</version>
</dependency>
```

Then define a model class:
Expand Down Expand Up @@ -51,48 +57,139 @@ data class CurrentBalanceModel(
)
}
```

The plugin scans for any `@MessageHandler` annotation on either constructors or methods.

**Constructors:**
- the model class must have either a default constructor or an annotated constructor accepting the first event of the event stream

- The model class must have either a default constructor or an annotated constructor that accepts the first event of the
event stream.

**Event handlers:**
- methods which are annotated with `@MessageHandler` (or `@EventHandler` - works too) are considered to be able to handle events from the event stream
- the parameter signature is the same as for regular EventHandler methods, you can use annotations like `@SequenceNumber`, `@Timestamp` etc.
- there are only two allowed return types:
- void: the model class will be treated as mutable class and the next event will be applied to the same instance
- model class type: the model will be treated as immutable and the next event will be applied to the returned instance
- if an incoming event has no matching event handler, the event will simply be ignored

Then define a repository extending from `ModelRepository` class
- Methods which are annotated with `@MessageHandler` (or `@EventHandler` - works too) are considered to be able to
handle events from the event stream.
- The parameter signature is the same as for regular EventHandler methods, you can use annotations
like `@SequenceNumber`, `@Timestamp` etc.
- There are only two allowed return types:
- Void: the model class will be treated as mutable class and the next event will be applied to the same instance.
- Model class type: the model will be treated as immutable and the next event will be applied to the returned
instance.
- If an incoming event has no matching event handler, the event will simply be ignored.

Then define a repository that extends the `ModelRepository` class

```kotlin
class CurrentBalanceModelRepository(eventStore: EventStore) :
ModelRepository<CurrentBalanceModel>(eventStore, CurrentBalanceModel::class.java, NoCache.INSTANCE)
ModelRepository<CurrentBalanceModel>(eventStore, CurrentBalanceModel::class.java)
```
The repository can use a cache in the same manner aggregates can be cached. By default, the `NoCache` will be used.
When using any cache implemented as in-memory solution, it is strongly advised to use an immutable model to be threadsafe.

The repository can use a cache in the same way that aggregates can be cached. By default, the `NoCache` is used.
When using a cache implemented as in-memory solution, it is strongly recommended to use an immutable model to be
thread-safe.

Normally the repository will start reading events from the latest snapshot event onwards (if snapshotting is enabled).
If you always want to read the events from the beginning, you can enable this via the
If you always want to read the events from the beginning, you can enable this via the
boolean option `ignoreSnapshotEvents` in the `ModelRepository` class.

```kotlin
class CurrentBalanceModelRepository(eventStore: EventStore) :
ModelRepository<CurrentBalanceModel>(eventStore, CurrentBalanceModel::class.java, NoCache.INSTANCE, ignoreSnapshotEvents = true)
ModelRepository<CurrentBalanceModel>(
eventStore,
CurrentBalanceModel::class.java,
NoCache.INSTANCE,
ignoreSnapshotEvents = true
)
```

The final usage is fairly simple:

```kotlin
val repository = CurrentBalanceModelRepository(eventStore)

val model: Optional<CurrentBalanceModel> = repository.findById(bankAccountId)
```
This method returns the model instance as `Optional` or an empty `Optional`, if the aggregate was not found.
If a cache was specified, the current instance is also put to the cache. When retrieving cached model instances, the repository always
compares the cached instances sequenceNumber with the lastSequenceNumber in the eventStore and applies missing events if any.

For further examples refer to the *examples* module of this repository.
This method returns the model instance as `Optional` or an empty `Optional`, if the aggregate was not found.
If a cache was specified, the current instance is also put in the cache. When fetching cached model instances, the
repository always
compares the sequenceNumber of the cached instance with the lastSequenceNumber in the eventStore and applies any missing
events.

For more examples, see to the *examples* module in this repository.

### Self-updating cache projection

As an extension to the ModelRepository, the `UpdatingModelRepository` is able to update the underlying cache as new
events arrive for cached model entities.

```kotlin
@Component
class CurrentBalanceModelRepository(eventStore: EventStore) :
UpdatingModelRepository<CurrentBalanceModel>(eventStore, CurrentBalanceModel::class.java) {
companion object : KLogging()

init {
/**
* Add a listener which fires on every model change. Can be used to trigger query subscriptions.
*/
addModelUpdatedListener { logger.debug { "Updated ${it.bankAccountId}" } }
}
}
```

Currently, the easiest way to use the `UpdatingModelListener` is to use the Spring module of this plugin. This uses an
auto-configuration to register all
found beans of type `UpdatingModelRepository` as Axon EventHandler.

```xml

<dependency>
<groupId>io.holixon.axon</groupId>
<artifactId>axon-adhoc-projection-spring</artifactId>
<version>0.1.0</version>
</dependency>
```

All UpdatingModelRepositories use the same Axon processing group `adhoc-event-message-handler`. This processing group
can
be configured in the same way as any other processing group.

Since all repositories are processed in the same processing group, an error in processing an event in one of the
repositories lead to a
failure of the whole event for the processor so the event may be dead-lettered or retried with a backoff (depending on
the configured behavior of the processingGroup).
However, the event will still be forwarded to _all_ repositories, even if one threw an error. Since the cache entry also
holds the seqNo of the last processed
event for each repository, an event will not be processed twice by any repository.

The event processing itself is not further configured, so by default a TrackingEventProcessor is created using a tail
token. It is strongly recommended to add a configuration
to use a head token so that the entire eventStore is not replayed:

```kotlin
@Bean
fun adhocProjectionConfigurerModule(): ConfigurerModule? {
return ConfigurerModule { configurer: Configurer ->
configurer.eventProcessing { processingConfigurer: EventProcessingConfigurer ->
processingConfigurer.registerTrackingEventProcessorConfiguration(AdhocEventMessageHandler.PROCESSING_GROUP) {
TrackingEventProcessorConfiguration.forParallelProcessing(4).andInitialTrackingToken { it.createHeadToken() }
}
}
}
}
```

## Configuration

The `ModelRepository` and subclasses take a `ModelRepositoryConfig` object for more detailed configuration.

| Parameter | Description | Default value |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
| cache | the cache implementation to use | LRUCache(1024) |
| cacheRefreshTime | Time in ms a cache entry is considered up-to-date and no eventStore will be queried for new/missed events.<br/>When using the UpdatingModelRepository consider a value other than 0 to use the advantage of the self-updating repo. | 0 (ms) |
| forceCacheInsert | Just for UpdatingModelRepository - Configures the behavior when an event of an uncached entity is received.<br/>When _false_ the event is ignored, when _true_, a full replay of this entity is performed and the result is added to the cache. | false |
| ignoreSnapshotEvents | By default, a DomainEventStream for an aggregate starts at the last snapshot event and ignores all prior events. With this flag set to <code>true</code>, the stream will always start at sequenceNo 0. | false |

## License

Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
target: 80% # the required coverage value
patch:
default:
target: 80% # the required coverage value
23 changes: 23 additions & 0 deletions examples/banking-java/http/testcalls.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### create bank account
PUT http://localhost:8088/bankaccount?owner=Heinz

> {%
client.global.set("bankAccountId", response.body.trim());
%}

### get current balance
GET http://localhost:8088/bankaccount/{{bankAccountId}}/balance
Content-Type: application/json

### get current owner
GET http://localhost:8088/bankaccount/{{bankAccountId}}/owner
Content-Type: application/json

### deposit money
POST http://localhost:8088/bankaccount/{{bankAccountId}}/deposit?euroInCent=1000

### withdraw money
POST http://localhost:8088/bankaccount/{{bankAccountId}}/withdraw?euroInCent=500

### change owner
POST http://localhost:8088/bankaccount/{{bankAccountId}}/change-owner?newOwner=Harald
61 changes: 61 additions & 0 deletions examples/banking-java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.holixon.axon</groupId>
<artifactId>axon-adhoc-projection-examples-root</artifactId>
<version>0.1.0</version>
</parent>

<artifactId>axon-adhoc-projection-examples-banking-java</artifactId>
<description>Java example</description>

<dependencies>
<dependency>
<groupId>io.holixon.axon</groupId>
<artifactId>axon-adhoc-projection-spring</artifactId>
<version>${project.version}</version>
</dependency>


<!-- Spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Axon -->
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-modelling</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-eventsourcing</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.holixon.axon.projection.adhoc.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JavaBankingApplication {

public static void main(String[] args) {
SpringApplication.run(JavaBankingApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.holixon.axon.projection.adhoc.example.events;

import java.util.UUID;

public record BankAccountCreatedEvent(
String bankAccountId,
String owner
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.holixon.axon.projection.adhoc.example.events;

import java.util.UUID;

public record MoneyDepositedEvent(
String bankAccountId,
int euroInCent) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.holixon.axon.projection.adhoc.example.events;

import java.util.UUID;

public record MoneyWithdrawnEvent(
String bankAccountId,
int euroInCent) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.holixon.axon.projection.adhoc.example.events;

import java.util.UUID;

public record OwnerChangedEvent(
String bankAccountId,
String newOwner) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.holixon.axon.projection.adhoc.example.model;

import io.holixon.axon.projection.adhoc.example.events.BankAccountCreatedEvent;
import io.holixon.axon.projection.adhoc.example.events.MoneyDepositedEvent;
import io.holixon.axon.projection.adhoc.example.events.MoneyWithdrawnEvent;
import org.axonframework.messaging.annotation.MessageHandler;

public record CurrentBalanceModel(
String bankAccountId,
int currentBalanceInEuroCent
) {

@MessageHandler
public CurrentBalanceModel(BankAccountCreatedEvent event) {
this(event.bankAccountId(), 0);
}

@MessageHandler
public CurrentBalanceModel on(MoneyDepositedEvent event) {
return new CurrentBalanceModel(bankAccountId, this.currentBalanceInEuroCent + event.euroInCent());
}

@MessageHandler
public CurrentBalanceModel on(MoneyWithdrawnEvent event) {
return new CurrentBalanceModel(bankAccountId, this.currentBalanceInEuroCent - event.euroInCent());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.holixon.axon.projection.adhoc.example.model;

import io.holixon.axon.projection.adhoc.ModelRepository;
import io.holixon.axon.projection.adhoc.ModelRepositoryConfig;
import org.axonframework.eventsourcing.eventstore.EventStore;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

@Component
public class CurrentBalanceRepository extends ModelRepository<CurrentBalanceModel> {
public CurrentBalanceRepository(EventStore eventStore) {
super(eventStore, CurrentBalanceModel.class, new ModelRepositoryConfig());
}
}
Loading

0 comments on commit 56ba316

Please sign in to comment.