Polish Auction example

* adjust the style in the Scala example
  * AuctionEntity class instead of Setup class that is passed around
* add timer in recovery completed
This commit is contained in:
Patrik Nordwall 2020-08-11 09:49:02 +02:00 committed by Christopher Batey
parent 5e9e490d88
commit ac469e1a56
8 changed files with 507 additions and 396 deletions

View file

@ -0,0 +1,121 @@
# Auction example
In this example we want to show that real-world applications can be implemented by designing events in a way that they
don't conflict. In the end, you will end up with a solution based on a custom CRDT.
We are building a small auction service. It has the following operations:
* Place a bid
* Get the highest bid
* Finish the auction
We model those operations as commands to be sent to the auction actor:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #commands }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #commands }
The events:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #events }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #events }
The winner does not have to pay the highest bid but only enough to beat the second highest so the `highestCounterOffer` is in the `AuctionFinished` event.
Let's have a look at the auction entity that will handle incoming commands:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #command-handler }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #command-handler }
There is nothing specific to Replicated Event Sourcing about the command handler. It is the same as a command handler for a standard `EventSourcedBehavior`.
For `OfferBid` and `AuctionFinished` we do nothing more than to emit
events corresponding to the command. For `GetHighestBid` we respond with details from the state. Note, that we overwrite the actual
offer of the highest bid here with the amount of the `highestCounterOffer`. This is done to follow the popular auction style where
the actual highest bid is never publicly revealed.
The auction entity is started with the initial parameters for the auction.
The minimum bid is modelled as an `initialBid`.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #setup }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #setup }
@@@ div { .group-scala }
The auction moves through the following phases:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #phase }
@@@
The closing and closed states are to model waiting for all replicas to see the result of the auction before
actually closing the action.
Let's have a look at our state class, `AuctionState` which also represents the CRDT in our example.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #state }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #state }
The state consists of a flag that keeps track of whether the auction is still active, the currently highest bid,
and the highest counter offer so far.
In the `eventHandler`, we handle persisted events to drive the state change. When a new bid is registered,
* it needs to be decided whether the new bid is the winning bid or not
* the state needs to be updated accordingly
The point of CRDTs is that the state must be end up being the same regardless of the order the events have been processed.
We can see how this works in the auction example: we are only interested in the highest bid, so, if we can define an
ordering on all bids, it should suffice to compare the new bid with currently highest to eventually end up with the globally
highest regardless of the order in which the events come in.
The ordering between bids is crucial, therefore. We need to ensure that it is deterministic and does not depend on local state
outside of our state class so that all replicas come to the same result. We define the ordering as this:
* A higher bid wins.
* If there's a tie between the two highest bids, the bid that was registered earlier wins. For that we keep track of the
(local) timestamp the bid was registered.
* We need to make sure that no timestamp is used twice in the same replica (missing in this example).
* If there's a tie between the timestamp, we define an arbitrary but deterministic ordering on the replicas, in our case
we just compare the name strings of the replicas. That's why we need to keep the identifier of the replica where a bid was registered
for every `Bid`.
If the new bid was higher, we keep this one as the new highest and keep the amount of the former highest as the `highestCounterOffer`.
If the new bid was lower, we just update the `highestCounterOffer` if necessary.
Using those rules, the order of incoming does not matter. Replicas will eventually converge to the same result.
## Triggering closing
In the auction we want to ensure that all bids are seen before declaring a winner. That means that an auction can only be closed once
all replicas have seen all bids.
In the event handler above, when recovery is not running, it calls `eventTriggers`.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #event-triggers }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #event-triggers }
The event trigger uses the `ReplicationContext` to decide when to trigger the Finish of the action.
When a replica saves the `AuctionFinished` event it checks whether it should close the auction.
For the close to happen the replica must be the one designated to close and all replicas must have
reported that they have finished.

View file

