Docs 3.0 (#256)
* Initial structure for the documentation of Baker 3 * Added the examples module and started working on the design a recipe section * continued work on documentation * More work on documentation * Worked on the documentation * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * documentation work * example work * Documentation work * Example app work * Example app work * Example web app done in Scala * Documentation work * Gatling setup * Work on example app metrics * Grafana setup * Loadtesting mods * Documentation work * Example app memory dump work * Load testing work * proof read
14
CHANGELOG.md
@@ -159,7 +159,7 @@ info.getRecipeCreatedTime();
|
||||
Now its just a configuration with a boolean on the RetryWithIncrementalBackoff retry strategy
|
||||
|
||||
## 1.1.17
|
||||
- Fixed [#72](https://github.com/ing-bank/baker/issues/72): do not join to akka cluster when there are persistence problems. `akka.cluster.seed-nodes` configuration should be renamed to `baker.cluster.seed-nodes` to support this "late cluster join" feature.
|
||||
- Fixed [#72](https://github.com/ing-bank/baker/issues/72): do not join to Akka cluster when there are persistence problems. `akka.cluster.seed-nodes` configuration should be renamed to `baker.cluster.seed-nodes` to support this "late cluster join" feature.
|
||||
|
||||
## 1.1.16
|
||||
- Fixed [#55](https://github.com/ing-bank/baker/issues/55): Improved readability of duration of scheduled retry log entries
|
||||
@@ -185,7 +185,7 @@ info.getRecipeCreatedTime();
|
||||
- Fixed [#53](https://github.com/ing-bank/baker/issues/53): EventListeners are now notified of retry-exhausted events.
|
||||
- Fixed [#49](https://github.com/ing-bank/baker/issues/49): improved error message when receiving invalid sensory event
|
||||
- Added a method to CompiledRecipe to obtain an SVG String: ```getVisualRecipeAsSVG```
|
||||
- Updated to akka 2.5.6
|
||||
- Updated to Akka 2.5.6
|
||||
|
||||
## 1.1.13
|
||||
- Fixed [#47](https://github.com/ing-bank/baker/issues/47): added writeVisualrecipeToSVGFile to write away the CompiledRecipe to a file.
|
||||
@@ -250,7 +250,7 @@ instance will be stopped and all persisted messages (history) will be deleted.
|
||||
|
||||
## 1.0.9
|
||||
|
||||
- Changed serialization mechanism to allow custom akka serializers for ingredients where before only kryo was used.
|
||||
- Changed serialization mechanism to allow custom Akka serializers for ingredients where before only kryo was used.
|
||||
|
||||
You might see these messages for ingredient types without bindings:
|
||||
|
||||
@@ -263,8 +263,8 @@ instance will be stopped and all persisted messages (history) will be deleted.
|
||||
|
||||
## 1.0.8
|
||||
- Added the functionality that if an Ingredient of Java Optional or Scala Option is needed but not provided its provided as empty.
|
||||
- Side note (no impact for baker users): kagara library is merged into baker, therefore baker has 2 new artifacts now: petrinet-api and petrinet-akka.
|
||||
- slf4j MDC field 'kageraEvent' is renamed to 'petrinetEvent' due to new petrinet modules in baker.
|
||||
- Side note (no impact for baker users): kagera library is merged into baker, therefore Baker has two new artifacts now: petrinet-api and petrinet-akka.
|
||||
- slf4j MDC field 'kageraEvent' is renamed to 'petrinetEvent' due to new Petri-net modules in baker.
|
||||
- If io.kagera packages are used/imported in your application (maybe in logback files), you need to change them as com.ing.baker.petrinet
|
||||
- baker.conf file disables java serialization, you don't need to have 'akka.actor.allow-java-serialization = off' setting anymore in your application.conf files
|
||||
|
||||
@@ -279,7 +279,7 @@ include "baker.conf"
|
||||
- IMPORTANT: This change is not backwards compatible, on going processing cannot be resumed after a restart
|
||||
|
||||
## 1.0.4
|
||||
- Migrated to akka 2.5.x
|
||||
- Migrated to Akka 2.5.x
|
||||
- A local event bus is implemented so that a listener can be registered to act on baker events. Ex:
|
||||
```scala
|
||||
baker.registerEventListener(new EventListener {
|
||||
@@ -317,7 +317,7 @@ include "baker.conf"
|
||||
- bug fix for the actor passivation logic
|
||||
|
||||
## 0.2.14
|
||||
- bug fixes related to akka sharding
|
||||
- bug fixes related to Akka sharding
|
||||
|
||||
## 0.2.13
|
||||
- Baker can now persist encrypted ingredients if enabled. This feature is disabled by default.
|
||||
|
||||
39
build.sbt
@@ -231,3 +231,42 @@ lazy val integration = project.in(file("integration"))
|
||||
)
|
||||
.enablePlugins(MultiJvmPlugin)
|
||||
.configs(MultiJvm)
|
||||
|
||||
lazy val examples = project
|
||||
.in(file("examples"))
|
||||
.enablePlugins(JavaAppPackaging)
|
||||
.settings(commonSettings)
|
||||
.settings(noPublishSettings)
|
||||
.settings(
|
||||
moduleName := "examples",
|
||||
scalacOptions ++= Seq(
|
||||
"-Ypartial-unification"
|
||||
),
|
||||
libraryDependencies ++=
|
||||
compileDeps(
|
||||
slf4jApi,
|
||||
slf4jSimple,
|
||||
http4s,
|
||||
http4sDsl,
|
||||
http4sServer,
|
||||
http4sCirce,
|
||||
circe,
|
||||
circeGeneric,
|
||||
kamon,
|
||||
kamonPrometheus
|
||||
) ++ testDeps(
|
||||
scalaTest,
|
||||
scalaCheck,
|
||||
junitInterface,
|
||||
slf4jApi,
|
||||
mockito,
|
||||
logback
|
||||
)
|
||||
)
|
||||
.settings(
|
||||
maintainer in Docker := "The Apollo Squad",
|
||||
packageSummary in Docker := "A web-shop checkout service example running baker",
|
||||
packageName in Docker := "checkout-service-baker-example",
|
||||
dockerExposedPorts := Seq(8080)
|
||||
)
|
||||
.dependsOn(bakertypes, runtime, recipeCompiler, recipeDsl, intermediateLanguage)
|
||||
|
||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -1,15 +1,17 @@
|
||||
# Execution sementics
|
||||
# Execution Semantics
|
||||
|
||||
### Execution loop
|
||||
|
||||
This is a short description of the execution loop of a process instance
|
||||
|
||||
1. An [event](concepts.md#event) is raised and provides ingredients.
|
||||
1. A `EventInstance` is raised and provides `IngredientInstances`.
|
||||
|
||||
Either given to baker as a [sensory event](process-execution.md#providing-a-sensory-event) or by an interaction.
|
||||
Either given to baker as a "SensoryEvent" (nickname for `EventInstances` that you fire using Baker APIs)
|
||||
or by an `InteractionInstance`.
|
||||
|
||||
2. A check is done which [interactions](concepts.md#interaction) have all their requirements met and those are executed.
|
||||
3. An interaction completes its execution and outputs an event (`GOTO 1.`)
|
||||
2. A check is done to find the `Interactions` that have all their input `IngredientInstances` provided and those are executed.
|
||||
3. An `InteractionInstance` completes its execution and outputs an `EventInstances` which provides more `IngredientInstances`
|
||||
for the next `InteractionInstnaces` (repeating step 1).
|
||||
|
||||
### Notes
|
||||
|
||||
@@ -28,11 +30,11 @@ A recipe can be represented (and [visualized](recipe-visualization.md)) as a gra
|
||||
|
||||
This graph is actually a higher level representation of a [petri net](https://en.wikipedia.org/wiki/Petri_net) (which is also a graph).
|
||||
|
||||
The execution of a process instance based around this petri net.
|
||||
The execution of a process instance is based around the state of a petri net.
|
||||
|
||||
The recipe compiler takes a recipe and creates a petri net.
|
||||
The recipe compiler takes a recipe and creates a petri net from it.
|
||||
|
||||
Generally the petri net is graph more complicated with extra layers of wiring nodes.
|
||||
Generally the petri net graph is more complicated with extra layers of wiring nodes.
|
||||
|
||||
## Translation rules
|
||||
|
||||
@@ -55,9 +57,8 @@ to produce a token in a place for that interaction.
|
||||
|
||||
### Interaction with precodition (OR)
|
||||
|
||||
Events that are grouped in an OR combinator for an interaction output a token to the same place.
|
||||
|
||||
Therefor when one of them fires the condition for the transition to fire is met.
|
||||
Events that are grouped in an OR combinator for an interaction output a token to the same place, therefor when one of
|
||||
them fires the condition for the transition to fire is met.
|
||||
|
||||

|
||||
|
||||
@@ -39,18 +39,18 @@ An explanation of the baker modules.
|
||||
|
||||
| Module | Description |
|
||||
| --- | --- |
|
||||
| recipe-dsl | [DSL](documentation/recipe-dsl.md) to describe your recipes (process blueprints) *declaritively* |
|
||||
| runtime | [Runtime](documentation/baker-runtime.md) based on [akka](htts://www.akka.io) to manage and execute your recipes |
|
||||
| compiler | [Compiles your recipe](documentation/baker-runtime.md#compiling-your-recipe) description into a model that the runtime can execute |
|
||||
| intermediate-language | Recipe and Petri Net model that the runtime can execute |
|
||||
| recipe-dsl | [DSL](recipe-dsl.md) to describe your recipes (process blueprints) *declaritively* |
|
||||
| runtime | [Runtime](baker-runtime.md) based on [Akka](htts://www.akka.io) to manage and execute your recipes |
|
||||
| compiler | [Compiles your recipe](baker-runtime.md#compiling-your-recipe) description into a model that the runtime can execute |
|
||||
| intermediate-language | Recipe and Petri-net model that the runtime can execute |
|
||||
|
||||
This is the dependency graph between the modules.
|
||||
|
||||

|
||||

|
||||
|
||||
## Continuing from here
|
||||
|
||||
After adding the dependencies you can continue to:
|
||||
|
||||
- Familiarize yourself with the [concepts](documentation/concepts.md).
|
||||
- Immediately start [writing your recipes](documentation/recipe-dsl.md).
|
||||
- Familiarize yourself with the [concepts](concepts.md).
|
||||
- Immediately start [writing your recipes](recipe-dsl.md).
|
||||
30
docs/archive/index.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## Introduction
|
||||
|
||||
Baker is a library that reduces the effort to orchestrate (micro)service-based process flows.
|
||||
|
||||
Developers declare the orchestration logic in a *Recipe* (process blueprint).
|
||||
|
||||
A *Recipe* is made out of:
|
||||
|
||||
- *Interactions* (functions)
|
||||
- *Ingredients* (data)
|
||||
- *Events*
|
||||
|
||||
More about these concepts [here](../sections/reference).
|
||||
|
||||
## Overview
|
||||
|
||||
Baker allows you to:
|
||||
|
||||
- *Declaritavely* design your processes using a [recipe DSL](recipe-dsl.md).
|
||||
- [Visualize](recipe-visualization.md) your recipe allowing product owners, architects and developers to talk the same language.
|
||||
- Manage your recipes using the [baker runtime](baker-runtime.md).
|
||||
- [Create process instances](process-execution.md#create-a-process-instance) of your recipes.
|
||||
- [Fire sensory events](process-execution.md#providing-a-sensory-event).
|
||||
- [Inquire the state](process-execution.md#state-inquiry) of your process instances.
|
||||
|
||||
## Visual representation
|
||||
|
||||
Below an example of a simple web shop recipe:
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 7.4 MiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 815 KiB After Width: | Height: | Size: 815 KiB |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 409 KiB |
|
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 779 KiB After Width: | Height: | Size: 779 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 303 KiB |
@@ -54,7 +54,7 @@ baker.bake(recipe.recipeId(), recipeInstanceId);
|
||||
|
||||
## Providing a sensory event
|
||||
|
||||
In our [webshop example](../index.md#visual-representation) the first events that can happen are `OrderPlaced`, `PaymentMade` and `CustomerInfoReceived`.
|
||||
In our [webshop example](index.md#visual-representation) the first events that can happen are `OrderPlaced`, `PaymentMade` and `CustomerInfoReceived`.
|
||||
|
||||
These are so called [sensory events](dictionary.md#sensory-event) since they are not the result of an interaction but must be provided by the user of Baker.
|
||||
|
||||
@@ -118,7 +118,7 @@ SensoryEventStatus statusB = baker.processEvent(recipeInstanceId, new OrderPlace
|
||||
|
||||
## Sensory event status
|
||||
|
||||
In response to recieving a sensory event baker returns a status code indicating how it processed it.
|
||||
In response to receiving a sensory event Baker returns a status code indicating how it processed it.
|
||||
|
||||
| Status | Description |
|
||||
| --- | --- |
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The recipe DSL allows you to declaritively describe your process.
|
||||
|
||||
Let's start with the [web shop](../index.md#visual-representation) recipe as an example.
|
||||
Let's start with the [web shop](index.md#visual-representation) recipe as an example.
|
||||
|
||||
The complete code example can be found [here](https://github.com/ing-bank/baker/blob/master/runtime/src/test/java/com/ing/baker/Webshop.java).
|
||||
|
||||
@@ -46,7 +46,7 @@ public class CustomerInfoReceived {
|
||||
}
|
||||
```
|
||||
|
||||
The field types of the `POJO` class must be compatible with the baker type system.
|
||||
The field types of the `POJO` class must be compatible with the Baker type system.
|
||||
|
||||
See the [supported types](type-system.md#default-supported-types) for more information.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Recipe Visualization
|
||||
|
||||
Here we explain how to create a visual representation of your recipe like [this one](../index.md#visual-representation)
|
||||
Here we explain how to create a visual representation of your recipe like [this one](index.md#visual-representation)
|
||||
|
||||
## Generate a .dot representation
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
## Description
|
||||
Note: This feature is applicable to clustered baker configuration. If
|
||||
your Baker application is using local actors, thus not using akka
|
||||
your Baker application is using local actors, thus not using Akka
|
||||
cluster, a Split Brain Resolver is not needed.
|
||||
|
||||
Baker library, when configured to be a cluster, runs in an akka cluster
|
||||
Baker library, when configured to be a cluster, runs in an Akka cluster
|
||||
to distribute the baker processes over multiple nodes. Running a
|
||||
cluster with multiple nodes with shared state has some difficulties to
|
||||
tackle in some network failure scenarios, like network partitions.
|
||||
@@ -22,16 +22,16 @@ during network partitions, or huge network delays, or non-responding
|
||||
cluster members.
|
||||
|
||||
Baker Split Brain Resolver is a general purpose implementation for Akka
|
||||
which could be configured for a baker cluster as well as for another
|
||||
akka cluster without baker.
|
||||
which could be configured for a Baker cluster as well as for another
|
||||
Akka cluster without bBker.
|
||||
|
||||
## Strategies
|
||||
The current version of the Split Brain Resolver algorithm supports only
|
||||
the `Majority` strategy which makes the majority of the nodes survive
|
||||
and downs (terminates) the nodes at the minority side of the network
|
||||
partition. In case of the number of nodes on each side of the network
|
||||
partition are equal, the side with the oldest akka node survives. By
|
||||
deciding to down one side, you do not end up with 2 akka clusters
|
||||
partition are equal, the side with the oldest Akka node survives. By
|
||||
deciding to down one side, you do not end up with twi Akka clusters
|
||||
during the network partition.
|
||||
|
||||
There could be other strategies implemented later, for now the
|
||||
@@ -59,24 +59,24 @@ libraryDependencies += "com.ing.baker" %% "baker-split-brain-resolver" % "2.0.3"
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Then the algorithm needs to be configured as the akka downing provider
|
||||
Then the algorithm needs to be configured as the Akka downing provider
|
||||
and the `stable-after` config needs to set to some duration depending
|
||||
on your cluster size.
|
||||
|
||||
`stable-after` config is needed to decide on how quickly to react on
|
||||
the akka cluster state changes. Very short durations may allow quicker
|
||||
the Akka cluster state changes. Very short durations may allow quicker
|
||||
'downing' decisions for unreachable nodes, but may also cause to down
|
||||
some nodes unnecessarily too early. Please see the suggested values for
|
||||
this in the [documentation](https://developer.lightbend.com/docs/akka-commercial-addons/current/split-brain-resolver.html#stable-after)
|
||||
of the commercial Lightbend Split Brain Resolver.
|
||||
|
||||
One other akka cluster configuration suggested keep in sync with
|
||||
One other Akka cluster configuration suggested keep in sync with
|
||||
`stable-after` is the `akka.cluster.down-removal-margin` config.
|
||||
The suggested values and more information on this config can be found
|
||||
in the [Cluster Singleton and Cluster Sharding](https://developer.lightbend.com/docs/akka-commercial-addons/current/split-brain-resolver.html#cluster-singleton-and-cluster-sharding)
|
||||
section of the commercial Lightbend Split Brain Resolver.
|
||||
|
||||
Example config for a baker cluster having less than 10 nodes is the
|
||||
Example config for a Baker cluster having less than 10 nodes is the
|
||||
following:
|
||||
```
|
||||
akka.cluster.down-removal-margin = 7 seconds
|
||||
2
docs/images/RecipeCompiler-draw.io-ANDPrecondition.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
2
docs/images/RecipeCompiler-draw.io-FiringLimit.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="271px" height="121px" version="1.1"><defs/><g transform="translate(0.5,0.5)"><path d="M 90 80 L 120 100 L 90 120 L 60 100 Z" fill="#767676" stroke="#666666" stroke-miterlimit="10" pointer-events="none"/><g transform="translate(85.5,93.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="8" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 10px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;"><font color="#ffffff">E</font><br /></div></div></foreignObject><text x="4" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">[Not supported by viewer]</text></switch></g><path d="M 250 40 L 250 73.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="none"/><path d="M 250 78.88 L 246.5 71.88 L 250 73.63 L 253.5 71.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="none"/><ellipse cx="250" cy="20" rx="20" ry="20" fill="#cdeb8b" stroke="#36393d" pointer-events="none"/><rect x="230" y="80" width="40" height="40" fill="#cce5ff" stroke="#36393d" pointer-events="none"/><g transform="translate(245.5,93.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="8" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 10px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">E</div></div></foreignObject><text x="4" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">E</text></switch></g><rect x="0" y="0" width="180" height="65" fill="none" stroke="#808080" stroke-dasharray="3 3" pointer-events="none"/><g transform="translate(2.5,15.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="163" height="34" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 10px; font-family: "Lucida Console"; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 164px; white-space: nowrap; word-wrap: normal;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;"> .withSensoryEvent(<br /> eventClass = E.class, <br /> maxFiringLimit = 1)</div></div></foreignObject><text x="82" y="22" fill="#000000" text-anchor="middle" font-size="10px" font-family="Lucida Console">[Not supported by viewer]</text></switch></g><ellipse cx="250" cy="20" rx="5" ry="5" fill="#000000" stroke="#808080" pointer-events="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
2
docs/images/RecipeCompiler-draw.io-ORPrecondition.svg
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
134
docs/images/webshop-example-1.svg
Normal file
@@ -0,0 +1,134 @@
|
||||
<svg width="970pt" height="905pt" viewBox="0.00 0.00 969.80 904.80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(14.4 890.4)">
|
||||
<title>_anonymous_0</title>
|
||||
<polygon fill="white" stroke="white" points="-14.4,15.4 -14.4,-890.4 956.4,-890.4 956.4,15.4 -14.4,15.4"></polygon>
|
||||
<!-- ReserveItems -->
|
||||
<g id="node1" class="node"><title>ReserveItems</title>
|
||||
<polygon fill="#525199" stroke="#525199" stroke-width="2" points="610.292,-548.401 443.708,-548.401 431.708,-536.401 431.708,-461.599 443.708,-449.599 610.292,-449.599 622.292,-461.599 622.292,-536.401 610.292,-548.401"></polygon>
|
||||
<path fill="#525199" stroke="#525199" stroke-width="2" d="M443.708,-548.401C437.708,-548.401 431.708,-542.401 431.708,-536.401"></path>
|
||||
<path fill="#525199" stroke="#525199" stroke-width="2" d="M431.708,-461.599C431.708,-455.599 437.708,-449.599 443.708,-449.599"></path>
|
||||
<path fill="#525199" stroke="#525199" stroke-width="2" d="M610.292,-449.599C616.292,-449.599 622.292,-455.599 622.292,-461.599"></path>
|
||||
<path fill="#525199" stroke="#525199" stroke-width="2" d="M622.292,-536.401C622.292,-542.401 616.292,-548.401 610.292,-548.401"></path>
|
||||
<polyline fill="none" stroke="#525199" stroke-width="2" points="610.292,-548.401 443.708,-548.401 "></polyline>
|
||||
<path fill="none" stroke="#525199" stroke-width="2" d="M443.708,-548.401C437.708,-548.401 431.708,-542.401 431.708,-536.401"></path>
|
||||
<polyline fill="none" stroke="#525199" stroke-width="2" points="431.708,-536.401 431.708,-461.599 "></polyline>
|
||||
<path fill="none" stroke="#525199" stroke-width="2" d="M431.708,-461.599C431.708,-455.599 437.708,-449.599 443.708,-449.599"></path>
|
||||
<polyline fill="none" stroke="#525199" stroke-width="2" points="443.708,-449.599 610.292,-449.599 "></polyline>
|
||||
<path fill="none" stroke="#525199" stroke-width="2" d="M610.292,-449.599C616.292,-449.599 622.292,-455.599 622.292,-461.599"></path>
|
||||
<polyline fill="none" stroke="#525199" stroke-width="2" points="622.292,-461.599 622.292,-536.401 "></polyline>
|
||||
<path fill="none" stroke="#525199" stroke-width="2" d="M622.292,-536.401C622.292,-542.401 616.292,-548.401 610.292,-548.401"></path>
|
||||
<text text-anchor="middle" x="527" y="-492.4" font-family="ING Me" font-size="22.00" fill="white">ReserveItems</text>
|
||||
</g>
|
||||
<!-- OrderHadUnavailableItems -->
|
||||
<g id="node3" class="node"><title>OrderHadUnavailableItems</title>
|
||||
<polygon fill="#767676" stroke="#767676" points="275.344,-410.447 11.5025,-345.854 11.5025,-340.146 275.344,-275.553 298.656,-275.553 562.498,-340.146 562.498,-345.854 298.656,-410.447 275.344,-410.447"></polygon>
|
||||
<path fill="#767676" stroke="#767676" d="M11.5025,-345.854C5.67457,-344.427 5.67457,-341.573 11.5025,-340.146"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M275.344,-275.553C281.172,-274.127 292.828,-274.127 298.656,-275.553"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M562.498,-340.146C568.325,-341.573 568.325,-344.427 562.498,-345.854"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M298.656,-410.447C292.828,-411.873 281.172,-411.873 275.344,-410.447"></path>
|
||||
<polyline fill="none" stroke="#767676" points="275.344,-410.447 11.5025,-345.854 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M11.5025,-345.854C5.67457,-344.427 5.67457,-341.573 11.5025,-340.146"></path>
|
||||
<polyline fill="none" stroke="#767676" points="11.5025,-340.146 275.344,-275.553 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M275.344,-275.553C281.172,-274.127 292.828,-274.127 298.656,-275.553"></path>
|
||||
<polyline fill="none" stroke="#767676" points="298.656,-275.553 562.498,-340.146 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M562.498,-340.146C568.325,-341.573 568.325,-344.427 562.498,-345.854"></path>
|
||||
<polyline fill="none" stroke="#767676" points="562.498,-345.854 298.656,-410.447 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M298.656,-410.447C292.828,-411.873 281.172,-411.873 275.344,-410.447"></path>
|
||||
<text text-anchor="middle" x="287" y="-336.4" font-family="ING Me" font-size="22.00" fill="white">OrderHadUnavailableItems</text>
|
||||
</g>
|
||||
<!-- ReserveItems->OrderHadUnavailableItems -->
|
||||
<g id="edge2" class="edge"><title>ReserveItems->OrderHadUnavailableItems</title>
|
||||
<path fill="none" stroke="black" d="M451.721,-449.696C427.02,-433.846 399.368,-416.103 373.879,-399.747"></path>
|
||||
<polygon fill="black" stroke="black" points="375.632,-396.714 365.326,-394.259 371.852,-402.605 375.632,-396.714"></polygon>
|
||||
</g>
|
||||
<!-- ItemsReserved -->
|
||||
<g id="node13" class="node"><title>ItemsReserved</title>
|
||||
<polygon fill="#767676" stroke="#767676" points="755.869,-408.818 603.549,-347.482 603.549,-338.518 755.869,-277.182 778.131,-277.182 930.451,-338.518 930.451,-347.482 778.131,-408.818 755.869,-408.818"></polygon>
|
||||
<path fill="#767676" stroke="#767676" d="M603.549,-347.482C597.983,-345.241 597.983,-340.759 603.549,-338.518"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M755.869,-277.182C761.434,-274.941 772.566,-274.941 778.131,-277.182"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M930.451,-338.518C936.017,-340.759 936.017,-345.241 930.451,-347.482"></path>
|
||||
<path fill="#767676" stroke="#767676" d="M778.131,-408.818C772.566,-411.059 761.434,-411.059 755.869,-408.818"></path>
|
||||
<polyline fill="none" stroke="#767676" points="755.869,-408.818 603.549,-347.482 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M603.549,-347.482C597.983,-345.241 597.983,-340.759 603.549,-338.518"></path>
|
||||
<polyline fill="none" stroke="#767676" points="603.549,-338.518 755.869,-277.182 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M755.869,-277.182C761.434,-274.941 772.566,-274.941 778.131,-277.182"></path>
|
||||
<polyline fill="none" stroke="#767676" points="778.131,-277.182 930.451,-338.518 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M930.451,-338.518C936.017,-340.759 936.017,-345.241 930.451,-347.482"></path>
|
||||
<polyline fill="none" stroke="#767676" points="930.451,-347.482 778.131,-408.818 "></polyline>
|
||||
<path fill="none" stroke="#767676" d="M778.131,-408.818C772.566,-411.059 761.434,-411.059 755.869,-408.818"></path>
|
||||
<text text-anchor="middle" x="767" y="-336.4" font-family="ING Me" font-size="22.00" fill="white">ItemsReserved</text>
|
||||
</g>
|
||||
<!-- ReserveItems->ItemsReserved -->
|
||||
<g id="edge14" class="edge"><title>ReserveItems->ItemsReserved</title>
|
||||
<path fill="none" stroke="black" d="M602.279,-449.696C630.808,-431.39 663.273,-410.558 691.795,-392.257"></path>
|
||||
<polygon fill="black" stroke="black" points="693.985,-395.01 700.511,-386.664 690.205,-389.118 693.985,-395.01"></polygon>
|
||||
</g>
|
||||
<!-- reservedItems -->
|
||||
<g id="node2" class="node"><title>reservedItems</title>
|
||||
<ellipse fill="#ff6200" stroke="#ff6200" cx="767" cy="-118" rx="98.3281" ry="98.6043"></ellipse>
|
||||
<text text-anchor="middle" x="767" y="-111.4" font-family="ING Me" font-size="22.00" fill="white">reservedItems</text>
|
||||
</g>
|
||||
<!-- unavailableItems -->
|
||||
<g id="node4" class="node"><title>unavailableItems</title>
|
||||
<ellipse fill="#ff6200" stroke="#ff6200" cx="287" cy="-118" rx="117.364" ry="117.652"></ellipse>
|
||||
<text text-anchor="middle" x="287" y="-111.4" font-family="ING Me" font-size="22.00" fill="white">unavailableItems</text>
|
||||
</g>
|
||||
<!-- OrderHadUnavailableItems->unavailableItems -->
|
||||
<g id="edge4" class="edge"><title>OrderHadUnavailableItems->unavailableItems</title>
|
||||
<path fill="none" stroke="black" d="M287,-272.664C287,-264.117 287,-255.184 287,-246.109"></path>
|
||||
<polygon fill="black" stroke="black" points="290.5,-245.817 287,-235.817 283.5,-245.817 290.5,-245.817"></polygon>
|
||||
</g>
|
||||
<!-- orderId -->
|
||||
<g id="node5" class="node"><title>orderId</title>
|
||||
<ellipse fill="#ff6200" stroke="#ff6200" cx="467" cy="-641" rx="57.1348" ry="57.1798"></ellipse>
|
||||
<text text-anchor="middle" x="467" y="-634.4" font-family="ING Me" font-size="22.00" fill="white">orderId</text>
|
||||
</g>
|
||||
<!-- orderId->ReserveItems -->
|
||||
<g id="edge12" class="edge"><title>orderId->ReserveItems</title>
|
||||
<path fill="none" stroke="black" d="M489.171,-588.267C493.465,-578.248 497.989,-567.692 502.356,-557.502"></path>
|
||||
<polygon fill="black" stroke="black" points="505.59,-558.842 506.312,-548.272 499.156,-556.085 505.59,-558.842"></polygon>
|
||||
</g>
|
||||
<!-- OrderPlaced -->
|
||||
<g id="node6" class="node"><title>OrderPlaced</title>
|
||||
<polygon fill="#d5d5d5" stroke="#d5d5d5" stroke-width="2" points="516.091,-870.301 384.505,-809.999 384.505,-800.001 516.091,-739.699 537.909,-739.699 669.495,-800.001 669.495,-809.999 537.909,-870.301 516.091,-870.301"></polygon>
|
||||
<path fill="#d5d5d5" stroke="#d5d5d5" stroke-width="2" d="M384.505,-809.999C379.05,-807.5 379.05,-802.5 384.505,-800.001"></path>
|
||||
<path fill="#d5d5d5" stroke="#d5d5d5" stroke-width="2" d="M516.091,-739.699C521.545,-737.199 532.455,-737.199 537.909,-739.699"></path>
|
||||
<path fill="#d5d5d5" stroke="#d5d5d5" stroke-width="2" d="M669.495,-800.001C674.95,-802.5 674.95,-807.5 669.495,-809.999"></path>
|
||||
<path fill="#d5d5d5" stroke="#d5d5d5" stroke-width="2" d="M537.909,-870.301C532.455,-872.801 521.545,-872.801 516.091,-870.301"></path>
|
||||
<polyline fill="none" stroke="#767676" stroke-width="2" points="516.091,-870.301 384.505,-809.999 "></polyline>
|
||||
<path fill="none" stroke="#767676" stroke-width="2" d="M384.505,-809.999C379.05,-807.5 379.05,-802.5 384.505,-800.001"></path>
|
||||
<polyline fill="none" stroke="#767676" stroke-width="2" points="384.505,-800.001 516.091,-739.699 "></polyline>
|
||||
<path fill="none" stroke="#767676" stroke-width="2" d="M516.091,-739.699C521.545,-737.199 532.455,-737.199 537.909,-739.699"></path>
|
||||
<polyline fill="none" stroke="#767676" stroke-width="2" points="537.909,-739.699 669.495,-800.001 "></polyline>
|
||||
<path fill="none" stroke="#767676" stroke-width="2" d="M669.495,-800.001C674.95,-802.5 674.95,-807.5 669.495,-809.999"></path>
|
||||
<polyline fill="none" stroke="#767676" stroke-width="2" points="669.495,-809.999 537.909,-870.301 "></polyline>
|
||||
<path fill="none" stroke="#767676" stroke-width="2" d="M537.909,-870.301C532.455,-872.801 521.545,-872.801 516.091,-870.301"></path>
|
||||
<text text-anchor="middle" x="527" y="-798.4" font-family="ING Me" font-size="22.00">OrderPlaced</text>
|
||||
</g>
|
||||
<!-- OrderPlaced->orderId -->
|
||||
<g id="edge8" class="edge"><title>OrderPlaced->orderId</title>
|
||||
<path fill="none" stroke="black" d="M504.999,-744.597C500.128,-731.445 494.953,-717.473 490.041,-704.212"></path>
|
||||
<polygon fill="black" stroke="black" points="493.275,-702.865 486.52,-694.703 486.711,-705.297 493.275,-702.865"></polygon>
|
||||
</g>
|
||||
<!-- items -->
|
||||
<g id="node10" class="node"><title>items</title>
|
||||
<ellipse fill="#ff6200" stroke="#ff6200" cx="587" cy="-641" rx="45.0248" ry="45.0331"></ellipse>
|
||||
<text text-anchor="middle" x="587" y="-634.4" font-family="ING Me" font-size="22.00" fill="white">items</text>
|
||||
</g>
|
||||
<!-- OrderPlaced->items -->
|
||||
<g id="edge6" class="edge"><title>OrderPlaced->items</title>
|
||||
<path fill="none" stroke="black" d="M549.001,-744.597C555.264,-727.688 562.028,-709.424 568.089,-693.058"></path>
|
||||
<polygon fill="black" stroke="black" points="571.377,-694.26 571.568,-683.666 564.813,-691.828 571.377,-694.26"></polygon>
|
||||
</g>
|
||||
<!-- items->ReserveItems -->
|
||||
<g id="edge10" class="edge"><title>items->ReserveItems</title>
|
||||
<path fill="none" stroke="black" d="M569.484,-599.13C563.948,-586.211 557.721,-571.682 551.776,-557.811"></path>
|
||||
<polygon fill="black" stroke="black" points="554.874,-556.154 547.718,-548.342 548.44,-558.912 554.874,-556.154"></polygon>
|
||||
</g>
|
||||
<!-- ItemsReserved->reservedItems -->
|
||||
<g id="edge16" class="edge"><title>ItemsReserved->reservedItems</title>
|
||||
<path fill="none" stroke="black" d="M767,-272.664C767,-258.19 767,-242.605 767,-227.107"></path>
|
||||
<polygon fill="black" stroke="black" points="770.5,-226.948 767,-216.948 763.5,-226.948 770.5,-226.948"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1,30 +1,63 @@
|
||||
## Introduction
|
||||
# Introduction
|
||||
|
||||
Baker is a library that reduces the effort to orchestrate (micro)service-based process flows.
|
||||
|
||||
Developers declare the orchestration logic in a *Recipe* (process blueprint).
|
||||
Developers declare the orchestration logic in a `Recipe` (process blueprint).
|
||||
|
||||
A *Recipe* is made out of:
|
||||
A `Recipe` is made out of:
|
||||
|
||||
- *Interactions* (functions)
|
||||
- *Ingredients* (data)
|
||||
- *Events*
|
||||
- `Interactions` (functions)
|
||||
- `Ingredients` (containers for data)
|
||||
- `Events`
|
||||
|
||||
More about these concepts [here](documentation/concepts).
|
||||
The Baker runtime on the other hand runs instances of the `Recipe` across a cluster of nodes in an asynchronous fashion.
|
||||
|
||||
## Overview
|
||||
### Baker allows you to
|
||||
|
||||
Baker allows you to:
|
||||
- *Declaratively* design your business processes using a [recipe Domain Specific Language (DSL)](sections/reference/dsls).
|
||||
- [Visualize](sections/reference/visualization.md) your recipe allowing product owners, architects and developers to talk the same language.
|
||||
- Manage your recipes using the [Baker runtime](sections/reference/runtime.md).
|
||||
- [Create process instances](sections/development-life-cycle/bake-fire-events-and-inquiry#bake) of your recipes.
|
||||
- [Fire sensory events](sections/development-life-cycle/bake-fire-events-and-inquiry#fire-events).
|
||||
- [Inquire the state](sections/development-life-cycle/bake-fire-events-and-inquiry#inquiry) of your recipe instances.
|
||||
|
||||
- *Declaritavely* design your processes using a [recipe DSL](documentation/recipe-dsl.md).
|
||||
- [Visualize](documentation/recipe-visualization.md) your recipe allowing product owners, architects and developers to talk the same language.
|
||||
- Manage your recipes using the [baker runtime](documentation/baker-runtime.md).
|
||||
- [Create process instances](documentation/process-execution.md#create-a-process-instance) of your recipes.
|
||||
- [Fire sensory events](documentation/process-execution.md#providing-a-sensory-event).
|
||||
- [Inquire the state](documentation/process-execution.md#state-inquiry) of your process instances.
|
||||
## Why Baker
|
||||
|
||||
## Visual representation
|
||||
Upgrading your business to an agile, adaptive and scalable microservice-based architecture does bring significant advantages,
|
||||
but also critical challenges that must be resolved:
|
||||
|
||||
Below an example of a simple web shop recipe:
|
||||
- the coupling of business logic to service technologies
|
||||
- and the inherent complexities of distributed systems
|
||||
|
||||
Baker solves these challenges by providing an expressive language to encode your business logic _(recipe)_, and a distributed runtime to scale _recipe instances_ with little
|
||||
configuration and no extra development.
|
||||
|
||||
**Decouple your business logic from your microservices**: When developing microservices it is easy to fall into bad practices
|
||||
where developers encode essential business logic into code which might get polluted with implementation details, and even worse,
|
||||
distributed over many independent projects/repositories. Baker, in contrast, requires the developer to _express the business
|
||||
logic as a Recipe_ by using the provided language DSL, and separately _code implementations of the data (events) and the
|
||||
process steps (interactions)_, enforcing decoupling of business from technology.
|
||||
|
||||
**Ease the friction of distributed systems**: When developing microservices you are confronted with all the inherent
|
||||
challenges of distributed systems, topics like communication models, consistency decisions, handling failure, scaling
|
||||
models, etc. Baker eases the development by providing out-of-the-box solutions from its clusterized runtime. Baker nodes
|
||||
are able to create and distribute _recipe instances_ between them, handle _failed interactions_ with several strategies,
|
||||
restore the state of long-lived process and more, allowing the developer to focus on what it matters for the business.
|
||||
|
||||
**Reason about your business process without the burdens of technology**: Baker can _visualize your recipes_, enabling developers
|
||||
and business stakeholders to better communicate and reason about the business processes.
|
||||
|
||||
## Example of a simple web shop recipe:
|
||||
|
||||

|
||||
|
||||
## How to read these docs
|
||||
|
||||
There are two big sections:
|
||||
|
||||
* The _Development Life Cycle_: works like a big tutorial of Baker, it is a "learning by making" type of documentation, it is
|
||||
for those who like a top-down approach to learning.
|
||||
|
||||
* The _Reference_: has descriptions of every part of Baker, it is a "dictionary/reference" type of documentation, it is for
|
||||
those who like a bottom-up approach to learning, and also works as a reference for quickly reviewing concepts in the future.
|
||||
|
||||
|
||||
137
docs/sections/concepts.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Concepts
|
||||
|
||||
Baker introduces *interactions*, *ingredients*, and *events* as a model of abstracting.
|
||||
|
||||
With these three components we can create recipes (process blue prints)
|
||||
|
||||
## Ingredient
|
||||
|
||||
Ingredients are *pure data*.
|
||||
|
||||
This data is **immutable** and can not be changed after entering the process.
|
||||
|
||||
There is **no hierarchy** in this data. (`Animal -> Dog -> Labrador` is not possible to express)
|
||||
|
||||
Examples:
|
||||
|
||||
- an IBAN
|
||||
- a track and trace code
|
||||
- a list of phone numbers
|
||||
- a customer information object with name, email, etc ...
|
||||
|
||||
An ingredient is defined by a *name* and *type*.
|
||||
|
||||
The *name* points to the intended meaning of the data. ("customerData", "orderNumber", ...)
|
||||
|
||||
The *type* sets limits on the form of data that is accepted. (a number, a list of strings, ...)
|
||||
|
||||
This type is expressed by the [Baker type system](/sections/reference/baker-types-and-values/).
|
||||
|
||||
## Interaction
|
||||
|
||||
An interaction is similar to a function.
|
||||
|
||||
It requires *input* ([ingredients](/sections/reference/main-abstractions/#ingredient-and-ingredientinstance)) and
|
||||
provides *output* ([events](/sections/reference/main-abstractions/#event-and-eventinstance)).
|
||||
|
||||
Within this contract it may do anything. For example:
|
||||
|
||||
- query an external system
|
||||
- put a message on a bus
|
||||
- generate a document or image
|
||||
- extract or compose ingredients into others
|
||||
|
||||
When finished, an interaction provides an event as its output.
|
||||
|
||||
### Interaction failure
|
||||
|
||||
An interaction may fail to fulfill its intended purpose.
|
||||
|
||||
We distinquish two types of failures.
|
||||
|
||||
1. A *technical* failure is one that could be retried and succeed. For example:
|
||||
* Time outs because of an unreliable network or packet loss
|
||||
* External system is temporarily down or unresponsive
|
||||
* External system returned a malformed/unexpected response
|
||||
|
||||
These failures are unexpected and are modeled by throwing an exception from the interaction.
|
||||
|
||||
2. A *functional* failure is one that cannot be retried. For example:
|
||||
* The customer is too young for the request.
|
||||
* Not enough credit to perform the transfer.
|
||||
|
||||
These failures are expected possible outcomes of the interaction. They are modelled by returning an event from the interaction.
|
||||
|
||||
### Failure mitigation
|
||||
|
||||
In case of technical failures, baker offers two mitigation strategies:
|
||||
|
||||
1. Retry with incremental back-off
|
||||
|
||||
This retries the interaction with some configurable parameters:
|
||||
|
||||
- `initialTimeout`: The initial delay for the first retry.
|
||||
- `backoffFactor`: The back-off factor.
|
||||
- `maximumInterval`: The maximum interval between retries.
|
||||
|
||||
2. Continue with an event.
|
||||
|
||||
This is analagous to a try/catch in Java code. The exception is logged but the process continues with a specified event.
|
||||
|
||||
The interaction gets *blocked* when no failure strategy is defined for it.
|
||||
|
||||
## Event
|
||||
|
||||
An event has a *name* and can (optionally) provide ingredients.
|
||||
|
||||
The purpose of events is therefore twofold.
|
||||
|
||||
1. It signifies that something of interest has happened for a [recipe instance](/sections/reference/main-abstractions/#recipe-and-recipeinstance).
|
||||
|
||||
Example, *"the customer placed the order"*, *"terms and conditions were accepted"*
|
||||
|
||||
2. The event may provide ingredients required to continue the process.
|
||||
|
||||
Example, *"OrderPlaced"* -> `<list of products>`
|
||||
|
||||
We distinguish two conceptual types of events.
|
||||
|
||||
1. Sensory events (*external*)
|
||||
|
||||
These events are provided from outside of the process.
|
||||
|
||||
2. Interaction output (*internal*)
|
||||
|
||||
These events are a result of an interaction being executed.
|
||||
|
||||
Both of these are still just instances of the `EventInstance` class, and the distinction is only used as practical terms.
|
||||
|
||||
## **Recipe**
|
||||
|
||||
*Events*, *Interactions* and *Ingredients* can be composed into recipes.
|
||||
|
||||
Recipes are similar to process blueprints.
|
||||
|
||||
Baker provides a [recipe DSL](/sections/reference/dsls/) in which you can declaratively describe your recipe.
|
||||
|
||||
A small example:
|
||||
``` java
|
||||
new Recipe("webshop")
|
||||
.withSensoryEvents(
|
||||
OrderPlaced.class,
|
||||
CustomerInfoReceived.class
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class),
|
||||
of(ManufactureGoods.class));
|
||||
```
|
||||
|
||||
The main take away is that when declaring your recipe you do not have to think about order.
|
||||
|
||||
Everything is automatically linked by the *data* requirements of the interactions.
|
||||
|
||||
## Continuing from here
|
||||
|
||||
After adding the dependencies you can continue to:
|
||||
|
||||
* Go through the [development life cycle section](/sections/development-life-cycle/design-a-recipe) if you like learning by doing;
|
||||
* Go through the [reference section](/sections/reference/main-abstractions) if you like learning by description.
|
||||
@@ -0,0 +1,283 @@
|
||||
# Bake, Fire Events and Inquiry
|
||||
|
||||
At this moment we already have a `Recipe` and `InteractionInstances`, the next step is to create a Baker runtime, and add
|
||||
to it the `ImplementationInstances` and the `Recipe` (in that order, since adding a `Recipe` validates that there exist
|
||||
valid `InteractionInstances` for each `Interaction`).
|
||||
|
||||
For this example we are going to create an Akka-based, non-cluster, local runtime. This runtime is based on a library called
|
||||
[Akka](https://akka.io/) which helps us manage concurrency and gives us distributed-systems semantics for the cluster mode,
|
||||
this is almost completely hidden from you, but currently for some configuration and managing a Baker cluster, it might be
|
||||
useful to check the Akka documentation.
|
||||
|
||||
Also note that to add a `Recipe` you need to first transform it into a `CompiledRecipe` by using the provided
|
||||
`RecipeCompiler.compileRecipe(recipe)` API.
|
||||
|
||||
_Note: Since Baker 3.0 all APIs of the runtime are asynchronous by default. That means all APIs return `Future[A]` for
|
||||
Scala (IO interface will come in the future depending on demand) and `CompletableFuture<A>` for Java. If you are not familiar
|
||||
with these constructs we highly recommend checking one of the many tutorials and documentation pages in the internet,
|
||||
otherwise for now you can do `.join` on any `CompletableFuture` or `Await.result(yourFuture, 1.second)` on any `Future`
|
||||
to block and do normal synchronous/blocking programming._
|
||||
|
||||
```scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
||||
implicit val actorSystem: ActorSystem =
|
||||
ActorSystem("WebshopSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
|
||||
val program: Future[Unit] = for {
|
||||
_ <- baker.addImplementation(WebshopInstancesReflection.reserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiledRecipe)
|
||||
} yield ()
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class JMain {
|
||||
|
||||
static public void main(String[] args) {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
CompletableFuture<String> asyncRecipeId = baker.addImplementation(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe));
|
||||
|
||||
// Blocks, not recommended but useful for testing or trying things out
|
||||
String recipeId = asyncRecipeId.join();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bake
|
||||
|
||||
Next, you can start one instance of your process by `baking` a recipe, this will internally create a `RecipeInstance`
|
||||
which will hold the state of your process, listen to `EventInstances`, execute your `InteractionInstances` when
|
||||
`IngredientInstances` are available and handle any failure state.
|
||||
|
||||
`RecipeInstances` can be created by choosing a `CompiledRecipe` by using the `recipeId` yielded by the
|
||||
`Baker.addRecipe(compiledRecipe)` API, and by providing a `recipeInstanceId` of your choosing; you will use this last id
|
||||
to reference and interact with the created `RecipeInstance`. Use the `Baker.bake(recipeId, recipeInstanceId)` API for
|
||||
creating a `RecipeInstance`.
|
||||
|
||||
_Note: In an Akka-cluster-based Baker, these `RecipeInstances` are also automatically distributed over nodes, and the
|
||||
cluster will ensure that there is 1 `RecipeInstance` running on 1 node, and if the node dies, it will detect it and
|
||||
restore the `RecipeInstance` in another available node, for this you need to configure an underlying distributed store;
|
||||
for more on this please refer to the [configuration section](/sections/development-life-cycle/configure/) and the [runtime section](/sections/reference/runtime/)._
|
||||
|
||||
```scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
||||
implicit val actorSystem: ActorSystem =
|
||||
ActorSystem("WebshopSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
|
||||
val program: Future[Unit] = for {
|
||||
_ <- baker.addImplementation(WebshopInstancesReflection.reserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiledRecipe)
|
||||
_ <- baker.bake(recipeId, "first-instance-id")
|
||||
} yield ()
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
// As a small quirk ok the Java API, all operations which are ment to not return something, will return a
|
||||
// scala.runtime.BoxedUnit object. You should think of it like Java's Void or void and you can safely
|
||||
// ignore it except for your type signatures.
|
||||
import scala.runtime.BoxedUnit;
|
||||
|
||||
public class JMain {
|
||||
|
||||
static public void main(String[] args) {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
CompletableFuture<BoxedUnit> asyncRecipeId = baker.addImplementation(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fire Events
|
||||
|
||||
Next, we want our process to start flowing through it's state, and start executing `InteractionInstances`, for that we
|
||||
need to fire the nicknamed `SensoryEvents` which are just `EventInstances` which match the root `Events` from our `Recipe`.
|
||||
|
||||
There are several supported semantics for firing an event. When you fire an event you might want to be notified and continue your
|
||||
asynchronous computation on 1 of 4 different moments:
|
||||
|
||||
1. When the event got accepted by the `RecipeInstance` but has not started cascading the execution of `InteractionInstances`.
|
||||
For this use the `Baker.fireEventAndResolveWhenReceived(recipeInstanceId, eventInstance)` API. This will return a
|
||||
`Future[SensoryEventStatus]` enum notifying of the outcome (the event might get rejected).
|
||||
|
||||
2. When the event got accepted by the `RecipeInstance` and has finished cascading the execution of `InteractionInstances`
|
||||
up to the point that it requires more `EventInstances` (`SensoryEvents`) to continue, or the process has finished.
|
||||
For this use the `Baker.fireEventAndResolveWhenCompleted(recipeInstanceId, eventInstance)` API. This will return a
|
||||
`Future[EventResult]` object containing a `SensoryEventStatus`, the `Event` names that got fired in consequence of this
|
||||
`SensoryEvent`, and the current available `Ingredients` output of the `InteractionInstances` that got executed as consequence
|
||||
of the `SensoryEvent`.
|
||||
|
||||
3. You want to do something on both of the previously mentioned moments, then use the
|
||||
`Baker.fireEvent(recipeInstanceId, eventInstance)` API, which will return an `EventResolutions` object which contains both
|
||||
`Future[SensoryEventStatus]` and `Future[EventResult]` (or its `CompletableFuture<A>` equivalents in Java).
|
||||
|
||||
4. As soon as an intermediate `Event` fires from one of the `InteractionInstances` that execute as consequence of the fired
|
||||
`SensoryEvent`. For this use the `Baker.fireEventAndResolveOnEvent(recipeInstanceId, eventInstance, onEventName)` API. This will return
|
||||
a similar `Future[EventResult` to the one returned by `Baker.fireEventAndResolveWhenCompleted` except the data will be up
|
||||
to the moment the `onEventName` was fired.
|
||||
|
||||
```scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
||||
implicit val actorSystem: ActorSystem =
|
||||
ActorSystem("WebshopSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
|
||||
val program: Future[Unit] = for {
|
||||
_ <- baker.addImplementation(WebshopInstancesReflection.reserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiledRecipe)
|
||||
_ <- baker.bake(recipeId, "first-instance-id")
|
||||
firstOrderPlaced: EventInstance =
|
||||
EventInstance.unsafeFrom(WebshopRecipeReflection.OrderPlaced("order-uuid", List("item1", "item2")))
|
||||
result <- baker.fireEventAndResolveWhenCompleted("first-instance-id", firstOrderPlaced)
|
||||
_ = assert(result.events == Seq(
|
||||
WebshopRecipe.Events.OrderPlaced.name,
|
||||
WebshopRecipe.Events.ItemsReserved.name
|
||||
)
|
||||
} yield ()
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.ing.baker.runtime.javadsl.EventInstance;
|
||||
import com.ing.baker.runtime.javadsl.EventResult;
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class JMain {
|
||||
|
||||
static public void main(String[] args) {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
String recipeInstanceId = "first-instance-id";
|
||||
CompletableFuture<List<String>> result = baker.addImplementation(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced))
|
||||
.thenApply(EventResult::events);
|
||||
|
||||
List<String> blockedResult = result.join();
|
||||
assert(blockedResult.contains("OrderPlaced") && blockedResult.contains("ReservedItems"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Inquiry
|
||||
|
||||
### Recipe Instance State
|
||||
|
||||
As a final step on what you might want to do with Baker (without considering handling failed `RecipeInstances`),
|
||||
is that you can query the state of a `RecipeInstance` at any given moment. For this you can use the
|
||||
`Baker.getInteractionInstanceState(recipeInstanceId)` API. This will return an `InteractionInstanceState` object which
|
||||
contains all the event names with timestamps that have executed, and the current available provided ingredients waiting
|
||||
for the next `InteractionInstances` to consume.
|
||||
|
||||
### Recipe Instance State Visualizations
|
||||
|
||||
Another method of fetching state is the visual representation of it. You can do that with the `Baker.getVisualState(recipeInstanceId)`
|
||||
API. This will return a GraphViz string like the [visualization api](/sections/development-life-cycle/use-visualizations/) that you can convert into an image.
|
||||
|
||||
Here is a visualization of the state of another webshop example, one can clearly see that the process is flowing correctly
|
||||
without failures and that it is still waiting for the payment sensory event to be fired.
|
||||
|
||||

|
||||
92
docs/sections/development-life-cycle/configure.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Configure
|
||||
|
||||
## Minimal configuration
|
||||
|
||||
When creating a baker instance using the constructor `Baker.akka(config, actorSystem, materializer)` baker will require you
|
||||
to add the minimal baker configuration, you can do this by adding this to your `application.conf` file:
|
||||
|
||||
```
|
||||
include "baker.conf"
|
||||
```
|
||||
|
||||
This will add the following minimal configuration:
|
||||
|
||||
```
|
||||
akka.cluster.sharding.state-store-mode = persistence
|
||||
akka.actor.allow-java-serialization = off
|
||||
```
|
||||
|
||||
## reference.conf
|
||||
|
||||
Here you will find the `reference.conf` of Baker, this represents the current default configuration of Baker.
|
||||
|
||||
_Note: Since the Baker runtime is based on Akka, there is extra configuration that can be done, please refer to the
|
||||
[Akka configuration documentation](https://doc.akka.io/docs/akka/current/general/configuration.html)_
|
||||
|
||||
```
|
||||
|
||||
baker {
|
||||
|
||||
actor {
|
||||
# the id of the journal to read events from
|
||||
read-journal-plugin = "inmemory-read-journal"
|
||||
|
||||
# either "local" or "cluster-sharded"
|
||||
provider = "local"
|
||||
|
||||
# the recommended nr is number-of-cluster-nodes * 10
|
||||
cluster.nr-of-shards = 50
|
||||
|
||||
# the time that inactive actors (processes) stay in memory
|
||||
idle-timeout = 5 minutes
|
||||
|
||||
# The interval that a check is done of processes should be deleted
|
||||
retention-check-interval = 1 minutes
|
||||
}
|
||||
|
||||
# the default timeout for Baker.bake(..) process creation calls
|
||||
bake-timeout = 10 seconds
|
||||
|
||||
# the timeout for refreshing the local recipe cache
|
||||
process-index-update-cache-timeout = 5 seconds
|
||||
|
||||
# the default timeout for Baker.processEvent(..)
|
||||
process-event-timeout = 10 seconds
|
||||
|
||||
# the default timeout for inquires on Baker, this means getIngredients(..) & getEvents(..)
|
||||
process-inquire-timeout = 10 seconds
|
||||
|
||||
# when baker starts up, it attempts to 'initialize' the journal connection, this may take some time
|
||||
journal-initialize-timeout = 30 seconds
|
||||
|
||||
# the default timeout for adding a recipe to Baker
|
||||
add-recipe-timeout = 10 seconds
|
||||
|
||||
# the time to wait for a gracefull shutdown
|
||||
shutdown-timeout = 30 seconds
|
||||
|
||||
# The ingredients that are filtered out when getting the process instance.
|
||||
# This should be used if there are big ingredients to improve performance and memory usage.
|
||||
# The ingredients will be in the ingredients map but there value will be an empty String.
|
||||
filtered-ingredient-values = []
|
||||
|
||||
# encryption settings
|
||||
encryption {
|
||||
|
||||
# whether to encrypt data stored in the journal, off or on
|
||||
enabled = off
|
||||
|
||||
# if enabled = on, a secret should be set
|
||||
# secret = ???
|
||||
}
|
||||
}
|
||||
|
||||
akka {
|
||||
|
||||
# by default we use the in memory journal from: https://github.com/dnvriend/akka-persistence-inmemory
|
||||
persistence.journal.plugin = "inmemory-journal"
|
||||
|
||||
persistence.snapshot-store.plugin = "inmemory-snapshot-store"
|
||||
}
|
||||
|
||||
```
|
||||
339
docs/sections/development-life-cycle/design-a-recipe.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# Design a Recipe
|
||||
|
||||
_Full project examples including tests and configuration can be found [here](https://github.com/ing-bank/baker/tree/master/examples)._
|
||||
|
||||
The _Development Life Cycle_ section provides a "top-down"/"by-example" guide to baker, all of the concepts
|
||||
are introduced through exemplification on hypothetical development situations.
|
||||
|
||||
## Modeling the order placement process for a webshop using Ingredients, Events and Recipes.
|
||||
|
||||
The recipe DSL allows you to declaratively describe your business process. The design always starts with the
|
||||
business requirements, lets say you are developing a webshop which will have many different microservices on
|
||||
the backend. The initial requirements for the order reservation process reads:
|
||||
|
||||
_"An order contains an order id and a list of store item identifiers, when an order is placed it must first
|
||||
be validated by reserving the items from the warehouse service, a success scenario yields the ids of the
|
||||
reserved items, but if at least one item is unavailable at the warehouse a failure yields the list of unavailable items
|
||||
and the process stops"_
|
||||
|
||||
When developing with Baker we must first translate the requirements into our 3 essential building blocks,
|
||||
`Ingredients` for raw data, `Events` for happenings (that might contain ingredients), and `Interactions` which
|
||||
have ingredients as input, execute actions with other systems, and yield more events. We will do this so that the
|
||||
baker runtime can orchestrate the execution of our process through the underlying microservices.
|
||||
|
||||
## Ingredients and Events
|
||||
|
||||
A recipe always starts with initial events, also called `Sensory Events`, in the case of our first requirement
|
||||
we could model the placing of an order as an event, which will provide 2 ingredients: the order id and the list
|
||||
of items.
|
||||
|
||||
``` scala tab="Scala"
|
||||
|
||||
import com.ing.baker.recipe.scaladsl._
|
||||
|
||||
object Ingredients {
|
||||
|
||||
val OrderId: Ingredient[String] =
|
||||
Ingredient[String]("orderId")
|
||||
|
||||
val Items: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("items")
|
||||
}
|
||||
|
||||
object Events {
|
||||
|
||||
val OrderPlaced: Event = Event(
|
||||
name = "OrderPlaced",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.OrderId,
|
||||
Ingredients.Items
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
|
||||
// In Java the data structures to represent Events and Ingredients
|
||||
// will be extraxted from a class by the reflection API when building
|
||||
// the Recipe. (see below for full example)
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Depending on your programming language, you might want to import the corresponding dsl, i.e. `scaladsl` vs `javadsl`.
|
||||
|
||||
Ingredients and events are just data structures that describe your process data, carrying not just the names, but
|
||||
also the type information, for example `Ingredient[String]("order-id")` creates an ingredient of name "order-id"
|
||||
of type "String", for more information about Baker types please refer to [this section](/sections/reference/baker-types-and-values/).
|
||||
As shown in the code, events might carry ingredients, and have a maximum about of times they are allowed to fire, the
|
||||
runtime will enforce this limit. For more information about this and other features of events please refer to [this section](/sections/reference/dsls/#events).
|
||||
|
||||
## Interactions
|
||||
|
||||
Then, the desired actions can be modeled as `interactions`, in our case we are told that it exists a warehouse service
|
||||
which we need to call to reserve the items, but this might either succeed or fail.
|
||||
|
||||
_Note: Notice that when using the reflection API, the Java interface or Scala trait that will represent your interaction
|
||||
must have a method named `apply`, this is the method that the reflection API will convert into Baker types/ingredients/events._
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
object Interactions {
|
||||
|
||||
val ReserveItems: Interaction = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredients.OrderId,
|
||||
Ingredients.Items,
|
||||
),
|
||||
output = Seq(
|
||||
Events.OrderHadMissingItems,
|
||||
Events.ItemsReserved
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object Ingredients {
|
||||
|
||||
// ... previous ingredients
|
||||
|
||||
val ReservedItems: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("reservedItems")
|
||||
|
||||
val UnavailableItems: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("unavailableItems")
|
||||
}
|
||||
|
||||
object Events {
|
||||
|
||||
// ... previous events
|
||||
|
||||
val OrderHadUnavailableItems: Event = Event(
|
||||
name = "OrderHadUnavailableItems",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.UnavailableItems
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
|
||||
val ItemsReserved: Event = Event(
|
||||
name = "ItemsReserved",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.ReservedItems
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```scala tab="Java (Reflection API)"
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
// ... previous event
|
||||
|
||||
// Interface that will represent our Interaction, notice that it is declaring inner events.
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// The @FireEvent annotation communicates the reflection API about several possible outcome events.
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
// The @RequiresIngredient annotation communicates the reflection API about the ingredient names that other events
|
||||
// must provide to execute this interaction.
|
||||
// The method MUST be named `apply`
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
An interaction resembles a function, it takes input ingredients and outputs 1 of several possible events. At runtime, when
|
||||
an event fires, baker tries to match the provided ingredients of 1 or several events to the input of awaiting interactions,
|
||||
as soon as there is a match on data, baker will execute the interactions, creating a cascading effect that will execute
|
||||
your business process in an asynchronous manner.
|
||||
|
||||
This was a simple example, but you might have already concluded that this can be further composed into bigger processes by
|
||||
making new interactions that require events and ingredients which are output of previous interactions.
|
||||
|
||||
You can create also interactions which take no input ingredients but are executed after events (with or without provided
|
||||
ingredients) are fired, for this and other features please refer to the conceptual documentation found [here](/sections/reference/dsls/#events).
|
||||
|
||||
## The Recipe
|
||||
|
||||
The final step is to create an object that will hold all of these descriptions into what we call a Recipe, this becomes
|
||||
the "blueprint" of your process, it can define failure handling strategies, and will "auto-bind" the interactions, that
|
||||
means it detects the composition between interactions by matching the ingredients provided by events to the input ingredients
|
||||
required by interactions.
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
object WebshopRecipe {
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Events.OrderPlaced
|
||||
)
|
||||
.withInteractions(
|
||||
Interactions.ReserveItems,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```scala tab="Java"
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
// ... previous events and interactions.
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(OrderPlaced.class)
|
||||
.withInteractions(of(ReserveItems.class));
|
||||
}
|
||||
```
|
||||
|
||||
Let us remember that this is just a _description_ of what our program across multiple services should do, on the next
|
||||
sections we will see how to visualize it, create runtime `instances` of our recipes and their parts, what common practices
|
||||
are there for testing, everything you need to know to deploy and monitor a baker cluster, and how Baker helps you handle
|
||||
and resolve failure which is not modeled in the domain (in the recipe).
|
||||
|
||||
As you might have realised `Ingredients`, `Events` and `Interactions` could be reused on different Recipes, giving common
|
||||
business verbs that your programs and organisation can use across teams, the same way different cooking recipes share
|
||||
same processes (simmering, boiling, cutting) you should reuse interactions across your different business recipes.
|
||||
|
||||
As a bonus; you might have though that this API is verbose, we agree and that is why we developed an alternative
|
||||
API which uses Java and Scala reflection.
|
||||
|
||||
``` scala tab="Scala (Reflection API)"
|
||||
|
||||
package webshop
|
||||
|
||||
import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe}
|
||||
|
||||
object WebshopRecipeReflection {
|
||||
|
||||
case class OrderPlaced(orderId: String, items: List[String])
|
||||
|
||||
sealed trait ReserveItemsOutput
|
||||
|
||||
case class OrderHadUnavailableItems(unavailableItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
case class ItemsReserved(reservedItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
val ReserveItems = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredient[String]("orderId"),
|
||||
Ingredient[List[String]]("items")
|
||||
),
|
||||
output = Seq(
|
||||
Event[OrderHadUnavailableItems],
|
||||
Event[ItemsReserved]
|
||||
)
|
||||
)
|
||||
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Event[OrderPlaced])
|
||||
.withInteractions(
|
||||
ReserveItems)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
``` scala tab="Java (Reflection API)"
|
||||
|
||||
package webshop;
|
||||
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
import com.ing.baker.recipe.annotations.RequiresIngredient;
|
||||
import com.ing.baker.recipe.javadsl.Interaction;
|
||||
import com.ing.baker.recipe.javadsl.Recipe;
|
||||
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// The @FireEvent annotation communicates the reflection API about several possible outcome events.
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
// The @RequiresIngredient annotation communicates the reflection API about the ingredient names that other events
|
||||
// must provide to execute this interaction.
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(OrderPlaced.class)
|
||||
.withInteractions(of(ReserveItems.class));
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# Implement Interactions
|
||||
|
||||
After creating a recipe, and before we can even run processes from it, we need to create `InteractionInstances` that the
|
||||
Baker runtime will use to execute the real actions of your `Recipe`, these instances must match the `Interactions` in the `Recipe`,
|
||||
that means that the name and the input types must match.
|
||||
|
||||
For the Scala DSL you need to create an object of `InteractionImplementation`, or use the reflection API in a similar way
|
||||
to the Java DSL.
|
||||
|
||||
For the Java DSL you need to implement the interface with the `apply` method that you used for the `Interaction` in the `Recipe`
|
||||
and then use the `InteractionImplementation.from(new Implementation())` reflection API, this will create a new `InteractionImplementation`
|
||||
that we will later on add to a baker runtime.
|
||||
|
||||
``` scala tab="Scala Reflection API"
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
trait ReserveItems {
|
||||
|
||||
def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput]
|
||||
}
|
||||
|
||||
class ReserveItemsInstance extends ReserveItems {
|
||||
|
||||
override def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput] = {
|
||||
|
||||
// Http call to the Warehouse service
|
||||
val response: Future[Either[List[String], List[String]]] =
|
||||
// This is mocked for the sake of the example
|
||||
Future.successful(Right(items))
|
||||
|
||||
// Build an event instance that Baker understands
|
||||
response.map {
|
||||
case Left(unavailableItems) =>
|
||||
WebshopRecipeReflection.OrderHadUnavailableItems(unavailableItems)
|
||||
case Right(reservedItems) =>
|
||||
WebshopRecipeReflection.ItemsReserved(reservedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val reserveItemsInstance: InteractionInstance =
|
||||
InteractionInstance.unsafeFrom(new ReserveItemsInstance)
|
||||
```
|
||||
|
||||
``` scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.types.{CharArray, ListType, ListValue, PrimitiveValue}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
val ReserveItemsInstance = InteractionInstance(
|
||||
name = ReserveItems.name,
|
||||
input = Seq(CharArray, ListType(CharArray))
|
||||
run = handleReserveItems
|
||||
)
|
||||
|
||||
def handleReserveItems(input: Seq[IngredientInstance]): Future[Option[EventInstance]] = ???
|
||||
// The body of this function is going to be executed by the Baker runtime when the ingredients are available.
|
||||
// ListValue and PrimitiveValue are used in the body
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
public class ReserveItems implements JWebshopRecipe.ReserveItems {
|
||||
|
||||
// The body of this method is going to be executed by the Baker runtime when the ingredients are available.
|
||||
@Override
|
||||
public ReserveItemsOutcome apply(String id, List<String> items) {
|
||||
return new JWebshopRecipe.ReserveItems.ItemsReserved(items);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
```
|
||||
13
docs/sections/development-life-cycle/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Development Life Cycle
|
||||
|
||||
In this section we will explain every step in the process of developing business functionality across your microservices using Baker, we will focus on practical aspects: how to do things and why you should do them. The general steps in the development lifecycle are:
|
||||
|
||||
1) Design a Recipe: In Baker you are always required to make a distinction between specification (Recipe) and implementation (Runtime) of your business process, you will use the Recipe DSL to express the interface of ingredients and events (data) and interactions (actions), which will help Baker understand your orchestration flow. Baker recipes are the interface of your business process, they specify Baker Types without values for Ingredients and Events, and specify input Ingredients and output Events without implementation for Interactions.
|
||||
2) Use Visualizations: Baker is able to create a graphical representation of your recipe, this becomes very useful for reasoning about your business process, and easily communicate and discuss about it.
|
||||
3) Implement Interactions: To execute the orchestration plan specified by the recipe, you must create interaction implementations, which are the code blocks that match the interfaces of the ingredient/event/interactions. These must be registered to baker, which will imply their correspondence with the recipe by matching the interfaces.
|
||||
4) Create Process Instances, Fire Events and Inquiry: After registering recipes and implementations, you are able to create instances of the recipes, which execute after you fire events, which will execute calls to your microservices. The state of a given process may be requested at any time for application utility.
|
||||
5) Test: Common methods of testing are, independently test each implementation, running a process instance and then inspect the current state, and running a process instance using mocked implementations.
|
||||
6) Configure: There are several parts of Baker that can be configured, including but not limited to: event store connection, clustering, etc.
|
||||
7) Deploy: Baker has a cluster mode, which must be deployed with a certain order or by configuring service discovery.
|
||||
8) Monitor: Baker provides event listeners, which will allow you to monitor the process instances.
|
||||
9) Resolve Failed Processes:
|
||||
42
docs/sections/development-life-cycle/monitor.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Monitor
|
||||
|
||||
To monitor a Baker application we recommend doing so by using the `baker.registerEventListener(recipeName?, listenerFunction)`
|
||||
and the `baker.registerBakerEventListener(listenerFunction)`. The former can notify of every `EventInstance` that is being
|
||||
fired globally or per `CompiledRecipe`. And the latter notifies of baker operations that happen like new `InteractionInstances`
|
||||
being executed or failing. These accept a function that can call your logging or metrics system:
|
||||
|
||||
```scala tab="Scala (Recipe Events)"
|
||||
baker.registerEventListener((recipeInstanceId: String, event: EventInstance) => {
|
||||
println(s"Recipe instance : $recipeInstanceId processed event ${event.name}")
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java (Recipe Events)"
|
||||
BiConsumer<String, EventInstance> handler = (String recipeInstanceId, EventInstance event) ->
|
||||
System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name());
|
||||
|
||||
baker.registerEventListener(handler);
|
||||
```
|
||||
|
||||
```scala tab="Scala (Baker Events)"
|
||||
import com.ing.baker.runtime.scaladsl._
|
||||
|
||||
baker.registerBakerEventListener((event: BakerEvent) => {
|
||||
event match {
|
||||
case e: EventReceived => println(e)
|
||||
case e: EventRejected => println(e)
|
||||
case e: InteractionFailed => println(e)
|
||||
case e: InteractionStarted => println(e)
|
||||
case e: InteractionCompleted => println(e)
|
||||
case e: ProcessCreated => println(e)
|
||||
case e: RecipeAdded => println(e)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java (Baker Events)"
|
||||
import com.ing.baker.runtime.javadsl.BakerEvent;
|
||||
|
||||
baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event));
|
||||
```
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Resolve Failed Recipes
|
||||
|
||||
## Interaction Failure strategy
|
||||
|
||||
When an interaction throws an exception there are a number of mitigation strategies:
|
||||
|
||||
## Block interaction
|
||||
|
||||
This is the *DEFAULT* strategy if no [default strategy](#default-failure-strategy) is defined.
|
||||
|
||||
This option is suitable for non idempotent interactions that cannot be retried.
|
||||
|
||||
When an exception is thrown from the interaction the interaction is *blocked*.
|
||||
|
||||
This means that the interaction cannot execute again automatically.
|
||||
|
||||
## Fire event
|
||||
|
||||
This option is analagous to a `try { } catch { }` in code. When an exception is raised from the interaction you specify an
|
||||
event to fire. So instead of failing the process continues.
|
||||
|
||||
Example:
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class)
|
||||
.withInteractionFailureStrategy(
|
||||
InteractionFailureStrategy.FireEvent("ValidateOrderFailed")
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Retry with incremental back-off
|
||||
|
||||
Incremental back-off allows you to configure a retry mechanism that takes longer for each retry.
|
||||
The idea here is that you quickly retry at first but slower over time. To not overload your system but give it time to recover.
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class)
|
||||
.withFailureStrategy(new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withBackoffFactor(2.0)
|
||||
.withMaxTimeBetweenRetries(Duration.ofSeconds(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.build())
|
||||
)
|
||||
```
|
||||
|
||||
What do these parameters mean?
|
||||
|
||||
| name | meaning |
|
||||
| --- | --- |
|
||||
| `initialDelay` | The delay for the first retry. |
|
||||
| `backoffFactor` | The back-off factor for the delay (optional, `default = 2`) |
|
||||
| `maxTimeBetweenRetries` | The maximum interval between retries. |
|
||||
| `deadLine` | The maximum total amount of time spend delaying. |
|
||||
|
||||
For our example this results in the following delay pattern:
|
||||
|
||||
`100 millis` -> `200 millis` -> `400 millis` -> `...` -> `100 seconds` -> `100 seconds`
|
||||
|
||||
Which can be visualized like this:
|
||||
|
||||

|
||||
|
||||
Note that these delays do **not** include interaction execution time.
|
||||
|
||||
For example, if the first retry execution takes `5` seconds (and fails again) then the second retry will
|
||||
be triggered after (from the start):
|
||||
|
||||
`(100 millis + 5 seconds + 200 millis) = 5.3 seconds`
|
||||
|
||||
This also means that the `24 hour` deadline **does not** include interaction execution time. It is advisable to take this
|
||||
into account when coming up with this number.
|
||||
|
||||
**Retry exhaustion**
|
||||
|
||||
It can happen that after some time, when an interaction keeps failing, that the retry is exhausted.
|
||||
|
||||
When this happens 2 things may happen.
|
||||
|
||||
Either the interaction becomes [blocked(#blocked-interaction).
|
||||
|
||||
Or if you configure so, the process continues with a predefined event:
|
||||
|
||||
```
|
||||
.withFailureStrategy(new RetryWithIncrementalBackoffBuilder()
|
||||
.withFireRetryExhaustedEvent(SomeEvent.class))
|
||||
|
||||
```
|
||||
|
||||
Note that this event class **requires** an empty constructor to be present and **cannot** provide ingredients.
|
||||
|
||||
## Default failure strategy
|
||||
|
||||
You can also define a default failure strategy on the recipe level.
|
||||
|
||||
This then serves as a fallback if none is defined for an interaction.
|
||||
|
||||
For example:
|
||||
|
||||
``` java
|
||||
final Recipe webshopRecipe = new Recipe("webshop")
|
||||
.withDefaultFailureStrategy(
|
||||
new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.withMaxTimeBetweenRetries(Duration.ofMinutes(10))
|
||||
.build());
|
||||
```
|
||||
|
||||
|
||||
|
||||
## baker.retryInteraction(recipeInstanceId, interactionName)
|
||||
|
||||
It is possible that during the execution of a `RecipeInstance` it becomes *blocked*, this can happen either because it
|
||||
is `directly blocked` by an exception (and the `FailureStrategy` of the `Interaction` of the `Recipe` was set to block)
|
||||
or that the retry strategy was exhausted. At this point it is possible to resolve the blocked interaction in 2 ways.
|
||||
This one involves forcing another try, resulting either on a successful continued process, or again on a failed state,
|
||||
to check this you will need to request the state of the `RecipeInstance` again.
|
||||
|
||||
_Note: this behaviour can be automatically preconfigured by using the `RetryWithIncrementalBackoff` `FailureStrategy`
|
||||
on the `Interaction` of the `Recipe`_
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.retryInteraction(recipeInstanceId, "ReserveItems")
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.retryInteraction(recipeInstanceId, "ReserveItems");
|
||||
```
|
||||
|
||||
## baker.resolveInteraction(recipeInstanceId, interactionName, event)
|
||||
|
||||
It is possible that during the execution of a `RecipeInstance` it becomes *blocked*, this can happen either because it
|
||||
is `directly blocked` by an exception or that the retry strategy was exhausted. At this point it is possible to resolve
|
||||
the blocked interaction in 2 ways. This one involves resolving the interaction with a chosen `EventInstance` to replace
|
||||
the one that would have had been computed by the `InteractionInstance`.
|
||||
|
||||
_Note: this behaviour can be automatically preconfigured by using the `FireEventAfterFailure(eventName)` `FailureStrategy`
|
||||
on the `Interaction` of the `Recipe`_
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.resolveInteraction(recipeInstanceId, "ReserveItems", ItemsReserved(List("item1")))
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.resolveInteraction(recipeInstanceId, "ReserveItems", new ItemsReserved(List("item1")));
|
||||
```
|
||||
|
||||
## baker.stopRetryingInteraction(recipeInstanceId, interactionName)
|
||||
|
||||
If an `Interaction` is configured with a `RetryWithIncrementalBackoff` `FailureStrategy` then it will not stop retrying
|
||||
until you call this API or a successful outcome happens from the `InteractionInstance`.
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems")
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems");
|
||||
```
|
||||
274
docs/sections/development-life-cycle/test.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Test
|
||||
|
||||
The Baker model already enforces separation and decoupling between parts of your system, which eases testability, it
|
||||
presents a model that divides units of code into modularized sections, more specifically, it provides a clear distinction
|
||||
between business logic and implementation through the `Recipe` and `InteractionImplementations`, and independence between
|
||||
`Interactions`, since every `Interaction` should only depend on the input.
|
||||
|
||||
We present the layers of testing when using Baker, and how to write tests for such layers, use them as necessary.
|
||||
|
||||
## Testing Recipes for Soundness: Compiling the Recipe in a Test
|
||||
|
||||
First, one important notice is that currently Baker does not check at compile time the soundness of your Recipe (if your
|
||||
Recipe makes any sense), but it does so when compiling it with the `RecipeCompiler.compileRecipe(recipe)` API. Errors are
|
||||
thrown in the form of exceptions describing the issue. So a simple unit test that simply compiles your `Recipe` is essential.
|
||||
|
||||
```scala tab="Scala"
|
||||
class WebshopRecipeSpec extends FlatSpec with Matchers {
|
||||
|
||||
"The WebshopRecipe" should "compile the recipe without errors" in {
|
||||
RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```scala tab="Java"
|
||||
public class JWebshopRecipeTests {
|
||||
|
||||
@Test
|
||||
public void shouldCompileTheRecipeWithoutIssues() {
|
||||
RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Recipes for Correctness: Mocking Interaction Implementations
|
||||
|
||||
The next layer is to test that the Recipe actually does what you expect it to do at the business logic level **independently**
|
||||
of the underlying implementations. One way of doing so is by providing `InteractionImplementations` that behave as expected
|
||||
according to a testing scenario.
|
||||
|
||||
On the next example we will:
|
||||
|
||||
1. Create a new local instance of baker.
|
||||
2. Setup mocked interaction instances that behave according to your desired test scenario.
|
||||
3. Wire the recipe and implementations to fire sensory events according to your desired test scenario.
|
||||
4. Inspect the recipe instance state.
|
||||
5. Assert expectations on the state and/or the sensory event results.
|
||||
|
||||
_Note: Take into consideration the asynchronous nature of Baker, some times the easiest is to use `fireAndResolveWhenCompleted`
|
||||
that will resolve when a sensory event has completely finished affecting the state of the recipe instance._
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
/** Interface used to mock the ReserveItems interaction using the reflection API. */
|
||||
trait ReserveItems {
|
||||
|
||||
def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput]
|
||||
}
|
||||
|
||||
/** Mock of the ReserveItems interaction. */
|
||||
class ReserveItemsMock extends ReserveItems {
|
||||
|
||||
override def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput] =
|
||||
Future.successful(WebshopRecipeReflection.ItemsReserved(items))
|
||||
}
|
||||
|
||||
"The Webshop Recipe" should "reserve items in happy conditions" in {
|
||||
|
||||
val system: ActorSystem = ActorSystem("baker-webshop-system")
|
||||
val materializer: Materializer = ActorMaterializer()(system)
|
||||
val baker: Baker = Baker.akkaLocalDefault(system, materializer)
|
||||
|
||||
val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
|
||||
val orderId: String = "order-id"
|
||||
val items: List[String] = List("item1", "item2")
|
||||
|
||||
val orderPlaced = EventInstance
|
||||
.unsafeFrom(WebshopRecipeReflection.OrderPlaced(orderId, items))
|
||||
val paymentMade = EventInstance
|
||||
.unsafeFrom(WebshopRecipeReflection.PaymentMade())
|
||||
|
||||
val reserveItemsInstance: InteractionInstance =
|
||||
InteractionInstance.unsafeFrom(new ReserveItemsMock)
|
||||
|
||||
for {
|
||||
_ <- baker.addInteractionInstace(reserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiled)
|
||||
_ <- baker.bake(recipeId, recipeInstanceId)
|
||||
_ <- baker.fireEventAndResolveWhenCompleted(
|
||||
recipeInstanceId, orderPlaced)
|
||||
_ <- baker.fireEventAndResolveWhenCompleted(
|
||||
recipeInstanceId, paymentMade)
|
||||
state <- baker.getRecipeInstanceState(recipeInstanceId)
|
||||
provided = state
|
||||
.ingredients
|
||||
.find(_._1 == "reservedItems")
|
||||
.map(_._2.as[List[String]])
|
||||
.map(_.mkString(", "))
|
||||
.getOrElse("No reserved items")
|
||||
} yield provided shouldBe items.mkString(", ")
|
||||
}
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
static public class HappyFlowReserveItems implements JWebshopRecipe.ReserveItems {
|
||||
|
||||
@Override
|
||||
public ReserveItemsOutcome apply(String id, List<String> items) {
|
||||
return new ItemsReserved(items);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRunSimpleInstance() {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
EventInstance paymentMade =
|
||||
EventInstance.from(new JWebshopRecipe.PaymentMade());
|
||||
|
||||
InteractionInstance reserveItemsInstance =
|
||||
InteractionInstance.from(new HappyFlowReserveItems());
|
||||
CompiledRecipe compiledRecipe =
|
||||
RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
String recipeInstanceId = "first-instance-id";
|
||||
CompletableFuture<List<String>> result = baker.addInteractionInstace(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, paymentMade))
|
||||
.thenCompose(ignore -> baker.getRecipeInstanceState(recipeInstanceId))
|
||||
.thenApply(x -> x.events().stream().map(EventMoment::getName).collect(Collectors.toList()));
|
||||
|
||||
List<String> blockedResult = result.join();
|
||||
|
||||
assert(blockedResult.contains("OrderPlaced") && blockedResult.contains("PaymentMade") && blockedResult.contains("ItemsReserved"));
|
||||
}
|
||||
```
|
||||
|
||||
This test is replicating a full round through a `Recipe`, the only difference with your normal production code is that
|
||||
`InteractionInstances` are adapted to fit the scenario you want to check, so the objective here is to test that the `Recipe`
|
||||
ends on the desired state given a certain order of firing sensory events and that the interaction instances return the
|
||||
specific data.
|
||||
|
||||
_Note: We recommend you abstract away these scenarios with a function that will run them given different
|
||||
interaction instances, so that you can run the same scenario on different environments, like when you want to do a E2E
|
||||
on a test environment._
|
||||
|
||||
## Mocking Interaction Implementations with Mockito
|
||||
|
||||
Baker supports `InteractionInstances` that are [mockito](https://site.mockito.org/) mocks, this will give you the added
|
||||
semantics of Mockito, like verifying that the interaction instance was called, or even called with the expected data.
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
trait ReserveItems {
|
||||
|
||||
def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput]
|
||||
}
|
||||
|
||||
|
||||
"The Webshop Recipe" should "reserve items in happy conditions (mockito)" in {
|
||||
|
||||
val system: ActorSystem = ActorSystem("baker-webshop-system")
|
||||
val materializer: Materializer = ActorMaterializer()(system)
|
||||
val baker: Baker = Baker.akkaLocalDefault(system, materializer)
|
||||
|
||||
val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
val recipeInstanceId: String = UUID.randomUUID().toString
|
||||
|
||||
val orderId: String = "order-id"
|
||||
val items: List[String] = List("item1", "item2")
|
||||
|
||||
val orderPlaced = EventInstance
|
||||
.unsafeFrom(WebshopRecipeReflection.OrderPlaced(orderId, items))
|
||||
val paymentMade = EventInstance
|
||||
.unsafeFrom(WebshopRecipeReflection.PaymentMade())
|
||||
|
||||
// The ReserveItems interaction being mocked by Mockito
|
||||
val mockedReserveItems: ReserveItems = mock[ReserveItems]
|
||||
val reserveItemsInstance: InteractionInstance =
|
||||
InteractionInstance.unsafeFrom(mockedReserveItems)
|
||||
|
||||
when(mockedReserveItems.apply(orderId, items))
|
||||
.thenReturn(Future.successful(WebshopRecipeReflection.ItemsReserved(items)))
|
||||
|
||||
for {
|
||||
_ <- baker.addInteractionInstace(reserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiled)
|
||||
_ <- baker.bake(recipeId, recipeInstanceId)
|
||||
_ <- baker.fireEventAndResolveWhenCompleted(
|
||||
recipeInstanceId, orderPlaced)
|
||||
_ <- baker.fireEventAndResolveWhenCompleted(
|
||||
recipeInstanceId, paymentMade)
|
||||
state <- baker.getRecipeInstanceState(recipeInstanceId)
|
||||
provided = state
|
||||
.ingredients
|
||||
.find(_._1 == "reservedItems")
|
||||
.map(_._2.as[List[String]])
|
||||
.map(_.mkString(", "))
|
||||
.getOrElse("No reserved items")
|
||||
|
||||
// Verify that the mock was called with the expected data
|
||||
_ = verify(mockedReserveItems).apply(orderId, items)
|
||||
} yield provided shouldBe items.mkString(", ")
|
||||
}
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@Test
|
||||
public void shouldRunSimpleInstanceMockitoSample() {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
EventInstance paymentMade =
|
||||
EventInstance.from(new JWebshopRecipe.PaymentMade());
|
||||
|
||||
// The ReserveItems interaction being mocked by Mockito
|
||||
JWebshopRecipe.ReserveItems reserveItemsMock =
|
||||
mock(JWebshopRecipe.ReserveItems.class);
|
||||
InteractionInstance reserveItemsInstance =
|
||||
InteractionInstance.from(reserveItemsMock);
|
||||
CompiledRecipe compiledRecipe =
|
||||
RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
// Add input expectations and their returned event instances
|
||||
when(reserveItemsMock.apply("order-uuid", items)).thenReturn(
|
||||
new JWebshopRecipe.ReserveItems.ItemsReserved(items));
|
||||
|
||||
String recipeInstanceId = "first-instance-id";
|
||||
CompletableFuture<List<String>> result = baker.addInteractionInstace(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, paymentMade))
|
||||
.thenCompose(ignore -> baker.getRecipeInstanceState(recipeInstanceId))
|
||||
.thenApply(x -> x.events().stream().map(EventMoment::getName).collect(Collectors.toList()));
|
||||
|
||||
List<String> blockedResult = result.join();
|
||||
|
||||
// Verify that the mock was called with the expected data
|
||||
verify(reserveItemsMock).apply("order-uuid", items);
|
||||
|
||||
assert(blockedResult.contains("OrderPlaced") && blockedResult.contains("PaymentMade") && blockedResult.contains("ItemsReserved"));
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Individual Implementations
|
||||
|
||||
The final layer is to individually test your implementations, which will resemble your normal e2e tests, interconnectivity
|
||||
tests, or unit tests which mock the dependencies. If we put a code example here it would be more of a tutorial on how to
|
||||
generally test code than Baker, which is a good argument to show that Baker si all about decoupling your code and automatically
|
||||
orchestrate it through distributed boundaries.
|
||||
60
docs/sections/development-life-cycle/use-visualizations.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Use Visualizations
|
||||
|
||||
One of the first big advantages of creating Baker recipes is visualization.
|
||||
|
||||
We have found that the visualization creates a great way to reason on very complex and big processes, and also they
|
||||
create a bridge between developers and business oriented people.
|
||||
|
||||
You can generate one from a compiled recipe.
|
||||
|
||||
``` scala tab="Scala"
|
||||
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
|
||||
val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
val visualization: String = compiled.getRecipeVisualization
|
||||
|
||||
```
|
||||
|
||||
``` scala tab="Java"
|
||||
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
|
||||
CompiledRecipe recipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
String visualization = recipe.getRecipeVisualization();
|
||||
|
||||
```
|
||||
|
||||
The visualization is a [graphviz](http://www.graphviz.org/) string that will look like this:
|
||||
|
||||
```
|
||||
digraph {
|
||||
node [fontname = "ING Me", fontsize = 22, fontcolor = white]
|
||||
pad = 0.2
|
||||
ReserveItems [shape = rect, style = "rounded, filled", color = "#525199", penwidth = 2, margin = 0.5]
|
||||
reservedItems [shape = circle, style = filled, color = "#FF6200"]
|
||||
OrderHadUnavailableItems [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3]
|
||||
unavailableItems [shape = circle, style = filled, color = "#FF6200"]
|
||||
orderId [shape = circle, style = filled, color = "#FF6200"]
|
||||
OrderPlaced [shape = diamond, style = "rounded, filled", color = "#767676", fillcolor = "#D5D5D5", fontcolor = black, penwidth = 2, margin = 0.3]
|
||||
ReserveItems -> OrderHadUnavailableItems
|
||||
OrderHadUnavailableItems -> unavailableItems
|
||||
OrderPlaced -> items
|
||||
OrderPlaced -> orderId
|
||||
items -> ReserveItems
|
||||
ItemsReserved [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3]
|
||||
orderId -> ReserveItems
|
||||
items [shape = circle, style = filled, color = "#FF6200"]
|
||||
ReserveItems -> ItemsReserved
|
||||
ItemsReserved -> reservedItems
|
||||
}
|
||||
```
|
||||
|
||||
You can use tools like [this web page](http://www.webgraphviz.com/) to create an svg image. For example the visualization of
|
||||
the Webshop recipe that we designed on the last section looks like this:
|
||||
|
||||

|
||||
|
||||
For complete documentation of how to configure the visualization, refer to [this section](/sections/reference/visualization/).
|
||||
15
docs/sections/feature-comparison.md
Normal file
@@ -0,0 +1,15 @@
|
||||
This is a comparison of Baker with similar solutions. Feedback and contributions to solutions not listed are most welcome.
|
||||
|
||||
| Feature | Baker | [Camunda](https://camunda.com/) | [Pega](https://www.pega.com/) | [Netflix Conductor](https://netflix.github.io/conductor/) | [Uber Cadence](https://github.com/uber/cadence) | [Apache Airflow](https://airflow.apache.org/) |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Owned By | ING | Camunda | PEGA Systems | Netflix | Uber | Community |
|
||||
| Primary Purpose | Orchestration of (micro-)services | Process Automation | Workflow or case management | Orchestration of (micro-)services | Orchestration of long-running business logic | Workflow of big-data pipelines |
|
||||
| Typical Use | Straight Through Processing (STP) | Business Processes with Decision Making | Business Processes with Decision Making | STP | STP | Big data |
|
||||
| Skill-set required | Java or Scala | Java, Business Process Modelling Notation (BPMN) | Pega-specific | JSON | Java | Python, Bash |
|
||||
| Execution Model | Petri-net | BPMN for workflows, Decision Model and Notation (DMN) for business rules | Don’t know | Queueing Theory | Queueing Theory | Graph Theory |
|
||||
| In-memory processing | Yes | Yes | No | Yes | No | No |
|
||||
| Data Persistence | Event sourcing with Cassandra |Relational DB via JDBC | Relational | Dedicated Storage (Dynomite) | Cassandra | N/A |
|
||||
| Process Visualization | [Graphviz](https://www.graphviz.org/) | Based on BPMN | Based on BPMN | Dedicated UI | No | Dedicated UI |
|
||||
| License Model | Open-source | Community Platform is open-source | Pay per Case | Open-source | Open-source |Open-source |
|
||||
| Rich UI | No | Yes | Yes | Yes | No | Yes |
|
||||
|
||||
57
docs/sections/getting-started.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Getting Started
|
||||
|
||||
## Project setup
|
||||
|
||||
Baker is released to [maven central](https://search.maven.org/search?q=com.ing.baker).
|
||||
|
||||
You can add following dependencies to your `maven` or `sbt` project to start using it:
|
||||
|
||||
``` scala tab="Sbt"
|
||||
dependencies += "com.ing.baker" %% "baker-recipe-dsl" % "3.0.0"
|
||||
dependencies += "com.ing.baker" %% "baker-compiler" % "3.0.0"
|
||||
dependencies += "com.ing.baker" %% "baker-runtime" % "3.0.0"
|
||||
```
|
||||
|
||||
``` maven tab="Maven"
|
||||
<dependencies>
|
||||
<groupId>com.ing.baker</groupId>
|
||||
<artifactId>baker-recipe-dsl_2.12</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependencies>
|
||||
<dependencies>
|
||||
<groupId>com.ing.baker</groupId>
|
||||
<artifactId>baker-compiler_2.12</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependencies>
|
||||
<dependencies>
|
||||
<groupId>com.ing.baker</groupId>
|
||||
<artifactId>baker-runtime_2.12</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependencies>
|
||||
|
||||
```
|
||||
|
||||
This includes *ALL* baker modules to your project. If you only need partial functionality you can pick and choose the modules you need.
|
||||
|
||||
### Modules
|
||||
|
||||
An explanation of the baker modules.
|
||||
|
||||
| Module | Description |
|
||||
| --- | --- |
|
||||
| recipe-dsl | [DSL](/sections/reference/dsls/) to describe your recipes (process blueprints) *declaritively* |
|
||||
| runtime | [Runtime](/sections/reference/runtime/) based on [akka](htts://www.akka.io) to manage and execute your recipes |
|
||||
| compiler | [Compiles your recipe](/sections/reference/runtime/#recipecompilercompilerecipe) description into a model that the runtime can execute |
|
||||
| intermediate-language | Recipe and Petri Net model that the runtime can execute |
|
||||
|
||||
This is the dependency graph between the modules.
|
||||
|
||||

|
||||
|
||||
## Continuing from here
|
||||
|
||||
After adding the dependencies you can continue to:
|
||||
|
||||
1. Understand the [high level concepts](/sections/concepts).
|
||||
2. If you like learning by doing, go through the [development life cycle section](/sections/development-life-cycle/design-a-recipe).
|
||||
3. If you like learning by description, go through the [reference section](/sections/reference/main-abstractions).
|
||||
114
docs/sections/reference/baker-types-and-values.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Baker Types and Values
|
||||
|
||||
Because of the distributed nature of Baker and how the runtime works, we need to have serializable types and values to
|
||||
transfer recipes and data between nodes and to match over such data, that is why we implemented a type system on top of Scala.
|
||||
They help not just to model your domain but also for Baker to identify when to execute interactions.
|
||||
|
||||
If you are using all of our reflection APIs then you will not use them directly, but it is good to know of their
|
||||
existence.
|
||||
|
||||
``` scala tab="Scala"
|
||||
import com.ing.baker.types._
|
||||
|
||||
val data: (Type, Value) = (Int32, PrimitiveValue(42))
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import com.ing.baker.types.*;
|
||||
|
||||
Type dataType = Int32$.MODULE$;
|
||||
Value dataValue = PrimitiveValue.apply(42);
|
||||
```
|
||||
|
||||
`Types` are specifically used to describe `Ingredients`, specifically `Ingredients` are just a relation between a name and
|
||||
a type.
|
||||
|
||||
In a similar way that a programming language variable is just a relation between a name and a type at compile time, the
|
||||
baker `Ingredient` is a relation between a name and a Baker `Type` at "recipe time"; the same happens with values, in a
|
||||
programming language values must respect types otherwise runtime exceptions are thrown, similarly in Baker, at runtime
|
||||
the name of an ingredient will hold a value that respects the ingredient's type.
|
||||
|
||||
Here is a complete list of `Types` and `Values` of Baker.
|
||||
|
||||
### Primitives
|
||||
|
||||
| Type | Java parallel | Description |
|
||||
| --- | --- | --- |
|
||||
| `Bool` | `boolean` | *single* bit, `true` or `false`, `1` or `0` |
|
||||
| `Char` | `char` | Unsigned `16` bit integer |
|
||||
| `Byte` | `byte` | Signed `8` bit integer |
|
||||
| `Int16` | `short` | Signed `16` bit integer |
|
||||
| `Int32` | `int` | Signed `32` bit integer |
|
||||
| `Int64` | `long` | Signed `64` bit integer |
|
||||
| `IntBig` | `BigInteger` | Integer of arbitrary size |
|
||||
| `Float32` | `float` | Signed `32` bit floating point |
|
||||
| `Float64` | `double` | Signed `64` bit floating point |
|
||||
| `FloatBig` | `BigDecimal` | Floating point of arbitrary size |
|
||||
| `Date` | `long` | A *UTC* date in the *ISO-8601* calendar system with *millisecond* precision |
|
||||
| `ByteArray` | `Array<Byte>` | Byte array, often used for binary data |
|
||||
| `CharArray` | `String` | Character array, or commmonly called `String` |
|
||||
|
||||
### Structured types
|
||||
|
||||
| Type | Java parallel | Description |
|
||||
| --- | --- | --- |
|
||||
| `ListType<T>` | `java.util.List<T>` | A list of values, all of the same type |
|
||||
| `OptionType<T>` | `java.util.Optional<T>` | Matches against `T` or `null` |
|
||||
| `EnumType` | `enum class` | A set of predifined options (strings) |
|
||||
| `RecordType` | `POJO class` | A record with a specific set of fields |
|
||||
| `MapType<T>` | `java.util.Map<String, T>` | A record with arbitrary fields, all of the same type |
|
||||
|
||||
## Values
|
||||
|
||||
Values are pure data without any direct associated type. These very closely match the *JSON* data format.
|
||||
|
||||
| Value | Description |
|
||||
| --- | --- |
|
||||
| `NullValue` | Analogues to `null`, `Optional.empty`, `None`, etc ... |
|
||||
| `PrimitiveValue` | Wrapper for for: <br/>- A Java primitive (or boxed variant)<br/> - `java.lang.String`<br/> - `java.math.BigInteger`<br/> - `java.math.BigDecimal`<br/> - `scala.math.BigInt`<br/> - `Array<Byte>`|
|
||||
| `ListValue` | A list of values |
|
||||
| `RecordValue` | A set of `String -> Value` pairs |
|
||||
|
||||
## Interoptability with java types
|
||||
|
||||
Because it is impractical to directly work with the baker types in java/scala code there is conversion system.
|
||||
|
||||
## Default supported types
|
||||
|
||||
### java
|
||||
|
||||
- primitives and their boxed variants
|
||||
- Enum types
|
||||
- java.util.List
|
||||
- java.util.Set
|
||||
- java.util.Map
|
||||
- java.math.BigInt
|
||||
- java.math.BigDecimal
|
||||
- java.util.Optional
|
||||
- POJO classes
|
||||
|
||||
### scala
|
||||
|
||||
- primitives and their boxed variants
|
||||
- case classes
|
||||
- scala.collection.immutable.List
|
||||
- scala.collection.immutable.Set
|
||||
- scala.collection.immutable.Map
|
||||
- BigInt
|
||||
- BigDecimal
|
||||
- scala.Option
|
||||
|
||||
## Registering a custom type adapter
|
||||
|
||||
All default type adapters are registered in the [reference.conf](https://github.com/ing-bank/baker/blob/master/bakertypes/src/main/resources/reference.conf) of the `baker-types` module.
|
||||
|
||||
You can add your custom type adapter by registering it in a `reference.conf`.
|
||||
|
||||
```
|
||||
baker.types {
|
||||
|
||||
"com.example.MyCustomType" = "com.example.MyCustomTypeAdpater"
|
||||
}
|
||||
```
|
||||
|
||||
For an example how to implement an adapter see [here](https://github.com/ing-bank/baker/blob/adf9b2edd4fe5ebdcec2bdd7f281cd151d64afe6/bakertypes/src/main/scala/com/ing/baker/types/modules/JavaModules.scala#L93)
|
||||
482
docs/sections/reference/dsls.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Recipe DSLs
|
||||
|
||||
Conceptually a `Recipe` allows you to declaratively describe your business process and is a "blueprint" that can be used to
|
||||
start a `RecipeInstance` on the runtime. To create such "blueprint" you need to use either the Java or Scala DSLs, there
|
||||
a `Recipe` is just a data structure that bundles `Events`, `Interactions` and some execution configuration like firing limits
|
||||
or error handling mechanics.
|
||||
|
||||
_Note: `Ingredients` are indirectly added to the `Recipe` because they come inside `Events`._
|
||||
|
||||
These data structures are just that, data, and to ease their construction and improve the user experience of the library
|
||||
we provide an API that uses Java and Scala reflection to generate most of the data from language constructions like case
|
||||
classes or interfaces.
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff
|
||||
import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff.UntilDeadline
|
||||
import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object WebshopRecipe {
|
||||
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Events.OrderPlaced,
|
||||
Events.PaymentMade)
|
||||
.withInteractions(
|
||||
Interactions.ReserveItems
|
||||
.withRequiredEvent(Events.PaymentMade))
|
||||
.withDefaultFailureStrategy(
|
||||
RetryWithIncrementalBackoff
|
||||
.builder()
|
||||
.withInitialDelay(100 milliseconds)
|
||||
.withUntil(Some(UntilDeadline(24 hours)))
|
||||
.withMaxTimeBetweenRetries(Some(10 minutes))
|
||||
.build())
|
||||
|
||||
object Ingredients {
|
||||
|
||||
val OrderId: Ingredient[String] =
|
||||
Ingredient[String]("orderId")
|
||||
|
||||
val Items: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("items")
|
||||
|
||||
val ReservedItems: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("reservedItems")
|
||||
|
||||
val UnavailableItems: Ingredient[List[String]] =
|
||||
Ingredient[List[String]]("unavailableItems")
|
||||
}
|
||||
|
||||
object Events {
|
||||
|
||||
val OrderPlaced: Event = Event(
|
||||
name = "OrderPlaced",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.OrderId,
|
||||
Ingredients.Items
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
|
||||
val PaymentMade: Event = Event(
|
||||
name = "PaymentMade",
|
||||
providedIngredients = Seq.empty,
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
|
||||
val OrderHadUnavailableItems: Event = Event(
|
||||
name = "OrderHadUnavailableItems",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.UnavailableItems
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
|
||||
val ItemsReserved: Event = Event(
|
||||
name = "ItemsReserved",
|
||||
providedIngredients = Seq(
|
||||
Ingredients.ReservedItems
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
}
|
||||
|
||||
object Interactions {
|
||||
|
||||
val ReserveItems: Interaction = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredients.OrderId,
|
||||
Ingredients.Items,
|
||||
),
|
||||
output = Seq(
|
||||
Events.OrderHadUnavailableItems,
|
||||
Events.ItemsReserved
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```scala tab="Scala (Reflection API)"
|
||||
|
||||
import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff
|
||||
import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff.UntilDeadline
|
||||
import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object WebshopRecipeReflection {
|
||||
|
||||
case class OrderPlaced(orderId: String, items: List[String])
|
||||
|
||||
case class PaymentMade()
|
||||
|
||||
sealed trait ReserveItemsOutput
|
||||
|
||||
case class OrderHadUnavailableItems(unavailableItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
case class ItemsReserved(reservedItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
val ReserveItems = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredient[String]("orderId"),
|
||||
Ingredient[List[String]]("items")
|
||||
),
|
||||
output = Seq(
|
||||
Event[OrderHadUnavailableItems],
|
||||
Event[ItemsReserved]
|
||||
)
|
||||
)
|
||||
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Event[OrderPlaced],
|
||||
Event[PaymentMade])
|
||||
.withInteractions(
|
||||
ReserveItems
|
||||
.withRequiredEvent(Event[PaymentMade]))
|
||||
.withDefaultFailureStrategy(
|
||||
RetryWithIncrementalBackoff
|
||||
.builder()
|
||||
.withInitialDelay(100 milliseconds)
|
||||
.withUntil(Some(UntilDeadline(24 hours)))
|
||||
.withMaxTimeBetweenRetries(Some(10 minutes))
|
||||
.build())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java (Reflection API)"
|
||||
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
import com.ing.baker.recipe.annotations.RequiresIngredient;
|
||||
import com.ing.baker.recipe.javadsl.InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder;
|
||||
import com.ing.baker.recipe.javadsl.Interaction;
|
||||
import com.ing.baker.recipe.javadsl.Recipe;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentMade {}
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(
|
||||
OrderPlaced.class,
|
||||
PaymentMade.class)
|
||||
.withInteractions(
|
||||
of(ReserveItems.class)
|
||||
.withRequiredEvent(PaymentMade.class))
|
||||
.withDefaultFailureStrategy(
|
||||
new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.withMaxTimeBetweenRetries(Duration.ofMinutes(10))
|
||||
.build());
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
[Events](/sections/reference/main-abstractions/#event-and-eventinstance) are simple `POJO` classes. For example:
|
||||
|
||||
``` scala tab="Scala"
|
||||
case class CustomerInfoReceived(customerInfo: CustomerInfo)
|
||||
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
public class CustomerInfoReceived {
|
||||
public final CustomerInfo customerInfo;
|
||||
|
||||
public CustomerInfoReceived(CustomerInfo customerInfo) {
|
||||
this.customerInfo = customerInfo;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The field types of the `POJO` class must be compatible with the baker type system.
|
||||
|
||||
See the [supported types](/sections/reference/baker-types-and-values/#primitives) for more information.
|
||||
|
||||
The names of the fields are obtained using reflection.
|
||||
|
||||
They can be added using the `.withSensoryEvents(..)` method.
|
||||
|
||||
|
||||
|
||||
### Firing limit
|
||||
|
||||
A *firing limit* is a limit on the number of times a sensory event may be received by a
|
||||
[recipe instance](/sections/reference/main-abstractions/#recipe-and-recipeinstance).
|
||||
|
||||
By default sensory events have a firing limit of `1` per process instance.
|
||||
|
||||
This means the event will be rejected with status `FiringLimitMet` after the first time it is received.
|
||||
|
||||
If you want to send an event more then once you may add it like this:
|
||||
|
||||
``` java
|
||||
.withSensoryEventsNoFiringLimit(CustomerInfoReceived.class)
|
||||
```
|
||||
|
||||
In this example the `CustomerInfoReceived` can now be received multiple times by a process instance.
|
||||
|
||||
## Interactions
|
||||
|
||||
Interactions are interfaces with some requirements. See [here](/sections/development-life-cycle/design-a-recipe/#interactions) how to define them.
|
||||
|
||||
You can include interactions in your recipe using the static `of(..)` method.
|
||||
|
||||
``` java
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
final Recipe webshopRecipe = new Recipe("webshop")
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class)
|
||||
)
|
||||
```
|
||||
|
||||
There are a number of options to tailor an interaction for your recipe.
|
||||
|
||||
### Maximum interaction count
|
||||
|
||||
By default there is *no* limit on the number of times an Interaction may fire.
|
||||
|
||||
Sometimes you may want to set a limit.
|
||||
|
||||
For example, to ensure the goods are shipped only once.
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ShipGoods.class).withMaximumInteractionCount(1)
|
||||
)
|
||||
```
|
||||
|
||||
### Predefining ingredients
|
||||
|
||||
An interaction normally requires all its input ingredients to be provided from [Events](/sections/reference/main-abstractions/#event-and-eventinstance).
|
||||
|
||||
Sometimes however it is useful to *predefine* (or *hard code*) the value of an ingredient.
|
||||
|
||||
For example:
|
||||
|
||||
- An email template
|
||||
- An application/requester id when calling an external system
|
||||
|
||||
This can be done by:
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(SendEmail.class)
|
||||
.withPredefinedIngredient("emailTemplate", "Welcome to ING!")
|
||||
)
|
||||
```
|
||||
|
||||
Note that *predefined* ingredients are **always** available and do not have to be provided by an event for each interaction call.
|
||||
|
||||
Each time all *remaining* ingredients are provided, the interaction will fire.
|
||||
|
||||
You can **not** predefine *ALL* input ingredients of an interaction.
|
||||
|
||||
### Event renames
|
||||
|
||||
Sometimes it useful to rename an interaction event and/or its ingredients to fit better in the context of your recipe.
|
||||
|
||||
For example, to rename the `GoodsManufactured` event and its ingredient.
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ManufactureGoods.class)
|
||||
.withEventTransformation(
|
||||
GoodsManufactured.class, "ManufacturingDone",
|
||||
ImmutableMap.of("goods", "manufacturedGoods")
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Event requirements
|
||||
|
||||
As mentioned before, the DSL is declarative, you do not have to think about order. This is implicit in the data requirements of the interactions.
|
||||
|
||||
However, sometimes data requirements are not enough.
|
||||
|
||||
For example, you might want to be sure to only send an invoice (`SendInvoice`) *AFTER* the goods where shipped (`GoodsShipped`).
|
||||
|
||||
``` java
|
||||
of(SendInvoice.class)
|
||||
.withRequiredEvents(ShipGoods.GoodsShipped.class)
|
||||
```
|
||||
|
||||
In this case the `GoodsShipped` event *MUST* happen before the interaction may execute.
|
||||
|
||||
You can specify multiple events in a single clause. These are bundled with an `AND` condition, meaning *ALL* events in the clause are required.
|
||||
|
||||
You can also require a single event from a number of options.
|
||||
|
||||
``` java
|
||||
of(SendInvoice.class)
|
||||
.withRequiredOneOfEvents(EventA.class, EventB.class)
|
||||
```
|
||||
|
||||
In this case the interaction may fire if *either* `EventA` OR `EventB` has occured.
|
||||
|
||||
### Interaction Failure strategy
|
||||
|
||||
When an interaction throws an exception there are a number of mitigation strategies:
|
||||
|
||||
#### Block interaction
|
||||
|
||||
This is the *DEFAULT* strategy if no other is defined and no [default strategy](#default-failure-strategy) is defined.
|
||||
|
||||
This option is suitable for non idempotent interactions that cannot be retried.
|
||||
|
||||
When an exception is thrown from the interaction the interaction is *blocked*.
|
||||
|
||||
This means that the interaction cannot execute again automatically.
|
||||
|
||||
It requires [manual intervening](/sections/development-life-cycle/resolve-failed-recipe-instances/) to continue the process from then on.
|
||||
|
||||
#### Fire event
|
||||
|
||||
This option is analagous to a `try { } catch { }` in code. When an exception is raised from the interaction you specify an
|
||||
event to fire. So instead of failing the process continues.
|
||||
|
||||
Example:
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class)
|
||||
.withInteractionFailureStrategy(
|
||||
InteractionFailureStrategy.FireEvent("ValidateOrderFailed")
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
#### Retry with incremental backoff
|
||||
|
||||
Incremental backoff allows you to configure a retry mechanism that takes longer for each retry.
|
||||
The idea here is that you quickly retry at first but slower over time. To not overload your system but give it time to recover.
|
||||
|
||||
``` java
|
||||
.withInteractions(
|
||||
of(ValidateOrder.class)
|
||||
.withFailureStrategy(new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withBackoffFactor(2.0)
|
||||
.withMaxTimeBetweenRetries(Duration.ofSeconds(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.build())
|
||||
)
|
||||
```
|
||||
|
||||
What do these parameters mean?
|
||||
|
||||
| name | meaning |
|
||||
| --- | --- |
|
||||
| `initialDelay` | The delay for the first retry. |
|
||||
| `backoffFactor` | The backoff factor for the delay (optional, `default = 2`) |
|
||||
| `maxTimeBetweenRetries` | The maximum interval between retries. |
|
||||
| `deadLine` | The maximum total amount of time spend delaying. |
|
||||
|
||||
For our example this results in the following delay pattern:
|
||||
|
||||
`100 millis` -> `200 millis` -> `400 millis` -> `...` -> `100 seconds` -> `100 seconds`
|
||||
|
||||
Which can be visualized like this:
|
||||
|
||||

|
||||
|
||||
Note that these delays do **not** include interaction execution time.
|
||||
|
||||
For example, if the first retry execution takes `5` seconds (and fails again) then the second retry will
|
||||
be triggered after (from the start):
|
||||
|
||||
`(100 millis + 5 seconds + 200 millis) = 5.3 seconds`
|
||||
|
||||
This also means that the `24 hour` deadline **does not** include interaction execution time. It is advisable to take this
|
||||
into account when coming up with this number.
|
||||
|
||||
**Retry exhaustion**
|
||||
|
||||
It can happen that after some time, when an interaction keeps failing, that the retry is exhausted.
|
||||
|
||||
When this happens 2 things may happen.
|
||||
|
||||
Either the interaction becomes [blocked(#blocked-interaction).
|
||||
|
||||
Or if you configure so, the process continues with a predefined event:
|
||||
|
||||
```
|
||||
.withFailureStrategy(new RetryWithIncrementalBackoffBuilder()
|
||||
.withFireRetryExhaustedEvent(SomeEvent.class))
|
||||
|
||||
```
|
||||
|
||||
Note that this event class **requires** an empty constructor to be present and **cannot** provide ingredients.
|
||||
|
||||
## Default failure strategy
|
||||
|
||||
You can also define a default failure strategy on the recipe level.
|
||||
|
||||
This then serves as a fallback if none is defined for an interaction.
|
||||
|
||||
For example:
|
||||
|
||||
``` java
|
||||
final Recipe webshopRecipe = new Recipe("webshop")
|
||||
.withDefaultFailureStrategy(
|
||||
new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.withMaxTimeBetweenRetries(Duration.ofMinutes(10))
|
||||
.build());
|
||||
```
|
||||
|
||||
69
docs/sections/reference/event-listener.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Event Listener
|
||||
|
||||
After creating a [baker runtime](/sections/reference/runtime/#bakerakkaconfig-actorsystem-materializer) you can attach
|
||||
functions that will be called once `EventInstances` are fired or when different baker occurrences happen:
|
||||
|
||||
## baker.registerEventListener(recipeName, listenerFunction)
|
||||
|
||||
Registers a listener to all runtime events for on a baker instance.
|
||||
|
||||
Note that:
|
||||
- The delivery guarantee is *AT MOST ONCE*. Practically this means you can miss events when the application terminates (unexpected or not).
|
||||
- The delivery is local (JVM) only, you will NOT receive events from other nodes when running in cluster mode.
|
||||
|
||||
Because of these constraints you should not use an event listener for critical functionality. Valid use cases might be:
|
||||
- logging
|
||||
- metrics
|
||||
- unit tests
|
||||
|
||||
```scala tab="Scala"
|
||||
baker.registerEventListener((recipeInstanceId: String, event: EventInstance) => {
|
||||
println(s"Recipe instance : $recipeInstanceId processed event ${event.name}")
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
BiConsumer<String, EventInstance> handler = (String recipeInstanceId, EventInstance event) ->
|
||||
System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name());
|
||||
|
||||
baker.registerEventListener(handler);
|
||||
```
|
||||
|
||||
## baker.registerBakerEventListener(listenerFunction)
|
||||
|
||||
Registers a listener to all runtime BAKER events, these are events that notify what Baker is doing, like `RecipeInstances`
|
||||
received `EventInstances` or `CompiledRecipes` being added to baker.
|
||||
|
||||
Note that:
|
||||
|
||||
* The delivery guarantee is *AT MOST ONCE*. Practically this means you can miss events when the application terminates (unexpected or not).
|
||||
* The delivery is local (JVM) only, you will NOT receive events from other nodes when running in cluster mode.
|
||||
|
||||
Because of these constraints you should not use an event listener for critical functionality. Valid use cases might be:
|
||||
|
||||
* logging
|
||||
* metrics
|
||||
* unit tests
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl._
|
||||
|
||||
baker.registerBakerEventListener((event: BakerEvent) => {
|
||||
event match {
|
||||
case e: EventReceived => println(e)
|
||||
case e: EventRejected => println(e)
|
||||
case e: InteractionFailed => println(e)
|
||||
case e: InteractionStarted => println(e)
|
||||
case e: InteractionCompleted => println(e)
|
||||
case e: ProcessCreated => println(e)
|
||||
case e: RecipeAdded => println(e)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.BakerEvent;
|
||||
|
||||
baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event));
|
||||
```
|
||||
|
||||
65
docs/sections/reference/execution-semantics.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Execution Semantics
|
||||
|
||||
## Execution loop
|
||||
|
||||
This is a short description of the execution loop of a `RecipeInstance`
|
||||
|
||||
1. An `EventInstance` is fired and provides ingredients, either given to baker as a sensory event or as an output
|
||||
of an `InteractionInstance`.
|
||||
2. Baker tries to match the currently available provided `IngredientInstances` with the input of awaiting `InteractionInstances`.
|
||||
3. `InteractionInstances` that can be called, execute and when complete they fire more `EventInstances`, repeating the
|
||||
loop from step 1.
|
||||
4. This continues until there are no more `InteractionInstances` to execute, and the process is considered complete.
|
||||
|
||||
### Notes
|
||||
|
||||
- A sensory event may be provided 1 or more times depending on its `firing limit`.
|
||||
- When `IngredientInstances` are provided multiple times, the latest value overrides the previous.
|
||||
- An `InteractionInstance` fires when all it's `IngredientInstances` and required `Events` are provided.
|
||||
This may happen 1 or more times depending on the `maximum interaction count`.
|
||||
|
||||
# In depth
|
||||
|
||||
This section explains deeply how `ProcessInstances` work, and how they execute your recipes. You don't have to understand
|
||||
this part to develop with Baker. It is just extra documentation for the curious and the contributors.
|
||||
|
||||
A recipe can be represented (and [visualized](/sections/reference/visualization/)) as a graph, which is actually a higher
|
||||
level representation of a [Petri net](https://en.wikipedia.org/wiki/Petri_net) (which is also a graph). When the process
|
||||
is represented as such it enables the `RecipeInstance` to execute the previously described execution loop, because Baker has
|
||||
your process state as a data structure that can be preserved as the state of the `RecipeInstance`. That is why you need
|
||||
to first use the `RecipeCompiler` and compile the recipe into a `CompiledRecipe` (petri net representation) before
|
||||
running a `RecipeInstance` from it.
|
||||
|
||||
## Translation rules
|
||||
|
||||
The compiler has some rules about translating recipe parts to `transitions` and `places` in the petri net.
|
||||
|
||||
### Ingredient used by multiple interactions
|
||||
|
||||
Often an ingredient will be used by multiple interactions in a recipe.
|
||||
|
||||
Because tokens can only be consumed by one transition we have to add a layer to duplicate the token for all transitions.
|
||||
|
||||

|
||||
|
||||
### Interaction with precondition (AND)
|
||||
|
||||
By default event preconditions use an AND combinator. In the petri net this means that each event transition has
|
||||
to produce a token in a place for that interaction.
|
||||
|
||||

|
||||
|
||||
### Interaction with precondition (OR)
|
||||
|
||||
Events that are grouped in an OR combinator for an interaction output a token to the same place.
|
||||
|
||||
Therefor when one of them fires the condition for the transition to fire is met.
|
||||
|
||||

|
||||
|
||||
### Sensory event with firing limit
|
||||
|
||||
When specifying a sensory event with a firing limit of `n` we generate an in-adjacent place with `n` tokens in the
|
||||
initial marking.
|
||||
|
||||

|
||||
556
docs/sections/reference/main-abstractions.md
Normal file
@@ -0,0 +1,556 @@
|
||||
# Main Abstractions
|
||||
|
||||
Baker makes a strong division between the specification of your business process and the runtime implementations.
|
||||
|
||||
| Specification | Runtime |
|
||||
|---------------|---------|
|
||||
| `Type` | `Value` |
|
||||
| `Ingredient` | `IngredientInstance` |
|
||||
| `Event` | `EventInstance` |
|
||||
| `Interaction` | `InteractionInstance` |
|
||||
| `Recipe` | `RecipeInstance` |
|
||||
|
||||
The first four are used to create `Recipe`s which serve as "blueprints" of your process. In the Baker
|
||||
runtime they are used within `RecipeInstance`s, which are created from a `Recipe` specification to execute the flow of
|
||||
your process.
|
||||
|
||||
## Type and Value
|
||||
|
||||
Because of the distributed nature of Baker and how the runtime works, we need to have serializable types and values to
|
||||
transfer recipes and data between nodes and to match over such data, that is why we implemented a type system on top of Scala.
|
||||
They help not just to model your domain, but also for Baker to identify when to execute interactions.
|
||||
|
||||
If you are using all of our reflection APIs then you will not use them directly, but it is good to know of their
|
||||
existence.
|
||||
|
||||
Full documentation about the type system can be found [here](/sections/reference/baker-types-and-values/).
|
||||
|
||||
``` scala tab="Scala"
|
||||
import com.ing.baker.types._
|
||||
|
||||
val data: (Type, Value) = (Int32, PrimitiveValue(42))
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import com.ing.baker.types.*;
|
||||
|
||||
Type dataType = Int32$.MODULE$;
|
||||
Value dataValue = PrimitiveValue.apply(42);
|
||||
```
|
||||
|
||||
## Ingredient and IngredientInstance
|
||||
|
||||
`Ingredients` are containers for the data in your process. This data is **immutable**, which means that it can only be created and never
|
||||
changes in the process. There is **no subtyping**, nor hierarchy. `Ingredients` are carried through your process
|
||||
with `Events`, and are inputs for `Interactions`.
|
||||
|
||||
For a `Recipe` there exist `Ingredients` which are a name and a `Type`, they are used to model the data of your process.
|
||||
|
||||
For a `RecipeInstance` there exist `IngredientInstance`s, which are a name and a `Value`, they are used to move data
|
||||
through your process.
|
||||
|
||||
``` scala tab="Scala"
|
||||
import com.ing.baker.recipe.scaladsl.Ingredient
|
||||
|
||||
val OrderId: Ingredient[String] =
|
||||
Ingredient[String]("orderId")
|
||||
|
||||
|
||||
import com.ing.baker.runtime.scaladsl.IngredientInstance
|
||||
|
||||
val orderIdInstance: IngredientInstance =
|
||||
IngredientInstance("orderId", PrimitiveValue("uuid-123456789"))
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
// In Java, Ingredients are extracted from a class
|
||||
// representing an Event by using java reflection.
|
||||
// See the full example at the "Recipe and RecipeInstance" section
|
||||
```
|
||||
|
||||
## Event and EventInstance
|
||||
|
||||
`Events` represent happenings in your process that might carry `Ingredients`, they represent an "asynchronous boundary",
|
||||
they are always either output from `Interactions` or a special case called `SensoryEvent`s; we call sensory events to the
|
||||
events that come from "outside" of your recipe and are normally used to start your process.
|
||||
|
||||
For a `Recipe` there exist `Events` which are a name, a set of `Ingredients` and a maximum amount of firings; the
|
||||
`maxFiringLimit` is a property which describes the number of times an event is allowed to fire, this is later on
|
||||
enforced by the Baker runtime. To know more about firing limits and all the other configurable properties, please
|
||||
refer to the [DSLs documentation](/sections/reference/dsls/).
|
||||
|
||||
For a `RecipeInstance` there exist `EventInstance`s which are data structures that must match their interface equivalent
|
||||
declared as `Events` on the `Recipe`. They notify a baker `RecipeInstance` of an actual happening and may carry `Ingredient`
|
||||
values; the baker `RecipeInstance` will then use available `Ingredients` to execute `InteractionInstance`s.
|
||||
|
||||
__Note: Names of sensory `Event` and `EventInstance` must match, so that Baker can correctly execute your process flow.__
|
||||
|
||||
``` scala tab="Scala"
|
||||
/** Event */
|
||||
import com.ing.baker.recipe.scaladsl.Event
|
||||
import com.ing.baker.recipe.scaladsl.Ingredient
|
||||
|
||||
val OrderPlaced: Event = Event(
|
||||
name = "OrderPlaced",
|
||||
providedIngredients = Seq(
|
||||
Ingredient[String]("orderId"),
|
||||
Ingredient[List[String]]("items")
|
||||
),
|
||||
maxFiringLimit = Some(1)
|
||||
)
|
||||
|
||||
/** EventInstance */
|
||||
import com.ing.baker.runtime.scaladsl.EventInstance
|
||||
import com.ing.baker.types.{PrimitiveValue, ListValue}
|
||||
|
||||
val firstOrderPlaced: EventInstance = EventInstance(
|
||||
name = "OrderPlaced",
|
||||
providedIngredients = Map(
|
||||
"orderId" -> PrimitiveValue("uuid-0123456789"),
|
||||
"items" -> ListValue(List(PrimitiveValue("item1-id"), PrimitiveValue("item2-id")))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
// In Java, Events and EventInstances are extracted from a
|
||||
// class by using java reflection.
|
||||
// Please check the full recipe example below on the
|
||||
// Recipe and RecipeInstance subsection
|
||||
|
||||
import com.ing.baker.runtime.javadsl.EventInstance;
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
OrderPlaced order1 = new OrderPlaced("uuid-0123456789", items);
|
||||
EventInstance order1Event = EventInstance.from(order1);
|
||||
|
||||
```
|
||||
|
||||
To fire a `SensoryEvent` use the `bakerRuntime.fireEvent(recipeInstanceId, event)` API variations,
|
||||
after creating the baker runtime, adding your recipe to the runtime and `baking` a `RecipeInstance`. For full
|
||||
documentation on this please refer to the [runtime documentation](/sections/reference/runtime/).
|
||||
|
||||
``` scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.runtime.common.EventResult
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
implicit val actorSystem: ActorSystem =
|
||||
ActorSystem("WebshopSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
// This example is using the reflection API `EventInstance.unsafeFrom`
|
||||
val FirstOrderPlaced: EventInstance = EventInstance
|
||||
.unsafeFrom(OrderPlaced("order-uuid", List("item1", "item2")))
|
||||
val recipeInstanceId: String = "recipe id from previously baked recipe instance"
|
||||
|
||||
val result: Future[EventResult] = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, FirstOrderPlaced)
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.ing.baker.runtime.javadsl.EventInstance;
|
||||
import com.ing.baker.runtime.javadsl.EventResult;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
String recipeInstanceId = "recipe id from previously baked recipe instance";
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
|
||||
CompletableFuture<EventResult> result = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced);
|
||||
```
|
||||
|
||||
## Interaction and InteractionInstance
|
||||
|
||||
An interaction resemblance a function, it requires input (`Ingredients`) and provides output (`Events`). Within this
|
||||
contract it may do anything. For example:
|
||||
|
||||
* Query a microservice
|
||||
* Send messages to an event broker like Kafka
|
||||
* Await for a message from an event broker
|
||||
* Generate a file
|
||||
* Do transformations on the ingredients (we like to call these interactions `Sieves`)
|
||||
|
||||
For a `Recipe` there exist `Interactions` which are a name, a sequence of `Ingredients` describing all the input (all of),
|
||||
and a sequence of `Events` describing the _possible_ event outputs (one of). At the recipe level there are several more
|
||||
specifics that can be configured, like requiring events without ingredients, adding predefined ingredients, overriding
|
||||
ingredient names, or handling unexpected failure, for these please refer to the [DSLs documentation](/sections/reference/dsls/).
|
||||
|
||||
For a `RecipeInstance` there exist `InteractionInstance`s which are a name, an input type description, and an implementation
|
||||
of a method/function that will be called when the interaction is executed. The input name and the input type description
|
||||
is used by the Baker runtime to find the correct `InteractionImplementation` to execute when the `Ingredients` are available.
|
||||
|
||||
__Note: Names of sensory `Event` and `EventInstance` must match, so that Baker can correctly execute your process flow.__
|
||||
|
||||
__Note: For asynchronous programming, the Scala DSL `InteractionInstance` can return a `Future[A]` and the Java DSL can
|
||||
return a `CompletableFuture<A>`, and the Baker runtime will handle the async results of the instances.__
|
||||
|
||||
``` scala tab="Scala"
|
||||
/** Interaction */
|
||||
import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction}
|
||||
|
||||
val ReserveItems: Interaction = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredient[String]("orderId"),
|
||||
Ingredient[List[String]]("items")
|
||||
),
|
||||
output = Seq(
|
||||
Events.OrderHadUnavailableItems,
|
||||
Events.ItemsReserved
|
||||
)
|
||||
)
|
||||
|
||||
/** InteractionInstance */
|
||||
import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, InteractionInstance}
|
||||
import com.ing.baker.types.{CharArray, ListType, ListValue, PrimitiveValue}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
val ReserveItemsInstance = InteractionInstance(
|
||||
name = ReserveItems.name,
|
||||
input = Seq(CharArray, ListType(CharArray))
|
||||
run = handleReserveItems
|
||||
)
|
||||
|
||||
def handleReserveItems(input: Seq[IngredientInstance]): Future[Option[EventInstance]] = ???
|
||||
// ListValue and PrimitiveValue are used in the body
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
/** Interaction */
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
import com.ing.baker.recipe.annotations.RequiresIngredient;
|
||||
import com.ing.baker.recipe.javadsl.Interaction;
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// Annotations are needed for wiring ingredients and validating events.
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
// The name of the method must be "apply" for the reflection API to work.
|
||||
// This method can also return a `CompletableFuture<ReserveItemsOutcome>` for asynchronous programming.
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
/** InteractionInstance */
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
public class ReserveItems implements JWebshopRecipe.ReserveItems {
|
||||
|
||||
@Override
|
||||
public ReserveItemsOutcome apply(String id, List<String> items) {
|
||||
return new JWebshopRecipe.ReserveItems.ItemsReserved(items);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```scala tab="Scala (Reflection API)"
|
||||
// See the DSLs documentation for more on the reflection API
|
||||
trait ReserveItems {
|
||||
|
||||
def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput]
|
||||
}
|
||||
|
||||
class ReserveItemsInstance extends ReserveItems {
|
||||
|
||||
override def apply(orderId: String, items: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput] = {
|
||||
|
||||
// Http call to the Warehouse service
|
||||
val response: Future[Either[List[String], List[String]]] =
|
||||
// This is mocked for the sake of the example
|
||||
Future.successful(Right(items))
|
||||
|
||||
// Build an event instance that Baker understands
|
||||
response.map {
|
||||
case Left(unavailableItems) =>
|
||||
WebshopRecipeReflection.OrderHadUnavailableItems(unavailableItems)
|
||||
case Right(reservedItems) =>
|
||||
WebshopRecipeReflection.ItemsReserved(reservedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val reserveItemsInstance: InteractionInstance =
|
||||
InteractionInstance.unsafeFrom(new ReserveItemsInstance)
|
||||
```
|
||||
|
||||
After creating your `InteractionInstance`s, you need to add them to the baker runtime so that Baker can match them to the `Interactions`
|
||||
of your `Recipe` and call them when needed.
|
||||
|
||||
``` scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
val done: Future[Unit] = baker.addImplementation(reserveItemsInstance)
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
|
||||
import scala.runtime.BoxedUnit;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
CompletableFuture<BoxedUnit> done = baker.addImplementation(reserveItemsInstance);
|
||||
```
|
||||
|
||||
## Recipe and RecipeInstance
|
||||
|
||||
All you `Ingredients`, `Events` and `Interactions` must be added to a single unit called the `Recipe`, which works as the
|
||||
"blueprint" of your precess.
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.recipe.scaladsl.Recipe
|
||||
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Events.OrderPlaced
|
||||
)
|
||||
.withInteractions(
|
||||
Interactions.ReserveItems,
|
||||
)
|
||||
```
|
||||
|
||||
```scala tab="Scala Full Example"
|
||||
package webshop
|
||||
|
||||
import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe}
|
||||
|
||||
object WebshopRecipeReflection {
|
||||
|
||||
case class OrderPlaced(orderId: String, items: List[String])
|
||||
|
||||
sealed trait ReserveItemsOutput
|
||||
|
||||
case class OrderHadUnavailableItems(unavailableItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
case class ItemsReserved(reservedItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
val ReserveItems = Interaction(
|
||||
name = "ReserveItems",
|
||||
inputIngredients = Seq(
|
||||
Ingredient[String]("orderId"),
|
||||
Ingredient[List[String]]("items")
|
||||
),
|
||||
output = Seq(
|
||||
Event[OrderHadUnavailableItems],
|
||||
Event[ItemsReserved]
|
||||
)
|
||||
)
|
||||
|
||||
val recipe: Recipe = Recipe("Webshop")
|
||||
.withSensoryEvents(
|
||||
Event[OrderPlaced],
|
||||
Event[OrderHadUnavailableItems],
|
||||
Event[ItemsReserved]
|
||||
)
|
||||
.withInteractions(
|
||||
ReserveItems
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.recipe.javadsl.Recipe;
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(OrderPlaced.class)
|
||||
.withInteractions(of(ReserveItems.class));
|
||||
```
|
||||
|
||||
```java tab="Java Full Example"
|
||||
package webshop;
|
||||
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
import com.ing.baker.recipe.annotations.RequiresIngredient;
|
||||
import com.ing.baker.recipe.javadsl.Interaction;
|
||||
import com.ing.baker.recipe.javadsl.Recipe;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(OrderPlaced.class)
|
||||
.withInteractions(of(ReserveItems.class));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
A recipe must be added to a baker runtime so that you can create "bake" a `RecipeInstance` from it. For that it must be
|
||||
first "compiled" by using the provided compiler.
|
||||
|
||||
Here is a full example of creating a baker runtime, adding the `InteractionInstances` and the compiled `Recipe` (order matters
|
||||
becase baker validates that all recipes have valid implementations when added), and firing an `Event` that will execute
|
||||
the `Interaction`
|
||||
|
||||
```scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.runtime.scaladsl.{Baker, EventInstance}
|
||||
|
||||
implicit val actorSystem: ActorSystem =
|
||||
ActorSystem("WebshopSystem")
|
||||
implicit val materializer: Materializer =
|
||||
ActorMaterializer()
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
|
||||
val program: Future[Unit] = for {
|
||||
_ <- baker.addImplementation(WebshopInstances.ReserveItemsInstance)
|
||||
recipeId <- baker.addRecipe(compiledRecipe)
|
||||
_ <- baker.bake(recipeId, "first-instance-id")
|
||||
firstOrderPlaced: EventInstance =
|
||||
EventInstance.unsafeFrom(WebshopRecipeReflection.OrderPlaced("order-uuid", List("item1", "item2")))
|
||||
result <- baker.fireEventAndResolveWhenCompleted("first-instance-id", firstOrderPlaced)
|
||||
} yield assert(result.events == Seq(
|
||||
WebshopRecipe.Events.OrderPlaced.name,
|
||||
WebshopRecipe.Events.ItemsReserved.name
|
||||
))
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.ing.baker.runtime.javadsl.EventInstance;
|
||||
import com.ing.baker.runtime.javadsl.EventResult;
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
String recipeInstanceId = "first-instance-id";
|
||||
CompletableFuture<List<String>> result = baker.addImplementation(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced))
|
||||
.thenApply(EventResult::events);
|
||||
|
||||
List<String> blockedResult = result.join();
|
||||
assert(blockedResult.contains("OrderPlaced") && blockedResult.contains("ReservedItems"));
|
||||
```
|
||||
551
docs/sections/reference/runtime.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# The Akka Based Runtime
|
||||
|
||||
## Baker.akka(config, actorSystem, materializer)
|
||||
|
||||
Baker provider several constructors to build a runtime to run your Recipes on. The current implementations are o
|
||||
[Akka](https://akka.io/) based, one in local mode, and another in cluster mode.
|
||||
|
||||
_Note: We recommend reviewing also Akka configuration._
|
||||
|
||||
```scala tab="Scala"
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.{ActorMaterializer, Materializer}
|
||||
import com.ing.baker.runtime.scaladsl.Baker
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
|
||||
|
||||
val actorSystem: ActorSystem = ActorSystem("WebshopSystem")
|
||||
val materializer: Materializer = ActorMaterializer()
|
||||
val config: Config = ConfigFactory.load()
|
||||
|
||||
val baker: Baker = Baker.akka(config, actorSystem, materializer)
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.runtime.javadsl.Baker;
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Config config = ConfigFactory.load();
|
||||
|
||||
Baker baker = Baker.akka(config, actorSystem, materializer);
|
||||
```
|
||||
|
||||
This last code snippet will build a Baker runtime and load all configuration from your default `application.conf` located
|
||||
in the resources directory. You can see more about configuration on [this section](../development-life-cycle/configure.md).
|
||||
|
||||
Alternatively there is a constructor that will provide the default configuration for a local mode Baker, this
|
||||
is recommended for tests.
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
```
|
||||
|
||||
### Advantages of the Cluster Mode
|
||||
|
||||
The capabilities gained when in cluster mode are:
|
||||
|
||||
* Elasticity: by adding/removing nodes to the cluster.
|
||||
* Resilience: `RecipeInstances` are automatically restored in a new node when the hosting node fails. (For this you need to configure
|
||||
a distributed data store like Cassandra)
|
||||
* Routing: You can fire `EventInstances` from anywhere on the cluster, and Baker will ensure that the corresponding `RecipeInstance`
|
||||
receives the firing event.
|
||||
|
||||
_Note: To run on cluster mode you need to configure a distributed data store, we highly recommend using Cassandra._
|
||||
|
||||
## InteractionInstance.from(object) (Reflection API)
|
||||
|
||||
As part of our efforts to ease the creation of `InteractionInstances` we created this function that uses the Scala and the
|
||||
Java reflection capabilities to create an `InteractionInstance` from an instance of a class.
|
||||
|
||||
The name of the `InteractionInstance` will be taken from the name of the implementing `interface` (Java) or `trait` (Scala)
|
||||
(it must match the name of the `Interaction` at the `Recipe`).
|
||||
|
||||
The interface MUST declare a public method called `apply`, and the types must match those of the expected provided ingredients.
|
||||
|
||||
Notice that his function might throw an exception if the instance is not correctly done (this is why in Scala the API is
|
||||
named "unsafe").
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl.InteractionInstance
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
sealed trait ReserveItemsOutput
|
||||
case class OrderHadUnavailableItems(unavailableItems: List[String]) extends ReserveItemsOutput
|
||||
case class ItemsReserved(reservedItems: List[String]) extends ReserveItemsOutput
|
||||
|
||||
trait ReserveItems {
|
||||
|
||||
def apply(orderId: String, items: List[String]): Future[ReserveItemsOutput]
|
||||
}
|
||||
|
||||
class ReserveItemsInstance extends ReserveItems {
|
||||
|
||||
override def apply(orderId: String, items: List[String]): Future[ReserveItemsOutput] = {
|
||||
|
||||
// Http call to the Warehouse service
|
||||
val response: Future[Either[List[String], List[String]]] =
|
||||
// This is mocked for the sake of the example
|
||||
Future.successful(Right(items))
|
||||
|
||||
// Build an event instance that Baker understands
|
||||
response.map {
|
||||
case Left(unavailableItems) =>
|
||||
OrderHadUnavailableItems(unavailableItems)
|
||||
case Right(reservedItems) =>
|
||||
ItemsReserved(reservedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val reserveItemsInstance: InteractionInstance =
|
||||
InteractionInstance.unsafeFrom(new ReserveItemsInstance)
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.InteractionInstance;
|
||||
import com.ing.baker.runtime.javadsl.Interaction;
|
||||
|
||||
/** Java interface used for the Recipe */
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
/** Implementation of the interface used for creating an InteractionInstance */
|
||||
public class ReserveItems implements JWebshopRecipe.ReserveItems {
|
||||
|
||||
// The body of this method is going to be executed by the Baker runtime when the ingredients are available.
|
||||
@Override
|
||||
public ReserveItemsOutcome apply(String id, List<String> items) {
|
||||
return new ReserveItems.ItemsReserved(items);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create an InteractionInstance from an instance of the ReserveItems implementation */
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
```
|
||||
|
||||
## baker.addInteractionInstance(interactionInstance)
|
||||
|
||||
The Baker runtime requires you to add all `InteractionInstances` before adding any related `CompiledRecipes`. This can
|
||||
be done using the `baker.addInteractionInstance(interactionInstance)` or the `baker.addInteractionInstances(intance1, instance2, ...)`
|
||||
APIs.
|
||||
|
||||
_Note: in Java the api returns a `CompletableFuture<BoxedUnit>`, this is because the API is implemented in Scala, so
|
||||
Scala's `Unit` get translated to `BoxedUnit`, but you should you ignore it and consider it as good as Java's `void`,
|
||||
except it comes in a `CompletableFuture` that will help you handle async programming._
|
||||
|
||||
```scala tab="Scala"
|
||||
|
||||
val baker: Baker = Baker.akkaLocalDefault(actorSystem, materializer)
|
||||
|
||||
val reserveItemsInstance: InteractionInstance = InteractionInstance.unsafeFrom(new ReserveItems())
|
||||
|
||||
val result: Future[Unit] = baker.addInteractionInstance(reserveItemsInstance)
|
||||
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems());
|
||||
|
||||
CompletableFuture<BoxedUnit> = baker.addInteractionInstance(reserveItemsInstance);
|
||||
|
||||
```
|
||||
|
||||
## RecipeCompiler.compile(recipe)
|
||||
|
||||
`Recipes` once built must be converted into a data structure called `CompiledRecipe` that lets `RecipeInstances`
|
||||
to understand, store and run your process. These can be used to create a new `RecipeInstance` from a `baker`
|
||||
runtime that contains both a `CompiledRecipe` and the required `InteractionInstances`, or they can as well be converted
|
||||
into a [visualziation](visualization.md).
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
|
||||
val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(recipe)
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(recipe);
|
||||
```
|
||||
|
||||
## baker.addRecipe(compiledRecipe)
|
||||
|
||||
Once `Recipes` have been transformed into `CompiledRecipes` they must be added to a baker runtime. The `baker.addRecipe(compiledRecipe)`
|
||||
API will do so and return an id that you can use to reference the added recipe later on.
|
||||
|
||||
_Note: Before doing this, baker requires you to add all related `InteractionInstances` to the runtime, this is because baker
|
||||
does validation to ensure that every recipe is runnable from the previously added `InteractionInstances`._
|
||||
|
||||
```scala tab="Scala"
|
||||
val recipeId Future[String] = baker.addRecipe(compiledRecipe)
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
CompletableFuture<String> recipeId = baker.addRecipe(compiledRecipe);
|
||||
```
|
||||
|
||||
## baker.getAllRecipes()
|
||||
|
||||
The baker at runtime can give you a map of all the currently available recipes that has been previously added to Baker.
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl.RecipeInformation
|
||||
import scala.concurrent.Future
|
||||
|
||||
val allRecipes: Future[Map[String, RecipeInformation]] = baker.getAllRecipes
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.RecipeInformation;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.Map;
|
||||
|
||||
CompletableFuture<Map<String, RecipeInformation>> allRecipe = baker.getAllRecipes();
|
||||
```
|
||||
|
||||
## baker.bake(recipeId, recipeInstanceId)
|
||||
|
||||
Once the Baker runtime contains a `CompiledRecipe` and all the associated `InteractionInstances` then you can use the
|
||||
`baker.bake(recipeId, recipeInstanceId)` API to create a `RecipeInstance` that will contain the state of your process
|
||||
and execute any `InteractionInstance` as soon as all its required `InteractionIngredients` are available.
|
||||
|
||||
_Note: This API requires you to choose a `recipeInstanceId`, the API does not provide one for you, this is so that
|
||||
you can manage this reference as required._
|
||||
|
||||
_Note: in Java the api returns a `CompletableFuture<BoxedUnit>`, this is because the API is implemented in Scala, so
|
||||
Scala's `Unit` get translated to `BoxedUnit`, but you should you ignore it and consider it as good as Java's `void`,
|
||||
except it comes in a `CompletableFuture` that will help you handle async programming._
|
||||
|
||||
```scala tab="Scala"
|
||||
val program: Future[Unit] = for {
|
||||
_ <- baker.addInteractionInstance(interactionInstances)
|
||||
recipeId <- baker.addRecipe(compiledRecipe)
|
||||
recipeInstanceId = "my-id"
|
||||
_ <- baker.bake(recipeId, recipeInstanceId)
|
||||
} yield ()
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
String recipeInstanceId = "my-id";
|
||||
CompletableFuture<BoxedUnit> result = baker.addInteractionInstace(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId));
|
||||
```
|
||||
|
||||
## EventInstance.from(object)
|
||||
|
||||
As part of our efforts to ease the creation of `EventInstances`, we added a function that uses Java and Scala reflection
|
||||
to create `EventInstances` from class objects.
|
||||
|
||||
The name of the `EventInstance` will be taken from the name of the `class` (Java) or `case class` (Scala)
|
||||
(it must match the name of the `Event` at the `Recipe`). And the argument names and types of the constructors will be
|
||||
translated to `IngredientInstances` with corresponding names and baker types.
|
||||
|
||||
Notice that this function might throw an exception if the event is not correctly done (this is why in Scala the API is
|
||||
named "unsafe").
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl.EventInstance
|
||||
|
||||
case class OrderPlaced(orderId: String, items: List[String])
|
||||
|
||||
val firstOrderPlaced: EventInstance =
|
||||
EventInstance.unsafeFrom(OrderPlaced("order-id", List("item1", "item2")))
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.EventInstance;
|
||||
|
||||
class OrderPlaced {
|
||||
|
||||
String orderId;
|
||||
List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
```
|
||||
|
||||
## baker.fireEvent(recipeInstanceId, eventInstance)
|
||||
|
||||
After creation of a `RecipeInstance`, you use one of the variations of `baker.fireEvent(recipeInstanceId, eventInstance)`
|
||||
to fire your `EventInstances` and start/continue the process flow. There are several supported semantics for firing an
|
||||
event which depend on the moment you want to get notified and continue your asynchronous computation, these are the 4
|
||||
different moments:
|
||||
|
||||
1. When the event got accepted by the `RecipeInstance` but has not started cascading the execution of `InteractionInstances`.
|
||||
For this use the `Baker.fireEventAndResolveWhenReceived(recipeInstanceId, eventInstance)` API. This will return a
|
||||
`Future[SensoryEventStatus]` enum notifying of the outcome (the event might get rejected).
|
||||
|
||||
2. When the event got accepted by the `RecipeInstance` and has finished cascading the execution of `InteractionInstances`
|
||||
up to the point that it requires more `EventInstances` (`SensoryEvents`) to continue, or the process has finished.
|
||||
For this use the `Baker.fireEventAndResolveWhenCompleted(recipeInstanceId, eventInstance)` API. This will return a
|
||||
`Future[EventResult]` object containing a `SensoryEventStatus`, the `Event` names that got fired in consequence of this
|
||||
`SensoryEvent`, and the current available `Ingredients` output of the `InteractionInstances` that got executed as consequence
|
||||
of the `SensoryEvent`.
|
||||
|
||||
3. You want to do something on both of the previously mentioned moments, then use the
|
||||
`Baker.fireEvent(recipeInstanceId, eventInstance)` API, which will return an `EventResolutions` object which contains both
|
||||
`Future[SensoryEventStatus]` and `Future[EventResult]` (or its `CompletableFuture<A>` equivalents in Java).
|
||||
|
||||
4. As soon as an intermediate `Event` fires from one of the `InteractionInstances` that execute as consequence of the fired
|
||||
`SensoryEvent`. For this use the `Baker.fireEventAndResolveOnEvent(recipeInstanceId, eventInstance, onEventName)` API. This will return
|
||||
a similar `Future[EventResult` to the one returned by `Baker.fireEventAndResolveWhenCompleted` except the data will be up
|
||||
to the moment the `onEventName` was fired.
|
||||
|
||||
### correlationId
|
||||
|
||||
Optionally you may provide a `correlation id` when firing a `EventInstance`. The purpose of this identifier is idempotent
|
||||
event delivery: when sending the same event correlation id multiple times, only the first will be processed.
|
||||
|
||||
This can be applied to the `OrderPlaced` event for example.
|
||||
|
||||
``` scala tab="Scala"
|
||||
val correlationOrderId = "a unique order id"
|
||||
|
||||
for {
|
||||
statusA <- baker.processEventAndResolveWhenReceived(recipeInstanceId, orderPlacedEvent, correlationOrderId)
|
||||
_ = assert(statusA == Received)
|
||||
statusB <- baker.processEventAndResolveWhenReceived(recipeInstanceId, orderPlacedEvent, correlationOrderId)
|
||||
_ = assert(statusB == AlreadyReceived)
|
||||
} yield ()
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
String correlationOrderId = "a unique order id";
|
||||
|
||||
SensoryEventStatus statusA = baker
|
||||
.processEventAndResolveWhenReceived(recipeInstanceId, orderPlacedEvent, correlationOrderId);
|
||||
.join();
|
||||
assert(statusA == Received);
|
||||
|
||||
SensoryEventStatus statusB = baker
|
||||
.processEventAndResolveWhenReceived(recipeInstanceId, orderPlacedEvent, correlationOrderId);
|
||||
.join();
|
||||
assert(statusB == AlreadyReceived);
|
||||
|
||||
```
|
||||
|
||||
### SensoryEventStatus
|
||||
|
||||
| Status | Description |
|
||||
| --- | --- |
|
||||
| `Received` | The event was received normally |
|
||||
| `AlreadyReceived` | An event with the same correlation id was already received |
|
||||
| `ProcessDeleted` | The process instance was deleted |
|
||||
| `ReceivePeriodExpired` | The receive period for the process instance has passed |
|
||||
| `FiringLimitMet` | The `firing limit` for the event was met |
|
||||
|
||||
|
||||
## baker.getRecipeInstanceState(recipeInstanceId)
|
||||
|
||||
`baker.getInteractionInstanceState(recipeInstanceId)` will return an `InteractionInstanceState` object which
|
||||
contains all the event names with timestamps that have executed, and the current available provided ingredient data.
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl.RecipeInstanceState
|
||||
|
||||
val state: Future[RecipeInstanceState] = baker.getRecipeInstanceState(recipeInstanceId)
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.RecipeInstanceState;
|
||||
|
||||
CompletableFuture<RecipeInstanceState> state = baker.getRecipeInstanceState(recipeInstanceId);
|
||||
```
|
||||
|
||||
## baker.getAllInteractionInstancesMetadata()
|
||||
|
||||
Returns the recipeId, recipeInstanceId and creation timestamp of all running `RecipeInstances`.
|
||||
|
||||
_Note: Can potentially return a partial result when baker runs in cluster mode because not all shards might be reached within
|
||||
the given timeout._
|
||||
|
||||
_Note: Does not include deleted `RecipeInstances`._
|
||||
|
||||
## baker.getVisualState(recipeInstanceId, style)
|
||||
|
||||
Another method of fetching state is the visual representation of it. You can do that with the `Baker.getVisualState(recipeInstanceId)`
|
||||
API. This will return a GraphViz string like the [visualization api]() that you can convert into an image.
|
||||
|
||||
Here is a visualization of the state of another webshop example, one can clearly see that the process is flowing correctly
|
||||
without failures and that it is still waiting for the payment sensory event to be fired.
|
||||
|
||||
```scala tab="Scala"
|
||||
val state: Future[String] = baker.getVisualState(recipeInstanceId)
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
CompletableFuture<String> state = baker.getVisualState(recipeInstanceId);
|
||||
```
|
||||
|
||||

|
||||
|
||||
## baker.registerEventListener(recipeName, listenerFunction)
|
||||
|
||||
Registers a listener to all runtime events for this baker instance.
|
||||
|
||||
Note that:
|
||||
- The delivery guarantee is *AT MOST ONCE*. Practically this means you can miss events when the application terminates (unexpected or not).
|
||||
- The delivery is local (JVM) only, you will NOT receive events from other nodes when running in cluster mode.
|
||||
|
||||
Because of these constraints you should not use an event listener for critical functionality. Valid use cases might be:
|
||||
- logging
|
||||
- metrics
|
||||
- unit tests
|
||||
|
||||
```scala tab="Scala"
|
||||
baker.registerEventListener((recipeInstanceId: String, event: EventInstance) => {
|
||||
println(s"Recipe instance : $recipeInstanceId processed event ${event.name}")
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
BiConsumer<String, EventInstance> handler = (String recipeInstanceId, EventInstance event) ->
|
||||
System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name());
|
||||
|
||||
baker.registerEventListener(handler);
|
||||
```
|
||||
|
||||
## baker.registerBakerEventListener(listenerFunction)
|
||||
|
||||
Registers a listener to all runtime BAKER events, these are events that notify what Baker is doing, like `RecipeInstances`
|
||||
received `EventInstances` or `CompiledRecipes` being added to baker.
|
||||
|
||||
Note that:
|
||||
- The delivery guarantee is *AT MOST ONCE*. Practically this means you can miss events when the application terminates (unexpected or not).
|
||||
- The delivery is local (JVM) only, you will NOT receive events from other nodes when running in cluster mode.
|
||||
|
||||
Because of these constraints you should not use an event listener for critical functionality. Valid use cases might be:
|
||||
- logging
|
||||
- metrics
|
||||
- unit tests
|
||||
|
||||
```scala tab="Scala"
|
||||
import com.ing.baker.runtime.scaladsl._
|
||||
|
||||
baker.registerBakerEventListener((event: BakerEvent) => {
|
||||
event match {
|
||||
case e: EventReceived => println(e)
|
||||
case e: EventRejected => println(e)
|
||||
case e: InteractionFailed => println(e)
|
||||
case e: InteractionStarted => println(e)
|
||||
case e: InteractionCompleted => println(e)
|
||||
case e: ProcessCreated => println(e)
|
||||
case e: RecipeAdded => println(e)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```java tab="Java"
|
||||
import com.ing.baker.runtime.javadsl.BakerEvent;
|
||||
|
||||
baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event));
|
||||
```
|
||||
|
||||
## baker.retryInteraction(recipeInstanceId, interactionName)
|
||||
|
||||
It is possible that during the execution of a `RecipeInstance` it becomes *blocked*, this can happen either because it
|
||||
is `directly blocked` by an exception (and the `FailureStrategy` of the `Interaction` of the `Recipe` was set to block)
|
||||
or that the retry strategy was exhausted. At this point it is possible to resolve the blocked interaction in 2 ways.
|
||||
This one involves forcing another try, resulting either on a successful continued process, or again on a failed state,
|
||||
to check this you will need to request the state of the `RecipeInstance` again.
|
||||
|
||||
_Note: this behaviour can be automatically preconfigured by using the `RetryWithIncrementalBackoff` `FailureStrategy`
|
||||
on the `Interaction` of the `Recipe`_
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.retryInteraction(recipeInstanceId, "ReserveItems")
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.retryInteraction(recipeInstanceId, "ReserveItems");
|
||||
```
|
||||
|
||||
## baker.resolveInteraction(recipeInstanceId, interactionName, event)
|
||||
|
||||
It is possible that during the execution of a `RecipeInstance` it becomes *blocked*, this can happen either because it
|
||||
is `directly blocked` by an exception or that the retry strategy was exhausted. At this point it is possible to resolve
|
||||
the blocked interaction in two ways. This one involves resolving the interaction with a chosen `EventInstance` to replace
|
||||
the one that would have had been computed by the `InteractionInstance`.
|
||||
|
||||
_Note: this behaviour can be automatically preconfigured by using the `FireEventAfterFailure(eventName)` `FailureStrategy`
|
||||
on the `Interaction` of the `Recipe`_
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.resolveInteraction(recipeInstanceId, "ReserveItems", ItemsReserved(List("item1")))
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.resolveInteraction(recipeInstanceId, "ReserveItems", new ItemsReserved(List("item1")));
|
||||
```
|
||||
|
||||
## baker.stopRetryingInteraction(recipeInstanceId, interactionName)
|
||||
|
||||
If an `Interaction` is configured with a `RetryWithIncrementalBackoff` `FailureStrategy` then it will not stop retrying
|
||||
until you call this API or a successful outcome happens from the `InteractionInstance`.
|
||||
|
||||
``` scala tab="Scala"
|
||||
val program: Future[Unit] =
|
||||
baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems")
|
||||
```
|
||||
|
||||
``` java tab="Java"
|
||||
CompletableFuture<BoxedUnit> program =
|
||||
baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems");
|
||||
```
|
||||
85
docs/sections/reference/stores.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Event Stores and Cluster Configuration
|
||||
|
||||
Baker keeps the state of your `RecipeInstances` using a technique called event sourcing, such technique still requires
|
||||
you to save data into a data store if you want to restore state or move it around. Baker's event sourcing uses
|
||||
[Akka's Persistence](https://doc.akka.io/docs/akka/current/persistence.html), and even though you don't need to know how
|
||||
it works, we recommend understanding the implications of it, specially when it comes to configuring and choosing the underlying
|
||||
data store.
|
||||
|
||||
The two main categories you have is local vs distributed, the former being used mainly for testing, and the latter for
|
||||
production grade clusters, more specifically if you are going to use Baker on cluster mode, you NEED a distributed data store
|
||||
for Baker to work as expected. We recommend the usage of Cassandra, since it is the store the team has tested and used on
|
||||
production.
|
||||
|
||||
## Configuration examples
|
||||
|
||||
`application.conf`
|
||||
|
||||
```config tab="Local Store"
|
||||
include "baker.conf"
|
||||
|
||||
service {
|
||||
|
||||
actorSystemName = "CheckoutService"
|
||||
actorSystemName = ${?ACTOR_SYSTEM_NAME}
|
||||
|
||||
clusterHost = "127.0.0.1"
|
||||
clusterHost = ${?CLUSTER_HOST}
|
||||
|
||||
clusterPort = 2551
|
||||
clusterPort = ${?CLUSTER_PORT}
|
||||
|
||||
seedHost = "127.0.0.1"
|
||||
seedHost = ${?CLUSTER_SEED_HOST}
|
||||
|
||||
seedPort = 2551
|
||||
seedPort = ${?CLUSTER_SEED_PORT}
|
||||
|
||||
}
|
||||
|
||||
baker {
|
||||
actor {
|
||||
provider = "cluster-sharded"
|
||||
}
|
||||
|
||||
cluster {
|
||||
nr-of-shards = 52
|
||||
seed-nodes = [
|
||||
"akka.tcp://"${service.actorSystemName}"@"${service.seedHost}":"${service.seedPort}]
|
||||
}
|
||||
}
|
||||
|
||||
akka {
|
||||
|
||||
actor {
|
||||
provider = "cluster"
|
||||
}
|
||||
|
||||
persistence {
|
||||
journal.plugin = "inmemory-journal"
|
||||
snapshot-store.plugin = "inmemory-snapshot-store"
|
||||
}
|
||||
|
||||
remote {
|
||||
log-remote-lifecycle-events = off
|
||||
netty.tcp {
|
||||
hostname = ${service.clusterHost}
|
||||
port = ${service.clusterPort}
|
||||
}
|
||||
}
|
||||
|
||||
cluster {
|
||||
|
||||
seed-nodes = [
|
||||
"akka.tcp://"${service.actorSystemName}"@"${service.seedHost}":"${service.seedPort}]
|
||||
|
||||
# auto downing is NOT safe for production deployments.
|
||||
# you may want to use it during development, read more about it in the akka docs.
|
||||
auto-down-unreachable-after = 10s
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```config tab="Distributed Store"
|
||||
|
||||
```
|
||||
158
docs/sections/reference/visualization.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Visualization
|
||||
|
||||
A visualization is a visual graph representation of a Recipe and it is built from a compiled recipe.
|
||||
|
||||
You can see an example of the output and of a rendered visualization [here](../../development-life-cycle/use-visualizations).
|
||||
|
||||
``` scala tab="Scala"
|
||||
|
||||
import com.ing.baker.il.CompiledRecipe
|
||||
import com.ing.baker.compiler.RecipeCompiler
|
||||
|
||||
val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe)
|
||||
val visualization: String = compiled.getRecipeVisualization
|
||||
|
||||
```
|
||||
|
||||
``` scala tab="Java"
|
||||
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
|
||||
CompiledRecipe recipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
String visualization = recipe.getRecipeVisualization();
|
||||
|
||||
```
|
||||
|
||||
The aesthetics can be configured by passing a `com.ing.baker.il.RecipeVisualStyle` object to the
|
||||
`recipe.getRecipeVisualization()` method, that object has `scalax.collection.io.dot._` objects that will change how
|
||||
your ingredients, events and interactions are rendered.
|
||||
|
||||
The default configuration is:
|
||||
|
||||
``` scala tab="Scala"
|
||||
|
||||
case class RecipeVisualStyle(
|
||||
|
||||
rootAttributes: List[DotAttr] = List(
|
||||
DotAttr("pad", 0.2)
|
||||
),
|
||||
|
||||
commonNodeAttributes: List[DotAttrStmt] = List(
|
||||
DotAttrStmt(
|
||||
Elem.node,
|
||||
List(
|
||||
DotAttr("fontname", "ING Me"),
|
||||
DotAttr("fontsize", 22),
|
||||
DotAttr("fontcolor", "white")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
ingredientAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "circle"),
|
||||
DotAttr("style", "filled"),
|
||||
DotAttr("color", "\"#FF6200\"")
|
||||
),
|
||||
|
||||
providedIngredientAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "circle"),
|
||||
DotAttr("style", "filled"),
|
||||
DotAttr("color", "\"#3b823a\"")
|
||||
),
|
||||
|
||||
missingIngredientAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "circle"),
|
||||
DotAttr("style", "filled"),
|
||||
DotAttr("color", "\"#EE0000\""),
|
||||
DotAttr("penwidth", "5.0")
|
||||
),
|
||||
|
||||
eventAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "diamond"),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#767676\""),
|
||||
DotAttr("margin", 0.3D)
|
||||
),
|
||||
|
||||
sensoryEventAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "diamond"),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#767676\""),
|
||||
DotAttr("fillcolor", "\"#D5D5D5\""),
|
||||
DotAttr("fontcolor", "black"),
|
||||
DotAttr("penwidth", 2),
|
||||
DotAttr("margin", 0.3D)
|
||||
),
|
||||
|
||||
interactionAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "rect"),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#525199\""),
|
||||
DotAttr("penwidth", 2),
|
||||
DotAttr("margin", 0.5D),
|
||||
),
|
||||
|
||||
eventFiredAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "diamond"),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#3b823a\""),
|
||||
DotAttr("margin", 0.3D)
|
||||
),
|
||||
|
||||
firedInteractionAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "rect"),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#3b823a\""),
|
||||
DotAttr("penwidth", 2),
|
||||
DotAttr("margin", 0.5D),
|
||||
),
|
||||
|
||||
eventMissingAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "diamond"),
|
||||
DotAttr("margin", 0.3D),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("color", "\"#EE0000\""),
|
||||
DotAttr("penwidth", "5.0")
|
||||
),
|
||||
|
||||
choiceAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "point"),
|
||||
DotAttr("fillcolor", "\"#D0D93C\""),
|
||||
DotAttr("width", 0.3),
|
||||
DotAttr("height", 0.3)
|
||||
),
|
||||
|
||||
emptyEventAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "point"),
|
||||
DotAttr("fillcolor", "\"#D0D93C\""),
|
||||
DotAttr("width", 0.1),
|
||||
DotAttr("height", 0.1)
|
||||
),
|
||||
|
||||
preconditionORAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "circle"),
|
||||
DotAttr("fillcolor", "\"#D0D93C\""),
|
||||
DotAttr("fontcolor", "black"),
|
||||
DotAttr("label", "OR"),
|
||||
DotAttr("style", "filled")
|
||||
),
|
||||
|
||||
// this will be removed soon
|
||||
sieveAttributes: List[DotAttr] = List(
|
||||
DotAttr("shape", "rect"),
|
||||
DotAttr("margin", 0.5D),
|
||||
DotAttr("color", "\"#7594d6\""),
|
||||
DotAttr("style", "rounded, filled"),
|
||||
DotAttr("penwidth", 2)
|
||||
)
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
## Recipe Instance State Visualizations
|
||||
|
||||
Another type of visualization that can be done is the `Baker.getVisualState(recipeInstanceId)` API, this will generate the
|
||||
same GraphViz string but of the state of a currently running `ProcessInstance`, referenced by the input recipeInstanceId.
|
||||
|
||||

|
||||
78
docs/sections/versions/baker-2.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Migration Guide v2
|
||||
|
||||
## From 1.3.x to 2.0.0
|
||||
|
||||
This guide only describes how to migrate your existing application.
|
||||
|
||||
Summary:
|
||||
|
||||
- *ALL* persisted data from baker `1.3.x` *IS COMPATIBLE* and can be used with `2.0.x`
|
||||
- When running a cluster *DOWNTIME IS REQUIRED* because of binary incompatible changes in the message protocol.
|
||||
- Some small code refactors are necessary (see below).
|
||||
|
||||
For a full list new features see the [changelog](https://github.com/ing-bank/baker/blob/master/CHANGELOG.md).
|
||||
|
||||
### Downtime required for clusters with state
|
||||
|
||||
In `2.0.0` some binary incompatible changes where made in the message protocol.
|
||||
|
||||
This requires you to bring down the entire cluster (`1.3.x`) and bring it up again (`2.0.0`).
|
||||
|
||||
A rolling deploy *IS NOT* tested and *NOT* recommended.
|
||||
|
||||
### Removed Ingredient interface
|
||||
|
||||
`com.ing.baker.recipe.javadsl.Ingredient` was removed.
|
||||
|
||||
This was a tagging interface that was not used in the project.
|
||||
|
||||
You can remove all references to this interface in your project.
|
||||
|
||||
One thing to note is that `Ingredient` extended from `scala.Serializable`.
|
||||
|
||||
If you depended on this behaviour just replace `Ingredient` by `scala.Serializable`.
|
||||
|
||||
### @ProvidesIngredient removed
|
||||
|
||||
In `1.3.x` you could directly provide an ingredient from an interaction. For example:
|
||||
|
||||
``` java
|
||||
|
||||
import com.ing.baker.recipe.annotations.ProvidesIngredient;
|
||||
|
||||
interface GetEmail {
|
||||
|
||||
@ProvidesIngredient("email")
|
||||
String apply(@RequiresIngredient("customer") Customer customer);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This feature has been removed. Internally this was already translated to an implicitly generated event: `$interactionName + Successful`.
|
||||
|
||||
Now it is required that you do this expclitly to avoid confusion.
|
||||
|
||||
The refactor is very straight forwfard:
|
||||
|
||||
``` java
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
|
||||
interface GetEmail {
|
||||
|
||||
public class GetEmailSuccessful {
|
||||
public final String email;
|
||||
public ExampleInteractionSuccessful(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
|
||||
@FiresEvent(oneOf = { GetEmailSuccessful.class } )
|
||||
GetEmailSuccessful apply(@RequiresIngredient("customer") Customer customer);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you use [lombok](https://projectlombok.org) annotations you can get rid of a lot of the boiler plate by using `@Value` on the event class.
|
||||
|
||||
In `scala` it is recommended to use case classes.
|
||||
|
||||
2
docs/sections/versions/baker-3.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Migration Guide v3
|
||||
|
||||
54
examples/src/main/java/webshop/simple/JMain.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package webshop.simple;
|
||||
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.ActorMaterializer;
|
||||
import akka.stream.Materializer;
|
||||
import com.ing.baker.compiler.RecipeCompiler;
|
||||
import com.ing.baker.il.CompiledRecipe;
|
||||
import com.ing.baker.runtime.javadsl.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JMain {
|
||||
|
||||
static public void main_ignore(String[] args) {
|
||||
|
||||
ActorSystem actorSystem = ActorSystem.create("WebshopSystem");
|
||||
Materializer materializer = ActorMaterializer.create(actorSystem);
|
||||
Baker baker = Baker.akkaLocalDefault(actorSystem, materializer);
|
||||
|
||||
List<String> items = new ArrayList<>(2);
|
||||
items.add("item1");
|
||||
items.add("item2");
|
||||
|
||||
EventInstance firstOrderPlaced =
|
||||
EventInstance.from(new JWebshopRecipe.OrderPlaced("order-uuid", items));
|
||||
EventInstance paymentMade =
|
||||
EventInstance.from(new JWebshopRecipe.PaymentMade());
|
||||
|
||||
InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItemsInstance());
|
||||
CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe);
|
||||
|
||||
BiConsumer<String, EventInstance> handler = (String recipeInstanceId, EventInstance event) ->
|
||||
System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name());
|
||||
baker.registerEventListener(handler);
|
||||
|
||||
baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event));
|
||||
|
||||
String recipeInstanceId = "first-instance-id";
|
||||
CompletableFuture<List<String>> result = baker.addInteractionInstace(reserveItemsInstance)
|
||||
.thenCompose(ignore -> baker.addRecipe(compiledRecipe))
|
||||
.thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced))
|
||||
.thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, paymentMade))
|
||||
.thenCompose(ignore -> baker.getRecipeInstanceState(recipeInstanceId))
|
||||
.thenApply(x -> x.events().stream().map(EventMoment::getName).collect(Collectors.toList()));
|
||||
|
||||
List<String> blockedResult = result.join();
|
||||
assert(blockedResult.contains("OrderPlaced") && blockedResult.contains("PaymentMade") && blockedResult.contains("ReservedItems"));
|
||||
}
|
||||
}
|
||||
69
examples/src/main/java/webshop/simple/JWebshopRecipe.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package webshop.simple;
|
||||
|
||||
import com.ing.baker.recipe.annotations.FiresEvent;
|
||||
import com.ing.baker.recipe.annotations.RequiresIngredient;
|
||||
import com.ing.baker.recipe.javadsl.InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder;
|
||||
import com.ing.baker.recipe.javadsl.Interaction;
|
||||
import com.ing.baker.recipe.javadsl.Recipe;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of;
|
||||
|
||||
public class JWebshopRecipe {
|
||||
|
||||
public static class OrderPlaced {
|
||||
|
||||
public final String orderId;
|
||||
public final List<String> items;
|
||||
|
||||
public OrderPlaced(String orderId, List<String> items) {
|
||||
this.orderId = orderId;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentMade {}
|
||||
|
||||
public interface ReserveItems extends Interaction {
|
||||
|
||||
interface ReserveItemsOutcome {
|
||||
}
|
||||
|
||||
class OrderHadUnavailableItems implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> unavailableItems;
|
||||
|
||||
public OrderHadUnavailableItems(List<String> unavailableItems) {
|
||||
this.unavailableItems = unavailableItems;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsReserved implements ReserveItemsOutcome {
|
||||
|
||||
public final List<String> reservedItems;
|
||||
|
||||
public ItemsReserved(List<String> reservedItems) {
|
||||
this.reservedItems = reservedItems;
|
||||
}
|
||||
}
|
||||
|
||||
@FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class})
|
||||
ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List<String> items);
|
||||
}
|
||||
|
||||
public final static Recipe recipe = new Recipe("WebshopRecipe")
|
||||
.withSensoryEvents(
|
||||
OrderPlaced.class,
|
||||
PaymentMade.class)
|
||||
.withInteractions(
|
||||
of(ReserveItems.class)
|
||||
.withRequiredEvent(PaymentMade.class))
|
||||
.withDefaultFailureStrategy(
|
||||
new RetryWithIncrementalBackoffBuilder()
|
||||
.withInitialDelay(Duration.ofMillis(100))
|
||||
.withDeadline(Duration.ofHours(24))
|
||||
.withMaxTimeBetweenRetries(Duration.ofMinutes(10))
|
||||
.build());
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package webshop.simple;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ReserveItemsInstance implements JWebshopRecipe.ReserveItems {
|
||||
|
||||
@Override
|
||||
public ReserveItemsOutcome apply(String id, List<String> items) {
|
||||
return new ItemsReserved(items);
|
||||
}
|
||||
}
|
||||
84
examples/src/main/resources/application.conf
Normal file
@@ -0,0 +1,84 @@
|
||||
include "baker.conf"
|
||||
|
||||
service {
|
||||
|
||||
actorSystemName = "CheckoutService"
|
||||
actorSystemName = ${?ACTOR_SYSTEM_NAME}
|
||||
|
||||
clusterHost = "127.0.0.1"
|
||||
clusterHost = ${?CLUSTER_HOST}
|
||||
|
||||
clusterPort = 2551
|
||||
clusterPort = ${?CLUSTER_PORT}
|
||||
|
||||
seedHost = "127.0.0.1"
|
||||
seedHost = ${?CLUSTER_SEED_HOST}
|
||||
|
||||
seedPort = 2551
|
||||
seedPort = ${?CLUSTER_SEED_PORT}
|
||||
|
||||
memory-dump-path = "/home/demiourgos728/memdump"
|
||||
memory-dump-path = ${?APP_MEMORY_DUMP_PATH}
|
||||
}
|
||||
|
||||
baker {
|
||||
actor {
|
||||
provider = "cluster-sharded"
|
||||
idle-timeout = 1 minute
|
||||
}
|
||||
|
||||
cluster {
|
||||
nr-of-shards = 52
|
||||
seed-nodes = [
|
||||
"akka.tcp://"${service.actorSystemName}"@"${service.seedHost}":"${service.seedPort}]
|
||||
}
|
||||
}
|
||||
|
||||
akka {
|
||||
|
||||
actor {
|
||||
provider = "cluster"
|
||||
}
|
||||
|
||||
persistence {
|
||||
journal.plugin = "inmemory-journal"
|
||||
snapshot-store.plugin = "inmemory-snapshot-store"
|
||||
}
|
||||
|
||||
remote {
|
||||
log-remote-lifecycle-events = off
|
||||
netty.tcp {
|
||||
hostname = ${service.clusterHost}
|
||||
port = ${service.clusterPort}
|
||||
}
|
||||
}
|
||||
|
||||
cluster {
|
||||
|
||||
seed-nodes = [
|
||||
"akka.tcp://"${service.actorSystemName}"@"${service.seedHost}":"${service.seedPort}]
|
||||
|
||||
# auto downing is NOT safe for production deployments.
|
||||
# you may want to use it during development, read more about it in the docs.
|
||||
#
|
||||
# auto-down-unreachable-after = 10s
|
||||
}
|
||||
}
|
||||
|
||||
kamon.instrumentation.akka.filters {
|
||||
|
||||
actors.track {
|
||||
includes = [ ${service.actorSystemName}"/user/*" ]
|
||||
excludes = []
|
||||
# ${service.actorSystemName}"/system/**", ${service.actorSystemName}"/user/worker-helper"
|
||||
#]
|
||||
}
|
||||
|
||||
dispatchers {
|
||||
includes = [ ${service.actorSystemName}"/akka.actor.default-dispatcher" ]
|
||||
}
|
||||
|
||||
routers {
|
||||
includes = [ ${service.actorSystemName}"/user/*" ]
|
||||
}
|
||||
}
|
||||
71
examples/src/main/resources/docker-compose.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
version: '3'
|
||||
services:
|
||||
node1:
|
||||
image: "checkout-service-baker-example:3.0.0-SNAPSHOT"
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "5261:5266"
|
||||
- "9091:9095"
|
||||
#logging:
|
||||
# driver: none
|
||||
environment:
|
||||
CLUSTER_HOST: node1
|
||||
CLUSTER_PORT: 2551
|
||||
CLUSTER_SEED_HOST: node1
|
||||
CLUSTER_SEED_PORT: 2551
|
||||
node2:
|
||||
image: "checkout-service-baker-example:3.0.0-SNAPSHOT"
|
||||
ports:
|
||||
- "8082:8080"
|
||||
- "5262:5266"
|
||||
- "9092:9095"
|
||||
logging:
|
||||
driver: none
|
||||
environment:
|
||||
CLUSTER_HOST: node2
|
||||
CLUSTER_PORT: 2551
|
||||
CLUSTER_SEED_HOST: node1
|
||||
CLUSTER_SEED_PORT: 2551
|
||||
node3:
|
||||
image: "checkout-service-baker-example:3.0.0-SNAPSHOT"
|
||||
ports:
|
||||
- "8083:8080"
|
||||
- "5263:526"
|
||||
- "9093:9095"
|
||||
logging:
|
||||
driver: none
|
||||
environment:
|
||||
CLUSTER_HOST: node3
|
||||
CLUSTER_PORT: 2551
|
||||
CLUSTER_SEED_HOST: node1
|
||||
CLUSTER_SEED_PORT: 2551
|
||||
prometheus:
|
||||
build: "./prometheus"
|
||||
container_name: "prometheus"
|
||||
ports:
|
||||
- "9090:9090"
|
||||
grafana:
|
||||
build: "./grafana"
|
||||
container_name: "grafana"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_PASSWORD: "admin"
|
||||
logging:
|
||||
driver: none
|
||||
haproxy:
|
||||
build: "./haproxy"
|
||||
container_name: "haproxy"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
#elk:
|
||||
#image: sebp/elk
|
||||
#ports:
|
||||
# Kibana
|
||||
#- "5601:5601"
|
||||
# Elastic Search
|
||||
#- "9200:9200"
|
||||
# Logstash Beats interface
|
||||
#- "5044:5044"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package webshop.webservice
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
import io.gatling.jdbc.Predef._
|
||||
|
||||
class CheckoutFlowSimulation extends Simulation {
|
||||
|
||||
val httpProtocol = http
|
||||
.baseUrl("http://localhost:8080")
|
||||
.inferHtmlResources()
|
||||
.acceptHeader("*/*")
|
||||
.acceptEncodingHeader("gzip, deflate")
|
||||
|
||||
val scn = scenario("CheckoutFlowSimulation")
|
||||
.exec(http("Create Order")
|
||||
.post("/api/order")
|
||||
.header(HttpHeaderNames.ContentType, HttpHeaderValues.ApplicationJson)
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson)
|
||||
.body(StringBody("""{"items": ["item1", "item2"]}"""))
|
||||
.check(jsonPath("$..orderId").ofType[String].saveAs("orderId")))
|
||||
.pause(8)
|
||||
|
||||
.exec(http("Check Status 1")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
.pause(6)
|
||||
|
||||
.exec(http("Add Address")
|
||||
.put("/api/order/${orderId}/address")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson)
|
||||
.body(StringBody("""{"address": "Some Address #16"}""")))
|
||||
.pause(4)
|
||||
|
||||
.exec(http("Check Status 2")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
.pause(16)
|
||||
|
||||
.exec(http("Add Payment Information")
|
||||
.put("/api/order/${orderId}/payment")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson)
|
||||
.body(StringBody("""{"payment": "VISA 0000 0000 0000 0000"}""")))
|
||||
.pause(3)
|
||||
|
||||
.exec(http("Poll Payment Outcome 1")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
.pause(1)
|
||||
|
||||
.exec(http("Poll Payment Outcome 2")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
.pause(1)
|
||||
|
||||
.exec(http("Poll Payment Outcome 3")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
.pause(1)
|
||||
|
||||
.exec(http("Poll Payment Outcome 4")
|
||||
.get("/api/order/${orderId}")
|
||||
.header(HttpHeaderNames.Accept, HttpHeaderValues.ApplicationJson))
|
||||
|
||||
setUp(scn.inject(constantUsersPerSec(1) during (180 minutes))).protocols(httpProtocol)
|
||||
}
|
||||
5
examples/src/main/resources/grafana/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM grafana/grafana
|
||||
|
||||
ADD ./provisioning /etc/grafana/provisioning
|
||||
ADD ./config.ini /etc/grafana/config.ini
|
||||
ADD ./dashboards /var/lib/grafana/dashboards
|
||||
8
examples/src/main/resources/grafana/config.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
[paths]
|
||||
provisioning = /etc/grafana/provisioning
|
||||
|
||||
[server]
|
||||
enable_gzip = true
|
||||
|
||||
[users]
|
||||
default_theme = light
|
||||
2196
examples/src/main/resources/grafana/dashboards/akka_metrics.json
Normal file
@@ -0,0 +1,6 @@
|
||||
- name: 'default'
|
||||
org_id: 1
|
||||
folder: ''
|
||||
type: 'file'
|
||||
options:
|
||||
folder: '/var/lib/grafana/dashboards'
|
||||
@@ -0,0 +1,9 @@
|
||||
datasources:
|
||||
- name: 'Prometheus'
|
||||
type: 'prometheus'
|
||||
access: 'proxy'
|
||||
org_id: 1
|
||||
url: 'http://prometheus:9090'
|
||||
is_default: true
|
||||
version: 1
|
||||
editable: true
|
||||
14
examples/src/main/resources/haproxy/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM haproxy:1.7
|
||||
|
||||
ENV HAPROXY_USER haproxy
|
||||
|
||||
RUN groupadd --system ${HAPROXY_USER} && \
|
||||
useradd --system --gid ${HAPROXY_USER} ${HAPROXY_USER} && \
|
||||
mkdir --parents /var/lib/${HAPROXY_USER} && \
|
||||
chown -R ${HAPROXY_USER}:${HAPROXY_USER} /var/lib/${HAPROXY_USER}
|
||||
|
||||
RUN mkdir /run/haproxy/
|
||||
|
||||
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
|
||||
|
||||
CMD ["haproxy", "-db", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]
|
||||
42
examples/src/main/resources/haproxy/haproxy.cfg
Normal file
@@ -0,0 +1,42 @@
|
||||
global
|
||||
log /dev/log local0
|
||||
log /dev/log local1 notice
|
||||
chroot /var/lib/haproxy
|
||||
stats socket /run/haproxy/admin.sock mode 660 level admin
|
||||
stats timeout 30s
|
||||
user haproxy
|
||||
group haproxy
|
||||
daemon
|
||||
|
||||
# Default SSL material locations
|
||||
ca-base /etc/ssl/certs
|
||||
crt-base /etc/ssl/private
|
||||
|
||||
# Default ciphers to use on SSL-enabled listening sockets.
|
||||
# For more information, see ciphers(1SSL).
|
||||
ssl-default-bind-ciphers kEECDH+aRSA+AES:kRSA+AES:+AES256:RC4-SHA:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
option httplog
|
||||
option dontlognull
|
||||
timeout connect 5000
|
||||
timeout client 50000
|
||||
timeout server 50000
|
||||
|
||||
frontend localnodes
|
||||
bind *:8080
|
||||
mode http
|
||||
default_backend nodes
|
||||
|
||||
backend nodes
|
||||
mode http
|
||||
balance roundrobin
|
||||
option forwardfor
|
||||
http-request set-header X-Forwarded-Port %[dst_port]
|
||||
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||
option httpchk HEAD / HTTP/1.1\r\nHost:localhost
|
||||
server web01 node1:8080 check
|
||||
server web02 node2:8080 check
|
||||
server web03 node3:8080 check
|
||||