40 releases (23 breaking)
new 0.24.0 | Mar 27, 2024 |
---|---|
0.22.0 | Mar 18, 2024 |
0.12.0 | Dec 28, 2023 |
0.8.5 | Nov 30, 2023 |
0.6.0 | Jan 31, 2023 |
#600 in Parser implementations
902 downloads per month
Used in 3 crates
50KB
981 lines
EventSourced
Event sourced entities in Rust.
Concepts
EventSourced is inspired to a large degree by the amazing Akka Persistence library. It provides a framework for implementing Event Sourcing and CQRS.
The EvtLog
and SnapshotStore
traits define a pluggable event log and a pluggable snapshot
store respectively. For NATS and Postgres
these are implemented in the respective crates.
The Entity
struct represents event sourced entities, identifiable by a type name and ID. To
create one, an event handler function and one or more commands – implementations of the Cmd
trait – have to be given. Cmd
defines a command handler function to either reject a command or
return an event. An event gets persisted to the event log and then applied to the event handler
to return the new state of the entity.
Entity::spawn
puts the entity on the given event log and snapshot store, returning an
EntityRef
which can be cheaply cloned and used to pass commands to the entity. Conversion of
events and snapshot state to and from bytes happens via the given Binarize
implementation; for
prost and serde_json
these are already provided. Snapshots are taken after the configured number of processed events
to speed up future spawning.
EntityRef::handle_cmd
either returns Cmd::Error
for a rejected command or Cmd::Reply
for
an accepted one.
Events can be queried from the event log by ID or by entity type. These queries can be used to
build read side projections. There is early support for projections in the
eventsourced-projection
crate.
Requirements for building the project and examples
Before building the project and examples, please make sure you have installed the protobuf dependency that is not only needed for the optional byte conversion with prost, but also for eventsourced-nats. The only way to get away without protobuf
is to not use prost and not build eventsourced-nats.
On macOS protobuf
can be installed via Homebrew:
brew install protobuf
Counter example (no pun intended)
The counter
package in the example
directory contains a simple example: a counter which handles Inc
and Dec
commands and emits/handles Increased
and Decreased
events.
#[derive(Debug)]
struct Increase(u64);
impl Cmd<Uuid, Evt, Counter> for Increase {
type Error = Overflow;
type Reply = u64;
fn handle(self, id: &Uuid, state: &Counter) -> Result<Evt, Self::Error> {
if u64::MAX - state.0 < self.0 {
Err(Overflow)
} else {
Ok(Evt::Increased(*id))
}
}
fn reply(state: &Counter) -> Self::Reply {
state.0
}
}
#[derive(Debug)]
struct Overflow;
#[derive(Debug)]
struct Decrease(u64);
impl Cmd<Uuid, Evt, Counter> for Decrease {
type Error = Underflow;
type Reply = Counter;
fn handle(self, id: &Uuid, state: &Counter) -> Result<Evt, Self::Error> {
if state.0 < self.0 {
Err(Underflow)
} else {
Ok(Evt::Decreased(*id))
}
}
fn reply(state: &Counter) -> Self::Reply {
*state
}
}
#[derive(Debug, PartialEq, Eq)]
struct Underflow;
#[derive(Debug, Serialize, Deserialize)]
enum Evt {
Increased(Uuid),
Decreased(Uuid),
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
struct Counter(u64);
impl Counter {
fn handle_evt(self, evt: Evt) -> Self {
match evt {
Evt::Increased(_) => Self(self.0 + 1),
Evt::Decreased(_) => Self(self.0 - 1),
}
}
}
There are also the two counter-nats
and counter-postgres
packages, with a binary crate each, using eventsourced-nats
and eventsourced-postgres
respectively for the event log.
...
let evt_log = evt_log.clone();
let snapshot_store = snapshot_store.clone();
let counter = Counter::default();
let counter = counter
.spawn(
id,
unsafe { NonZeroUsize::new_unchecked(42) },
evt_log,
snapshot_store,
convert::serde_json::binarizer(),
)
.await
.context("cannot spawn entity")?;
tasks.spawn(async move {
for n in 0..config.evt_count / 2 {
if n > 0 && n % 2_500 == 0 {
println!("{id}: {} events persisted", n * 2);
}
counter
.handle_cmd(Cmd::Inc(n as u64))
.await
.context("cannot handle Inc command")
.unwrap();
...
}
});
...
Take a look at the examples directory for more details.
Running the counter-nats example
For the counter-nats
example, nats-server needs to be installed. On macOS just use Homebrew:
brew install nats-server
Before running the example, start the nats-server with the jetstream
feature enabled:
nats-server --jetstream
Then use the following command to run the example:
RUST_LOG=info \
CONFIG_DIR=examples/counter-nats/config \
cargo run \
--release \
--package counter-nats
Notice that you can change the configuration either by changing the defaul.toml
file at examples/counter-nats/config
or by overriding the configuration settings with environment variables, e.g. APP__COUNTER__EVT_COUNT=42
:
RUST_LOG=info \
APP__COUNTER__EVT_COUNT=42 \
CONFIG_DIR=examples/counter-nats/config \
cargo run \
--release \
--package counter-nats
Running the counter-postgres example
For the counter-postgres
example, PostgreSQL needs to be installed. On macOS just use Homebrew:
brew install postgresql@14
Before running the example, start PostgreSQL:
brew services run postgresql@14
Make sure you know the following connection parameters:
- host
- port
- user
- password
- dbname
Change the configuration either by changing the default.toml
file at examples/counter-postgres/config
or by overriding the configuration settings with environment variables, e.g. APP__EVT_LOG__DBNAME=test
or APP__COUNTER__EVT_COUNT=42
:
Then use the following command to run the example:
RUST_LOG=info \
APP__EVT_LOG__DBNAME=test \
APP__COUNTER__EVT_COUNT=42 \
CONFIG_DIR=examples/counter-postgres/config \
cargo run \
--release \
--package counter-postgres
License
This code is open source software licensed under the Apache 2.0 License.
Dependencies
~3.5–5.5MB
~98K SLoC