@ -2,125 +2,11 @@
The following are more realistic examples of building systems with Replicated Event Sourcing.
## Auction
@@toc { depth=1 }
In this example we want to show that real-world applications can be implemented by designing events in a way that they
don't conflict. In the end, you will end up with a solution based on a custom CRDT.
@@@ index
We are building a small auction service. It has the following operations:
* Place a bid
* Get the highest bid
* Finish the auction
We model those operations as commands to be sent to the auction actor:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #commands }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #commands }
The events:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #events }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #events }
The winner does not have to pay the highest bid but only enough to beat the second highest so the `highestCounterOffer` is in the `AuctionFinished` event.
Let's have a look at the auction entity that will handle incoming commands:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #command-handler }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #command-handler }
There is nothing specific to Replicated Event Sourcing about the command handler. It is the same as a command handler for a standard `EventSourcedBehavior`.
For `OfferBid` and `AuctionFinished` we do nothing more than to emit
events corresponding to the command. For `GetHighestBid` we respond with details from the state. Note, that we overwrite the actual
offer of the highest bid here with the amount of the `highestCounterOffer`. This is done to follow the popular auction style where
the actual highest bid is never publicly revealed.
The auction entity is started with the initial parameters for the auction.
The initial state is taken from a `AuctionSetup` instance. The minimum bid is modelled as
an `initialBid`.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #setup }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #setup }
@@@ div { .group-scala }
The auction moves through the following phases:
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #phase }
* [auction](replicated-eventsourcing-auction.md)
@@@
The closing and closed states are to model waiting for all replicas to see the result of the auction before
actually closing the action.
Let's have a look at our state class, `AuctionState` which also represents the CRDT in our example.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #state }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #state }
The state consists of a flag that keeps track of whether the auction is still active, the currently highest bid,
and the highest counter offer so far.
In the `eventHandler`, we handle persisted events to drive the state change. When a new bid is registered,
* it needs to be decided whether the new bid is the winning bid or not
* the state needs to be updated accordingly
The point of CRDTs is that the state must be end up being the same regardless of the order the events have been processed.
We can see how this works in the auction example: we are only interested in the highest bid, so, if we can define an
ordering on all bids, it should suffice to compare the new bid with currently highest to eventually end up with the globally
highest regardless of the order in which the events come in.
The ordering between bids is crucial, therefore. We need to ensure that it is deterministic and does not depend on local state
outside of our state class so that all replicas come to the same result. We define the ordering as this:
* A higher bid wins.
* If there's a tie between the two highest bids, the bid that was registered earlier wins. For that we keep track of the
(local) timestamp the bid was registered.
* We need to make sure that no timestamp is used twice in the same replica (missing in this example).
* If there's a tie between the timestamp, we define an arbitrary but deterministic ordering on the replicas, in our case
we just compare the name strings of the replicas. That's why we need to keep the identifier of the replica where a bid was registered
for every `Bid`.
If the new bid was higher, we keep this one as the new highest and keep the amount of the former highest as the `highestCounterOffer`.
If the new bid was lower, we just update the `highestCounterOffer` if necessary.
Using those rules, the order of incoming does not matter. Replicas will eventually converge to the same result.
## Triggering closing
In the auction we want to ensure that all bids are seen before declaring a winner. That means that an auction can only be closed once
all replicas have seen all bids.
In the event handler above, when recovery is not running, it calls `eventTriggers`.
Scala
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/scala/docs/akka/persistence/typed/ReplicatedAuctionExampleSpec.scala) { #event-triggers }
Java
: @@snip [AuctionExample](/akka-persistence-typed-tests/src/test/java/jdocs/akka/persistence/typed/ReplicatedAuctionExampleTest.java) { #event-triggers }
The event trigger uses the `ReplicationContext` to decide when to trigger the Finish of the action.
When a replica saves the `AuctionFinished` event it checks whether it should close the auction.
For the close to happen the replica must be the one designated to close and all replicas must have
reported that they have finished.

View file

@ -199,17 +199,17 @@ all data centers and all bids have been replicated.
The @api[ReplicationContext] contains the current replica, the origin replica for the event processes, and if a recovery is running. These can be used to
implement side effects that take place once events are fully replicated. If the side effect should happen only once then a particular replica can be
designated to do it. The @ref[Auction example](./replicated-eventsourcing-examples.md#auction) uses these techniques.
designated to do it. The @ref[Auction example](replicated-eventsourcing-auction.md) uses these techniques.
## How it works
You dont have to read this section to be able to use the feature, but to use the abstraction efficiently and for the right type of use cases it can be good to understand how its implemented. For example, it should give you the right expectations of the overhead that the solution introduces compared to using just `EventSourcedBehavior`s.
### Causal deliver order
### Causal delivery order
Causal delivery order means that events persisted in one replica are read in the same order in other replicas. The order of concurrent events is undefined, which should be no problem
when using [CRDT's](#conflict-free-replicated-data-types)
when using @ref:[CRDT's](#conflict-free-replicated-data-types)
and otherwise will be detected via the `ReplicationContext` concurrent method.
For example: