From 63f90bfa88cd91b49380ad948566eca1217fc28e Mon Sep 17 00:00:00 2001 From: Tim Linschoten Date: Tue, 19 Mar 2024 16:48:10 +0100 Subject: [PATCH] Release 4.0.0 (#1492) * Add UUIDModule, JavaDateTimeModule and RecordModule to support Java 17. Since UUID fields, Date/DateTime fields are final baker cannot instantiate them in the pojo way. * Remove java.util.Date * Disabled the scala 2.12 compilation. * Disabled the AkkaCassandraJmxMetricsSpec since it does not work with Java 17. * Enabled the getIngredient endpoint for Bakery * Made the java.util.Duration backwards compatible for Java versions. * Disabled incompatible modules/plugins for Scala 2.13. * Added support for the OffSetDateTime, ZonedDateTime & java.util.Date * Set target back to Java 11 to ensure it does not break for java 11. * Added missing function to Kotlin interface. * Improve Kotlin DSL * Added the getJavaBaker method to the kotlinDSL to ensure it can be used for frameworks that need a normal Baker. * Added an optional rememberProcessDuration that forgets a process after the configured duration. This keeps the ProcessIndex shards from growing indefinitely. * Fix to the ProcessDeleted not being handled during Bake. * Made the lastupdate be set correctly after the RecipeInstance was used. (#1536) * Reprovider functionality * Fixed the cleanup for non Cassandra datastores. We use the original cleanup of the data as before our upgrade to the Cassandra cleanup tool for other datastores. (#1551) * Wrote a type modules for the java.util.Currency. * Added a section on Recipe Retention period to the docs (#1575) * Remove generics for Place and Transition from PetriNet and IL (#1582) * Fixed output ingredient rename example/doc for Java (#1574) * Fixed output ingredient rename example/doc for Java * fixed forgotten import * Added some missing imports (#1583) * Fixed the Wait interaction to work if you also have a retry strategy that fires and event. (#1585) * Fixed a bug in the getOutputEventName where it would not work if the FailureStrategy would also throw an event. * Added RecipeId and RecipeName to log lines (#1589) * Set the default retention period to 14 days for deletion if the recipe is not found. * Changed the behaviour of the FireEvent retry strategy. It now blocks the interaction instead of resolving it. This allows the interaction to be retried with the retyInteraction method. * RecipeInstanceMetadata and RecipeInstanceEventList as ingredient input for interactions (#1643) * Added extra logging if a recipe is not found during deletion of a process. * Set the default retention period to 14 days for deletion if the recipe is not found. * Changed the behaviour of the FireEvent retry strategy. It now blocks the interaction instead of resolving it. This allows the interaction to be retried with the retyInteraction method. * Added the RecipeInstanceMetadata as input ingredient. * Added a new Baker Event to represent internal interaction output events, checkpoint events and delayed transition events. * Added the RecipeInstanceEventList as a input ingredient. * Made the RecipeInstanceMetaData an internal concept instead of putting it in the ingredients and needing to transform it every time. * Added validations that the Baker internal ingredients are not provided in the recipe. * Filter out the CheckPoint event interactions from the visualisation --------- Co-authored-by: yl02pf Co-authored-by: Wim Leuning <131662987+wimleuning@users.noreply.github.com> Co-authored-by: Bas Co-authored-by: Willem Veelenturf Co-authored-by: Bas de Groot Co-authored-by: Elmar Wachtmeester Co-authored-by: Elmar Wachtmeester --- .github/workflows/security.yml | 4 +- README.md | 4 +- azure-build.yaml | 2 +- .../scala/com/ing/bakery/AkkaBakery.scala | 5 +- .../components/AkkaBakeryComponents.scala | 4 +- .../AkkaCassandraJmxMetricsSpec.scala | 8 +- build.sbt | 90 +-- .../main/protobuf/delayed_transition.proto | 6 +- .../src/main/protobuf/process_instance.proto | 13 + .../src/main/resources/reference.conf | 6 +- .../ing/baker/runtime/akka/AkkaBaker.scala | 40 +- .../baker/runtime/akka/AkkaBakerConfig.scala | 12 +- .../akka/actor/BakerActorCleanup.scala | 58 ++ .../actor/ClusterBakerActorProvider.scala | 7 +- .../akka/actor/LocalBakerActorProvider.scala | 6 +- .../DelayedTransitionActor.scala | 113 ++- .../DelayedTransitionActorProtocol.scala | 2 + .../DelayedTranstionProto.scala | 26 +- .../akka/actor/logging/LogAndSendEvent.scala | 10 +- .../actor/process_index/ProcessIndex.scala | 128 ++-- .../process_index/ProcessIndexProto.scala | 3 +- .../process_instance/ProcessInstance.scala | 193 +++-- .../ProcessInstanceEventSourcing.scala | 121 ++-- .../ProcessInstanceLogger.scala | 25 +- .../ProcessInstanceProtocol.scala | 9 +- .../ProcessInstanceRuntime.scala | 38 +- .../ProcessInstanceSerialization.scala | 55 +- .../internal/ExceptionStrategy.scala | 2 +- .../process_instance/internal/Instance.scala | 19 +- .../actor/process_instance/internal/Job.scala | 17 +- .../BakerTypedProtobufSerializer.scala | 8 +- .../runtime/akka/internal/RecipeRuntime.scala | 116 +-- .../recipe_manager/DefaultRecipeManager.scala | 2 +- .../com/ing/baker/BakerRuntimeTestBase.scala | 4 + .../baker/runtime/akka/BakerEventsSpec.scala | 39 +- .../runtime/akka/BakerExecutionSpec.scala | 123 +++- .../baker/runtime/akka/BakerSetupSpec.scala | 5 + .../runtime/akka/actor/AkkaTestBase.scala | 5 + .../DelayedTransitionActorSpec.scala | 88 ++- .../process_index/ProcessIndexSpec.scala | 83 ++- .../ProcessInstanceEventSourcingSpec.scala | 10 +- .../ProcessInstanceSpec.scala | 328 ++++++++- .../actor/process_instance/dsl/Place.scala | 19 - .../process_instance/dsl/SequenceNet.scala | 6 +- .../dsl/StateTransition.scala | 11 +- .../dsl/StateTransitionNet.scala | 13 +- .../process_instance/dsl/TestUtils.scala | 27 + .../process_instance/dsl/Transition.scala | 19 - .../actor/process_instance/dsl/package.scala | 7 +- .../serialization/SerializationSpec.scala | 4 +- .../akka/internal/RecipeRuntimeSpec.scala | 4 + .../annotations/RecipeInstanceEventList.java | 15 + .../annotations/RecipeInstanceMetadata.java | 15 + .../com/ing/baker/runtime/kotlindsl/Baker.kt | 7 + .../com/ing/baker/runtime/kotlindsl/Config.kt | 38 + .../baker/runtime/kotlindsl/InMemoryBaker.kt | 8 +- .../runtime/kotlindsl/RecipeInstanceConfig.kt | 23 + .../src/main/protobuf/common.proto | 11 + .../com/ing/baker/runtime/common/Baker.scala | 16 +- .../ing/baker/runtime/common/BakerEvent.scala | 15 +- .../runtime/common/RecipeInstanceState.scala | 5 +- .../runtime/inmemory/InMemoryBaker.scala | 7 +- .../InMemoryRecipeInstanceManager.scala | 35 +- .../com/ing/baker/runtime/javadsl/Baker.scala | 14 +- .../baker/runtime/javadsl/BakerConfig.scala | 82 +++ .../baker/runtime/javadsl/BakerEvent.scala | 28 + .../javadsl/RecipeInstanceConfig.scala | 24 + .../runtime/javadsl/RecipeInstanceState.scala | 10 +- .../com/ing/baker/runtime/model/BakerF.scala | 34 +- .../baker/runtime/model/BakerLogging.scala | 25 +- .../runtime/model/InteractionManager.scala | 2 +- .../runtime/model/RecipeInstanceManager.scala | 57 +- .../baker/runtime/model/RecipeManager.scala | 6 +- .../model/recipeinstance/RecipeInstance.scala | 15 +- .../RecipeInstanceEventValidation.scala | 5 +- .../recipeinstance/RecipeInstanceState.scala | 59 +- .../recipeinstance/TransitionExecution.scala | 60 +- .../baker/runtime/scaladsl/BakerEvent.scala | 19 + .../scaladsl/RecipeInstanceState.scala | 3 +- .../protomappings/BakerEventMapping.scala | 32 + .../protomappings/CompiledRecipeMapping.scala | 9 +- .../protomappings/ProcessStateMapping.scala | 5 +- .../inmemory/InMemoryMemoryCleanupSpec.scala | 179 ++++- .../runtime/model/BakerModelFixtures.scala | 4 + ...akerModelSpecExecutionSemanticsTests.scala | 57 +- .../ing/baker/runtime/model/TestRecipe.scala | 24 + .../src/main/resources/reference.conf | 13 +- .../scala/com/ing/baker/types/Value.scala | 11 + .../ing/baker/types/modules/ClassModule.scala | 6 +- .../baker/types/modules/CurrencyModule.scala | 46 ++ .../baker/types/modules/DurationModule.scala | 43 ++ .../ing/baker/types/modules/JavaModules.scala | 2 +- .../baker/types/modules/JavaTimeModule.scala | 50 ++ .../ing/baker/types/modules/PojoModule.scala | 4 +- .../baker/types/modules/PrimitiveModule.scala | 9 +- .../baker/types/modules/RecordModule.scala | 65 ++ .../baker/types/modules/ScalaModules.scala | 23 +- .../ing/baker/types/modules/UUIDModule.scala | 38 + .../scala/com/ing/baker/types/package.scala | 25 +- .../types/modules/CurrencyModulesSpec.scala | 30 + .../types/modules/DurationModuleSpec.scala | 50 ++ .../types/modules/JavaTimeModuleSpec.scala | 80 +++ .../types/modules/PrimitiveModuleSpec.scala | 4 +- .../com/ing/baker/il/RecipeValidations.scala | 38 +- .../com/ing/baker/il/RecipeVisualizer.scala | 15 +- .../main/scala/com/ing/baker/il/package.scala | 2 + .../il/petrinet/InteractionTransition.scala | 18 +- .../com/ing/baker/il/petrinet/package.scala | 2 +- .../com/ing/baker/petrinet/api/Marking.scala | 2 +- .../ing/baker/petrinet/api/MarkingOps.scala | 20 +- .../com/ing/baker/petrinet/api/MultiSet.scala | 6 +- .../ing/baker/petrinet/api/MultiSetOps.scala | 20 +- .../com/ing/baker/petrinet/api/PetriNet.scala | 28 +- .../baker/petrinet/api/PetriNetAnalysis.scala | 40 +- .../com/ing/baker/petrinet/api/package.scala | 37 +- .../petrinet/api/PetriNetAnalysisSpec.scala | 33 +- .../com/ing/baker/compiler/Assertions.scala | 8 +- .../ing/baker/compiler/RecipeCompiler.scala | 40 +- .../com/ing/baker/compiler/package.scala | 11 +- .../baker/compiler/RecipeCompilerSpec.scala | 59 ++ .../ing/baker/recipe/kotlindsl/KotlinDsl.kt | 45 +- .../baker/recipe/kotlindsl/KotlinDslTest.kt | 66 +- .../recipe/common/InteractionDescriptor.scala | 5 + .../com/ing/baker/recipe/common/package.scala | 2 + .../javadsl/InteractionDescriptor.scala | 11 + .../recipe/javadsl/ReflectionHelpers.scala | 7 +- .../ing/baker/recipe/javadsl/package.scala | 10 +- .../baker/recipe/kotlindsl/Interaction.scala | 16 +- .../baker/recipe/scaladsl/Interaction.scala | 15 +- .../ing/baker/recipe/scaladsl/package.scala | 2 + .../com/ing/baker/recipe/TestRecipeJava.java | 26 +- .../com/ing/baker/recipe/TestRecipe.scala | 24 + docs/images/deps.svg | 49 -- docs/images/module-dependencies.svg | 103 +++ docs/images/web-shop-visual-state.svg | 187 +++++ docs/images/web-shop-visualization.svg | 187 +++++ docs/images/webshop-example-1.svg | 134 ---- docs/images/webshop-state-2.svg | 205 ------ docs/index.md | 92 +-- docs/sections/concepts.md | 153 +--- docs/sections/cookbook/error-handling.md | 232 ++++++ docs/sections/cookbook/fire-sensory-events.md | 136 ++++ .../cookbook/interaction-execution.md | 44 ++ docs/sections/cookbook/monitoring.md | 54 ++ docs/sections/cookbook/recipe-dsl.md | 289 ++++++++ docs/sections/cookbook/testing.md | 303 ++++++++ docs/sections/cookbook/visualizations.md | 89 +++ .../bake-fire-events-and-inquiry.md | 281 -------- .../development-life-cycle/configure.md | 95 --- .../development-life-cycle/design-a-recipe.md | 305 -------- .../implement-interactions.md | 67 -- docs/sections/development-life-cycle/index.md | 13 - .../development-life-cycle/introduction.md | 18 - .../development-life-cycle/monitor.md | 50 -- .../resolve-failed-recipe-instances.md | 181 ----- docs/sections/development-life-cycle/test.md | 547 -------------- .../use-visualizations.md | 64 -- docs/sections/getting-started.md | 61 -- docs/sections/quickstart-guide.md | 105 +++ docs/sections/reference/dsls.md | 671 ------------------ docs/sections/reference/event-listener.md | 76 -- docs/sections/reference/runtime.md | 18 +- docs/sections/reference/visualization.md | 164 ----- docs/sections/tutorial.md | 285 ++++++++ .../examples/java/application/FireEvent.java | 22 + .../FireEventAndResolveOnEvent.java | 19 + .../FireEventAndResolveWhenCompleted.java | 19 + .../FireEventAndResolveWhenReceived.java | 19 + .../java/application/InquiryExample.java | 24 + .../application/ManualResolveInteraction.java | 15 + .../application/ManualRetryInteraction.java | 10 + .../application/ManualStopInteraction.java | 11 + .../RegisterBakerEventListener.java | 19 + .../application/RegisterEventListener.java | 19 + .../examples/java/application/WebShopApp.java | 36 + .../java/events/FraudCheckCompleted.java | 4 + .../examples/java/events/OrderPlaced.java | 12 + .../examples/java/events/PaymentReceived.java | 10 + .../examples/java/ingredients/Address.java | 8 + .../java/interactions/CancelOrder.java | 18 + .../java/interactions/CancelOrderImpl.java | 11 + .../java/interactions/CheckStock.java | 24 + .../java/interactions/CheckStockImpl.java | 18 + .../examples/java/interactions/ShipOrder.java | 17 + .../java/interactions/ShipOrderImpl.java | 11 + .../recipes/InteractionWithCustomName.java | 13 + .../java/recipes/RecipeBlockInteraction.java | 12 + .../java/recipes/RecipeFireEvent.java | 12 + .../recipes/RecipeRetryExhaustedEvent.java | 20 + .../RecipeRetryWithBackOffUntilDeadline.java | 19 + ...RecipeRetryWithBackOffUntilMaxRetries.java | 19 + .../recipes/RecipeWithCheckpointEvent.java | 18 + .../RecipeWithDefaultFailureStrategy.java | 12 + .../recipes/RecipeWithEventReceivePeriod.java | 11 + .../RecipeWithEventTransformation.java | 20 + .../recipes/RecipeWithFailureStrategy.java | 17 + .../RecipeWithIngredientNameOverrides.java | 14 + .../RecipeWithMaxInteractionCount.java | 14 + .../RecipeWithPredefinedIngredients.java | 22 + .../RecipeWithReproviderInteraction.java | 17 + .../recipes/RecipeWithRequiredEvents.java | 16 + .../recipes/RecipeWithRetentionPeriod.java | 11 + .../java/recipes/RecipeWithSensoryEvents.java | 14 + .../examples/java/recipes/SimpleRecipe.java | 15 + .../examples/java/recipes/WebShopRecipe.java | 21 + .../visualization/WebShopVisualization.java | 13 + .../examples/kotlin/application/FireEvent.kt | 16 + .../application/FireEventAndResolveOnEvent.kt | 13 + .../FireEventAndResolveWhenCompleted.kt | 13 + .../FireEventAndResolveWhenReceived.kt | 13 + .../kotlin/application/InquiryExample.kt | 18 + .../application/ManualResolveInteraction.kt | 12 + .../application/ManualRetryInteraction.kt | 7 + .../application/ManualStopInteraction.kt | 7 + .../application/RegisterBakerEventListener.kt | 12 + .../application/RegisterEventListener.kt | 12 + .../examples/kotlin/application/WebShopApp.kt | 44 ++ .../kotlin/events/FraudCheckCompleted.kt | 3 + .../examples/kotlin/events/OrderPlaced.kt | 10 + .../examples/kotlin/events/PaymentReceived.kt | 9 + .../examples/kotlin/ingredients/Address.kt | 8 + .../kotlin/interactions/CancelOrder.kt | 9 + .../kotlin/interactions/CancelOrderImpl.kt | 8 + .../kotlin/interactions/CheckStock.kt | 16 + .../kotlin/interactions/CheckStockImpl.kt | 13 + .../examples/kotlin/interactions/ShipOrder.kt | 10 + .../kotlin/interactions/ShipOrderImpl.kt | 10 + .../recipes/InteractionWithCustomName.kt | 14 + .../kotlin/recipes/RecipeBlockInteraction.kt | 11 + .../kotlin/recipes/RecipeFireEvent.kt | 11 + .../recipes/RecipeRetryExhaustedEvent.kt | 20 + .../RecipeRetryWithBackOffUntilDeadline.kt | 19 + .../RecipeRetryWithBackOffUntilMaxRetries.kt | 18 + .../recipes/RecipeWithCheckpointEvent.kt | 18 + .../RecipeWithDefaultFailureStrategy.kt | 12 + .../recipes/RecipeWithEventReceivePeriod.kt | 12 + .../recipes/RecipeWithEventTransformation.kt | 18 + .../recipes/RecipeWithFailureStrategy.kt | 14 + .../RecipeWithIngredientNameOverrides.kt | 16 + .../recipes/RecipeWithMaxInteractionCount.kt | 14 + .../RecipeWithPredefinedIngredients.kt | 17 + .../RecipeWithReproviderInteraction.kt | 20 + .../recipes/RecipeWithRequiredEvents.kt | 22 + .../recipes/RecipeWithRetentionPeriod.kt | 13 + .../kotlin/recipes/RecipeWithSensoryEvents.kt | 18 + .../examples/kotlin/recipes/SimpleRecipe.kt | 16 + .../examples/kotlin/recipes/WebShopRecipe.kt | 29 + .../visualization/WebShopVisualization.kt | 12 + .../scala/application/FireEvent.scala | 15 + .../FireEventAndResolveOnEvent.scala | 12 + .../FireEventAndResolveWhenCompleted.scala | 12 + .../FireEventAndResolveWhenReceived.scala | 12 + .../scala/application/InquiryExample.scala | 18 + .../ManualResolveInteraction.scala | 14 + .../application/ManualRetryInteraction.scala | 9 + .../application/ManualStopInteraction.scala | 9 + .../RegisterBakerEventListener.scala | 12 + .../application/RegisterEventListener.scala | 12 + .../scala/application/WebShopApp.scala | 53 ++ .../scala/events/FraudCheckCompleted.scala | 11 + .../examples/scala/events/OrderPlaced.scala | 10 + .../scala/events/PaymentReceived.scala | 16 + .../examples/scala/ingredients/Address.scala | 8 + .../scala/interactions/CancelOrder.scala | 20 + .../scala/interactions/CancelOrderImpl.scala | 14 + .../scala/interactions/CheckStock.scala | 23 + .../scala/interactions/CheckStockImpl.scala | 21 + .../scala/interactions/ShipOrder.scala | 20 + .../scala/interactions/ShipOrderImpl.scala | 16 + .../recipes/InteractionWithCustomName.scala | 12 + .../recipes/RecipeBlockInteraction.scala | 11 + .../scala/recipes/RecipeFireEvent.scala | 11 + .../recipes/RecipeRetryExhaustedEvent.scala | 21 + .../RecipeRetryWithBackOffUntilDeadline.scala | 20 + ...ecipeRetryWithBackOffUntilMaxRetries.scala | 20 + .../recipes/RecipeWithCheckpointEvent.scala | 18 + .../RecipeWithDefaultFailureStrategy.scala | 11 + .../RecipeWithEventReceivePeriod.scala | 10 + .../RecipeWithEventTransformation.scala | 20 + .../recipes/RecipeWithFailureStrategy.scala | 15 + .../RecipeWithIngredientNameOverrides.scala | 12 + .../RecipeWithMaxInteractionCount.scala | 12 + .../RecipeWithPredefinedIngredients.scala | 15 + .../RecipeWithReproviderInteraction.scala | 15 + .../recipes/RecipeWithRequiredEvents.scala | 19 + .../recipes/RecipeWithRetentionPeriod.scala | 10 + .../recipes/RecipeWithSensoryEvents.scala | 13 + .../examples/scala/recipes/SimpleRecipe.scala | 11 + .../scala/recipes/WebShopRecipe.scala | 23 + .../visualization/WebShopVisualization.scala | 12 + .../java/recipes/WebShopRecipeTest.java | 18 + .../kotlin/recipes/WebShopRecipeTest.kt | 16 + .../scala/recipes/WebShopRecipeTest.scala | 12 + .../http/client/scaladsl/BakerClient.scala | 16 +- http/baker-http-dashboard/package.json | 2 +- .../src/app/app-routing.module.ts | 20 - .../src/app/app.component.html | 8 +- .../src/app/app.component.ts | 1 - .../src/app/app.settings.ts | 36 +- .../src/app/bakery.service.ts | 2 +- .../src/app/instances/instances.component.ts | 2 +- .../src/app/recipes/recipes.component.ts | 2 +- .../javadsl/BakerWithHttpResponse.scala | 2 +- .../server/scaladsl/Http4sBakerServer.scala | 2 +- mkdocs.yml | 31 +- ...BuildInteractionDockerImageSBTPlugin.scala | 205 ------ project/Dependencies.scala | 65 +- project/plugins.sbt | 2 +- version.sbt | 2 +- 309 files changed, 7308 insertions(+), 4327 deletions(-) create mode 100644 core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/BakerActorCleanup.scala delete mode 100644 core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Place.scala create mode 100644 core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/TestUtils.scala delete mode 100644 core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Transition.scala create mode 100644 core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceEventList.java create mode 100644 core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceMetadata.java create mode 100644 core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Config.kt create mode 100644 core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/RecipeInstanceConfig.kt create mode 100644 core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerConfig.scala create mode 100644 core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceConfig.scala create mode 100644 core/baker-types/src/main/scala/com/ing/baker/types/modules/CurrencyModule.scala create mode 100644 core/baker-types/src/main/scala/com/ing/baker/types/modules/DurationModule.scala create mode 100644 core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaTimeModule.scala create mode 100644 core/baker-types/src/main/scala/com/ing/baker/types/modules/RecordModule.scala create mode 100644 core/baker-types/src/main/scala/com/ing/baker/types/modules/UUIDModule.scala create mode 100644 core/baker-types/src/test/scala/com/ing/baker/types/modules/CurrencyModulesSpec.scala create mode 100644 core/baker-types/src/test/scala/com/ing/baker/types/modules/DurationModuleSpec.scala create mode 100644 core/baker-types/src/test/scala/com/ing/baker/types/modules/JavaTimeModuleSpec.scala delete mode 100644 docs/images/deps.svg create mode 100644 docs/images/module-dependencies.svg create mode 100644 docs/images/web-shop-visual-state.svg create mode 100644 docs/images/web-shop-visualization.svg delete mode 100644 docs/images/webshop-example-1.svg delete mode 100644 docs/images/webshop-state-2.svg create mode 100644 docs/sections/cookbook/error-handling.md create mode 100644 docs/sections/cookbook/fire-sensory-events.md create mode 100644 docs/sections/cookbook/interaction-execution.md create mode 100644 docs/sections/cookbook/monitoring.md create mode 100644 docs/sections/cookbook/recipe-dsl.md create mode 100644 docs/sections/cookbook/testing.md create mode 100644 docs/sections/cookbook/visualizations.md delete mode 100644 docs/sections/development-life-cycle/bake-fire-events-and-inquiry.md delete mode 100644 docs/sections/development-life-cycle/configure.md delete mode 100644 docs/sections/development-life-cycle/design-a-recipe.md delete mode 100644 docs/sections/development-life-cycle/implement-interactions.md delete mode 100644 docs/sections/development-life-cycle/index.md delete mode 100644 docs/sections/development-life-cycle/introduction.md delete mode 100644 docs/sections/development-life-cycle/monitor.md delete mode 100644 docs/sections/development-life-cycle/resolve-failed-recipe-instances.md delete mode 100644 docs/sections/development-life-cycle/test.md delete mode 100644 docs/sections/development-life-cycle/use-visualizations.md delete mode 100644 docs/sections/getting-started.md create mode 100644 docs/sections/quickstart-guide.md delete mode 100644 docs/sections/reference/dsls.md delete mode 100644 docs/sections/reference/event-listener.md delete mode 100644 docs/sections/reference/visualization.md create mode 100644 docs/sections/tutorial.md create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/FireEvent.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveOnEvent.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenCompleted.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenReceived.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/InquiryExample.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/ManualResolveInteraction.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/ManualRetryInteraction.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/ManualStopInteraction.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/RegisterBakerEventListener.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/RegisterEventListener.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/application/WebShopApp.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/events/FraudCheckCompleted.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/events/OrderPlaced.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/events/PaymentReceived.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/ingredients/Address.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrder.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrderImpl.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStock.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStockImpl.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrder.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrderImpl.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/InteractionWithCustomName.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeBlockInteraction.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeFireEvent.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryExhaustedEvent.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilDeadline.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilMaxRetries.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithCheckpointEvent.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithDefaultFailureStrategy.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventReceivePeriod.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventTransformation.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithFailureStrategy.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithIngredientNameOverrides.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithMaxInteractionCount.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithPredefinedIngredients.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithReproviderInteraction.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRequiredEvents.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRetentionPeriod.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithSensoryEvents.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/SimpleRecipe.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/recipes/WebShopRecipe.java create mode 100644 examples/docs-code-snippets/src/main/java/examples/java/visualization/WebShopVisualization.java create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEvent.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveOnEvent.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenCompleted.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenReceived.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/InquiryExample.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualResolveInteraction.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualRetryInteraction.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualStopInteraction.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterBakerEventListener.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterEventListener.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/WebShopApp.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/FraudCheckCompleted.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/OrderPlaced.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/PaymentReceived.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/ingredients/Address.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrder.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrderImpl.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStock.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStockImpl.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrder.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrderImpl.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/InteractionWithCustomName.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeBlockInteraction.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeFireEvent.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryExhaustedEvent.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilDeadline.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilMaxRetries.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithCheckpointEvent.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithDefaultFailureStrategy.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventReceivePeriod.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventTransformation.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithFailureStrategy.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithIngredientNameOverrides.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithMaxInteractionCount.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithPredefinedIngredients.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithReproviderInteraction.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRequiredEvents.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRetentionPeriod.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithSensoryEvents.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/SimpleRecipe.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/WebShopRecipe.kt create mode 100644 examples/docs-code-snippets/src/main/kotlin/examples/kotlin/visualization/WebShopVisualization.kt create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEvent.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveOnEvent.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenCompleted.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenReceived.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/InquiryExample.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualResolveInteraction.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualRetryInteraction.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualStopInteraction.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterBakerEventListener.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterEventListener.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/application/WebShopApp.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/events/FraudCheckCompleted.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/events/OrderPlaced.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/events/PaymentReceived.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/ingredients/Address.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrder.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrderImpl.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStock.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStockImpl.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrder.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrderImpl.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/InteractionWithCustomName.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeBlockInteraction.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeFireEvent.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryExhaustedEvent.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilDeadline.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilMaxRetries.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithCheckpointEvent.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithDefaultFailureStrategy.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventReceivePeriod.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventTransformation.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithFailureStrategy.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithIngredientNameOverrides.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithMaxInteractionCount.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithPredefinedIngredients.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithReproviderInteraction.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRequiredEvents.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRetentionPeriod.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithSensoryEvents.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/SimpleRecipe.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/recipes/WebShopRecipe.scala create mode 100644 examples/docs-code-snippets/src/main/scala/examples/scala/visualization/WebShopVisualization.scala create mode 100644 examples/docs-code-snippets/src/test/java/examples/java/recipes/WebShopRecipeTest.java create mode 100644 examples/docs-code-snippets/src/test/kotlin/examples/kotlin/recipes/WebShopRecipeTest.kt create mode 100644 examples/docs-code-snippets/src/test/scala/examples/scala/recipes/WebShopRecipeTest.scala delete mode 100644 project/BuildInteractionDockerImageSBTPlugin.scala diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3c68aa8d1..8982011a9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,11 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: distribution: adopt - java-version: 11 + java-version: 17 - name: Dependency check run: sbt test:compile updateClassifiers diff --git a/README.md b/README.md index 343dcc413..47fa1cde6 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Use the list below to learn more about Baker: - [Documentation](https://ing-bank.github.io/baker/) - [Concepts](https://ing-bank.github.io/baker/sections/concepts/): high level concepts and terminology - - [Recipe DSL](https://ing-bank.github.io/baker/sections/reference/dsls/): how to use the recipe DSL - - [Visualization](https://ing-bank.github.io/baker/sections/reference/visualization/): how to visualize a recipe + - [Recipe DSL](https://ing-bank.github.io/baker/sections/cookbook/recipe-dsl/): how to use the recipe DSL + - [Visualization](https://ing-bank.github.io/baker/sections/cookbook/visualizations/): how to visualize a recipe - [Baker Talk @ J-Fall 2021](https://www.youtube.com/watch?v=U4aCUT9zIFk): API orchestration taken to the next level ## A bird's-eye view of Baker diff --git a/azure-build.yaml b/azure-build.yaml index 631f1a159..953912275 100644 --- a/azure-build.yaml +++ b/azure-build.yaml @@ -67,7 +67,7 @@ steps: - task: JavaToolInstaller@0 inputs: - versionSpec: '11' + versionSpec: '17' jdkArchitectureOption: 'x64' jdkSourceOption: 'PreInstalled' cleanDestinationDirectory: false diff --git a/bakery/state/src/main/scala/com/ing/bakery/AkkaBakery.scala b/bakery/state/src/main/scala/com/ing/bakery/AkkaBakery.scala index f86251790..afc50714b 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/AkkaBakery.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/AkkaBakery.scala @@ -9,6 +9,7 @@ import com.ing.baker.runtime.model.InteractionManager import com.ing.baker.runtime.recipe_manager.RecipeManager import com.ing.baker.runtime.scaladsl.Baker import com.ing.bakery.components.AkkaBakeryComponents +import com.ing.bakery.metrics.MetricService import com.typesafe.config.Config import com.typesafe.scalalogging.LazyLogging import io.prometheus.client.CollectorRegistry @@ -78,10 +79,10 @@ object Bakery { externalContext: Option[Any] = None, interactionManager: Option[InteractionManager[IO]] = None, recipeManager: Option[RecipeManager] = None, - registry: CollectorRegistry = CollectorRegistry.defaultRegistry): Resource[IO, AkkaBakery] = { + metricService: MetricService = new MetricService(CollectorRegistry.defaultRegistry)): Resource[IO, AkkaBakery] = { val akkaBakeryComponents: AkkaBakeryComponents = - new AkkaBakeryComponents(optionalConfig, externalContext, registry) { + new AkkaBakeryComponents(optionalConfig, externalContext, metricService) { override def interactionManagerResource(config: Config, actorSystem: ActorSystem, diff --git a/bakery/state/src/main/scala/com/ing/bakery/components/AkkaBakeryComponents.scala b/bakery/state/src/main/scala/com/ing/bakery/components/AkkaBakeryComponents.scala index 07b0953d4..ec4b39948 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/components/AkkaBakeryComponents.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/components/AkkaBakeryComponents.scala @@ -25,11 +25,9 @@ import scala.concurrent.ExecutionContext */ class AkkaBakeryComponents(optionalConfig: Option[Config] = None, externalContext: Option[Any] = None, - registry: CollectorRegistry = CollectorRegistry.defaultRegistry + metricService: MetricService = new MetricService(CollectorRegistry.defaultRegistry) ) extends LazyLogging { - val metricService: MetricService = new MetricService(registry) - def configResource: Resource[IO, Config] = Resource.eval(IO { val configPath = sys.env.getOrElse("CONFIG_DIRECTORY", "/opt/docker/conf") val config = optionalConfig.getOrElse(ConfigFactory.load(ConfigFactory.parseFile(new File(s"$configPath/application.conf")))) diff --git a/bakery/state/src/test/scala/com/ing/bakery/components/AkkaCassandraJmxMetricsSpec.scala b/bakery/state/src/test/scala/com/ing/bakery/components/AkkaCassandraJmxMetricsSpec.scala index dbab2236f..26565c4cd 100644 --- a/bakery/state/src/test/scala/com/ing/bakery/components/AkkaCassandraJmxMetricsSpec.scala +++ b/bakery/state/src/test/scala/com/ing/bakery/components/AkkaCassandraJmxMetricsSpec.scala @@ -13,7 +13,7 @@ import com.typesafe.scalalogging.LazyLogging import io.prometheus.client.CollectorRegistry import io.prometheus.client.exporter.common.TextFormat import org.cassandraunit.utils.EmbeddedCassandraServerHelper._ -import org.scalatest.BeforeAndAfterAll +import org.scalatest.{BeforeAndAfterAll, Ignore} import org.scalatest.concurrent.Eventually import org.scalatest.freespec.AnyFreeSpec import org.scalatest.time.{Millis, Seconds, Span} @@ -28,7 +28,13 @@ import scala.collection.convert.ImplicitConversions._ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} + +/** + * Test is ignored since CassandraUnit is not supported in Java 17. + * We can enable this once this is supported again. + */ @nowarn +@Ignore class AkkaCassandraJmxMetricsSpec extends AnyFreeSpec with LazyLogging with Eventually with BeforeAndAfterAll { import TestActors._ diff --git a/build.sbt b/build.sbt index aedcd477f..cd852e5f6 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val baker: Project = project.in(file(".")) `bakery-interaction-k8s-interaction-manager`, // Examples `baker-example`, `bakery-client-example`, `interaction-example-make-payment-and-ship-items`, - `interaction-example-reserve-items`, `bakery-kafka-listener-example` + `interaction-example-reserve-items`, `bakery-kafka-listener-example`, `docs-code-snippets` ) def testScope(project: ProjectReference): ClasspathDep[ProjectReference] = project % "test->test;test->compile" @@ -39,17 +39,17 @@ lazy val buildExampleDockerCommand: Command = Command.command("buildExampleDocke state }) -lazy val scala212 = "2.12.16" +//lazy val scala212 = "2.12.16" lazy val scala213 = "2.13.8" -lazy val supportedScalaVersions = List(scala213, scala212) +lazy val supportedScalaVersions = List(scala213) val commonSettings: Seq[Setting[_]] = Defaults.coreDefaultSettings ++ Seq( organization := "com.ing.baker", fork := true, testOptions += Tests.Argument(TestFrameworks.JUnit, "-v"), - javacOptions := Seq("-source", "1.8", "-target", "1.8"), + javacOptions := Seq("-source", "17", "-target", "17"), scalacOptions := Seq( - s"-target:jvm-1.8", + s"-target:jvm-17", "-unchecked", "-deprecation", "-feature", @@ -175,12 +175,7 @@ lazy val `baker-interface`: Project = project.in(file("core/baker-interface")) catsEffect, fs2Core, fs2Io, - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, n)) if n <= 12 => - scalaJava8Compat091 - case _ => - scalaJava8Compat100 - }, + scalaJava8Compat100, javaxInject, guava ) ++ providedDeps(findbugs) ++ testDeps( @@ -202,8 +197,8 @@ lazy val `baker-interface-kotlin`: Project = project.in(file("core/baker-interfa .settings(Publish.settings) .settings( moduleName := "baker-interface-kotlin", - kotlinVersion := "1.7.22", - kotlincJvmTarget := "1.8", + kotlinVersion := "1.8.21", + kotlincJvmTarget := "17", kotlinLib("stdlib-jdk8"), kotlinLib("reflect"), libraryDependencies ++= @@ -315,8 +310,8 @@ lazy val `baker-recipe-dsl-kotlin`: Project = project.in(file("core/recipe-dsl-k .settings(Publish.settings) .settings( moduleName := "baker-recipe-dsl-kotlin", - kotlinVersion := "1.7.22", - kotlincJvmTarget := "1.8", + kotlinVersion := "1.8.21", + kotlincJvmTarget := "17", kotlinLib("stdlib-jdk8"), kotlinLib("reflect"), libraryDependencies ++= @@ -340,8 +335,8 @@ lazy val `baker-recipe-compiler`: Project = project.in(file("core/recipe-compile .settings(Publish.settings) .settings( moduleName := "baker-compiler", - kotlinVersion := "1.7.22", - kotlincJvmTarget := "1.8", + kotlinVersion := "1.8.21", + kotlincJvmTarget := "17", libraryDependencies ++= testDeps(scalaTest, scalaCheck, junitJupiter) ) @@ -660,27 +655,6 @@ lazy val `bakery-integration-tests`: Project = project.in(file("bakery/integrati `interaction-example-make-payment-and-ship-items`, `interaction-example-reserve-items`) -lazy val `sbt-bakery-docker-generate`: Project = project.in(file("docker/sbt-bakery-docker-generate")) - .settings(scalaVersion := scala212, crossScalaVersions := Nil) - .settings(defaultModuleSettings212) - .settings(noPublishSettings) // docker plugin can't be published, at least not to azure feed - .settings( - crossScalaVersions := Nil, - // workaround to let plugin be used in the same project without publishing it - Compile / sourceGenerators += Def.task { - val file = (Compile / sourceManaged).value / "bakery" / "sbt" / "BuildInteractionDockerImageSBTPlugin.scala" - val sourceFile = IO.readBytes(baseDirectory.value.getParentFile.getParentFile / "project" / "BuildInteractionDockerImageSBTPlugin.scala") - IO.write(file, sourceFile) - Seq(file) - }.taskValue, - addSbtPlugin(("com.github.sbt" % "sbt-native-packager" % "1.9.9") cross CrossVersion.constant(scala212)), - addSbtPlugin(("org.vaslabs.kube" % "sbt-kubeyml" % "0.4.0") cross CrossVersion.constant(scala212)) - ) - .enablePlugins(SbtPlugin) - .enablePlugins(bakery.sbt.BuildInteractionDockerImageSBTPlugin) - .dependsOn(`bakery-interaction`, `bakery-interaction-spring`) - - lazy val `baker-example`: Project = project .in(file("examples/baker-example")) .enablePlugins(JavaAppPackaging) @@ -721,6 +695,44 @@ lazy val `baker-example`: Project = project ) .dependsOn(`baker-types`, `baker-akka-runtime`, `baker-recipe-compiler`, `baker-recipe-dsl`, `baker-intermediate-language`) +lazy val `docs-code-snippets`: Project = project + .in(file("examples/docs-code-snippets")) + .enablePlugins(JavaAppPackaging) + .settings(commonSettings) + .settings(noPublishSettings) + .settings(yPartialUnificationSetting) + .settings(crossBuildSettings) + .settings( + moduleName := "docs-code-snippets", + kotlinVersion := "1.8.21", + kotlincJvmTarget := "17", + kotlinLib("stdlib-jdk8"), + kotlinLib("reflect"), + libraryDependencies ++= + compileDeps( + slf4jApi, + http4s, + http4sDsl, + http4sServer, + http4sCirce, + circe, + circeGeneric, + akkaPersistenceCassandra, + akkaPersistenceQuery + ) ++ testDeps( + scalaTest, + scalaCheck, + mockitoScala, + junitInterface, + slf4jApi, + akkaTestKit + ) + ) + .settings( + coverageEnabled := false + ) + .dependsOn(`baker-types`, `baker-akka-runtime`, `baker-recipe-compiler`, `baker-recipe-dsl-kotlin`, `baker-intermediate-language`, `baker-interface-kotlin`) + lazy val `bakery-client-example`: Project = project .in(file("examples/bakery-client-example")) .enablePlugins(JavaAppPackaging) @@ -782,7 +794,6 @@ lazy val `bakery-kafka-listener-example`: Project = project lazy val `interaction-example-reserve-items`: Project = project.in(file("examples/bakery-interaction-examples/reserve-items")) .enablePlugins(JavaAppPackaging) - .enablePlugins(bakery.sbt.BuildInteractionDockerImageSBTPlugin) .settings(noPublishSettings) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) @@ -804,7 +815,6 @@ lazy val `interaction-example-reserve-items`: Project = project.in(file("example lazy val `interaction-example-make-payment-and-ship-items`: Project = project.in(file("examples/bakery-interaction-examples/make-payment-and-ship-items")) .enablePlugins(JavaAppPackaging) - .enablePlugins(bakery.sbt.BuildInteractionDockerImageSBTPlugin) .settings(noPublishSettings) .settings(defaultModuleSettings) .settings(yPartialUnificationSetting) diff --git a/core/akka-runtime/src/main/protobuf/delayed_transition.proto b/core/akka-runtime/src/main/protobuf/delayed_transition.proto index 12fc1e15e..d66c731ec 100644 --- a/core/akka-runtime/src/main/protobuf/delayed_transition.proto +++ b/core/akka-runtime/src/main/protobuf/delayed_transition.proto @@ -14,7 +14,7 @@ message DelayedTransitionInstance { optional int64 jobId = 3; optional int64 transitionId = 4; optional string eventToFire = 5; - optional bool fired = 6; + optional bool fired = 6; //REMOVED } message DelayedTransitionScheduled { @@ -25,4 +25,8 @@ message DelayedTransitionScheduled { message DelayedTransitionExecuted { optional string id = 1; optional DelayedTransitionInstance delayedTransitionInstance = 2; +} + +message DelayedTransitionSnapshot { + map waitingTransitions = 1; } \ No newline at end of file diff --git a/core/akka-runtime/src/main/protobuf/process_instance.proto b/core/akka-runtime/src/main/protobuf/process_instance.proto index 5fb1fdeb7..a2ca802cc 100644 --- a/core/akka-runtime/src/main/protobuf/process_instance.proto +++ b/core/akka-runtime/src/main/protobuf/process_instance.proto @@ -49,6 +49,19 @@ message TransitionFired { optional SerializedData data = 8; } +message TransitionFailedWithOutput { + option (scalapb.message).extends = "com.ing.baker.runtime.akka.actor.serialization.BakerSerializable"; + + optional int64 job_id = 1; + optional string correlation_id = 9; + optional int64 transition_id = 3; + optional int64 time_started = 4; + optional int64 time_completed = 5; + repeated ConsumedToken consumed = 6; + repeated ProducedToken produced = 7; + optional SerializedData data = 8; +} + message TransitionDelayed { option (scalapb.message).extends = "com.ing.baker.runtime.akka.actor.serialization.BakerSerializable"; optional int64 job_id = 1; diff --git a/core/akka-runtime/src/main/resources/reference.conf b/core/akka-runtime/src/main/resources/reference.conf index 1bec3956d..a3221ab26 100644 --- a/core/akka-runtime/src/main/resources/reference.conf +++ b/core/akka-runtime/src/main/resources/reference.conf @@ -13,8 +13,11 @@ baker { # The interval that a check is done of processes should be deleted retention-check-interval = 1 minutes - # snapShotInterval + # The interval of messages between creating snapshots for the actors snapshot-interval = 100 + + # The amount of snapshots to keep for the actors + snapshot-count = 3 } # the default timeout for Baker.bake(..) process creation calls @@ -47,6 +50,7 @@ baker { restart-maxBackoff = 30 seconds restart-randomFactor = 0.2 start-all-shards = true + remember-process-duration = null } process-instance { diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala index 150e52c61..9ec21639a 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala @@ -14,7 +14,9 @@ import com.ing.baker.runtime.akka.actor.recipe_manager.RecipeManagerProtocol import com.ing.baker.runtime.akka.actor.recipe_manager.RecipeManagerProtocol.RecipeFound import com.ing.baker.runtime.akka.internal.CachingInteractionManager import com.ing.baker.runtime.common.BakerException._ +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName import com.ing.baker.runtime.common.{InteractionExecutionFailureReason, RecipeRecord, SensoryEventStatus} +import com.ing.baker.runtime.model.recipeinstance.RecipeInstanceState.getMetaDataFromIngredients import com.ing.baker.runtime.recipe_manager.{ActorBasedRecipeManager, DefaultRecipeManager, RecipeManager} import com.ing.baker.runtime.scaladsl._ import com.ing.baker.runtime.{javadsl, scaladsl} @@ -181,7 +183,9 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker case None => cats.effect.IO.pure(InteractionExecutionResult(Left(InteractionExecutionResult.Failure( InteractionExecutionFailureReason.INTERACTION_NOT_FOUND, None, None)))) case Some(interactionInstance) => - interactionInstance.execute(ingredients, Map()) + interactionInstance.execute( + ingredients.filter(ingredientInstance => ingredientInstance.name != RecipeInstanceMetadataName), + getMetaDataFromIngredients(ingredients).getOrElse(Map.empty)) .map(executionSuccess => InteractionExecutionResult(Right(InteractionExecutionResult.Success(executionSuccess)))) .recover { case e => InteractionExecutionResult(Left(InteractionExecutionResult.Failure( @@ -208,6 +212,8 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker processIndexActor.ask(CreateProcess(recipeId, recipeInstanceId))(config.timeouts.defaultBakeTimeout).javaTimeoutToBakerTimeout("bake").flatMap { case _: Initialized => Future.successful(()) + case ProcessDeleted => + Future.failed(ProcessDeletedException(recipeInstanceId)) case ProcessAlreadyExists(_) => Future.failed(ProcessAlreadyExistsException(recipeInstanceId)) case RecipeManagerProtocol.NoRecipeFound(_) => @@ -423,22 +429,22 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker case ProcessDeleted(id) => Future.failed(ProcessDeletedException(id)) } -// /** -// * @param recipeInstanceId The recipeInstance Id. -// * @param name The name of the ingredient. -// * @return The provided ingredients. -// */ -// override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = -// processIndexActor -// .ask(GetProcessIngredient(recipeInstanceId, name))(Timeout.durationToTimeout(config.timeouts.defaultInquireTimeout)) -// .javaTimeoutToBakerTimeout("getRecipeInstanceState") -// .flatMap { -// case ingredientFound: IngredientFound => Future.successful(ingredientFound.value) -// case IngredientNotFound => Future.failed(NoSuchIngredientException(name)) -// case Uninitialized(id) => Future.failed(NoSuchProcessException(id)) -// case NoSuchProcess(id) => Future.failed(NoSuchProcessException(id)) -// case ProcessDeleted(id) => Future.failed(ProcessDeletedException(id)) -// } + /** + * @param recipeInstanceId The recipeInstance Id. + * @param name The name of the ingredient. + * @return The provided ingredients. + */ + override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = + processIndexActor + .ask(GetProcessIngredient(recipeInstanceId, name))(Timeout.durationToTimeout(config.timeouts.defaultInquireTimeout)) + .javaTimeoutToBakerTimeout("getRecipeInstanceState") + .flatMap { + case ingredientFound: IngredientFound => Future.successful(ingredientFound.value) + case IngredientNotFound => Future.failed(NoSuchIngredientException(name)) + case Uninitialized(id) => Future.failed(NoSuchProcessException(id)) + case NoSuchProcess(id) => Future.failed(NoSuchProcessException(id)) + case ProcessDeleted(id) => Future.failed(ProcessDeletedException(id)) + } /** * Returns all provided ingredients for a given process id. diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBakerConfig.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBakerConfig.scala index 61a69008a..a1e4f9c3c 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBakerConfig.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBakerConfig.scala @@ -82,7 +82,8 @@ object AkkaBakerConfig extends LazyLogging { actorIdleTimeout = Some(5.minutes), configuredEncryption = Encryption.NoEncryption, timeouts = defaultTimeouts, - blacklistedProcesses = List.empty + blacklistedProcesses = List.empty, + rememberProcessDuration = None ) AkkaBakerConfig( @@ -110,7 +111,8 @@ object AkkaBakerConfig extends LazyLogging { seedNodes = ClusterBakerActorProvider.SeedNodesList(seedNodes), configuredEncryption = Encryption.NoEncryption, timeouts = defaultTimeouts, - blacklistedProcesses = List.empty + blacklistedProcesses = List.empty, + rememberProcessDuration = None ) AkkaBakerConfig( @@ -150,7 +152,8 @@ object AkkaBakerConfig extends LazyLogging { actorIdleTimeout = config.as[Option[FiniteDuration]]("baker.actor.idle-timeout"), configuredEncryption = encryption, Timeouts.apply(config), - blacklistedProcesses = config.as[List[String]]("baker.blacklisted-processes") + blacklistedProcesses = config.as[List[String]]("baker.blacklisted-processes"), + rememberProcessDuration = config.as[Option[FiniteDuration]]("baker.process-index.remember-process-duration") ) case Some("cluster-sharded") => new ClusterBakerActorProvider( @@ -169,7 +172,8 @@ object AkkaBakerConfig extends LazyLogging { providedIngredientFilter = config.as[List[String]]("baker.filtered-ingredient-values") ++ config.as[List[String]]("baker.filtered-ingredient-values-for-stream"), configuredEncryption = encryption, Timeouts.apply(config), - blacklistedProcesses = config.as[List[String]]("baker.blacklisted-processes") + blacklistedProcesses = config.as[List[String]]("baker.blacklisted-processes"), + rememberProcessDuration = config.as[Option[FiniteDuration]]("baker.process-index.remember-process-duration") ) case Some(other) => throw new IllegalArgumentException(s"Unsupported actor provider: $other") } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/BakerActorCleanup.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/BakerActorCleanup.scala new file mode 100644 index 000000000..7f6916815 --- /dev/null +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/BakerActorCleanup.scala @@ -0,0 +1,58 @@ +package com.ing.baker.runtime.akka.actor + +import akka.Done +import akka.actor.ActorSystem +import akka.persistence.{SnapshotMetadata, cassandra} +import com.typesafe.scalalogging.LazyLogging + +import scala.concurrent.{ExecutionContext, Future} + +/** + * The cleanup of Actor based data for Baker event store. + * This abstraction iis build since for Cassandra we can use the Cassandra cleanup tool to cleanup the data in a optimized way. + * For none Cassandra event stores we provide the ActorBasedBakerCleanup that uses the regular available cleanup for event stores. + */ +abstract class BakerCleanup { + + def supportsCleanupOfStoppedActors: Boolean + + def deleteAllEvents(persistenceId: String, neverUsePersistenceIdAgain: Boolean): Future[Done] + + def deleteEventsAndSnapshotBeforeSnapshot(persistenceId: String, maxSnapshotsToKeep: Int)(implicit ec: ExecutionContext): Future[Done] +} + +class CassandraBakerCleanup(system: ActorSystem) extends BakerCleanup with LazyLogging { + + private val cleanup = new cassandra.cleanup.Cleanup(system) + + override def supportsCleanupOfStoppedActors = true + + override def deleteAllEvents(persistenceId: String, neverUsePersistenceIdAgain: Boolean): Future[Done] = + cleanup.deleteAllEvents(persistenceId, neverUsePersistenceIdAgain) + + override def deleteEventsAndSnapshotBeforeSnapshot(persistenceId: String, maxSnapshotsToKeep: Int)(implicit ec: ExecutionContext): Future[Done] = { + cleanup.deleteBeforeSnapshot(persistenceId, maxSnapshotsToKeep).flatMap { + case Some(snapshotMetadata: SnapshotMetadata) => + //TODO Make this configurable instead of always deleting the message data + logger.debug("SnapshotMetadata found, starting deleting messages") + cleanup.deleteEventsTo(persistenceId, snapshotMetadata.sequenceNr) + case None => + logger.debug("SnapshotMetadata not found") + Future.successful(Done) + } + } +} + +class ActorBasedBakerCleanup() extends BakerCleanup { + + override def supportsCleanupOfStoppedActors = false + + override def deleteAllEvents(persistenceId: String, neverUsePersistenceIdAgain: Boolean): Future[Done] = { + Future.successful(Done) + } + + override def deleteEventsAndSnapshotBeforeSnapshot(persistenceId: String, maxSnapshotsToKeep: Int)(implicit ec: ExecutionContext): Future[Done] = { + //We did not remove old snapshots and messages before from actors so we do not do this without the CassandraCleanup tool + Future.successful(Done) + } +} diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/ClusterBakerActorProvider.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/ClusterBakerActorProvider.scala index 3e213f497..653b7ce17 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/ClusterBakerActorProvider.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/ClusterBakerActorProvider.scala @@ -73,8 +73,8 @@ class ClusterBakerActorProvider( providedIngredientFilter: List[String], configuredEncryption: Encryption, timeouts: AkkaBakerConfig.Timeouts, - blacklistedProcesses: List[String] - ) extends BakerActorProvider with LazyLogging { + blacklistedProcesses: List[String], + rememberProcessDuration: Option[Duration]) extends BakerActorProvider with LazyLogging { def initialize(implicit system: ActorSystem): Unit = { /** @@ -121,7 +121,8 @@ class ClusterBakerActorProvider( recipeManager, getIngredientsFilter, providedIngredientFilter, - blacklistedProcesses), + blacklistedProcesses, + rememberProcessDuration), settings = clusterShardingSettings, extractEntityId = ClusterBakerActorProvider.entityIdExtractor(nrOfShards), extractShardId = ClusterBakerActorProvider.shardIdExtractor(nrOfShards), diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/LocalBakerActorProvider.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/LocalBakerActorProvider.scala index 00c2b4a30..e9dee6797 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/LocalBakerActorProvider.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/LocalBakerActorProvider.scala @@ -22,7 +22,8 @@ class LocalBakerActorProvider( actorIdleTimeout: Option[FiniteDuration], configuredEncryption: Encryption, timeouts: AkkaBakerConfig.Timeouts, - blacklistedProcesses: List[String] + blacklistedProcesses: List[String], + rememberProcessDuration: Option[Duration] ) extends BakerActorProvider { override def initialize(implicit system: ActorSystem): Unit = () @@ -45,7 +46,8 @@ class LocalBakerActorProvider( recipeManager, getIngredientsFilter, providedIngredientFilter, - blacklistedProcesses), + blacklistedProcesses, + rememberProcessDuration), childName = "ProcessIndexActor", minBackoff = restartMinBackoff, maxBackoff = restartMaxBackoff, diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActor.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActor.scala index f7ea47aee..052c9127d 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActor.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActor.scala @@ -1,37 +1,49 @@ package com.ing.baker.runtime.akka.actor.delayed_transition_actor -import akka.actor.{ActorLogging, ActorRef, Props} -import akka.persistence.{PersistentActor, RecoveryCompleted} -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.{DelayedTransitionExecuted, DelayedTransitionInstance, DelayedTransitionScheduled, prefix} -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.{FireDelayedTransition, ScheduleDelayedTransition, StartTimer, TickTimer} +import akka.actor.{ActorRef, Props} +import akka.persistence._ +import akka.sensors.actor.PersistentActorMetrics +import com.ing.baker.runtime.akka.actor.BakerCleanup +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor._ +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol._ +import com.ing.baker.runtime.akka.actor.process_index.ProcessIndexProtocol.{NoSuchProcess, ProcessDeleted} import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol import com.ing.baker.runtime.akka.actor.serialization.BakerSerializable -import scala.collection.mutable import scala.concurrent.duration.FiniteDuration object DelayedTransitionActor { def prefix(id: String) = s"timer-interaction-$id-" - def props(processIndex: ActorRef) = Props(new DelayedTransitionActor(processIndex: ActorRef)) + def props(processIndex: ActorRef, + cleanup: BakerCleanup, + snapShotInterval: Int, + snapshotCount: Int) = Props(new DelayedTransitionActor(processIndex, cleanup, snapShotInterval, snapshotCount)) case class DelayedTransitionInstance(recipeInstanceId: String, timeToFire: Long, jobId: Long, transitionId: Long, eventToFire: String, - fired: Boolean, optionalActorRef: Option[ActorRef]) extends BakerSerializable case class DelayedTransitionScheduled(id: String, delayedTransitionInstance: DelayedTransitionInstance) extends BakerSerializable case class DelayedTransitionExecuted(id: String, delayedTransitionInstance: DelayedTransitionInstance) extends BakerSerializable + + case class DelayedTransitionSnapshot(waitingTransitions: Map[String, DelayedTransitionInstance]) extends BakerSerializable + + private def getId(recipeInstanceId: String, jobId: Long): String = + recipeInstanceId + jobId } -class DelayedTransitionActor(processIndex: ActorRef) extends PersistentActor with ActorLogging { +class DelayedTransitionActor(processIndex: ActorRef, + cleanup: BakerCleanup, + snapShotInterval: Int, + snapshotCount: Int) extends PersistentActor with PersistentActorMetrics { - private val waitingTimer: mutable.Map[String, DelayedTransitionInstance] = mutable.Map[String, DelayedTransitionInstance]() + private var waitingTransitions: Map[String, DelayedTransitionInstance] = Map[String, DelayedTransitionInstance]() import context.dispatcher @@ -46,19 +58,18 @@ class DelayedTransitionActor(processIndex: ActorRef) extends PersistentActor wit } private def handleScheduleDelayedTransition(event: ScheduleDelayedTransition) = { - val id = event.recipeInstanceId + event.jobId + val id = getId(event.recipeInstanceId, event.jobId) val instance = DelayedTransitionInstance( event.recipeInstanceId, System.currentTimeMillis() + event.waitTimeInMillis, event.jobId, event.transitionId, event.eventToFire, - fired = false, Some(event.originalSender) ) - persist(DelayedTransitionScheduled(id, instance)) { _ => - if (!waitingTimer.contains(id)) { - waitingTimer.put(id, instance) + persistWithSnapshot(DelayedTransitionScheduled(id, instance)) { _ => + if (!waitingTransitions.contains(id)) { + waitingTransitions += (id -> instance) sender() ! ProcessInstanceProtocol.TransitionDelayed(event.jobId, event.transitionId, event.consumed) } else { @@ -73,41 +84,81 @@ class DelayedTransitionActor(processIndex: ActorRef) extends PersistentActor wit handleScheduleDelayedTransition(event) case StartTimer => + //TODO Make this configurable context.system.scheduler.scheduleAtFixedRate(FiniteDuration.apply(1, "seconds"), FiniteDuration.apply(1, "seconds"), context.self, TickTimer) context.become(running) } - // TODO Do not grow indefinitely (Use snapshots/remove old entries) def running: Receive = { + case SaveSnapshotSuccess(metadata) => + log.debug("Snapshot saved") + cleanupSnapshots(metadata.persistenceId, snapshotCount) + + case SaveSnapshotFailure(_, _) => + log.error("Saving snapshot failed") + case event@ScheduleDelayedTransition(recipeInstanceId, waitTimeInMillis, jobId, transitionId, consumed, eventToFire, originalSender) => handleScheduleDelayedTransition(event) case TickTimer => - waitingTimer.foreach { case (id, instance) => - val newInstance = instance.copy(fired = true) - if(!instance.fired && System.currentTimeMillis() > instance.timeToFire) { - persist(DelayedTransitionExecuted(id, newInstance)) { _ => - processIndex ! FireDelayedTransition(instance.recipeInstanceId, - instance.jobId, - instance.transitionId, - instance.eventToFire, - instance.optionalActorRef.getOrElse(self)) - waitingTimer.put(id, newInstance) - } + waitingTransitions.foreach { case (id, instance) => + if (System.currentTimeMillis() > instance.timeToFire) { + processIndex ! FireDelayedTransition(instance.recipeInstanceId, + instance.jobId, + instance.transitionId, + instance.eventToFire, + instance.optionalActorRef.getOrElse(self)) } } + + case FireDelayedTransitionAck(recipeInstanceId, jobId) => + val id = getId(recipeInstanceId, jobId) + waitingTransitions.get(id) match { + case Some(instance) => + persistWithSnapshot(DelayedTransitionExecuted(id, instance)) { _ => + waitingTransitions -= id + } + case None => log.error(s"FireDelayedTransitionAck received for $id but not found") + } + + case ProcessDeleted(recipeInstanceId) => + log.error(s"ProcessDeleted received in DelayedTransitionInstance for $recipeInstanceId") + waitingTransitions --= waitingTransitions.filter(d => d._2.recipeInstanceId == recipeInstanceId).keys + + case NoSuchProcess(recipeInstanceId) => + log.error(s"NoSuchProcess received in DelayedTransitionInstance for $recipeInstanceId") + waitingTransitions --= waitingTransitions.filter(d => d._2.recipeInstanceId == recipeInstanceId).keys } override def persistenceId: String = prefix(context.self.path.name) + def persistWithSnapshot[A](event: A)(handler: A => Unit): Unit = { + persist(event)(handler) + if (lastSequenceNr % snapShotInterval == 0 && lastSequenceNr != 0) { + log.debug("Writing Snapshots") + saveSnapshot(DelayedTransitionSnapshot(waitingTransitions)) + } + } + + def cleanupSnapshots(persistenceId: String, snapShotsToKeep: Int) : Unit = { + cleanup.deleteEventsAndSnapshotBeforeSnapshot(persistenceId, snapShotsToKeep) + log.debug("Snapshots cleaned") + } + override def receiveRecover: Receive = { + + case SnapshotOffer(_, delayedTransitionSnapshot: DelayedTransitionSnapshot) => + log.debug("Loading Snapshots") + waitingTransitions = delayedTransitionSnapshot.waitingTransitions + case SnapshotOffer(_, _) => + val message = "could not load snapshot because snapshot was not of type ProcessIndexSnapShot" + log.error(message) + throw new IllegalArgumentException(message) case scheduled: DelayedTransitionScheduled => - waitingTimer.put( - scheduled.id, + waitingTransitions += ( + scheduled.id -> scheduled.delayedTransitionInstance) case fired: DelayedTransitionExecuted => - waitingTimer.put( - fired.id, - fired.delayedTransitionInstance) + waitingTransitions -= fired.id } } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorProtocol.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorProtocol.scala index c57322eef..ea93f0f02 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorProtocol.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorProtocol.scala @@ -14,5 +14,7 @@ object DelayedTransitionActorProtocol { case class FireDelayedTransition(recipeInstanceId: String, jobId: Long, transitionId: Long, eventToFire: String, originalSender: ActorRef) extends DelayedTransitionActorProtocol + case class FireDelayedTransitionAck(recipeInstanceId: String, jobId: Long) extends DelayedTransitionActorProtocol + case object TickTimer extends DelayedTransitionActorProtocol } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTranstionProto.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTranstionProto.scala index 0bfa5169f..c99c7d46f 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTranstionProto.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTranstionProto.scala @@ -1,6 +1,6 @@ package com.ing.baker.runtime.akka.actor.delayed_transition_actor -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.{DelayedTransitionExecuted, DelayedTransitionInstance, DelayedTransitionScheduled} +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.{DelayedTransitionExecuted, DelayedTransitionInstance, DelayedTransitionScheduled, DelayedTransitionSnapshot} import com.ing.baker.runtime.serialization.ProtoMap import com.ing.baker.runtime.serialization.ProtoMap.{ctxFromProto, ctxToProto, versioned} import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionProto._ @@ -18,8 +18,7 @@ object DelayedTransitionProto { Some(a.timeToFire), Some(a.jobId), Some(a.transitionId), - Some(a.eventToFire), - Some(a.fired) + Some(a.eventToFire) ) } @@ -30,8 +29,7 @@ object DelayedTransitionProto { jobId <- versioned(message.jobId, "jobId") transitionId <- versioned(message.transitionId, "transitionId") eventToFire <- versioned(message.eventToFire, "eventToFire") - fired <- versioned(message.fired, "fired") - } yield DelayedTransitionInstance(recipeInstanceId, timeToFire, jobId, transitionId, eventToFire, fired, None) + } yield DelayedTransitionInstance(recipeInstanceId, timeToFire, jobId, transitionId, eventToFire, None) } implicit def delayedTransitionScheduledProto: ProtoMap[DelayedTransitionScheduled, protobuf.DelayedTransitionScheduled] = @@ -71,4 +69,22 @@ object DelayedTransitionProto { delayedTransitionInstance <- DelayedTransitionProto.delayedTransitionInstanceProto.fromProto(delayedTransitionInstanceProto) } yield DelayedTransitionExecuted(id, delayedTransitionInstance) } + + implicit def delayedTransitionSnapshotProto: ProtoMap[DelayedTransitionSnapshot, protobuf.DelayedTransitionSnapshot] = + new ProtoMap[DelayedTransitionSnapshot, protobuf.DelayedTransitionSnapshot] { + val companion = protobuf.DelayedTransitionSnapshot + + def toProto(a: DelayedTransitionSnapshot): protobuf.DelayedTransitionSnapshot = { + protobuf.DelayedTransitionSnapshot( + a.waitingTransitions.map( + entry => entry._1 -> ctxToProto(entry._2) + ) + ) + } + + def fromProto(message: protobuf.DelayedTransitionSnapshot): Try[DelayedTransitionSnapshot] = + Try { + DelayedTransitionSnapshot(message.waitingTransitions.map(entry => entry._1 -> ctxFromProto(entry._2).get)) + } + } } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/logging/LogAndSendEvent.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/logging/LogAndSendEvent.scala index fcc6fd18a..49ab1d855 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/logging/LogAndSendEvent.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/logging/LogAndSendEvent.scala @@ -2,7 +2,7 @@ package com.ing.baker.runtime.akka.actor.logging import akka.event.EventStream import com.ing.baker.il.petrinet.Transition -import com.ing.baker.runtime.common.{EventReceived, EventRejected, InteractionCompleted, InteractionFailed, InteractionStarted, RecipeAdded, RecipeInstanceCreated} +import com.ing.baker.runtime.common.{EventFired, EventReceived, EventRejected, InteractionCompleted, InteractionFailed, InteractionStarted, RecipeAdded, RecipeInstanceCreated} import com.ing.baker.runtime.model.BakerLogging object LogAndSendEvent { @@ -46,9 +46,9 @@ object LogAndSendEvent { bakerLogging.eventRejected(eventRejected) } - def firingEvent(recipeInstanceId: String, executionId: Long, transition: Transition, timeStarted: Long): Unit = { - //TODO This does not have a corrosponding BakerEvent, this should be created - bakerLogging.firingEvent(recipeInstanceId, executionId, transition, timeStarted) + def eventFired(eventFired: EventFired, + eventStream: EventStream): Unit = { + eventStream.publish(eventFired) + bakerLogging.eventFired(eventFired) } - } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndex.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndex.scala index ce1689176..13967ca52 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndex.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndex.scala @@ -13,6 +13,7 @@ import com.ing.baker.il.petrinet.{InteractionTransition, Place, Transition} import com.ing.baker.il.{CompiledRecipe, EventDescriptor} import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka._ +import com.ing.baker.runtime.akka.actor.{ActorBasedBakerCleanup, BakerCleanup, CassandraBakerCleanup} import com.ing.baker.runtime.akka.actor.Util.logging._ import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.{FireDelayedTransition, StartTimer} @@ -34,6 +35,8 @@ import com.ing.baker.runtime.serialization.Encryption import com.ing.baker.types.Value import com.typesafe.config.Config +import java.util.concurrent.TimeUnit +import scala.Console.println import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} @@ -49,7 +52,8 @@ object ProcessIndex { recipeManager: RecipeManager, getIngredientsFilter: Seq[String], providedIngredientFilter: Seq[String], - blacklistedProcesses: Seq[String]): Props = + blacklistedProcesses: Seq[String], + rememberProcessDuration: Option[Duration]): Props = Props(new ProcessIndex( recipeInstanceIdleTimeout, retentionCheckInterval, @@ -58,7 +62,8 @@ object ProcessIndex { recipeManager, getIngredientsFilter, providedIngredientFilter, - blacklistedProcesses)) + blacklistedProcesses, + rememberProcessDuration)) sealed trait ProcessStatus @@ -114,7 +119,8 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], recipeManager: RecipeManager, getIngredientsFilter: Seq[String], providedIngredientFilter: Seq[String], - blacklistedProcesses: Seq[String]) extends PersistentActor with PersistentActorMetrics { + blacklistedProcesses: Seq[String], + rememberProcessDuration: Option[Duration]) extends PersistentActor with PersistentActorMetrics { override val log: DiagnosticLoggingAdapter = Logging.getLogger(logSource = this) @@ -132,6 +138,7 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], private val config: Config = context.system.settings.config private val snapShotInterval: Int = config.getInt("baker.actor.snapshot-interval") + private val snapshotCount: Int = config.getInt("baker.actor.snapshot-count") private val processInquireTimeout: FiniteDuration = config.getDuration("baker.process-inquire-timeout").toScala private val updateCacheTimeout: FiniteDuration = config.getDuration("baker.process-index-update-cache-timeout").toScala @@ -143,10 +150,17 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], private val index: mutable.Map[String, ActorMetadata] = mutable.Map[String, ActorMetadata]() private val recipeCache: mutable.Map[String, (CompiledRecipe, Long)] = mutable.Map[String, (CompiledRecipe, Long)]() - private val cleanup = new akka.persistence.cassandra.cleanup.Cleanup(context.system) + //TODO chose if to use the CassandraBakerCleanup or the ActorBasedBakerCleanup + private val cleanup: BakerCleanup = { + if(config.hasPath("akka.persistence.journal.plugin") && + config.getString("akka.persistence.journal.plugin") == "akka.persistence.cassandra.journal") + new CassandraBakerCleanup(context.system) + else + new ActorBasedBakerCleanup() + } private val delayedTransitionActor: ActorRef = context.actorOf( - props = DelayedTransitionActor.props(this.self), + props = DelayedTransitionActor.props(this.self, cleanup, snapShotInterval, snapshotCount), name = s"${self.path.name}-timer") // if there is a retention check interval defined we schedule a recurring message @@ -164,6 +178,8 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], result.map(r => r.recipe -> r.updated) } + def getRecipeIdFromActor(actorRef: ActorRef) : String = actorRef.path.name + def getRecipeWithTimeStamp(recipeId: String): Option[(CompiledRecipe, Long)] = recipeCache.get(recipeId) match { case None => @@ -191,15 +207,15 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], // creates a ProcessInstanceActor, does not do any validation def createProcessActor(recipeInstanceId: String, compiledRecipe: CompiledRecipe): ActorRef = { - val runtime: ProcessInstanceRuntime[Place, Transition, RecipeInstanceState, EventInstance] = + val runtime: ProcessInstanceRuntime[RecipeInstanceState, EventInstance] = new RecipeRuntime(compiledRecipe, interactionManager, context.system.eventStream) val processActorProps = BackoffSupervisor.props( BackoffOpts.onStop( - ProcessInstance.props[Place, Transition, RecipeInstanceState, EventInstance]( + ProcessInstance.props[RecipeInstanceState, EventInstance]( compiledRecipe.name, - compiledRecipe.petriNet, + compiledRecipe, runtime, ProcessInstance.Settings( executionContext = bakerExecutionContext, @@ -222,37 +238,61 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], } def shouldDelete(meta: ActorMetadata): Boolean = { - meta.processStatus != Deleted && - getCompiledRecipe(meta.recipeId) - .flatMap(_.retentionPeriod) - .exists { p => meta.createdDateTime + p.toMillis < System.currentTimeMillis() } + if(meta.processStatus != Deleted) + getCompiledRecipe(meta.recipeId) match { + case Some(recipe) => + recipe.retentionPeriod.exists { p => meta.createdDateTime + p.toMillis < System.currentTimeMillis() } + case None => + log.error(s"Could not find recipe: ${meta.recipeId} during deletion for recipeInstanceId: ${meta.recipeInstanceId} using default 14 days") + meta.createdDateTime + (14 days).toMillis < System.currentTimeMillis() + } + else false } private def deleteProcess(meta: ActorMetadata): Unit = { - getProcessActor(meta.recipeInstanceId) match { - case Some(actorRef: ActorRef) => - log.debug(s"Deleting ${meta.recipeInstanceId} via actor message") - actorRef ! Stop(delete = true) - case None => - log.debug(s"Deleting ${meta.recipeInstanceId} via cleanup tool") - getCompiledRecipe(meta.recipeId) match { - case Some(compiledRecipe) => - val persistenceId = ProcessInstance.recipeInstanceId2PersistenceId(compiledRecipe.name, meta.recipeInstanceId) - log.debug(s"Deleting with persistenceId: ${persistenceId}") - persistWithSnapshot(ActorDeleted(meta.recipeInstanceId)) { _ => - //Using deleteAllEvents since we do not use Snapshots for ProcessInstances - cleanup.deleteAllEvents(persistenceId, neverUsePersistenceIdAgain = false) - .map(_ -> { - log.processHistoryDeletionSuccessful(meta.recipeInstanceId, 0) - index.update(meta.recipeInstanceId, meta.copy(processStatus = Deleted)) - }) - } - case None => - log.debug(s"Recipe not found for ${meta.recipeInstanceId}, marking as deleted") - persistWithSnapshot(ActorDeleted(meta.recipeInstanceId)) { _ => - index.update(meta.recipeInstanceId, meta.copy(processStatus = Deleted)) - } + //The new way of cleaning ProcessInstances, this can only be done if the datastore is Cassandra + if(cleanup.supportsCleanupOfStoppedActors) { + getProcessActor(meta.recipeInstanceId) match { + case Some(actorRef: ActorRef) => + log.debug(s"Deleting ${meta.recipeInstanceId} via actor message") + actorRef ! Stop(delete = true) + case None => + log.debug(s"Deleting ${meta.recipeInstanceId} via cleanup tool") + getCompiledRecipe(meta.recipeId) match { + case Some(compiledRecipe) => + val persistenceId = ProcessInstance.recipeInstanceId2PersistenceId(compiledRecipe.name, meta.recipeInstanceId) + log.debug(s"Deleting with persistenceId: ${persistenceId}") + persistWithSnapshot(ActorDeleted(meta.recipeInstanceId)) { _ => + //Using deleteAllEvents since we do not use Snapshots for ProcessInstances + cleanup.deleteAllEvents(persistenceId, neverUsePersistenceIdAgain = false) + .map(_ -> { + log.processHistoryDeletionSuccessful(meta.recipeInstanceId, 0) + index.update(meta.recipeInstanceId, meta.copy(processStatus = Deleted)) + }) + } + case None => + log.debug(s"Recipe not found for ${meta.recipeInstanceId}, marking as deleted") + persistWithSnapshot(ActorDeleted(meta.recipeInstanceId)) { _ => + index.update(meta.recipeInstanceId, meta.copy(processStatus = Deleted)) + } + } + } + } + // The old way to cleanup ProcessInstances, we will let Akka Persistence handle the cleanup. + // This will first start the ProcessInstance actor even if is passivated so will have bigger memory impact and more reads on the datastore. + else { + getOrCreateProcessActor(meta.recipeInstanceId).foreach(_ ! Stop(delete = true)) + } + } + + private def forgetProcesses(): Unit = { + rememberProcessDuration.map { + duration: Duration => + val currentTime = System.currentTimeMillis() + val toBeForgotten = index.filter { case (id, metadata) => + metadata.isDeleted && currentTime >= (metadata.createdDateTime + duration.toMillis) } + index --= toBeForgotten.keys } } @@ -282,10 +322,13 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], } yield (transition, jobId) } - override def receiveCommand: Receive = { - case SaveSnapshotSuccess(_) => log.debug("Snapshot saved") + override def receiveCommand: Receive = { + case SaveSnapshotSuccess(metadata) => + log.debug("Snapshot saved & cleaning old processes") + cleanup.deleteEventsAndSnapshotBeforeSnapshot(metadata.persistenceId, snapshotCount) - case SaveSnapshotFailure(_, _) => log.error("Saving snapshot failed") + case SaveSnapshotFailure(_, _) => + log.error("Saving snapshot failed") case GetIndex => sender() ! Index(index.values.filter(_.processStatus == Active).toSeq) @@ -295,9 +338,10 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], if (toBeDeleted.nonEmpty) log.debug(s"Deleting recipe instance: {}", toBeDeleted.mkString(",")) toBeDeleted.foreach(meta => deleteProcess(meta)) + forgetProcesses() case Terminated(actorRef) => - val recipeInstanceId = actorRef.path.name + val recipeInstanceId = getRecipeIdFromActor(actorRef) val mdc = Map( "processId" -> recipeInstanceId, @@ -335,7 +379,7 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], persistWithSnapshot(ActorCreated(recipeId, recipeInstanceId, createdTime)) { _ => // after that we actually create the ProcessInstance actor - val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty) + val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty) val initializeCmd = Initialize(compiledRecipe.initialMarking, processState) //TODO ensure the initialiseCMD is accepted before we add it ot the index @@ -361,7 +405,7 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], //Temporary solution for the situation that the initializeCmd is not send in the original Bake getCompiledRecipe(recipeId) match { case Some(compiledRecipe) => - val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty) + val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty) val initializeCmd = Initialize(compiledRecipe.initialMarking, processState) createProcessActor(recipeInstanceId, compiledRecipe) ! initializeCmd sender() ! ProcessAlreadyExists(recipeInstanceId) @@ -373,7 +417,7 @@ class ProcessIndex(recipeInstanceIdleTimeout: Option[FiniteDuration], //Temporary solution for the situation that the initializeCmd is not send in the original Bake getCompiledRecipe(recipeId) match { case Some(compiledRecipe) => - val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty) + val processState = RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty) val initializeCmd = Initialize(compiledRecipe.initialMarking, processState) actorRef ! initializeCmd sender() ! ProcessAlreadyExists(recipeInstanceId) diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexProto.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexProto.scala index 8143f834e..ecdf66584 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexProto.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexProto.scala @@ -113,7 +113,8 @@ object ProcessIndexProto { val companion = protobuf.ProcessIndexSnapShot override def toProto(processIndexSnapShot: ProcessIndexSnapShot): protobuf.ProcessIndexSnapShot = - protobuf.ProcessIndexSnapShot(processIndexSnapShot.index.map(entry => entry._1 -> ctxToProto(entry._2))) + protobuf.ProcessIndexSnapShot(processIndexSnapShot.index.map( + entry => entry._1 -> ctxToProto(entry._2))) override def fromProto(message: protobuf.ProcessIndexSnapShot): Try[ProcessIndexSnapShot] = { Try { diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstance.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstance.scala index f34608ea6..c729e059d 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstance.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstance.scala @@ -5,9 +5,12 @@ import akka.cluster.sharding.ShardRegion.Passivate import akka.event.{DiagnosticLoggingAdapter, Logging} import akka.persistence.{DeleteMessagesFailure, DeleteMessagesSuccess} import cats.effect.IO +import com.ing.baker.il.failurestrategy.{BlockInteraction, FireEventAfterFailure, RetryWithIncrementalBackoff} import com.ing.baker.il.petrinet.{EventTransition, InteractionTransition, Place, Transition} -import com.ing.baker.il.checkpointEventInteractionPrefix +import com.ing.baker.il.{CompiledRecipe, EventDescriptor, checkpointEventInteractionPrefix} import com.ing.baker.petrinet.api._ +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.{FireDelayedTransitionAck, ScheduleDelayedTransition} +import com.ing.baker.runtime.akka.actor.logging.LogAndSendEvent import com.ing.baker.runtime.akka.actor.process_index.ProcessIndexProtocol.FireSensoryEventRejection import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstance._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing._ @@ -16,12 +19,11 @@ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.{Continue, RetryWithDelay} import com.ing.baker.runtime.akka.actor.process_instance.internal._ import com.ing.baker.runtime.akka.actor.process_instance.{ProcessInstanceProtocol => protocol} -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.ScheduleDelayedTransition import com.ing.baker.runtime.akka.internal.{FatalInteractionException, RecipeRuntime} import com.ing.baker.runtime.model.BakerLogging -import com.ing.baker.runtime.scaladsl.{EventInstance, IngredientInstance, RecipeInstanceState} +import com.ing.baker.runtime.scaladsl.{EventFired, EventInstance, IngredientInstance, RecipeInstanceState} import com.ing.baker.runtime.serialization.Encryption -import com.ing.baker.types.{PrimitiveValue, Value} +import com.ing.baker.types.{FromValue, PrimitiveValue, Value} import scala.collection.immutable import scala.concurrent.ExecutionContext @@ -51,13 +53,13 @@ object ProcessInstance { None } - def props[P: Identifiable, T: Identifiable, S, E](processType: String, petriNet: PetriNet[P, T], - runtime: ProcessInstanceRuntime[P, T, S, E], + def props[S, E](processType: String, compiledRecipe: CompiledRecipe, + runtime: ProcessInstanceRuntime[S, E], settings: Settings, delayedTransitionActor: ActorRef): Props = - Props(new ProcessInstance[P, T, S, E]( + Props(new ProcessInstance[S, E]( processType, - petriNet, + compiledRecipe, settings, runtime, delayedTransitionActor) @@ -73,17 +75,61 @@ object ProcessInstance { ingredient } } + + def getOutputEventName(interactionTransition: InteractionTransition, + log: DiagnosticLoggingAdapter): String = { + def getOutputEventNameWithRetryStrategyFiltered(fireEvent: EventDescriptor, eventsToFire: Seq[EventDescriptor]): String = { + val outputEventsNames: Seq[String] = eventsToFire.map(_.name) + if (outputEventsNames.size != 2) + throw new FatalInteractionException(s"Delayed transitions must have 2 input ingredients if FireEventAfterFailure strategy is used") + outputEventsNames.filter(_ != fireEvent.name).head + } + + def getFirstOutputEventName(eventsToFire: Seq[EventDescriptor]): String = { + val outputEvents = interactionTransition.eventsToFire + if (outputEvents.size != 1) + throw new FatalInteractionException(s"Delayed transitions can only have 1 input ingredient") + outputEvents.head.name + } + + interactionTransition.failureStrategy match { + case FireEventAfterFailure(event) => + getOutputEventNameWithRetryStrategyFiltered(event, interactionTransition.eventsToFire) + case RetryWithIncrementalBackoff(_, _, _, _, Some(event)) => + getOutputEventNameWithRetryStrategyFiltered(event, interactionTransition.eventsToFire) + case RetryWithIncrementalBackoff(_, _, _, _, None) => + getFirstOutputEventName(interactionTransition.eventsToFire) + case BlockInteraction => + getFirstOutputEventName(interactionTransition.eventsToFire) + case _ => + log.error("Delayed transition firing with unrecognised Failure Strategy") + getFirstOutputEventName(interactionTransition.eventsToFire) + } + } + + private val javaDuration = FromValue[java.time.Duration]() + private val finiteDuration = FromValue[FiniteDuration]() + + def getWaitTimeInMillis(interactionTransition: InteractionTransition, state: RecipeInstanceState): Long = { + val input: immutable.Seq[IngredientInstance] = RecipeRuntime.createInteractionInput(interactionTransition, state) + if (input.size != 1) throw new FatalInteractionException(s"Delayed transitions can only have 1 input ingredient") + input.head.value match { + case javaDuration(d) => d.toMillis + case finiteDuration(fd) => fd.toMillis + case _ => throw new FatalInteractionException(s"Delayed transition ingredient not of type scala.concurrent.duration.FiniteDuration or java.time.Duration") + } + } } /** * This actor is responsible for maintaining the state of a single petri net instance. */ -class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( +class ProcessInstance[S, E]( processType: String, - petriNet: PetriNet[P, T], + compiledRecipe: CompiledRecipe, settings: Settings, - runtime: ProcessInstanceRuntime[P, T, S, E], - delayedTransitionActor: ActorRef) extends ProcessInstanceEventSourcing[P, T, S, E](petriNet, settings.encryption, runtime.eventSource) { + runtime: ProcessInstanceRuntime[S, E], + delayedTransitionActor: ActorRef) extends ProcessInstanceEventSourcing[S, E](compiledRecipe.petriNet, settings.encryption, runtime.eventSource) { import context.dispatcher @@ -108,9 +154,9 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( override def receiveCommand: Receive = uninitialized - private def marshallMarking(marking: Marking[Any]): Marking[Id] = marking.asInstanceOf[Marking[P]].marshall + private def marshallMarking(marking: Marking[Any]): Marking[Id] = marking.asInstanceOf[Marking[Place]].marshall - private def mapStateToProtocol(instance: internal.Instance[P, T, S]): protocol.InstanceState = { + private def mapStateToProtocol(instance: internal.Instance[S]): protocol.InstanceState = { protocol.InstanceState( instance.sequenceNr, instance.marking.marshall, @@ -123,7 +169,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( ) } - private def mapJobsToProtocol(job: internal.Job[P, T, S]): protocol.JobState = + private def mapJobsToProtocol(job: internal.Job[S]): protocol.JobState = protocol.JobState(job.id, job.transition.getId, job.consume.marshall, job.input, job.failure.map(mapExceptionTateToProtocol)) private def mapExceptionTateToProtocol(exceptionState: internal.ExceptionState): protocol.ExceptionState = @@ -132,7 +178,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( private def mapExceptionStrategyToProtocol(strategy: internal.ExceptionStrategy): protocol.ExceptionStrategy = strategy match { case internal.ExceptionStrategy.BlockTransition => protocol.ExceptionStrategy.BlockTransition case internal.ExceptionStrategy.RetryWithDelay(delay) => protocol.ExceptionStrategy.RetryWithDelay(delay) - case internal.ExceptionStrategy.Continue(marking, output) => protocol.ExceptionStrategy.Continue(marking.asInstanceOf[Marking[P]].marshall, output) + case internal.ExceptionStrategy.Continue(marking, output) => protocol.ExceptionStrategy.Continue(marking.asInstanceOf[Marking[Place]].marshall, output) } private def stopMe(): Unit = { @@ -142,7 +188,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( def uninitialized: Receive = { case Initialize(initialMarking, state) => - val uninitialized = Instance.uninitialized[P, T, S](petriNet) + val uninitialized = Instance.uninitialized[S](petriNet) val event = InitializedEvent(initialMarking, state) // persist the initialized event @@ -174,7 +220,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( stopMe() } - def waitForDeleteConfirmation(instance: Instance[P, T, S]): Receive = { + def waitForDeleteConfirmation(instance: Instance[S]): Receive = { case DeleteMessagesSuccess(toSequenceNr) => log.processHistoryDeletionSuccessful(recipeInstanceId, toSequenceNr) context.stop(self) @@ -205,7 +251,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( } } - def running(instance: Instance[P, T, S], + def running(instance: Instance[S], scheduledRetries: Map[Long, Cancellable]): Receive = { case Stop(deleteHistory) => scheduledRetries.values.foreach(_.cancel()) @@ -253,9 +299,25 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( context become running(instance, scheduledRetries)}) case event@TransitionFiredEvent(jobId, transitionId, correlationId, timeStarted, timeCompleted, consumed, produced, output) => + val transition = instance.petriNet.transitions.getById(transitionId) + log.transitionFired(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, transition, jobId, timeStarted, timeCompleted) + // persist the success event + persistEvent(instance, event)( + eventSource.apply(instance) + .andThen(step) + .andThen { + case (updatedInstance, newJobs) => + // the sender is notified of the transition having fired + sender() ! TransitionFired(jobId, transitionId, correlationId, consumed, produced, newJobs.map(_.id), filterIngredientValuesFromEventInstance(output)) + + // the job is removed from the state since it completed + context become running(updatedInstance, scheduledRetries - jobId) + } + ) + case event@TransitionFailedWithOutputEvent(jobId, transitionId, correlationId, timeStarted, timeCompleted, consumed, produced, output) => val transition = instance.petriNet.transitions.getById(transitionId) - log.transitionFired(recipeInstanceId, transition.asInstanceOf[Transition], jobId, timeStarted, timeCompleted) + log.transitionFired(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, transition, jobId, timeStarted, timeCompleted) // persist the success event persistEvent(instance, event)( eventSource.apply(instance) @@ -275,7 +337,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( // persist the event persistEvent(instance, internalEvent)( eventSource.apply(instance) - .andThen { case updatedInstance: Instance[P, T, S] => + .andThen { case updatedInstance: Instance[S] => if (updatedInstance.activeJobs.isEmpty) startIdleStop(updatedInstance.sequenceNr) context become running(updatedInstance, scheduledRetries - jobId) @@ -294,7 +356,10 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( ).marshall val internalEvent = ProcessInstanceEventSourcing.DelayedTransitionFired(jobId, transitionId, produced, out) - log.transitionFired(recipeInstanceId, transition.asInstanceOf[Transition], jobId, System.currentTimeMillis(), System.currentTimeMillis()) + val timestamp = System.currentTimeMillis() + log.transitionFired(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, transition.asInstanceOf[Transition], jobId, timestamp, timestamp) + + LogAndSendEvent.eventFired(EventFired(timestamp, compiledRecipe.name, compiledRecipe.recipeId, recipeInstanceId, out), context.system.eventStream) persistEvent(instance, internalEvent)( eventSource.apply(instance) @@ -303,13 +368,18 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( case (updatedInstance, newJobs) => // the sender is notified of the transition having fired sender() ! TransitionFired(jobId, transitionId, None, null, produced, newJobs.map(_.id), filterIngredientValuesFromEventInstance(out)) + // The DelayedTransition is acknowledged so that it is removed + delayedTransitionActor ! FireDelayedTransitionAck(recipeInstanceId, jobId) + // the job is removed from the state since it completed context become running(updatedInstance, scheduledRetries - jobId) } ) } else { log.warning("Ignoring DelayedTransitionFired since there is no open asyncConsumedMarkings") - instance.copy[P, T, S]( + //The DelayedTransition is acknowledged so that it is removed from the DelayedTransitionManager. + delayedTransitionActor ! FireDelayedTransitionAck(recipeInstanceId, jobId) + instance.copy[S]( sequenceNr = instance.sequenceNr + 1 ) } @@ -318,7 +388,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( val transition = instance.petriNet.transitions.getById(transitionId) - log.transitionFailed(recipeInstanceId, transition.asInstanceOf[Transition], jobId, timeStarted, timeFailed, reason) + log.transitionFailed(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, transition.asInstanceOf[Transition], jobId, timeStarted, timeFailed, reason) strategy match { case RetryWithDelay(delay) => @@ -346,10 +416,10 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( ) case Continue(produced, out) => - val transitionFiredEvent = TransitionFiredEvent( + val TransitionFailedWithOutput = TransitionFailedWithOutputEvent( jobId, transitionId, correlationId, timeStarted, timeFailed, consume, marshallMarking(produced), out) - persistEvent(instance, transitionFiredEvent)( + persistEvent(instance, TransitionFailedWithOutput)( eventSource.apply(instance) .andThen(step) .andThen { case (updatedInstance, newJobs) => @@ -386,7 +456,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( context become running(updatedInstance, scheduledRetries) case (_, Left(reason)) => - log.fireTransitionRejected(recipeInstanceId, transition.asInstanceOf[Transition], reason) + log.fireTransitionRejected(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, transition.asInstanceOf[Transition], reason) sender() ! FireSensoryEventRejection.FiringLimitMet(recipeInstanceId) } @@ -404,8 +474,8 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( val now = System.currentTimeMillis() // the job is updated so it cannot be retried again - val updatedJob: Job[P, T, S] = job.copy(failure = Some(blocked.copy(failureStrategy = internal.ExceptionStrategy.RetryWithDelay(timeout)))) - val updatedInstance: Instance[P, T, S] = instance.copy(jobs = instance.jobs + (jobId -> updatedJob)) + val updatedJob: Job[S] = job.copy(failure = Some(blocked.copy(failureStrategy = internal.ExceptionStrategy.RetryWithDelay(timeout)))) + val updatedInstance: Instance[S] = instance.copy(jobs = instance.jobs + (jobId -> updatedJob)) val originalSender = sender() // execute the job immediately if there is no timeout @@ -433,7 +503,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( // resolving is only allowed if the interaction is blocked by a failure case Some(internal.Job(_, correlationId, _, transition, consumed, _, Some(internal.ExceptionState(_, _, _, internal.ExceptionStrategy.BlockTransition)))) => - val producedMarking: Marking[P] = produce.unmarshall[P](petriNet.places) + val producedMarking: Marking[Place] = produce.unmarshall(petriNet.places) // the provided marking must be valid according to the petri net if (petriNet.outMarking(transition) != producedMarking.multiplicities) @@ -490,7 +560,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( * * It finds which transitions are enabled and executes those. */ - def step(instance: Instance[P, T, S]): (Instance[P, T, S], Set[Job[P, T, S]]) = { + def step(instance: Instance[S]): (Instance[S], Set[Job[S]]) = { runtime.allEnabledJobs.run(instance).value match { case (updatedInstance, jobs) => if (jobs.isEmpty && updatedInstance.activeJobs.isEmpty) @@ -500,23 +570,26 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( } } - def executeJob(job: Job[P, T, S], originalSender: ActorRef): Unit = { - log.fireTransition(recipeInstanceId, job.id, job.transition.asInstanceOf[Transition], System.currentTimeMillis()) + def executeJob(job: Job[S], originalSender: ActorRef): Unit = { + log.fireTransition(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, job.id, job.transition.asInstanceOf[Transition], System.currentTimeMillis()) job.transition match { - case _: EventTransition => - BakerLogging.default.firingEvent(recipeInstanceId, job.id, job.transition.asInstanceOf[Transition], System.currentTimeMillis()) + case eventTransition: EventTransition => + BakerLogging.default.firingEvent(recipeInstanceId, compiledRecipe.recipeId, compiledRecipe.name, job.id, job.transition.asInstanceOf[Transition], System.currentTimeMillis()) executeJobViaExecutor(job, originalSender) - //TODO rewrite if statement, this is a very naive implementation, the interface could be wrong! case i: InteractionTransition if isDelayedInteraction(i) => startDelayedTransition(i, job, originalSender) case i: InteractionTransition if isCheckpointEventInteraction(i) => - val event = jobToFinishedInteraction(job, i.eventsToFire.head.name) + val event: TransitionFiredEvent = jobToFinishedInteraction(job, i.eventsToFire.head.name) + + val currentTime = System.currentTimeMillis() + LogAndSendEvent.eventFired(EventFired(currentTime, compiledRecipe.name, compiledRecipe.recipeId, recipeInstanceId, event.output.asInstanceOf[EventInstance]), context.system.eventStream) + self.tell(event, originalSender) case _ => executeJobViaExecutor(job, originalSender) } } - def executeJobViaExecutor(job: Job[P, T, S], originalSender: ActorRef): Unit = { + def executeJobViaExecutor(job: Job[S], originalSender: ActorRef): Unit = { // context.self can be potentially throw NullPointerException in non graceful shutdown situations Try(context.self).foreach { self => // executes the IO task on the ExecutionContext @@ -529,7 +602,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( } } - def jobToFinishedInteraction(job: Job[P, T, S], outputEventName: String): TransitionFiredEvent = { + def jobToFinishedInteraction(job: Job[S], outputEventName: String): TransitionFiredEvent = { val startTime = System.currentTimeMillis() val outputEvent: EventInstance = EventInstance.apply(outputEventName) com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing.TransitionFiredEvent( @@ -539,7 +612,10 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( startTime, System.currentTimeMillis(), job.consume.marshall, - RecipeRuntime.createProducedMarking(petriNet.outMarking(job.transition).asInstanceOf[MultiSet[Place]], Some(outputEvent)).marshall, + + RecipeRuntime.createProducedMarking( + petriNet.outMarking(job.transition).asInstanceOf[MultiSet[Place]], + Some(outputEvent)).marshall, outputEvent ) } @@ -555,45 +631,18 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( interactionTransition.interactionName.startsWith(checkpointEventInteractionPrefix) } - def startDelayedTransition(interactionTransition: InteractionTransition, job: Job[P, T, S], originalSender: ActorRef): Unit = { + def startDelayedTransition(interactionTransition: InteractionTransition, job: Job[S], originalSender: ActorRef): Unit = { delayedTransitionActor ! ScheduleDelayedTransition( recipeInstanceId, - getWaitTimeInMillis(interactionTransition, job), + getWaitTimeInMillis(interactionTransition, job.processState.asInstanceOf[RecipeInstanceState]), job.id, job.transition.getId, job.consume.marshall, - getOutputEventName(interactionTransition), + getOutputEventName(interactionTransition, log), originalSender) } - private def getOutputEventName(interactionTransition: InteractionTransition): String = { - val outputEvents = interactionTransition.eventsToFire - if (outputEvents.size != 1) - throw new FatalInteractionException(s"Delayed transitions can only have 1 input ingredient") - outputEvents.head.name - } - - private def getWaitTimeInMillis(interactionTransition: InteractionTransition, job: Job[P, T, S]): Long = { - val state = job.processState.asInstanceOf[RecipeInstanceState] - val input: immutable.Seq[IngredientInstance] = RecipeRuntime.createInteractionInput(interactionTransition, state) - if (input.size != 1) - throw new FatalInteractionException(s"Delayed transitions can only have 1 input ingredient") - val scalaMillis = try { - Some(input.head.value.as[FiniteDuration].toMillis) - } catch { - case _: Exception => None - } - scalaMillis.getOrElse( - try { - input.head.value.as[java.time.Duration].toMillis - } catch { - case _: Exception => - throw new FatalInteractionException(s"Delayed transition ingredient not of type scala.concurrent.duration.FiniteDuration or java.time.Duration") - } - ) - } - - def scheduleFailedJobsForRetry(instance: Instance[P, T, S]): Map[Long, Cancellable] = { + def scheduleFailedJobsForRetry(instance: Instance[S]): Map[Long, Cancellable] = { instance.jobs.values.foldLeft(Map.empty[Long, Cancellable]) { case (map, j@Job(_, _, _, _, _, _, Some(internal.ExceptionState(failureTime, _, _, RetryWithDelay(delay))))) => val newDelay = failureTime + delay - System.currentTimeMillis() @@ -611,7 +660,7 @@ class ProcessInstance[P: Identifiable, T: Identifiable, S, E]( } } - override def onRecoveryCompleted(instance: Instance[P, T, S]) = { + override def onRecoveryCompleted(instance: Instance[S]) = { val scheduledRetries = scheduleFailedJobsForRetry(instance) val (updatedInstance, jobs) = step(instance) context become running(updatedInstance, scheduledRetries) diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcing.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcing.scala index 4dbbe4a8a..916dc9b55 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcing.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcing.scala @@ -6,11 +6,12 @@ import akka.persistence.query.scaladsl.CurrentEventsByPersistenceIdQuery import akka.persistence.{PersistentActor, RecoveryCompleted} import akka.sensors.actor.PersistentActorMetrics import akka.stream.scaladsl.Source +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing.Event import com.ing.baker.runtime.akka.actor.process_instance.internal.{ExceptionState, ExceptionStrategy, Instance, Job} import com.ing.baker.runtime.akka.actor.serialization.AkkaSerializerProvider -import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetaDataName +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName import com.ing.baker.runtime.scaladsl.RecipeInstanceState import com.ing.baker.runtime.serialization.Encryption import com.ing.baker.types.{CharArray, MapType, Value} @@ -39,6 +40,19 @@ object ProcessInstanceEventSourcing extends LazyLogging { produced: Marking[Id], output: Any) extends TransitionEvent + /** + * An event describing the fact that a transition failed but was continued with a given event + * This does not consume the input and puts the transition in a blocked state but does create the output. + */ + case class TransitionFailedWithOutputEvent(override val jobId: Long, + override val transitionId: Id, + correlationId: Option[String], + timeStarted: Long, + timeCompleted: Long, + consumed: Marking[Id], + produced: Marking[Id], + output: Any) extends TransitionEvent + /** * An event describing the fact that a transition failed to fire. */ @@ -79,43 +93,64 @@ object ProcessInstanceEventSourcing extends LazyLogging { */ case class MetaDataAdded(metaData: Map[String, String]) extends Event - def apply[P : Identifiable, T : Identifiable, S, E](sourceFn: T => (S => E => S)): Instance[P, T, S] => Event => Instance[P, T, S] = instance => { + def apply[S, E](sourceFn: Transition => (S => E => S)): Instance[S] => Event => Instance[S] = instance => { case InitializedEvent(initial, initialState) => - val initialMarking: Marking[P] = initial.unmarshall(instance.petriNet.places) + val initialMarking: Marking[Place] = initial.unmarshall(instance.petriNet.places) + + Instance[S](instance.petriNet, 1, initialMarking, Map.empty, initialState.asInstanceOf[S], Map.empty, Set.empty) - Instance[P, T, S](instance.petriNet, 1, initialMarking, Map.empty, initialState.asInstanceOf[S], Map.empty, Set.empty) case e: TransitionFiredEvent => val transition = instance.petriNet.transitions.getById(e.transitionId) val newState = sourceFn(transition)(instance.state)(e.output.asInstanceOf[E]) - val consumed: Marking[P] = e.consumed.unmarshall(instance.petriNet.places) - val produced: Marking[P] = e.produced.unmarshall(instance.petriNet.places) + val consumed: Marking[Place] = e.consumed.unmarshall(instance.petriNet.places) + val produced: Marking[Place] = e.produced.unmarshall(instance.petriNet.places) - instance.copy[P, T, S]( + instance.copy[S]( sequenceNr = instance.sequenceNr + 1, marking = (instance.marking |-| consumed) |+| produced, receivedCorrelationIds = instance.receivedCorrelationIds ++ e.correlationId, state = newState, jobs = instance.jobs - e.jobId ) + + case e: TransitionFailedWithOutputEvent => + val transition = instance.petriNet.transitions.getById(e.transitionId) + val newState = sourceFn(transition)(instance.state)(e.output.asInstanceOf[E]) + val consumed: Marking[Place] = e.consumed.unmarshall(instance.petriNet.places) + val produced: Marking[Place] = e.produced.unmarshall(instance.petriNet.places) + + val job = instance.jobs.getOrElse(e.jobId, { + Job[S](e.jobId, e.correlationId, instance.state, transition, consumed, null, None) + }) + val updatedJob: Job[S] = job.copy(failure = Some(ExceptionState(0, 1, "Blocked after FireEvent retry strategy", ExceptionStrategy.BlockTransition))) + + instance.copy[S]( + sequenceNr = instance.sequenceNr + 1, + marking = instance.marking |+| produced, + receivedCorrelationIds = instance.receivedCorrelationIds ++ e.correlationId, + state = newState, + jobs = instance.jobs + (job.id -> updatedJob) + ) + case e: TransitionFailedEvent => val transition = instance.petriNet.transitions.getById(e.transitionId) - val consumed: Marking[P] = e.consume.unmarshall(instance.petriNet.places) + val consumed: Marking[Place] = e.consume.unmarshall(instance.petriNet.places) val job = instance.jobs.getOrElse(e.jobId, { - Job[P, T, S](e.jobId, e.correlationId, instance.state, transition, consumed, e.input, None) + Job[S](e.jobId, e.correlationId, instance.state, transition, consumed, e.input, None) }) val failureCount = job.failureCount + 1 - val updatedJob: Job[P, T, S] = job.copy(failure = Some(ExceptionState(e.timeFailed, failureCount, e.failureReason, e.exceptionStrategy))) - instance.copy[P, T, S](jobs = instance.jobs + (job.id -> updatedJob)) + val updatedJob: Job[S] = job.copy(failure = Some(ExceptionState(e.timeFailed, failureCount, e.failureReason, e.exceptionStrategy))) + instance.copy[S](jobs = instance.jobs + (job.id -> updatedJob)) case e: TransitionDelayed => - val consumed: Marking[P] = e.consumed.unmarshall(instance.petriNet.places) + val consumed: Marking[Place] = e.consumed.unmarshall(instance.petriNet.places) val delayedInstanceCount: Int = instance.delayedTransitionIds.getOrElse(e.transitionId, 0) - instance.copy[P, T, S]( + instance.copy[S]( sequenceNr = instance.sequenceNr + 1, marking = (instance.marking |-| consumed), delayedTransitionIds = instance.delayedTransitionIds + (e.transitionId -> (delayedInstanceCount + 1)), //Claim the consumed tokens @@ -124,11 +159,11 @@ object ProcessInstanceEventSourcing extends LazyLogging { case e: DelayedTransitionFired => val delayedInstanceCount: Int = instance.delayedTransitionIds(e.transitionId) - val produced: Marking[P] = e.produced.unmarshall(instance.petriNet.places) + val produced: Marking[Place] = e.produced.unmarshall(instance.petriNet.places) val transition = instance.petriNet.transitions.getById(e.transitionId) val newState = sourceFn(transition)(instance.state)(e.output.asInstanceOf[E]) - instance.copy[P, T, S]( + instance.copy[S]( sequenceNr = instance.sequenceNr + 1, marking = instance.marking |+| produced, delayedTransitionIds = instance.delayedTransitionIds + (e.transitionId -> (delayedInstanceCount - 1)), @@ -138,46 +173,33 @@ object ProcessInstanceEventSourcing extends LazyLogging { case e: MetaDataAdded => val newState: S = instance.state match { case state: RecipeInstanceState => - val newBakerMetaData: Map[String, String] = - state.ingredients.get(RecipeInstanceMetaDataName) match { - case Some(value) => - if(value.isInstanceOf(MapType(CharArray))) { - val oldMetaData: Map[String, String] = value.asMap[String, String](classOf[String], classOf[String]).asScala.toMap - oldMetaData ++ e.metaData - } - else { - //If the old metadata is not of Type[String, String] we overwrite it since this is not allowed. - logger.info("Old RecipeInstanceMetaData was not of type Map[String, String]") - e.metaData - } - case None => - e.metaData - } - val newIngredients: Map[String, Value] = - state.ingredients + (RecipeInstanceMetaDataName -> com.ing.baker.types.Converters.toValue(newBakerMetaData)) - state.copy(ingredients = newIngredients).asInstanceOf[S] + val newRecipeInstanceMetaData: Map[String, String] = state.recipeInstanceMetadata ++ e.metaData + //We still add an ingredient for the metaData since this makes it easier to use it during interaction execution + val newIngredients: Map[String, Value] = state.ingredients + + (RecipeInstanceMetadataName -> com.ing.baker.types.Converters.toValue(newRecipeInstanceMetaData)) + state.copy(ingredients = newIngredients, recipeInstanceMetadata = newRecipeInstanceMetaData).asInstanceOf[S] case state => state } - instance.copy[P, T, S](state = newState) + instance.copy[S](state = newState) } - def eventsForInstance[P : Identifiable, T : Identifiable, S, E]( + def eventsForInstance[S, E]( processTypeName: String, recipeInstanceId: String, - topology: PetriNet[P, T], + topology: PetriNet, encryption: Encryption, readJournal: CurrentEventsByPersistenceIdQuery, - eventSourceFn: T => (S => E => S))(implicit actorSystem: ActorSystem): Source[(Instance[P, T, S], Event), NotUsed] = { + eventSourceFn: Transition => (S => E => S))(implicit actorSystem: ActorSystem): Source[(Instance[S], Event), NotUsed] = { - val serializer = new ProcessInstanceSerialization[P, T, S, E](AkkaSerializerProvider(actorSystem, encryption)) + val serializer = new ProcessInstanceSerialization[S, E](AkkaSerializerProvider(actorSystem, encryption)) val persistentId = ProcessInstance.recipeInstanceId2PersistenceId(processTypeName, recipeInstanceId) val src = readJournal.currentEventsByPersistenceId(persistentId, 0, Long.MaxValue) - val eventSource = ProcessInstanceEventSourcing.apply[P, T, S, E](eventSourceFn) + val eventSource = ProcessInstanceEventSourcing.apply[S, E](eventSourceFn) // TODO: remove null value - src.scan[(Instance[P, T, S], Event)]((Instance.uninitialized[P, T, S](topology), null.asInstanceOf[Event])) { + src.scan[(Instance[S], Event)]((Instance.uninitialized[S](topology), null.asInstanceOf[Event])) { case ((instance, _), e) => val serializedEvent = e.event.asInstanceOf[AnyRef] val deserializedEvent = serializer.deserializeEvent(serializedEvent)(instance) @@ -187,26 +209,26 @@ object ProcessInstanceEventSourcing extends LazyLogging { } } -abstract class ProcessInstanceEventSourcing[P : Identifiable, T : Identifiable, S, E]( - val petriNet: PetriNet[P, T], +abstract class ProcessInstanceEventSourcing[S, E]( + val petriNet: PetriNet, encryption: Encryption, - eventSourceFn: T => (S => E => S)) extends PersistentActor with PersistentActorMetrics { + eventSourceFn: Transition => (S => E => S)) extends PersistentActor with PersistentActorMetrics { protected implicit val system: ActorSystem = context.system - protected val eventSource: Instance[P, T, S] => Event => Instance[P, T, S] = - ProcessInstanceEventSourcing.apply[P, T, S, E](eventSourceFn) + protected val eventSource: Instance[S] => Event => Instance[S] = + ProcessInstanceEventSourcing.apply[S, E](eventSourceFn) - private val serializer = new ProcessInstanceSerialization[P, T, S, E](AkkaSerializerProvider(system, encryption)) + private val serializer = new ProcessInstanceSerialization[S, E](AkkaSerializerProvider(system, encryption)) - def onRecoveryCompleted(state: Instance[P, T, S]): Unit + def onRecoveryCompleted(state: Instance[S]): Unit - def persistEvent[O](instance: Instance[P, T, S], e: Event)(fn: Event => O): Unit = { + def persistEvent[O](instance: Instance[S], e: Event)(fn: Event => O): Unit = { val serializedEvent = serializer.serializeEvent(e)(instance) persist(serializedEvent) { persisted => fn(e) } } - private var recoveringState: Instance[P, T, S] = Instance.uninitialized[P, T, S](petriNet) + private var recoveringState: Instance[S] = Instance.uninitialized[S](petriNet) private def applyToRecoveringState(e: AnyRef): Unit = { val deserializedEvent = serializer.deserializeEvent(e)(recoveringState) @@ -216,6 +238,7 @@ abstract class ProcessInstanceEventSourcing[P : Identifiable, T : Identifiable, override def receiveRecover: Receive = { case e: protobuf.Initialized => applyToRecoveringState(e) case e: protobuf.TransitionFired => applyToRecoveringState(e) + case e: protobuf.TransitionFailedWithOutput => applyToRecoveringState(e) case e: protobuf.TransitionFailed => applyToRecoveringState(e) case e: protobuf.TransitionDelayed => applyToRecoveringState(e) case e: protobuf.DelayedTransitionFired => applyToRecoveringState(e) diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceLogger.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceLogger.scala index 8072a9eee..b47643716 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceLogger.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceLogger.scala @@ -51,11 +51,18 @@ object ProcessInstanceLogger { log.errorWithMDC(msg, mdc, cause) } - def fireTransition(recipeInstanceId: String, jobId: Long, transition: Transition, timeStarted: Long): Unit = { + def fireTransition(recipeInstanceId: String, + recipeId: String, + recipeName: String, + jobId: Long, + transition: Transition, + timeStarted: Long): Unit = { val mdc = Map( "processEvent" -> "FiringTransition", "processId" -> recipeInstanceId, "recipeInstanceId" -> recipeInstanceId, + "recipeName" -> recipeName, + "recipeId" -> recipeId, "jobId" -> jobId, "transitionId" -> transition.label, "timeStarted" -> timeStarted @@ -65,6 +72,8 @@ object ProcessInstanceLogger { } def transitionFired(recipeInstanceId: String, + recipeId: String, + recipeName: String, transition: Transition, jobId: Long, timeStarted: Long, @@ -73,6 +82,8 @@ object ProcessInstanceLogger { val mdc = Map( "processEvent" -> "TransitionFired", "processId" -> recipeInstanceId, + "recipeName" -> recipeName, + "recipeId" -> recipeId, "recipeInstanceId" -> recipeInstanceId, "jobId" -> jobId, "transitionId" -> transition.label, @@ -86,6 +97,8 @@ object ProcessInstanceLogger { } def transitionFailed(recipeInstanceId: String, + recipeId: String, + recipeName: String, transition: Transition, jobId: Long, timeStarted: Long, @@ -95,6 +108,8 @@ object ProcessInstanceLogger { "processEvent" -> "TransitionFailed", "processId" -> recipeInstanceId, "recipeInstanceId" -> recipeInstanceId, + "recipeName" -> recipeName, + "recipeId" -> recipeId, "jobId" -> jobId, "timeStarted" -> timeStarted, "timeFailed" -> timeFailed, @@ -107,10 +122,16 @@ object ProcessInstanceLogger { log.logWithMDC(Logging.ErrorLevel, msg, mdc) } - def fireTransitionRejected(recipeInstanceId: String, transition: Transition, rejectReason: String): Unit = { + def fireTransitionRejected(recipeInstanceId: String, + recipeId: String, + recipeName: String, + transition: Transition, + rejectReason: String): Unit = { val mdc = Map( "processEvent" -> "FireTransitionRejected", "recipeInstanceEvent" -> "FireInteractionRejected", + "recipeName" -> recipeName, + "recipeId" -> recipeId, "processId" -> recipeInstanceId, "recipeInstanceId" -> recipeInstanceId, "transitionId" -> transition.label, diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceProtocol.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceProtocol.scala index 0baf540a3..311e73722 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceProtocol.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceProtocol.scala @@ -1,5 +1,6 @@ package com.ing.baker.runtime.akka.actor.process_instance +import com.ing.baker.il.petrinet.Place import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka.actor.serialization.BakerSerializable import com.ing.baker.types.Value @@ -40,9 +41,9 @@ object ProcessInstanceProtocol { object Initialize { - def apply[P : Identifiable](marking: Marking[P]): Initialize = Initialize(marking.marshall, null) + def apply[P : Identifiable](marking: Marking[Place]): Initialize = Initialize(marking.marshall, null) - def apply[P : Identifiable](marking: Marking[P], state: Any): Initialize = Initialize(marking.marshall, state) + def apply[P : Identifiable](marking: Marking[Place], state: Any): Initialize = Initialize(marking.marshall, state) } /** @@ -102,9 +103,9 @@ object ProcessInstanceProtocol { object Initialized { - def apply[P : Identifiable](marking: Marking[P]): Initialized = Initialized(marking.marshall, null) + def apply[P : Identifiable](marking: Marking[Place]): Initialized = Initialized(marking.marshall, null) - def apply[P : Identifiable](marking: Marking[P], state: Any): Initialized = Initialized(marking.marshall, state) + def apply[P : Identifiable](marking: Marking[Place], state: Any): Initialized = Initialized(marking.marshall, state) } /** diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceRuntime.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceRuntime.scala index 2d42d4a51..7547a0477 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceRuntime.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceRuntime.scala @@ -1,9 +1,9 @@ package com.ing.baker.runtime.akka.actor.process_instance import java.io.{PrintWriter, StringWriter} - import cats.data.State import cats.effect.IO +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing._ @@ -21,7 +21,7 @@ import org.slf4j.{Logger, LoggerFactory} * @tparam S The state type * @tparam E The event type */ -trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { +trait ProcessInstanceRuntime[S, E] extends LazyLogging { val log: Logger = LoggerFactory.getLogger("com.ing.baker.runtime.core.actor.process_instance.ProcessInstanceRuntime") @@ -30,26 +30,26 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { * * By default the identity function is used. */ - val eventSource: T => S => E => S = _ => s => _ => s + val eventSource: Transition => S => E => S = _ => s => _ => s /** * This function is called when a transition throws an exception. * * By default the transition is blocked. */ - def handleException(job: Job[P, T, S])(throwable: Throwable, failureCount: Int, startTime: Long, outMarking: MultiSet[P]): ExceptionStrategy = BlockTransition + def handleException(job: Job[S])(throwable: Throwable, failureCount: Int, startTime: Long, outMarking: MultiSet[Place]): ExceptionStrategy = BlockTransition /** * Returns the task that should be executed for a transition. */ - def transitionTask(petriNet: PetriNet[P, T], t: T)(marking: Marking[P], state: S, input: Any): IO[(Marking[P], E)] + def transitionTask(petriNet: PetriNet, t: Transition)(marking: Marking[Place], state: S, input: Any): IO[(Marking[Place], E)] /** * Checks if a transition can be fired automatically by the runtime (not triggered by some outside input). * * By default, cold transitions (without in adjacent places) are not fired automatically */ - def canBeFiredAutomatically(instance: Instance[P, T, S], t: T): Boolean = instance.petriNet.incomingPlaces(t).nonEmpty + def canBeFiredAutomatically(instance: Instance[S], t: Transition): Boolean = instance.petriNet.incomingPlaces(t).nonEmpty /** * Defines which tokens from a marking for a particular place are consumable by a transition. @@ -58,7 +58,7 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { * * You can override this for example in case you use a colored (data) petri net model with filter rules on the edges. */ - def consumableTokens(petriNet: PetriNet[P, T])(marking: Marking[P], p: P, t: T): MultiSet[Any] = marking.getOrElse(p, MultiSet.empty) + def consumableTokens(petriNet: PetriNet)(marking: Marking[Place], p: Place, t: Transition): MultiSet[Any] = marking.getOrElse(p, MultiSet.empty) /** * Takes a Job specification, executes it and returns a TransitionEvent (asychronously using cats.effect.IO) @@ -71,7 +71,7 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { * However, since that is not used this can be refactored to a simple function: Job -> TransitionEvent * */ - def jobExecutor(topology: PetriNet[P, T])(implicit transitionIdentifier: Identifiable[T], placeIdentifier: Identifiable[P]): Job[P, T, S] => IO[TransitionEvent] = { + def jobExecutor(topology: PetriNet)(implicit transitionIdentifier: Identifiable[Transition], placeIdentifier: Identifiable[Place]): Job[S] => IO[TransitionEvent] = { def exceptionStackTrace(e: Throwable): String = e match { case _: RemoteInteractionExecutionException => e.getMessage @@ -109,10 +109,10 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { } } - def enabledParameters(petriNet: PetriNet[P, T])(m: Marking[P]): Map[T, Iterable[Marking[P]]] = + def enabledParameters(petriNet: PetriNet)(m: Marking[Place]): Map[Transition, Iterable[Marking[Place]]] = enabledTransitions(petriNet)(m).view.map(t => t -> consumableMarkings(petriNet)(m, t)).toMap - def consumableMarkings(petriNet: PetriNet[P, T])(marking: Marking[P], t: T): Iterable[Marking[P]] = { + def consumableMarkings(petriNet: PetriNet)(marking: Marking[Place], t: Transition): Iterable[Marking[Place]] = { // TODO this is not the most efficient, should break early when consumable tokens < edge weight val consumable = petriNet.inMarking(t).map { case (place, count) => (place, count, consumableTokens(petriNet)(marking, place, t)) @@ -134,18 +134,18 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { /** * Checks whether a transition is 'enabled' in a marking. */ - def isEnabled(petriNet: PetriNet[P, T])(marking: Marking[P], t: T): Boolean = consumableMarkings(petriNet)(marking, t).nonEmpty + def isEnabled(petriNet: PetriNet)(marking: Marking[Place], t: Transition): Boolean = consumableMarkings(petriNet)(marking, t).nonEmpty /** * Returns all enabled transitions for a marking. */ - def enabledTransitions(petriNet: PetriNet[P, T])(marking: Marking[P]): Iterable[T] = + def enabledTransitions(petriNet: PetriNet)(marking: Marking[Place]): Iterable[Transition] = petriNet.transitions.filter(t => consumableMarkings(petriNet)(marking, t).nonEmpty) /** * Creates a job for a specific transition with input, computes the marking it should consume */ - def createJob(transition: T, input: Any, correlationId: Option[String] = None): State[Instance[P, T, S], Either[String, Job[P, T, S]]] = + def createJob(transition: Transition, input: Any, correlationId: Option[String] = None): State[Instance[S], Either[String, Job[S]]] = State {instance => if (instance.isBlocked(transition)) (instance, Left("Transition is blocked by a previous failure")) @@ -154,8 +154,8 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { case None => (instance, Left(s"Not enough consumable tokens. This might have been caused because the event has already been fired up the the firing limit but the recipe requires more instances of the event, use withSensoryEventNoFiringLimit or increase the amount of firing limit on the recipe if such behaviour is desired")) case Some(params) => - val job = Job[P, T, S](instance.nextJobId(), correlationId, instance.state, transition, params.head, input) - val updatedInstance = instance.copy[P, T, S](jobs = instance.jobs + (job.id -> job)) + val job = Job[S](instance.nextJobId(), correlationId, instance.state, transition, params.head, input) + val updatedInstance = instance.copy[S](jobs = instance.jobs + (job.id -> job)) (updatedInstance, Right(job)) } } @@ -163,20 +163,20 @@ trait ProcessInstanceRuntime[P, T, S, E] extends LazyLogging { /** * Finds the (optional) first transition that is enabled and can be fired automatically */ - def firstEnabledJob: State[Instance[P, T, S], Option[Job[P, T, S]]] = State { instance => + def firstEnabledJob: State[Instance[S], Option[Job[S]]] = State { instance => enabledParameters(instance.petriNet)(instance.availableMarking).find { case (t, _) => !instance.isBlocked(t) && canBeFiredAutomatically(instance, t) }.map { case (t, markings) => - val job = Job[P, T, S](instance.nextJobId(), None, instance.state, t, markings.head, null) - (instance.copy[P, T, S](jobs = instance.jobs + (job.id -> job)), Some(job)) + val job = Job[S](instance.nextJobId(), None, instance.state, t, markings.head, null) + (instance.copy[S](jobs = instance.jobs + (job.id -> job)), Some(job)) }.getOrElse((instance, None)) } /** * Finds all automated enabled transitions. */ - def allEnabledJobs: State[Instance[P, T, S], Set[Job[P, T, S]]] = + def allEnabledJobs: State[Instance[S], Set[Job[S]]] = firstEnabledJob.flatMap { case None => State.pure(Set.empty) case Some(job) => allEnabledJobs.map(_ + job) diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSerialization.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSerialization.scala index 398256a03..845cb9955 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSerialization.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSerialization.scala @@ -21,7 +21,7 @@ import scala.util.{Failure, Success} * * (which is generated by scalaPB and serializes to protobuf) */ -class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](provider: AkkaSerializerProvider) { +class ProcessInstanceSerialization[S, E](provider: AkkaSerializerProvider) { implicit private val p: AkkaSerializerProvider = provider @@ -29,9 +29,10 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro * De-serializes a persistence.protobuf.Event to a EvenSourcing.Event. An Instance is required to 'wire' or 'reference' * the message back into context. */ - def deserializeEvent(event: AnyRef): Instance[P, T, S] => ProcessInstanceEventSourcing.Event = event match { + def deserializeEvent(event: AnyRef): Instance[S] => ProcessInstanceEventSourcing.Event = event match { case e: protobuf.Initialized => deserializeInitialized(e) case e: protobuf.TransitionFired => deserializeTransitionFired(e) + case e: protobuf.TransitionFailedWithOutput => deserializeTransitionFailedWithOutput(e) case e: protobuf.TransitionDelayed => deserializeTransitionDelayed(e) case e: protobuf.DelayedTransitionFired => deserializeDelayedTransitionFired(e) case e: protobuf.TransitionFailed => deserializeTransitionFailed(e) @@ -42,10 +43,11 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro /** * Serializes an EventSourcing.Event to a persistence.protobuf.Event. */ - def serializeEvent(e: ProcessInstanceEventSourcing.Event): Instance[P, T, S] => AnyRef = + def serializeEvent(e: ProcessInstanceEventSourcing.Event): Instance[S] => AnyRef = _ => e match { case e: InitializedEvent => serializeInitialized(e) case e: TransitionFiredEvent => serializeTransitionFired(e) + case e: TransitionFailedWithOutputEvent => serializeTransitionFailedWithOutput(e) case e: TransitionDelayed => serializeTransitionDelayed(e) case e: DelayedTransitionFired => serializeDelayedTransitionFired(e) case e: TransitionFailedEvent => serializeTransitionFailed(e) @@ -67,7 +69,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro case Failure(exception) => throw exception } - private def deserializeProducedMarking(instance: Instance[P, T, S], produced: Seq[ProducedToken]): Marking[Id] = { + private def deserializeProducedMarking(instance: Instance[S], produced: Seq[ProducedToken]): Marking[Id] = { produced.foldLeft(Marking.empty[Long]) { case (accumulated, ProducedToken(Some(placeId), Some(_), Some(count), data)) => val value = data.map(deserializeObject).orNull // In the colored petrinet, tokens have values and they could be null. @@ -100,7 +102,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro } } - private def deserializeConsumedMarking(instance: Instance[P, T, S], persisted: Seq[protobuf.ConsumedToken]): Marking[Id] = { + private def deserializeConsumedMarking(instance: Instance[S], persisted: Seq[protobuf.ConsumedToken]): Marking[Id] = { persisted.foldLeft(Marking.empty[Long]) { case (accumulated, protobuf.ConsumedToken(Some(placeId), Some(tokenId), Some(count))) => val place = instance.petriNet.places.getById(placeId, "place in the petrinet") @@ -113,7 +115,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro } } - private def deserializeInitialized(e: protobuf.Initialized)(instance: Instance[P, T, S]): InitializedEvent = { + private def deserializeInitialized(e: protobuf.Initialized)(instance: Instance[S]): InitializedEvent = { val initialMarking = deserializeProducedMarking(instance, e.initialMarking) // TODO not really safe to return null here, throw exception ? val initialState = e.initialState.map(deserializeObject).orNull @@ -126,7 +128,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro protobuf.Initialized(initialMarking, initialState) } - private def deserializeTransitionFailed(e: protobuf.TransitionFailed): Instance[P, T, S] => TransitionFailedEvent = { + private def deserializeTransitionFailed(e: protobuf.TransitionFailed): Instance[S] => TransitionFailedEvent = { instance => val jobId = e.jobId.getOrElse(missingFieldException("job_id")) @@ -166,7 +168,6 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro } private def serializeTransitionFired(e: TransitionFiredEvent): protobuf.TransitionFired = { - val consumedTokens = serializeConsumedMarking(e.consumed) val producedTokens = serializeProducedMarking(e.produced) @@ -181,6 +182,21 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro ) } + private def serializeTransitionFailedWithOutput(e: TransitionFailedWithOutputEvent): protobuf.TransitionFailedWithOutput = { + val consumedTokens = serializeConsumedMarking(e.consumed) + val producedTokens = serializeProducedMarking(e.produced) + + protobuf.TransitionFailedWithOutput( + jobId = Some(e.jobId), + transitionId = Some(e.transitionId), + timeStarted = Some(e.timeStarted), + timeCompleted = Some(e.timeCompleted), + consumed = consumedTokens, + produced = producedTokens, + data = serializeObject(e.output) + ) + } + private def serializeTransitionDelayed(e: TransitionDelayed): protobuf.TransitionDelayed = { val consumedTokens = serializeConsumedMarking(e.consumed) protobuf.TransitionDelayed( @@ -202,8 +218,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro ) } - private def deserializeTransitionFired(e: protobuf.TransitionFired): Instance[P, T, S] => TransitionFiredEvent = instance => { - + private def deserializeTransitionFired(e: protobuf.TransitionFired): Instance[S] => TransitionFiredEvent = instance => { val consumed: Marking[Id] = deserializeConsumedMarking(instance, e.consumed) val produced: Marking[Id] = deserializeProducedMarking(instance, e.produced) @@ -217,7 +232,21 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro TransitionFiredEvent(jobId, transitionId, e.correlationId, timeStarted, timeCompleted, consumed, produced, output) } - private def deserializeTransitionDelayed(e: protobuf.TransitionDelayed): Instance[P, T, S] => TransitionDelayed = instance => { + private def deserializeTransitionFailedWithOutput(e: protobuf.TransitionFailedWithOutput): Instance[S] => TransitionFailedWithOutputEvent = instance => { + val consumed: Marking[Id] = deserializeConsumedMarking(instance, e.consumed) + val produced: Marking[Id] = deserializeProducedMarking(instance, e.produced) + + val output = e.data.map(deserializeObject).orNull + + val transitionId = e.transitionId.getOrElse(missingFieldException("transition_id")) + val jobId = e.jobId.getOrElse(missingFieldException("job_id")) + val timeStarted = e.timeStarted.getOrElse(missingFieldException("time_started")) + val timeCompleted = e.timeCompleted.getOrElse(missingFieldException("time_completed")) + + TransitionFailedWithOutputEvent(jobId, transitionId, e.correlationId, timeStarted, timeCompleted, consumed, produced, output) + } + + private def deserializeTransitionDelayed(e: protobuf.TransitionDelayed): Instance[S] => TransitionDelayed = instance => { val consumed: Marking[Id] = deserializeConsumedMarking(instance, e.consumed) val transitionId = e.transitionId.getOrElse(missingFieldException("transition_id")) @@ -225,7 +254,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro TransitionDelayed(jobId, transitionId, consumed) } - private def deserializeDelayedTransitionFired(e: protobuf.DelayedTransitionFired): Instance[P, T, S] => DelayedTransitionFired = instance => { + private def deserializeDelayedTransitionFired(e: protobuf.DelayedTransitionFired): Instance[S] => DelayedTransitionFired = instance => { val produced: Marking[Id] = deserializeProducedMarking(instance, e.produced) @@ -246,7 +275,7 @@ class ProcessInstanceSerialization[P : Identifiable, T : Identifiable, S, E](pro } private def deserializeMetaDataAdded(e: protobuf.MetaDataAdded): - Instance[P, T, S] => com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing.MetaDataAdded = instance => { + Instance[S] => com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing.MetaDataAdded = instance => { com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing.MetaDataAdded( e.metaData.map(record => (record.key.get -> record.value.get)).toMap ) diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/ExceptionStrategy.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/ExceptionStrategy.scala index dd332c1ec..23320d641 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/ExceptionStrategy.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/ExceptionStrategy.scala @@ -16,7 +16,7 @@ object ExceptionStrategy { require(delay >= 0, "Delay must be greater then zero") } - case class Continue[P, O](marking: Marking[P], output: O) extends ExceptionStrategy + case class Continue[X, O](marking: Marking[X], output: O) extends ExceptionStrategy } sealed trait ExceptionStrategy \ No newline at end of file diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Instance.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Instance.scala index b5668f35d..55be53fe8 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Instance.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Instance.scala @@ -1,29 +1,30 @@ package com.ing.baker.runtime.akka.actor.process_instance.internal +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api._ import scala.util.Random object Instance { - def uninitialized[P, T, S](process: PetriNet[P, T]): Instance[P, T, S] = Instance[P, T, S](process, 0, Marking.empty, Map.empty, null.asInstanceOf[S], Map.empty, Set.empty) + def uninitialized[S](process: PetriNet): Instance[S] = Instance[S](process, 0, Marking.empty, Map.empty, null.asInstanceOf[S], Map.empty, Set.empty) } /** * Keeps the state of a petri net instance. */ -case class Instance[P, T, S]( - petriNet: PetriNet[P, T], +case class Instance[S]( + petriNet: PetriNet, sequenceNr: Long, - marking: Marking[P], + marking: Marking[Place], delayedTransitionIds: Map[Id, Int], state: S, - jobs: Map[Long, Job[P, T, S]], + jobs: Map[Long, Job[S]], receivedCorrelationIds: Set[String]) { /** * The marking that is already used by running jobs */ - lazy val reservedMarking: Marking[P] = jobs.map { + lazy val reservedMarking: Marking[Place] = jobs.map { case (id, job) => job.consume } .reduceOption(_ |+| _) .getOrElse(Marking.empty) @@ -31,17 +32,17 @@ case class Instance[P, T, S]( /** * The marking that is available for new jobs */ - lazy val availableMarking: Marking[P] = marking |-| reservedMarking + lazy val availableMarking: Marking[Place] = marking |-| reservedMarking /** * The active jobs for the process instance. */ - def activeJobs: Iterable[Job[P, T, S]] = jobs.values.filter(_.isActive) + def activeJobs: Iterable[Job[S]] = jobs.values.filter(_.isActive) /** * Checks whether a transition is blocked by a previous failure. */ - def isBlocked(transition: T): Boolean = jobs.values.collectFirst { + def isBlocked(transition: Transition): Boolean = jobs.values.collectFirst { case Job(_, _, _, `transition`, _, _, Some(ExceptionState(_, _, reason, _))) => s"Transition '$transition' is blocked because it failed previously with: $reason" }.isDefined diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Job.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Job.scala index 80f2a768f..8c7dbc6b8 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Job.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/process_instance/internal/Job.scala @@ -1,19 +1,20 @@ package com.ing.baker.runtime.akka.actor.process_instance.internal +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api.Marking import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.RetryWithDelay /** * A Job encapsulates all the parameters that make a firing transition in a petri net. */ -case class Job[P, T, S]( - id: Long, - correlationId: Option[String], - processState: S, - transition: T, - consume: Marking[P], - input: Any, - failure: Option[ExceptionState] = None) { +case class Job[S]( + id: Long, + correlationId: Option[String], + processState: S, + transition: Transition, + consume: Marking[Place], + input: Any, + failure: Option[ExceptionState] = None) { def isActive: Boolean = failure match { case Some(ExceptionState(_, _, _, RetryWithDelay(_))) => true diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/serialization/BakerTypedProtobufSerializer.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/serialization/BakerTypedProtobufSerializer.scala index e0d27afab..1f75c0f4e 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/serialization/BakerTypedProtobufSerializer.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/actor/serialization/BakerTypedProtobufSerializer.scala @@ -13,7 +13,7 @@ import com.ing.baker.runtime.akka.actor.recipe_manager.{RecipeManagerActor, Reci import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeEventMetadata, RecipeInstanceState} import com.ing.baker.runtime.serialization.ProtoMap import SerializedDataProto._ -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.{DelayedTransitionExecuted, DelayedTransitionInstance, DelayedTransitionScheduled} +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.{DelayedTransitionExecuted, DelayedTransitionInstance, DelayedTransitionScheduled, DelayedTransitionSnapshot} import com.ing.baker.runtime.akka.actor.serialization.TypedProtobufSerializer.{BinarySerializable, forType} object BakerTypedProtobufSerializer { @@ -147,6 +147,8 @@ object BakerTypedProtobufSerializer { .register("ProcessInstanceProtocol.MetaDataAdded"), forType[com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFired] .register("TransitionFired")(ProtoMap.identityProtoMap(com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFired)), + forType[com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFailedWithOutput] + .register("TransitionFailedWithOutput")(ProtoMap.identityProtoMap(com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFailedWithOutput)), forType[com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFailed] .register("TransitionFailed")(ProtoMap.identityProtoMap(com.ing.baker.runtime.akka.actor.process_instance.protobuf.TransitionFailed)), forType[com.ing.baker.runtime.akka.actor.process_instance.protobuf.Initialized] @@ -186,7 +188,9 @@ object BakerTypedProtobufSerializer { forType[DelayedTransitionScheduled] .register("DelayedTransitionScheduled"), forType[DelayedTransitionExecuted] - .register("DelayedTransitionExecuted") + .register("DelayedTransitionExecuted"), + forType[DelayedTransitionSnapshot] + .register("DelayedTransitionSnapshot") ) } } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/internal/RecipeRuntime.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/internal/RecipeRuntime.scala index 7cd97d9e1..59370c86d 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/internal/RecipeRuntime.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/internal/RecipeRuntime.scala @@ -1,13 +1,13 @@ package com.ing.baker.runtime.akka.internal -import java.lang.reflect.InvocationTargetException import akka.event.EventStream import cats.effect.{ContextShift, IO} import com.ing.baker.il import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome +import com.ing.baker.il.petrinet.Place.IngredientPlace import com.ing.baker.il.petrinet._ import com.ing.baker.il.{CompiledRecipe, IngredientDescriptor} -import com.ing.baker.petrinet.api._ +import com.ing.baker.petrinet.api.{MultiSet, _} import com.ing.baker.runtime.akka.actor.logging.LogAndSendEvent import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceRuntime import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.{BlockTransition, Continue, RetryWithDelay} @@ -15,10 +15,10 @@ import com.ing.baker.runtime.akka.actor.process_instance.internal._ import com.ing.baker.runtime.akka.internal.RecipeRuntime._ import com.ing.baker.runtime.model.InteractionManager import com.ing.baker.runtime.scaladsl._ -import com.ing.baker.types.{PrimitiveValue, Value} -import org.slf4j.MDC +import com.ing.baker.types.{ListValue, PrimitiveValue, RecordValue, Value} -import scala.collection.immutable.Seq +import java.lang.reflect.InvocationTargetException +import scala.collection.immutable.{Map, Seq} import scala.concurrent.ExecutionContext object RecipeRuntime { @@ -94,11 +94,24 @@ object RecipeRuntime { def createInteractionInput(interaction: InteractionTransition, state: RecipeInstanceState): Seq[IngredientInstance] = { // the process id is a special ingredient that is always available - val recipeInstanceId: (String, Value) = il.recipeInstanceIdName -> PrimitiveValue(state.recipeInstanceId.toString) - val processId: (String, Value) = il.processIdName -> PrimitiveValue(state.recipeInstanceId.toString) + val recipeInstanceId: (String, Value) = il.recipeInstanceIdName -> PrimitiveValue(state.recipeInstanceId) + + // Only map the recipeInstanceEventList if is it required, otherwise give an empty list + val recipeInstanceEventList: (String, Value) = + if(interaction.requiredIngredients.exists(_.name == il.recipeInstanceEventListName)) + il.recipeInstanceEventListName -> ListValue(state.events.map(e => PrimitiveValue(e.name)).toList) + else + il.recipeInstanceEventListName -> ListValue(List()) + + val processId: (String, Value) = il.processIdName -> PrimitiveValue(state.recipeInstanceId) // a map of all ingredients, the order is important, the predefined parameters and recipeInstanceId have precedence over the state ingredients. - val allIngredients: Map[String, Value] = state.ingredients ++ interaction.predefinedParameters + recipeInstanceId + processId + val allIngredients: Map[String, Value] = + state.ingredients ++ + interaction.predefinedParameters + + recipeInstanceId + + processId + + recipeInstanceEventList // arranges the ingredients in the expected order interaction.requiredIngredients.map { @@ -120,13 +133,14 @@ object RecipeRuntime { } } -class RecipeRuntime(recipe: CompiledRecipe, interactionManager: InteractionManager[IO], eventStream: EventStream)(implicit ec: ExecutionContext) extends ProcessInstanceRuntime[Place, Transition, RecipeInstanceState, EventInstance] { +class RecipeRuntime(recipe: CompiledRecipe, interactionManager: InteractionManager[IO], eventStream: EventStream)(implicit ec: ExecutionContext) extends ProcessInstanceRuntime[RecipeInstanceState, EventInstance] { protected implicit lazy val contextShift: ContextShift[IO] = IO.contextShift(ec) + /** * All transitions except sensory event interactions are auto-fireable by the runtime */ - override def canBeFiredAutomatically(instance: Instance[Place, Transition, RecipeInstanceState], t: Transition): Boolean = t match { + override def canBeFiredAutomatically(instance: Instance[RecipeInstanceState], t: Transition): Boolean = t match { case EventTransition(_, true, _) => false case _ => true } @@ -134,7 +148,7 @@ class RecipeRuntime(recipe: CompiledRecipe, interactionManager: InteractionManag /** * Tokens which are inhibited by the Edge filter may not be consumed. */ - override def consumableTokens(petriNet: PetriNet[Place, Transition])(marking: Marking[Place], p: Place, t: Transition): MultiSet[Any] = { + override def consumableTokens(petriNet: PetriNet)(marking: Marking[Place], p: Place, t: Transition): MultiSet[Any] = { val edge = petriNet.findPTEdge(p, t).map(_.asInstanceOf[Edge]).get marking.get(p) match { @@ -145,7 +159,7 @@ class RecipeRuntime(recipe: CompiledRecipe, interactionManager: InteractionManag override val eventSource: Transition => RecipeInstanceState => EventInstance => RecipeInstanceState = recipeEventSourceFn - override def handleException(job: Job[Place, Transition, RecipeInstanceState]) + override def handleException(job: Job[RecipeInstanceState]) (throwable: Throwable, failureCount: Int, startTime: Long, outMarking: MultiSet[Place]): ExceptionStrategy = { if (throwable.isInstanceOf[Error]) @@ -175,66 +189,64 @@ class RecipeRuntime(recipe: CompiledRecipe, interactionManager: InteractionManag } } - override def transitionTask(petriNet: PetriNet[Place, Transition], t: Transition)(marking: Marking[Place], state: RecipeInstanceState, input: Any): IO[(Marking[Place], EventInstance)] = + override def transitionTask(petriNet: PetriNet, t: Transition)(marking: Marking[Place], state: RecipeInstanceState, input: Any): IO[(Marking[Place], EventInstance)] = t match { case interaction: InteractionTransition => interactionTask(interaction, petriNet.outMarking(t), state) - case t: EventTransition => IO.pure(petriNet.outMarking(t).toMarking, input.asInstanceOf[EventInstance]) + case eventTransition: EventTransition => + if(input != null) { + // Send EventFired for SensoryEvents + LogAndSendEvent.eventFired(EventFired(System.currentTimeMillis(), recipe.name, recipe.recipeId, state.recipeInstanceId, input.asInstanceOf[EventInstance]), eventStream) + } + IO.pure(petriNet.outMarking(eventTransition).toMarking, input.asInstanceOf[EventInstance]) case t => IO.pure(petriNet.outMarking(t).toMarking, null.asInstanceOf[EventInstance]) } def interactionTask(interaction: InteractionTransition, outAdjacent: MultiSet[Place], processState: RecipeInstanceState): IO[(Marking[Place], EventInstance)] = { - // returns a delayed task that will get executed by the process instance - // add MDC values for logging - MDC.put("recipeInstanceId", processState.recipeInstanceId) - MDC.put("recipeId", recipe.recipeId) - MDC.put("recipeName", recipe.name) + // create the interaction input + val input = createInteractionInput(interaction, processState) - try { - // create the interaction input - val input = createInteractionInput(interaction, processState) + val timeStarted = System.currentTimeMillis() - val timeStarted = System.currentTimeMillis() + // publish the fact that we started the interaction + LogAndSendEvent.interactionStarted(InteractionStarted(timeStarted, recipe.name, recipe.recipeId, processState.recipeInstanceId, interaction.interactionName), eventStream) - // publish the fact that we started the interaction - LogAndSendEvent.interactionStarted(InteractionStarted(timeStarted, recipe.name, recipe.recipeId, processState.recipeInstanceId, interaction.interactionName), eventStream) + // executes the interaction and obtain the (optional) output event + interactionManager.execute(interaction, input, Some(processState.recipeInstanceMetadata)).map { interactionOutput => - // executes the interaction and obtain the (optional) output event - interactionManager.execute(interaction, input, - com.ing.baker.runtime.model.recipeinstance.RecipeInstanceState.getMetaDataFromIngredients(processState.ingredients)).map { interactionOutput => - - // validates the event, throws a FatalInteraction exception if invalid - RecipeRuntime.validateInteractionOutput(interaction, interactionOutput).foreach { validationError => - throw new FatalInteractionException(validationError) - } + // validates the event, throws a FatalInteraction exception if invalid + RecipeRuntime.validateInteractionOutput(interaction, interactionOutput).foreach { validationError => + throw new FatalInteractionException(validationError) + } - // transform the event if there is one - val outputEvent: Option[EventInstance] = interactionOutput - .map(e => transformInteractionEvent(interaction, e)) + // transform the event if there is one + val outputEvent: Option[EventInstance] = interactionOutput + .map(e => transformInteractionEvent(interaction, e)) - val timeCompleted = System.currentTimeMillis() + val timeCompleted = System.currentTimeMillis() - // publish the fact that the interaction completed - LogAndSendEvent.interactionCompleted(InteractionCompleted(timeCompleted, timeCompleted - timeStarted, recipe.name, recipe.recipeId, processState.recipeInstanceId, interaction.interactionName, outputEvent), eventStream) + // publish the fact that the interaction completed + LogAndSendEvent.interactionCompleted(InteractionCompleted(timeCompleted, timeCompleted - timeStarted, recipe.name, recipe.recipeId, processState.recipeInstanceId, interaction.interactionName, outputEvent), eventStream) - // create the output marking for the petri net - val outputMarking: Marking[Place] = RecipeRuntime.createProducedMarking(outAdjacent, outputEvent) + // create the output marking for the petri net + val outputMarking: Marking[Place] = RecipeRuntime.createProducedMarking(outAdjacent, outputEvent) - (outputMarking, outputEvent.orNull) - } - - } finally { - // remove the MDC values - MDC.remove("recipeInstanceId") - MDC.remove("recipeId") - MDC.remove("recipeName") + outputEvent.foreach { event: EventInstance => + // Send EventFired for Interaction output events + LogAndSendEvent.eventFired(EventFired(timeCompleted, recipe.name, recipe.recipeId, processState.recipeInstanceId, event), eventStream) } - } handleErrorWith { - case e: InvocationTargetException => IO.raiseError(e.getCause) - case e: Throwable => IO.raiseError(e) + val reproviderMarkings: Marking[Place] = if (interaction.isReprovider) { + outAdjacent.toMarking.filter((input: (Place, MultiSet[Any])) => input._1.placeType == IngredientPlace) + } else Map.empty + + (outputMarking ++ reproviderMarkings, outputEvent.orNull) } + } handleErrorWith { + case e: InvocationTargetException => IO.raiseError(e.getCause) + case e: Throwable => IO.raiseError(e) + } } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/recipe_manager/DefaultRecipeManager.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/recipe_manager/DefaultRecipeManager.scala index 92f74981a..43127d2d3 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/recipe_manager/DefaultRecipeManager.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/recipe_manager/DefaultRecipeManager.scala @@ -5,7 +5,7 @@ import com.ing.baker.runtime.common.RecipeRecord import scala.collection.concurrent.TrieMap import scala.concurrent.{ExecutionContext, Future} -private class DefaultRecipeManager(implicit val ex: ExecutionContext) extends RecipeManager { +class DefaultRecipeManager(implicit val ex: ExecutionContext) extends RecipeManager { val state:TrieMap[String, RecipeRecord] = TrieMap.empty diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/BakerRuntimeTestBase.scala b/core/akka-runtime/src/test/scala/com/ing/baker/BakerRuntimeTestBase.scala index 440643758..234edc635 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/BakerRuntimeTestBase.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/BakerRuntimeTestBase.scala @@ -81,6 +81,8 @@ trait BakerRuntimeTestBase ) protected val testInteractionOneMock: InteractionOne = mock[InteractionOne] + protected val testInteractionOneWithMetaDataMock: InteractionOneWithMetaData = mock[InteractionOneWithMetaData] + protected val testInteractionOneWithEventListMock: InteractionOneWithEventList = mock[InteractionOneWithEventList] protected val testInteractionTwoMock: InteractionTwo = mock[InteractionTwo] protected val testInteractionThreeMock: InteractionThree = mock[InteractionThree] protected val testInteractionFourMock: InteractionFour = mock[InteractionFour] @@ -98,6 +100,8 @@ trait BakerRuntimeTestBase protected val mockImplementations: List[InteractionInstance] = List( testInteractionOneMock, + testInteractionOneWithMetaDataMock, + testInteractionOneWithEventListMock, testInteractionTwoMock, testInteractionThreeMock, testInteractionFourMock, diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerEventsSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerEventsSpec.scala index 87afe7af7..f0d14e548 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerEventsSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerEventsSpec.scala @@ -1,16 +1,17 @@ package com.ing.baker.runtime.akka import akka.actor.ActorRef -import akka.persistence.inmemory.extension.{ InMemoryJournalStorage, StorageExtension } +import akka.persistence.inmemory.extension.{InMemoryJournalStorage, StorageExtension} import akka.testkit.TestProbe import com.ing.baker._ import com.ing.baker.recipe.TestRecipe._ import com.ing.baker.recipe.common.InteractionFailureStrategy -import com.ing.baker.recipe.scaladsl.Recipe +import com.ing.baker.recipe.scaladsl.{CheckPointEvent, Event, Recipe} import com.ing.baker.runtime.common.RejectReason._ -import com.ing.baker.runtime.scaladsl.{ EventInstance, _ } +import com.ing.baker.runtime.scaladsl.{EventInstance, _} import com.ing.baker.types.PrimitiveValue import com.typesafe.scalalogging.LazyLogging + import java.util.UUID import scala.concurrent.Future import scala.concurrent.duration._ @@ -57,6 +58,8 @@ object BakerEventsSpec extends LazyLogging { providesNothingInteraction, sieveInteraction ) + .withCheckpointEvent(CheckPointEvent("CheckPointEvent") + .withRequiredEvents(Event[InteractionOneSuccessful], Event[EventFromInteractionTwo], Event[InteractionThreeSuccessful])) .withSensoryEvents( initialEvent.withMaxFiringLimit(1), initialEventExtendedName, @@ -97,18 +100,24 @@ class BakerEventsSpec extends BakerRuntimeTestBase { _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent(initialIngredientValue)), "someId") // TODO check the order of the timestamps later _ = expectMsgInAnyOrderPF(listenerProbe, - {case msg@RecipeInstanceCreated(_, `recipeId`, `recipeName`, `recipeInstanceId`) => msg}, - {case msg@EventReceived(_, _, _, `recipeInstanceId`, Some("someId"), EventInstance("InitialEvent", ingredients)) if ingredients == Map("initialIngredient" -> PrimitiveValue(`initialIngredientValue`)) => msg}, - {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "SieveInteraction") => msg}, - {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionOne") => msg}, - {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionTwo") => msg}, - {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionThree") => msg}, - {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "ProvidesNothingInteraction") => msg}, - {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionOne", Some(EventInstance("InteractionOneSuccessful", ingredients))) if ingredients == Map("interactionOneIngredient" -> PrimitiveValue("interactionOneIngredient")) => msg}, - {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionTwo", Some(EventInstance("EventFromInteractionTwo", ingredients))) if ingredients == Map("interactionTwoIngredient" -> PrimitiveValue("interactionTwoIngredient")) => msg}, - {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionThree", Some(EventInstance("InteractionThreeSuccessful", ingredients))) if ingredients == Map("interactionThreeIngredient" -> PrimitiveValue("interactionThreeIngredient")) => msg}, - {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "ProvidesNothingInteraction", None) => msg}, - {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "SieveInteraction", Some(EventInstance("SieveInteractionSuccessful", ingredients))) if ingredients == Map("sievedIngredient" -> PrimitiveValue("sievedIngredient")) => msg} + {case msg@RecipeInstanceCreated(_, `recipeId`, `recipeName`, `recipeInstanceId`) => msg}, + {case msg@EventReceived(_, _, _, `recipeInstanceId`, Some("someId"), EventInstance("InitialEvent", ingredients)) if ingredients == Map("initialIngredient" -> PrimitiveValue(`initialIngredientValue`)) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("InitialEvent", ingredients)) => msg}, + {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "SieveInteraction") => msg}, + {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionOne") => msg}, + {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionTwo") => msg}, + {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "InteractionThree") => msg}, + {case msg@InteractionStarted(_, _, _, `recipeInstanceId`, "ProvidesNothingInteraction") => msg}, + {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionOne", Some(EventInstance("InteractionOneSuccessful", ingredients))) if ingredients == Map("interactionOneIngredient" -> PrimitiveValue("interactionOneIngredient")) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("InteractionOneSuccessful", _)) => msg}, + {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionTwo", Some(EventInstance("EventFromInteractionTwo", ingredients))) if ingredients == Map("interactionTwoIngredient" -> PrimitiveValue("interactionTwoIngredient")) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("EventFromInteractionTwo", ingredients)) => msg}, + {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "InteractionThree", Some(EventInstance("InteractionThreeSuccessful", ingredients))) if ingredients == Map("interactionThreeIngredient" -> PrimitiveValue("interactionThreeIngredient")) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("InteractionThreeSuccessful", _)) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("CheckPointEvent", _)) => msg}, + {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "ProvidesNothingInteraction", None) => msg}, + {case msg@InteractionCompleted(_, _, _, _, `recipeInstanceId`, "SieveInteraction", Some(EventInstance("SieveInteractionSuccessful", ingredients))) if ingredients == Map("sievedIngredient" -> PrimitiveValue("sievedIngredient")) => msg}, + {case msg@EventFired(_, _, _, `recipeInstanceId`, EventInstance("SieveInteractionSuccessful", ingredients)) => msg} ) _ = listenerProbe.expectNoMessage(eventReceiveTimeout) } yield succeed diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerExecutionSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerExecutionSpec.scala index a916e5800..b93e1aac3 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerExecutionSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerExecutionSpec.scala @@ -13,16 +13,16 @@ import com.ing.baker.compiler.RecipeCompiler import com.ing.baker.recipe.TestRecipe._ import com.ing.baker.recipe.common.InteractionFailureStrategy import com.ing.baker.recipe.common.InteractionFailureStrategy.FireEventAfterFailure -import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe, CheckPointEvent} +import com.ing.baker.recipe.scaladsl.{CheckPointEvent, Event, Ingredient, Interaction, Recipe} import com.ing.baker.runtime.akka.internal.CachingInteractionManager import com.ing.baker.runtime.common.BakerException._ -import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetaDataName +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName import com.ing.baker.runtime.common._ import com.ing.baker.runtime.scaladsl.{Baker, EventInstance, InteractionInstance, InteractionInstanceInput, RecipeEventMetadata} import com.ing.baker.types.{CharArray, Int32, PrimitiveValue, Value} import com.typesafe.config.{Config, ConfigFactory} import io.prometheus.client.CollectorRegistry -import org.mockito.ArgumentMatchers.{any, anyString, argThat, eq => mockitoEq} +import org.mockito.ArgumentMatchers.{any, anyMap, anyString, argThat, eq => mockitoEq} import org.mockito.Mockito._ import org.mockito.invocation.InvocationOnMock import org.mockito.stubbing.Answer @@ -144,7 +144,7 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { id = UUID.randomUUID().toString _ <- baker.bake(recipeId, id, Map("key" -> "value")) ingredients: Map[String, Value] <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert(metaData.containsKey("key") && metaData.get("key") == "value") } @@ -179,7 +179,7 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value")) _ <- baker.addMetaData(id, Map.apply[String, String]("key2" -> "value2")) ingredients: Map[String, Value] <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.containsKey("key") && metaData.get("key") == "value" && metaData.containsKey("key2") && metaData.get("key2") == "value2") @@ -193,7 +193,7 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value")) _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value2")) ingredients: Map[String, Value] <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.containsKey("key") && metaData.get("key") == "value2") @@ -206,7 +206,7 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { _ <- baker.bake(recipeId, id) _ <- baker.addMetaData(id, Map.empty) ingredients: Map[String, Value] <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.size() == 0) @@ -218,7 +218,7 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { id = UUID.randomUUID().toString _ <- baker.bake(recipeId, id) ingredients: Map[String, Value] <- baker.getIngredients(id) - } yield assert(!ingredients.contains(RecipeInstanceMetaDataName)) + } yield assert(!ingredients.contains(RecipeInstanceMetadataName)) } "throw a NoSuchProcessException" when { @@ -279,6 +279,85 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { "interactionOneOriginalIngredient" -> interactionOneIngredientValue) } + "execute an interaction when its ingredient is provided with MetaData requirement" in { + val recipe = + Recipe("IngredientProvidedRecipeWithSpecial") + .withInteraction(interactionOneWithMetaData) + .withSensoryEvent(initialEvent) + + for { + (baker, recipeId) <- setupBakerWithRecipe(recipe, mockImplementations) + _ = when(testInteractionOneWithMetaDataMock.apply(anyString(), anyString(), any())).thenReturn(Future.successful(InteractionOneSuccessful(interactionOneIngredientValue))) + recipeInstanceId = UUID.randomUUID().toString + metaData = Map("MetaDataKey" -> "MetaDataValue") + _ <- baker.bake(recipeId, recipeInstanceId, metaData) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(EventInstance.unsafeFrom(InitialEvent(initialIngredientValue)))) + _ = verify(testInteractionOneWithMetaDataMock).apply(recipeInstanceId.toString, "initialIngredient", metaData) + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield + state.ingredients shouldBe + ingredientMap( + "RecipeInstanceMetaData" -> metaData, + "initialIngredient" -> initialIngredientValue, + "interactionOneOriginalIngredient" -> interactionOneIngredientValue) + } + + "execute an interaction when its ingredient is provided with EventList requirement" in { + val recipe = + Recipe("IngredientProvidedRecipeWithSpecial") + .withInteraction(interactionOneWithEventList) + .withSensoryEvent(initialEvent) + + for { + (baker, recipeId) <- setupBakerWithRecipe(recipe, mockImplementations) + _ = when(testInteractionOneWithEventListMock.apply(anyString(), anyString(), any())).thenReturn(Future.successful(InteractionOneSuccessful(interactionOneIngredientValue))) + recipeInstanceId = UUID.randomUUID().toString + eventList = List("InitialEvent") + _ <- baker.bake(recipeId, recipeInstanceId) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(EventInstance.unsafeFrom(InitialEvent(initialIngredientValue)))) + _ = verify(testInteractionOneWithEventListMock).apply(recipeInstanceId.toString, "initialIngredient", eventList) + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield + state.ingredients shouldBe + ingredientMap( + "initialIngredient" -> initialIngredientValue, + "interactionOneOriginalIngredient" -> interactionOneIngredientValue) + } + + "re-execute an interaction when set to reprovider and event is fired two times" in { + val recipe = + Recipe("IngredientProvidedRecipe") + .withInteractions( + interactionOne + .isReprovider(true) + .withRequiredEvents(emptyEvent), + //This is added to ensure interactions that do not have reprovider added are fired two times. + interactionOne + .withName("interactionOne2") + .withRequiredEvents(emptyEvent) + ) + .withSensoryEvents( + initialEvent, + emptyEvent.withMaxFiringLimit(2)) + + for { + (baker, recipeId) <- setupBakerWithRecipe(recipe, mockImplementations) + _ = when(testInteractionOneMock.apply(anyString(), anyString())) + .thenReturn(Future.successful(InteractionOneSuccessful(interactionOneIngredientValue))) + .thenReturn(Future.successful(InteractionOneSuccessful(interactionOneIngredientValue))) + .thenReturn(Future.successful(InteractionOneSuccessful(interactionOneIngredientValue))) + recipeInstanceId = UUID.randomUUID().toString + _ <- baker.bake(recipeId, recipeInstanceId) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(EventInstance.unsafeFrom(InitialEvent(initialIngredientValue)))) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(EventInstance.unsafeFrom(EmptyEvent()))) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(EventInstance.unsafeFrom(EmptyEvent()))) + _ = verify(testInteractionOneMock, times(3)).apply(recipeInstanceId, "initialIngredient") + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield + state.eventNames shouldBe + Seq("InitialEvent", "EmptyEvent", "InteractionOneSuccessful", "InteractionOneSuccessful", "EmptyEvent", "InteractionOneSuccessful") + } + "execute an interaction when its ingredient is provided in cluster" in { val recipe = Recipe("IngredientProvidedRecipeCluster") @@ -1148,6 +1227,34 @@ class BakerExecutionSpec extends BakerRuntimeTestBase { "interactionOneOriginalIngredient" -> "success!") } + + "retry a blocked interaction after it had the FireEvent retry strategy" in { + val recipe = + Recipe("RetryBlockedInteractionRecipe") + .withInteraction(interactionOne + .withFailureStrategy(InteractionFailureStrategy.FireEventAfterFailure(Some("interactionOneSuccessful")))) + .withSensoryEvent(initialEvent) + + for { + (baker, recipeId) <- setupBakerWithRecipe(recipe, mockImplementations) + _ = when(testInteractionOneMock.apply(anyString(), anyString())) + .thenThrow(new RuntimeException("Expected test failure")) + .thenReturn(Future.successful(InteractionOneSuccessful("success!"))) + recipeInstanceId = UUID.randomUUID().toString + _ <- baker.bake(recipeId, recipeInstanceId) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent(initialIngredientValue))) + state0 <- baker.getRecipeInstanceState(recipeInstanceId) + _ = state0.ingredients shouldBe + ingredientMap( + "initialIngredient" -> initialIngredientValue) + _ <- baker.retryInteraction(recipeInstanceId, interactionOne.name) + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield state.ingredients shouldBe + ingredientMap( + "initialIngredient" -> initialIngredientValue, + "interactionOneOriginalIngredient" -> "success!") + } + "be able to return" when { "all occurred events" in { for { diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerSetupSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerSetupSpec.scala index b8e42d935..72787e040 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerSetupSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/BakerSetupSpec.scala @@ -11,6 +11,7 @@ import com.ing.baker.runtime.common.BakerException.{ImplementationsException, Re import com.ing.baker.runtime.common.RecipeRecord import com.ing.baker.runtime.scaladsl.InteractionInstance import com.typesafe.config.ConfigFactory +import io.prometheus.client.CollectorRegistry import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.when @@ -27,6 +28,10 @@ class BakerSetupSpec extends BakerRuntimeTestBase { resetMocks() } + override def beforeAll() = { + CollectorRegistry.defaultRegistry.clear() + } + "The Baker execution engine during setup" should { "bootstrap correctly without throwing an error if provided a correct recipe and correct implementations" when { diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/AkkaTestBase.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/AkkaTestBase.scala index a93070a05..c33bf424b 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/AkkaTestBase.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/AkkaTestBase.scala @@ -12,6 +12,11 @@ abstract class AkkaTestBase(actorSystemName: String = "testActorSystem") extends with ImplicitSender with BeforeAndAfterAll { + override def beforeAll() = { + super.beforeAll() + CollectorRegistry.defaultRegistry.clear() + } + override def afterAll() = { super.afterAll() shutdown(system) diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorSpec.scala index 200126d1e..22f459055 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/delayed_transition_actor/DelayedTransitionActorSpec.scala @@ -1,19 +1,19 @@ package com.ing.baker.runtime.akka.actor.delayed_transition_actor -import akka.actor.ActorSystem +import akka.actor.{ActorSystem, Props} +import akka.persistence.{SaveSnapshotSuccess, SnapshotMetadata} import akka.testkit.{ImplicitSender, TestKit, TestProbe} -import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.{FireDelayedTransition, ScheduleDelayedTransition, StartTimer} -import com.ing.baker.runtime.akka.actor.process_index.ProcessIndexSpec +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActor.DelayedTransitionSnapshot +import com.ing.baker.runtime.akka.actor.delayed_transition_actor.DelayedTransitionActorProtocol.{FireDelayedTransition, FireDelayedTransitionAck, ScheduleDelayedTransition, StartTimer} import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol.TransitionDelayed import com.typesafe.config.{Config, ConfigFactory} import org.scalatest.concurrent.Eventually -import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll} import org.scalatestplus.mockito.MockitoSugar import java.util.UUID -import scala.concurrent.duration.FiniteDuration object DelayedTransitionActorSpec { val config: Config = ConfigFactory.parseString( @@ -40,7 +40,8 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition "acknowledge the ScheduleDelayedTransition with a TransitionDelayed" in { val index = TestProbe("index-probe") - val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor)) + val delayedTransitionActor = system.actorOf( + DelayedTransitionActor.props(index.testActor, null, 1000, 1000)) val recipeId = UUID.randomUUID().toString val jobId: Long = 1 @@ -55,7 +56,7 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition "Fire the FireDelayedTransition when the Time is up" in { val index = TestProbe("index-probe") - val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor)) + val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor, null, 1000, 1000)) val recipeId = UUID.randomUUID().toString val jobId: Long = 1 @@ -74,7 +75,7 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition "Not fire the FireDelayedTransition when the StartTimer is not given" in { val index = TestProbe("index-probe") - val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor)) + val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor, null, 1000, 1000)) val recipeId = UUID.randomUUID().toString val jobId: Long = 1 @@ -91,7 +92,7 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition "Fire old messages after when the StartTimer is not given" in { val index = TestProbe("index-probe") - val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor), + val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor, null, 1000, 1000), s"DelayedTransitionActor4") val recipeId = UUID.randomUUID().toString @@ -112,7 +113,49 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition "Not Fire the FireDelayedTransition multiple times if given" in { val index = TestProbe("index-probe") - val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor)) + val delayedTransitionActor = system.actorOf(DelayedTransitionActor.props(index.testActor, null, 1000, 1000)) + + val recipeId = UUID.randomUUID().toString + val jobId: Long = 1 + val transitionId: Long = 2 + val eventToFire = "EventToFire" + + delayedTransitionActor ! StartTimer + + val receiver = TestProbe() + delayedTransitionActor.tell(ScheduleDelayedTransition(recipeId, 0, jobId, transitionId, null, eventToFire, receiver.testActor), receiver.testActor) + receiver.expectMsg(TransitionDelayed(jobId, transitionId, null)) + delayedTransitionActor.tell(ScheduleDelayedTransition(recipeId, 0, jobId, transitionId, null, eventToFire, receiver.testActor), receiver.testActor) + receiver.expectMsg(TransitionDelayed(jobId, transitionId, null)) + + Thread.sleep(10) + index.expectMsg(FireDelayedTransition(recipeId, jobId, transitionId, eventToFire, receiver.testActor)) + index.expectNoMessage() + } + + "Create snapshots and cleanup Snapshots" in { + val index = TestProbe("index-probe") + + var sequenceCount = 0 + var snapshotCount = 0 + + var latestSnapshot: Any = null + + class temp extends DelayedTransitionActor(index.testActor, null, 1, 5) { + override def saveSnapshot(snapshot: Any): Unit = { + sequenceCount += 1 + snapshotCount += 1 + latestSnapshot = snapshot + self.tell(SaveSnapshotSuccess(SnapshotMetadata("persistenceId", sequenceCount)), self) + } + + override def cleanupSnapshots(persistenceId: String, snapShotsToKeep: Int): Unit = { + if(snapshotCount > snapShotsToKeep ) snapshotCount = snapShotsToKeep + } + } + + val delayedTransitionActor = + system.actorOf(Props(new temp)) val recipeId = UUID.randomUUID().toString val jobId: Long = 1 @@ -121,15 +164,40 @@ class DelayedTransitionActorSpec extends TestKit(ActorSystem("DelayedTransition delayedTransitionActor ! StartTimer + // Test 1 should create 1 Snapshot after 2 messages (first message snapshot is skipped for first message) val receiver = TestProbe() delayedTransitionActor.tell(ScheduleDelayedTransition(recipeId, 0, jobId, transitionId, null, eventToFire, receiver.testActor), receiver.testActor) receiver.expectMsg(TransitionDelayed(jobId, transitionId, null)) + Thread.sleep(10) + index.expectMsg(FireDelayedTransition(recipeId, jobId, transitionId, eventToFire, receiver.testActor)) + index.expectNoMessage() + delayedTransitionActor.tell(FireDelayedTransitionAck(recipeId, jobId), receiver.testActor) + Thread.sleep(10) + assert(snapshotCount == 1) + + // Test 2 should create 3 Snapshot after 4 messages delayedTransitionActor.tell(ScheduleDelayedTransition(recipeId, 0, jobId, transitionId, null, eventToFire, receiver.testActor), receiver.testActor) receiver.expectMsg(TransitionDelayed(jobId, transitionId, null)) + Thread.sleep(10) + index.expectMsg(FireDelayedTransition(recipeId, jobId, transitionId, eventToFire, receiver.testActor)) + index.expectNoMessage() + delayedTransitionActor.tell(FireDelayedTransitionAck(recipeId, jobId), receiver.testActor) + Thread.sleep(10) + assert(snapshotCount == 3) + // Test 2 should create 6 Snapshot after 7 messages but cleanup after 5 + delayedTransitionActor.tell(ScheduleDelayedTransition(recipeId, 0, jobId, transitionId, null, eventToFire, receiver.testActor), receiver.testActor) + receiver.expectMsg(TransitionDelayed(jobId, transitionId, null)) Thread.sleep(10) index.expectMsg(FireDelayedTransition(recipeId, jobId, transitionId, eventToFire, receiver.testActor)) index.expectNoMessage() + delayedTransitionActor.tell(FireDelayedTransitionAck(recipeId, jobId), receiver.testActor) + Thread.sleep(10) + assert(snapshotCount == 5) + + // Validate that the latest snapshot only has the final open request left over + val finalSnapshot = latestSnapshot.asInstanceOf[DelayedTransitionSnapshot].waitingTransitions + assert(finalSnapshot.size == 1) } } } diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexSpec.scala index 8c892ff84..dce38c269 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_index/ProcessIndexSpec.scala @@ -21,6 +21,7 @@ import com.ing.baker.runtime.serialization.Encryption import com.ing.baker.types import com.ing.baker.types.Value import com.typesafe.config.{Config, ConfigFactory} +import io.prometheus.client.CollectorRegistry import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito import org.mockito.Mockito.when @@ -65,7 +66,13 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn Mockito.reset(otherMsg) } + override def beforeAll() = { + super.beforeAll() + CollectorRegistry.defaultRegistry.clear() + } + override def afterAll(): Unit = { + super.afterAll() TestKit.shutdownActorSystem(system) } @@ -91,7 +98,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn "create the PetriNetInstance actor when Initialize message is received" in { val recipeInstanceId = UUID.randomUUID().toString val initializeMsg = - Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) val petriNetActorProbe = TestProbe() val actorIndex = createActorIndex(petriNetActorProbe.ref, recipeManager) actorIndex ! CreateProcess(recipeId, recipeInstanceId) @@ -136,7 +143,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn "not create the PetriNetInstance actor if already created" in { val recipeInstanceId = UUID.randomUUID().toString - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) val petriNetActorProbe = TestProbe() val actorIndex = createActorIndex(petriNetActorProbe.ref, recipeManager) actorIndex ! CreateProcess(recipeId, recipeInstanceId) @@ -146,9 +153,12 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn expectMsg(ProcessAlreadyExists(recipeInstanceId)) } - "delete a process if a retention period is defined, stop command is received" in { + "delete a process if a retention period is defined, CheckForProcessesToBeDeleted received" in { + val recipeInstanceId = UUID.randomUUID().toString + val recipeRetentionPeriod = 500.milliseconds - val processProbe = TestProbe() + val processProbe = TestProbe(recipeInstanceId) + val recipeManager = mock[RecipeManager] when(recipeManager.get(anyString())).thenReturn(Future.successful(Some(RecipeRecord.of( @@ -158,15 +168,59 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn )))) val actorIndex = createActorIndex(processProbe.ref, recipeManager) + + actorIndex ! CreateProcess(recipeId, recipeInstanceId) + + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) + processProbe.expectMsg(initializeMsg) + Thread.sleep(recipeRetentionPeriod.toMillis) + // inform the index to check for processes to be cleaned up + actorIndex ! CheckForProcessesToBeDeleted + processProbe.expectMsg(15.seconds, Stop(delete = true)) + + processProbe.testActor ! PoisonPill + + val probe2 = TestProbe() + probe2.send(actorIndex, GetProcessState(recipeInstanceId)) + probe2.expectMsg(10.seconds, ProcessDeleted(recipeInstanceId)) + } + + "forget a process if a remember duration is defined and this is passed" in { val recipeInstanceId = UUID.randomUUID().toString + + val recipeRetentionPeriod = 500.milliseconds + val processProbe = TestProbe(recipeInstanceId) + + val recipeManager = mock[RecipeManager] + + when(recipeManager.get(anyString())).thenReturn(Future.successful(Some(RecipeRecord.of( + CompiledRecipe("name", recipeId, new PetriNet(Graph.empty), Marking.empty, Seq.empty, + Option.empty, Some(recipeRetentionPeriod)), + updated = 0L + )))) + + val actorIndex = createActorIndex(processProbe.ref, recipeManager, Some(1.milliseconds)) + actorIndex ! CreateProcess(recipeId, recipeInstanceId) - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) processProbe.expectMsg(initializeMsg) Thread.sleep(recipeRetentionPeriod.toMillis) // inform the index to check for processes to be cleaned up actorIndex ! CheckForProcessesToBeDeleted processProbe.expectMsg(15.seconds, Stop(delete = true)) + + processProbe.testActor ! PoisonPill + + //Wait for 100 millis to give the Index time to stop the actor + Thread.sleep(100) + + // Second trigger to cleanup this time after the process was deleted + actorIndex ! CheckForProcessesToBeDeleted + + val probe2 = TestProbe() + probe2.send(actorIndex, GetProcessState(recipeInstanceId)) + probe2.expectMsg(10.seconds, NoSuchProcess(recipeInstanceId)) } "Forward the FireTransition command when a valid HandleEvent is sent" in { @@ -188,7 +242,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn val recipeInstanceId = UUID.randomUUID().toString - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) actorIndex ! CreateProcess(recipeId, recipeInstanceId) @@ -236,7 +290,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn val recipeInstanceId = UUID.randomUUID().toString - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) actorIndex ! CreateProcess(recipeId, recipeInstanceId) @@ -268,7 +322,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn val recipeInstanceId = UUID.randomUUID().toString - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) actorIndex ! CreateProcess(recipeId, recipeInstanceId) @@ -300,7 +354,7 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn val recipeInstanceId = UUID.randomUUID().toString - val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], List.empty)) + val initializeMsg = Initialize(Marking.empty[Place], RecipeInstanceState(recipeId, recipeInstanceId, Map.empty[String, Value], Map.empty[String, String], List.empty)) @@ -352,7 +406,9 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn } } - private def createActorIndex(petriNetActorRef: ActorRef, recipeManager: RecipeManager): ActorRef = { + private def createActorIndex(petriNetActorRef: ActorRef, + recipeManager: RecipeManager, + rememberProcessDuration: Option[Duration] = None): ActorRef = { val props = Props(new ProcessIndex( recipeInstanceIdleTimeout = None, retentionCheckInterval = None, @@ -361,7 +417,8 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn recipeManager = recipeManager, Seq.empty, Seq.empty, - Seq.empty) { + Seq.empty, + rememberProcessDuration) { override def createProcessActor(id: String, compiledRecipe: CompiledRecipe) = { context.watch(petriNetActorRef) petriNetActorRef @@ -369,6 +426,10 @@ class ProcessIndexSpec extends TestKit(ActorSystem("ProcessIndexSpec", ProcessIn override def getProcessActor(recipeInstanceId: String): Option[ActorRef] = { Some(petriNetActorRef) } + override def getRecipeIdFromActor(actorRef: ActorRef): String = { + val result = if (petriNetActorRef.path.name.size >= 36) petriNetActorRef.path.name.substring(0, 36) else petriNetActorRef.path.name + result + } }) system.actorOf(props, s"actorIndex-${UUID.randomUUID().toString}") } diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcingSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcingSpec.scala index b74d7f3cb..96857f818 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcingSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceEventSourcingSpec.scala @@ -6,11 +6,13 @@ import akka.persistence.query.scaladsl._ import akka.stream.testkit.scaladsl.TestSink import akka.testkit.TestProbe import akka.util.Timeout +import com.ing.baker.il.petrinet.Place import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka.actor.AkkaTestBase import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceEventSourcing._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceSpec._ +import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils.{PlaceMethods, place} import com.ing.baker.runtime.akka.actor.process_instance.dsl._ import com.ing.baker.runtime.serialization.Encryption.NoEncryption import org.scalatest.BeforeAndAfterEach @@ -42,9 +44,9 @@ class ProcessInstanceEventSourcingSpec extends AkkaTestBase("ProcessQuerySpec") val readJournal = PersistenceQuery(system).readJournalFor[ReadJournal with CurrentEventsByPersistenceIdQuery]("inmemory-read-journal") - val p1 = Place(id = 1) - val p2 = Place(id = 2) - val p3 = Place(id = 3) + val p1 = place(1) + val p2 = place(2) + val p3 = place(3) val t1 = nullTransition(id = 1, automated = true) val t2 = nullTransition(id = 2, automated = true) @@ -58,7 +60,7 @@ class ProcessInstanceEventSourcingSpec extends AkkaTestBase("ProcessQuerySpec") expectMsgPF(timeOut) { case TransitionFired(_, 1, _, _, _, _, _) => } expectMsgPF(timeOut) { case TransitionFired(_, 2, _, _, _, _, _) => } - ProcessInstanceEventSourcing.eventsForInstance[Place, Transition, Unit, Unit]( + ProcessInstanceEventSourcing.eventsForInstance[Unit, Unit]( processTypeName = "test", recipeInstanceId = recipeInstanceId, topology = petriNet, diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSpec.scala index ecc51a135..cb0d48e28 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/ProcessInstanceSpec.scala @@ -1,8 +1,12 @@ package com.ing.baker.runtime.akka.actor.process_instance import akka.actor.{ActorRef, ActorSystem, PoisonPill, Props, Terminated} +import akka.event.DiagnosticLoggingAdapter import akka.testkit.{TestDuration, TestProbe} import akka.util.Timeout +import com.ing.baker.il.{CompiledRecipe, EventDescriptor, IngredientDescriptor} +import com.ing.baker.il.failurestrategy.{BlockInteraction, FireEventAfterFailure, InteractionFailureStrategy, RetryWithIncrementalBackoff} +import com.ing.baker.il.petrinet.{InteractionTransition, Place} import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.akka.actor.AkkaTestBase import com.ing.baker.runtime.akka.actor.process_index.ProcessIndexProtocol.FireSensoryEventRejection @@ -11,11 +15,16 @@ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstance.Setting import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol.ExceptionStrategy.BlockTransition import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceProtocol._ import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceSpec._ +import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils.{PlaceMethods, place} import com.ing.baker.runtime.akka.actor.process_instance.dsl._ import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.RetryWithDelay import com.ing.baker.runtime.akka.actor.process_instance.{ProcessInstanceProtocol => protocol} +import com.ing.baker.runtime.akka.internal.FatalInteractionException import com.ing.baker.runtime.akka.namedCachedThreadPool +import com.ing.baker.runtime.scaladsl.RecipeInstanceState import com.ing.baker.runtime.serialization.Encryption.NoEncryption +import com.ing.baker.types +import com.ing.baker.types.{Converters, Value} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito._ import org.mockito.invocation.InvocationOnMock @@ -25,11 +34,13 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.time.{Milliseconds, Span} import org.scalatestplus.mockito.MockitoSugar +import java.time.Duration import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import scala.collection.immutable.Seq import scala.concurrent.Promise +import scala.concurrent.duration.Duration.Zero import scala.concurrent.duration._ import scala.util.Success @@ -61,15 +72,16 @@ object ProcessInstanceSpec { ) def processInstanceProps[S, E]( - topology: PetriNet[Place, Transition], - runtime: ProcessInstanceRuntime[Place, Transition, S, E], - settings: Settings): Props = { - Props(new ProcessInstance[Place, Transition, S, E]( + topology: PetriNet, + runtime: ProcessInstanceRuntime[S, E], + settings: Settings, + delayedTransitionActor: ActorRef): Props = { + Props(new ProcessInstance[S, E]( "test", - topology, + CompiledRecipe("name", UUID.randomUUID().toString, topology, Marking.empty, Seq.empty, Option.empty, Option.empty), settings, runtime, - null) + delayedTransitionActor) ) } @@ -77,9 +89,12 @@ object ProcessInstanceSpec { system.actorOf(props, name) } - def createProcessInstance[S, E](petriNet: PetriNet[Place, Transition], runtime: ProcessInstanceRuntime[Place, Transition, S, E], recipeInstanceId: String = UUID.randomUUID().toString)(implicit system: ActorSystem): ActorRef = { + def createProcessInstance[S, E](petriNet: PetriNet, + runtime: ProcessInstanceRuntime[S, E], + recipeInstanceId: String = UUID.randomUUID().toString, + delayedTransitionActor: ActorRef = null)(implicit system: ActorSystem): ActorRef = { - createPetriNetActor(processInstanceProps(petriNet, runtime, instanceSettings), recipeInstanceId) + createPetriNetActor(processInstanceProps(petriNet, runtime, instanceSettings, delayedTransitionActor), recipeInstanceId) } } @@ -594,7 +609,7 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala transition(automated = false)(_ => Added(2)) ) - val petriNetActor = createPetriNetActor(processInstanceProps(petriNet, runtime, customSettings), UUID.randomUUID().toString) + val petriNetActor = createPetriNetActor(processInstanceProps(petriNet, runtime, customSettings, null), UUID.randomUUID().toString) watch(petriNetActor) implicit val timeout = Timeout(dilatedMillis(2000), MILLISECONDS) @@ -607,8 +622,8 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala override val eventSourceFunction: Unit => Unit => Unit = s => e => s - val p1 = Place(id = 1) - val p2 = Place(id = 2) + val p1 = place(id = 1) + val p2 = place(id = 2) val t1 = nullTransition(id = 1, automated = false) val t2 = stateTransition(id = 2, automated = true)(_ => Thread.sleep(dilatedMillis(500))) @@ -661,5 +676,296 @@ class ProcessInstanceSpec extends AkkaTestBase("ProcessInstanceSpec") with Scala receiver.expectNoMessage() } + + "Should correctly determine the OutputEventName for Delayed Transitions" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val failureStrategy: InteractionFailureStrategy = BlockInteraction + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq.empty, + "Name", + "Name", + Map.empty, + Option.empty, + failureStrategy, Map.empty, false) + + val logMock = mock[DiagnosticLoggingAdapter] + + val output: String = ProcessInstance.getOutputEventName(interactionTransition, logMock) + + assert(output == eventName) + } + + "Should fail to determine the OutputEventName for Delayed Transitions if there are 2 outcomes" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + val eventName2 = "orignalEvent2" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty), + EventDescriptor(eventName2, Seq.empty) + ) + + val failureStrategy: InteractionFailureStrategy = BlockInteraction + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq.empty, + "Name", + "Name", + Map.empty, + Option.empty, + failureStrategy, Map.empty, false) + + val logMock = mock[DiagnosticLoggingAdapter] + + var exceptionThrown = false + + try { + ProcessInstance.getOutputEventName(interactionTransition, logMock) + } catch { + case e: FatalInteractionException => exceptionThrown = true + } + assert(exceptionThrown) + } + + "Should correctly determine the OutputEventName for Delayed Transitions if FireEventAfterFailure retry strategy is used" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + val exhaustedEvent = "exhaustedEvent" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty), + EventDescriptor(exhaustedEvent, Seq.empty) + ) + + val failureStrategy: InteractionFailureStrategy = FireEventAfterFailure(EventDescriptor(exhaustedEvent, Seq.empty)) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq.empty, + "Name", + "Name", + Map.empty, + Option.empty, + failureStrategy, Map.empty, false) + + val logMock = mock[DiagnosticLoggingAdapter] + + val output: String = ProcessInstance.getOutputEventName(interactionTransition, logMock) + + assert(output == eventName) + } + + "Should correctly determine the OutputEventName for Delayed Transitions if RetryWithIncrementalBackoff is used with FireEvent" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + val exhaustedEvent = "exhaustedEvent" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty), + EventDescriptor(exhaustedEvent, Seq.empty) + ) + + val failureStrategy: InteractionFailureStrategy = + RetryWithIncrementalBackoff(Zero, 1.0, 1, Option.empty, Some(EventDescriptor(exhaustedEvent, Seq.empty))) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq.empty, + "Name", + "Name", + Map.empty, + Option.empty, + failureStrategy, Map.empty, false) + + val logMock = mock[DiagnosticLoggingAdapter] + + val output: String = ProcessInstance.getOutputEventName(interactionTransition, logMock) + + assert(output == eventName) + } + + "Should correctly determine the OutputEventName for Delayed Transitions if RetryWithIncrementalBackoff is used without FireEvent" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + val exhaustedEvent = "exhaustedEvent" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val failureStrategy: InteractionFailureStrategy = + RetryWithIncrementalBackoff(Zero, 1.0, 1, Option.empty, Option.empty) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq.empty, + "Name", + "Name", + Map.empty, + Option.empty, + failureStrategy, Map.empty, false) + + val logMock = mock[DiagnosticLoggingAdapter] + + val output: String = ProcessInstance.getOutputEventName(interactionTransition, logMock) + + assert(output == eventName) + } + + "Should get correct wait time from state (Java Duration)" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq(IngredientDescriptor("waitTime", Converters.readJavaType[Duration])), + "Name", + "Name", + Map.empty, + Option.empty, + BlockInteraction, Map.empty, false) + + val ingredients = Map[String, Value]("waitTime" -> Converters.toValue(Duration.ofMillis(60000L))) + val output: Long = ProcessInstance.getWaitTimeInMillis(interactionTransition, RecipeInstanceState("id", "id", ingredients, Map.empty[String, String], Seq.empty)) + + assert(output == 60000L) + } + + "Should get correct wait time from state (Scala FiniteDuration)" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq(IngredientDescriptor("waitTime", Converters.readJavaType[FiniteDuration])), + "Name", + "Name", + Map.empty, + Option.empty, + BlockInteraction, Map.empty, false) + + val ingredients = Map[String, Value]("waitTime" -> Converters.toValue(FiniteDuration.apply(60000L, TimeUnit.MILLISECONDS))) + + val output: Long = ProcessInstance.getWaitTimeInMillis(interactionTransition, RecipeInstanceState("id", "id", ingredients, Map.empty[String, String], Seq.empty)) + + assert(output == 60000L) + } + + "Should get correct wait time from the predefined ingredients" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq(IngredientDescriptor("waitTime", Converters.readJavaType[Duration])), + "Name", + "Name", + Map[String, Value]("waitTime" -> Converters.toValue(Duration.ofMillis(60000L))), + Option.empty, + BlockInteraction, Map.empty, false) + + val ingredients = Map[String, Value]("waitTime" -> Converters.toValue(Duration.ofMillis(1200000L))) + + val output: Long = ProcessInstance.getWaitTimeInMillis(interactionTransition, RecipeInstanceState("id", "id", ingredients, Map.empty[String, String], Seq.empty)) + + assert(output == 60000L) + } + + "Should reject getting the wait time if there is more then 1 ingredient" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq(IngredientDescriptor("waitTime", Converters.readJavaType[Duration]), + IngredientDescriptor("waitTime2", Converters.readJavaType[Duration])), + "Name", + "Name", + Map.empty, + Option.empty, + BlockInteraction, Map.empty, false) + + val ingredients = Map[String, Value]("waitTime" -> Converters.toValue(Duration.ofMillis(60000L))) + + var exceptionThrown = false + try { + ProcessInstance.getWaitTimeInMillis(interactionTransition, RecipeInstanceState("id", "id", ingredients, Map.empty[String, String], Seq.empty)) + } catch { + case _: FatalInteractionException => exceptionThrown = true + } + assert(exceptionThrown) + } + + "Should reject getting the wait time if the ingredient is the wrong type" in new TestSequenceNet { + override val sequence = Seq( + transition() { _ => Added(1) }) + + val eventName = "originalEvent1" + + val eventsToFire: Seq[EventDescriptor] = Seq( + EventDescriptor(eventName, Seq.empty) + ) + + val interactionTransition: InteractionTransition = InteractionTransition( + eventsToFire, eventsToFire, + Seq(IngredientDescriptor("waitTime", types.Bool)), + "Name", + "Name", + Map.empty, + Option.empty, + BlockInteraction, Map.empty, false) + + val ingredients = Map[String, Value]("waitTime" -> Converters.toValue(false)) + var exceptionThrown = false + try { + ProcessInstance.getWaitTimeInMillis(interactionTransition, RecipeInstanceState("id", "id", ingredients, Map.empty[String, String], Seq.empty)) + } catch { + case _: FatalInteractionException => exceptionThrown = true + } + assert(exceptionThrown) + } } } diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Place.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Place.scala deleted file mode 100644 index 4c326db3a..000000000 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Place.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.ing.baker.runtime.akka.actor.process_instance.dsl - -import com.ing.baker.petrinet.api._ - -object Place { - def apply(id: Long): Place = Place(id, s"p$id") - - implicit val identifiable: Identifiable[Place] = p => p.id -} - -/** - * A Place in a colored petri net. - */ -case class Place(id: Long, label: String) { - - def apply(tokens: Any*): (Place, MultiSet[Any]) = (this, MultiSet.copyOff(tokens)) - - def markWithN(n: Int): Marking[Place] = Map[Place, MultiSet[Any]](this -> Map[Any, Int](Tuple2(null, n))) -} \ No newline at end of file diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/SequenceNet.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/SequenceNet.scala index b56f18889..52583d59c 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/SequenceNet.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/SequenceNet.scala @@ -1,7 +1,9 @@ package com.ing.baker.runtime.akka.actor.process_instance.dsl import cats.effect.IO +import com.ing.baker.il.petrinet.Place import com.ing.baker.petrinet.api._ +import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils.PlaceMethods import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.BlockTransition @@ -24,7 +26,7 @@ trait SequenceNet[S, E] extends StateTransitionNet[S, E] { def sequence: Seq[TransitionBehaviour[S, E]] - lazy val places: Seq[Place] = (1 to (sequence.size + 1)).map(i => Place(id = i, label = s"p$i")) + lazy val places: Seq[Place] = (1 to (sequence.size + 1)).map(i => TestUtils.place(i)) lazy val initialMarking: Marking[Place] = place(1).markWithN(1) def place(n: Int) = places(n - 1) @@ -35,7 +37,7 @@ trait SequenceNet[S, E] extends StateTransitionNet[S, E] { val nrOfSteps = sequence.size val transitions = sequence.zipWithIndex.map { case (t, index) => t.asTransition(index + 1) } - val places = (1 to (nrOfSteps + 1)).map(i => Place(id = i, label = s"p$i")) + val places = (1 to (nrOfSteps + 1)).map(i => TestUtils.place(id = i)) val tpedges = transitions.zip(places.tail).map { case (t, p) => arc(t, p, 1) } val ptedges = places.zip(transitions).map { case (p, t) => arc(p, t, 1) } createPetriNet[S]((tpedges ++ ptedges): _*) diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransition.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransition.scala index 03253c077..067f2e542 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransition.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransition.scala @@ -1,11 +1,12 @@ package com.ing.baker.runtime.akka.actor.process_instance.dsl import cats.effect.IO +import com.ing.baker.il.petrinet.{Place, Transition} case class StateTransition[S, E]( - override val id: Long, - override val label: String, - override val isAutomated: Boolean, - override val exceptionStrategy: TransitionExceptionHandler[Place], - produceEvent: S => IO[E]) extends Transition with com.ing.baker.il.petrinet.Transition { + override val id: Long, + override val label: String, + val isAutomated: Boolean, + val exceptionStrategy: TransitionExceptionHandler[Place], + produceEvent: S => IO[E]) extends Transition { } diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransitionNet.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransitionNet.scala index 41e616836..5165224c1 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransitionNet.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/StateTransitionNet.scala @@ -1,8 +1,11 @@ package com.ing.baker.runtime.akka.actor.process_instance.dsl import cats.effect.IO +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api._ +import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils import com.ing.baker.runtime.akka.actor.process_instance.ProcessInstanceRuntime +import com.ing.baker.runtime.akka.actor.process_instance.dsl.TestUtils.TransitionMethods import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.BlockTransition import com.ing.baker.runtime.akka.actor.process_instance.internal._ @@ -12,21 +15,21 @@ trait StateTransitionNet[S, E] { def eventSourceFunction: S => E => S - val runtime: ProcessInstanceRuntime[Place, Transition, S, E] = new ProcessInstanceRuntime[Place, Transition, S, E] { - override val eventSource: Transition => S => E => S = _ => eventSourceFunction + val runtime: ProcessInstanceRuntime[S, E] = new ProcessInstanceRuntime[S, E] { + override val eventSource: com.ing.baker.il.petrinet.Transition => S => E => S = _ => eventSourceFunction - override def transitionTask(petriNet: PetriNet[Place, Transition], t: Transition) + override def transitionTask(petriNet: PetriNet, t: com.ing.baker.il.petrinet.Transition) (marking: Marking[Place], state: S, input: Any): IO[(Marking[Place], E)] = { val eventTask = t.asInstanceOf[StateTransition[S, E]].produceEvent(state) val produceMarking: Marking[Place] = petriNet.outMarking(t).toMarking eventTask.map(e => (produceMarking, e)) } - override def handleException(job: Job[Place, Transition, S]) + override def handleException(job: Job[S]) (throwable: Throwable, failureCount: Int, startTime: Long, outMarking: MultiSet[Place]): ExceptionStrategy = job.transition.exceptionStrategy(throwable, failureCount, outMarking) - override def canBeFiredAutomatically(instance: Instance[Place, Transition, S], t: Transition): Boolean = + override def canBeFiredAutomatically(instance: Instance[S], t: com.ing.baker.il.petrinet.Transition): Boolean = t.isAutomated && !instance.isBlocked(t) } diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/TestUtils.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/TestUtils.scala new file mode 100644 index 000000000..5aa71508e --- /dev/null +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/TestUtils.scala @@ -0,0 +1,27 @@ +package com.ing.baker.runtime.akka.actor.process_instance.dsl + +import com.ing.baker.il.petrinet.Place +import com.ing.baker.il.petrinet.Place.IngredientPlace +import com.ing.baker.petrinet.api._ + +object TestUtils { + def place(id: Long): Place = new Place(s"p$id", IngredientPlace) + implicit class PlaceMethods(val place: Place) { + + // def apply(tokens: Any*): (Place, MultiSet[Any]) = (this, MultiSet.copyOff(tokens)) + + def markWithN(n: Int): Marking[Place] = Map[Place, MultiSet[Any]](place -> Map[Any, Int](Tuple2(null, n))) + } + + implicit class TransitionMethods(val transition: com.ing.baker.il.petrinet.Transition) { + val exceptionStrategy = + transition match { + case st: StateTransition[_, _] => st.exceptionStrategy + } + val isAutomated = + transition match { + case st: StateTransition[_, _] => st.isAutomated + } + } +} + diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Transition.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Transition.scala deleted file mode 100644 index d506d7d03..000000000 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/Transition.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.ing.baker.runtime.akka.actor.process_instance.dsl - -import com.ing.baker.petrinet.api.Identifiable -import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy.BlockTransition - -object Transition { - implicit val identifiable: Identifiable[Transition] = p => p.id -} - -/** - * A transition in a Colored Petri Net - * - */ -trait Transition { - val id: Long - def label: String - def isAutomated: Boolean - def exceptionStrategy: TransitionExceptionHandler[Place] = (e, n, _) => BlockTransition -} \ No newline at end of file diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/package.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/package.scala index 62d07527b..1fbc3b8d4 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/package.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/process_instance/dsl/package.scala @@ -1,5 +1,6 @@ package com.ing.baker.runtime.akka.actor.process_instance +import com.ing.baker.il.petrinet.{Place, Transition} import com.ing.baker.petrinet.api.{MultiSet, PetriNet} import com.ing.baker.runtime.akka.actor.process_instance.internal.ExceptionStrategy import scalax.collection.edge.WLDiEdge @@ -40,8 +41,8 @@ package object dsl { @nowarn def arc(p: Place, t: Transition, weight: Long): Arc = WLDiEdge[Node, String](Left(p), Right(t))(weight, "") - def requireUniqueElements[T](i: Iterable[T], name: String = "Element"): Unit = { - (i foldLeft Set.empty[T]) { (set, e) => + def requireUniqueElements[X](i: Iterable[X], name: String = "Element"): Unit = { + (i foldLeft Set.empty[X]) { (set, e) => if (set.contains(e)) throw new IllegalArgumentException(s"$name '$e' is not unique!") else @@ -49,7 +50,7 @@ package object dsl { } } - def createPetriNet[S](params: Arc*): PetriNet[Place, Transition] = { + def createPetriNet[S](params: Arc*): PetriNet = { val petriNet = new PetriNet(Graph(params: _*)) requireUniqueElements(petriNet.places.toSeq.map(_.id), "Place identifier") diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/serialization/SerializationSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/serialization/SerializationSpec.scala index b9f9d6d7f..a1ebd67db 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/serialization/SerializationSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/actor/serialization/SerializationSpec.scala @@ -268,6 +268,7 @@ object SerializationSpec { implicit val eventNameGen: Gen[String] = Gen.alphaStr implicit val ingredientNameGen: Gen[String] = Gen.alphaStr implicit val ingredientsGen: Gen[(String, Value)] = GenUtil.tuple(ingredientNameGen, Types.anyValueGen) + implicit val metaDataGen: Gen[(String, String)] = GenUtil.tuple(ingredientNameGen, ingredientNameGen) implicit val runtimeEventGen: Gen[EventInstance] = for { eventName <- eventNameGen @@ -283,8 +284,9 @@ object SerializationSpec { recipeInstanceId <- recipeInstanceIdGen recipeId <- recipeIdGen ingredients <- Gen.mapOf(ingredientsGen) + recipeInstanceMetadata <- Gen.mapOf(metaDataGen) events <- Gen.listOf(eventMomentsGen) - } yield RecipeInstanceState(recipeId, recipeInstanceId, ingredients, events) + } yield RecipeInstanceState(recipeId, recipeInstanceId, ingredients, recipeInstanceMetadata, events) implicit val messagesGen: Gen[AnyRef] = Gen.oneOf(runtimeEventGen, processStateGen) diff --git a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/internal/RecipeRuntimeSpec.scala b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/internal/RecipeRuntimeSpec.scala index 876eea37a..679cfbf8d 100644 --- a/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/internal/RecipeRuntimeSpec.scala +++ b/core/akka-runtime/src/test/scala/com/ing/baker/runtime/akka/internal/RecipeRuntimeSpec.scala @@ -29,6 +29,10 @@ class RecipeRuntimeSpec extends AnyWordSpecLike with Matchers with MockitoSugar //in V3, process id from V1 and V2 is now called a recipe instance id when(mockState.recipeInstanceId).thenReturn(processId) + when(mockState.recipeInstanceMetadata).thenReturn(Map()) + + when(mockState.events).thenReturn(List()) + //this call would fail without the fix RecipeRuntime.createInteractionInput(mockTransition, mockState) } diff --git a/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceEventList.java b/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceEventList.java new file mode 100644 index 000000000..2e011537b --- /dev/null +++ b/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceEventList.java @@ -0,0 +1,15 @@ +package com.ing.baker.recipe.annotations; + +import javax.inject.Qualifier; +import java.lang.annotation.*; + +/** + * An annotation to be added to an argument of an action indicating that the Event List should be injected + * there. + */ +@Qualifier +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface RecipeInstanceEventList { +} diff --git a/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceMetadata.java b/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceMetadata.java new file mode 100644 index 000000000..90fb10320 --- /dev/null +++ b/core/baker-annotations/src/main/java/com/ing/baker/recipe/annotations/RecipeInstanceMetadata.java @@ -0,0 +1,15 @@ +package com.ing.baker.recipe.annotations; + +import javax.inject.Qualifier; +import java.lang.annotation.*; + +/** + * An annotation to be added to an argument of an action indicating that the Metadata should be injected + * there. + */ +@Qualifier +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface RecipeInstanceMetadata { +} diff --git a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Baker.kt b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Baker.kt index 24488eb26..0b5350d0a 100644 --- a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Baker.kt +++ b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Baker.kt @@ -31,6 +31,10 @@ class Baker internal constructor(private val jBaker: Baker) : AutoCloseable { } } + fun getJavaBaker(): Baker { + return jBaker + } + suspend fun gracefulShutdown() { jBaker.gracefulShutdown().await() } @@ -113,6 +117,9 @@ class Baker internal constructor(private val jBaker: Baker) : AutoCloseable { suspend fun getIngredients(recipeInstanceId: String): Map = jBaker.getIngredients(recipeInstanceId).await() + suspend fun getIngredient(recipeInstanceId: String, name: String): Value = + jBaker.getIngredient(recipeInstanceId, name).await() + suspend fun getEvents(recipeInstanceId: String): List = jBaker.getEvents(recipeInstanceId).await() diff --git a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Config.kt b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Config.kt new file mode 100644 index 000000000..0fd9a202a --- /dev/null +++ b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/Config.kt @@ -0,0 +1,38 @@ +package com.ing.baker.runtime.kotlindsl + +import com.ing.baker.runtime.model.BakerF +import scala.concurrent.duration.FiniteDuration +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +data class Config( + val allowAddingRecipeWithoutRequiringInstances: Boolean = false, + val recipeInstanceConfig: RecipeInstanceConfig = RecipeInstanceConfig(), + val idleTimeout: Duration = 60.seconds, + val retentionPeriodCheckInterval: Duration = 10.seconds, + val bakeTimeout: Duration = 10.seconds, + val processEventTimeout: Duration = 10.seconds, + val inquireTimeout: Duration = 60.seconds, + val shutdownTimeout: Duration = 10.seconds, + val addRecipeTimeout: Duration = 10.seconds, + val executeSingleInteractionTimeout: Duration = 60.seconds) { + + private fun Duration.toFiniteDuration(): FiniteDuration { + return FiniteDuration.fromNanos(this.inWholeNanoseconds) + } + + fun toBakerFConfig(): BakerF.Config { + return BakerF.Config( + allowAddingRecipeWithoutRequiringInstances, + recipeInstanceConfig.toBakerFRecipeInstanceConfig(), + idleTimeout.toFiniteDuration(), + retentionPeriodCheckInterval.toFiniteDuration(), + bakeTimeout.toFiniteDuration(), + processEventTimeout.toFiniteDuration(), + inquireTimeout.toFiniteDuration(), + shutdownTimeout.toFiniteDuration(), + addRecipeTimeout.toFiniteDuration(), + executeSingleInteractionTimeout.toFiniteDuration() + ) + } +} \ No newline at end of file diff --git a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/InMemoryBaker.kt b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/InMemoryBaker.kt index 1ed668e96..9c66f0975 100644 --- a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/InMemoryBaker.kt +++ b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/InMemoryBaker.kt @@ -1,10 +1,14 @@ package com.ing.baker.runtime.kotlindsl import com.ing.baker.runtime.inmemory.InMemoryBaker -import com.ing.baker.runtime.model.BakerF object InMemoryBaker { fun kotlin(implementations: List = emptyList()) = Baker(InMemoryBaker.java(implementations)) - fun kotlin(config: BakerF.Config, implementations: List) = Baker(InMemoryBaker.java(config, implementations)) + /** + * Creates a InMemoryBaker with the com.ing.baker.runtime.kotlindsl.InMemoryBaker.Config. + */ + fun kotlin(config: Config, + implementations: List = emptyList()) = Baker(InMemoryBaker.java(config.toBakerFConfig(), implementations)) + } diff --git a/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/RecipeInstanceConfig.kt b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/RecipeInstanceConfig.kt new file mode 100644 index 000000000..914649ba8 --- /dev/null +++ b/core/baker-interface-kotlin/src/main/kotlin/com/ing/baker/runtime/kotlindsl/RecipeInstanceConfig.kt @@ -0,0 +1,23 @@ +package com.ing.baker.runtime.kotlindsl + +import com.ing.baker.runtime.model.recipeinstance.RecipeInstance +import scala.Option +import scala.concurrent.duration.FiniteDuration +import scala.jdk.CollectionConverters +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +data class RecipeInstanceConfig( + val idleTTL: Duration? = 5.seconds, + val ingredientsFilter: List = emptyList()) { + + private fun Duration.toFiniteDuration(): FiniteDuration { + return FiniteDuration.fromNanos(this.inWholeNanoseconds) + } + fun toBakerFRecipeInstanceConfig(): RecipeInstance.Config { + return RecipeInstance.Config( + idleTTL?.let{ Option.apply(it.toFiniteDuration()) } ?: Option.empty(), + CollectionConverters.ListHasAsScala(ingredientsFilter).asScala().toSeq() + ) + } +} \ No newline at end of file diff --git a/core/baker-interface/src/main/protobuf/common.proto b/core/baker-interface/src/main/protobuf/common.proto index 65e26c217..abc3e7cdf 100644 --- a/core/baker-interface/src/main/protobuf/common.proto +++ b/core/baker-interface/src/main/protobuf/common.proto @@ -158,6 +158,7 @@ message ProcessState { optional string recipeId = 5; optional string recipeInstanceId = 1; repeated Ingredient ingredients = 2; + map recipeInstanceMetadata = 6; repeated EventMoment events = 4; } @@ -314,6 +315,7 @@ message InteractionTransition { optional int32 maximumInteractionCount = 9; optional InteractionFailureStrategy failureStrategy = 10; map eventOutputTransformers = 11; + optional bool isReprovider = 12; } // END PETRINET @@ -351,6 +353,14 @@ message EventRejectedBakerEvent { optional RejectReason reason = 5; } +message EventFiredBakerEvent { + optional int64 timeStamp = 1; + optional string recipeName = 2; + optional string recipeId = 3; + optional string recipeInstanceId = 4; + optional RuntimeEvent event = 6; +} + message InteractionFailedBakerEvent { optional int64 timeStamp = 1; optional int64 duration = 2; @@ -399,6 +409,7 @@ message BakerEvent { oneof oneof_baker_event { EventReceivedBakerEvent eventReceived = 1; EventRejectedBakerEvent eventRejected = 2; + EventFiredBakerEvent eventFired = 8; InteractionFailedBakerEvent interactionFailed = 3; InteractionStartedBakerEvent interactionStarted = 4; InteractionCompletedBakerEvent interactionCompleted = 5; diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/Baker.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/Baker.scala index 9f22e5125..c9b00afb6 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/Baker.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/Baker.scala @@ -295,14 +295,14 @@ trait Baker[F[_]] extends LanguageApi { */ def getRecipeInstanceState(recipeInstanceId: String): F[RecipeInstanceStateType] -// /** -// * Returns a specific ingredient for a given RecipeInstance id. -// * -// * @param recipeInstanceId The recipeInstance Id. -// * @param name The name of the ingredient. -// * @return The provided ingredients. -// */ -// def getIngredient(recipeInstanceId: String, name: String): F[Value] + /** + * Returns a specific ingredient for a given RecipeInstance id. + * + * @param recipeInstanceId The recipeInstance Id. + * @param name The name of the ingredient. + * @return The provided ingredients. + */ + def getIngredient(recipeInstanceId: String, name: String): F[Value] /** * Returns all provided ingredients for a given RecipeInstance id. diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerEvent.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerEvent.scala index f98132191..4e28f8308 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerEvent.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerEvent.scala @@ -11,7 +11,7 @@ trait BakerEvent extends LanguageApi { } /** - * Event describing the fact that an event was received for a process. + * Event describing the fact that a sensory event was received for a process. */ trait EventReceived extends BakerEvent { val timeStamp: Long @@ -23,7 +23,7 @@ trait EventReceived extends BakerEvent { } /** - * Event describing the fact that an event was received but rejected for a process + * Event describing the fact that an sensory event was received but rejected for a process */ trait EventRejected extends BakerEvent { val timeStamp: Long @@ -33,6 +33,17 @@ trait EventRejected extends BakerEvent { val reason: RejectReason } +/** + * Event describing the fact that an interaction outcome event was fired for a process + */ +trait EventFired extends BakerEvent { + val timeStamp: Long + val recipeName: String + val recipeId: String + val recipeInstanceId: String + val event: Event +} + /** * Event describing the fact that an interaction failed during execution */ diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/RecipeInstanceState.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/RecipeInstanceState.scala index a8e6c523e..330cd3b4d 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/RecipeInstanceState.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/RecipeInstanceState.scala @@ -1,11 +1,12 @@ package com.ing.baker.runtime.common +import com.ing.baker.il.recipeInstanceMetadataName import com.ing.baker.runtime.common.LanguageDataStructures.LanguageApi import com.ing.baker.types.Value object RecipeInstanceState { //The name used for the RecipeInstanceMetaData ingredient - val RecipeInstanceMetaDataName = "RecipeInstanceMetaData" + val RecipeInstanceMetadataName = recipeInstanceMetadataName } /** @@ -21,5 +22,7 @@ trait RecipeInstanceState extends LanguageApi { self => def ingredients: language.Map[String, Value] + def recipeInstanceMetadata: language.Map[String, String] + def events: language.Seq[EventType] } \ No newline at end of file diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryBaker.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryBaker.scala index 687342791..b6467c71a 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryBaker.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryBaker.scala @@ -2,6 +2,7 @@ package com.ing.baker.runtime.inmemory import cats.effect.{ConcurrentEffect, ContextShift, IO, Timer} import cats.~> +import com.ing.baker.runtime.javadsl.BakerConfig import com.ing.baker.runtime.model.{BakerComponents, BakerF, InteractionInstance} import com.ing.baker.runtime.{defaultinteractions, javadsl} @@ -15,7 +16,7 @@ object InMemoryBaker { def build(config: BakerF.Config = BakerF.Config(), implementations: List[InteractionInstance[IO]])(implicit timer: Timer[IO], cs: ContextShift[IO]): IO[BakerF[IO]] = for { - recipeInstanceManager <- InMemoryRecipeInstanceManager.build(config.idleTimeout) + recipeInstanceManager <- InMemoryRecipeInstanceManager.build(config.idleTimeout, config.retentionPeriodCheckInterval) recipeManager <- InMemoryRecipeManager.build eventStream <- InMemoryEventStream.build interactions <- InMemoryInteractionManager.build(implementations ++ defaultinteractions.all) @@ -43,6 +44,10 @@ object InMemoryBaker { new javadsl.Baker(bakerF) } + def java(config: BakerConfig, implementations: JavaList[AnyRef]): javadsl.Baker = { + java(config.toBakerFConfig(), implementations) + } + def java(implementations: JavaList[AnyRef]): javadsl.Baker = { java(BakerF.Config(), implementations) } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryRecipeInstanceManager.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryRecipeInstanceManager.scala index 0258e4b4e..7ee7b4ee9 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryRecipeInstanceManager.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/inmemory/InMemoryRecipeInstanceManager.scala @@ -6,37 +6,52 @@ import cats.implicits._ import com.google.common.cache.{CacheBuilder, CacheLoader} import com.ing.baker.runtime.common.BakerException.NoSuchProcessException import com.ing.baker.runtime.model.RecipeInstanceManager.RecipeInstanceStatus +import com.ing.baker.runtime.model.RecipeInstanceManager.RecipeInstanceStatus.Active import com.ing.baker.runtime.model.recipeinstance.RecipeInstance import com.ing.baker.runtime.model.{BakerComponents, RecipeInstanceManager} import com.ing.baker.runtime.scaladsl.RecipeInstanceMetadata -import java.util.concurrent.{ConcurrentMap, TimeUnit} +import java.util.concurrent.ConcurrentMap import scala.annotation.nowarn import scala.collection.JavaConverters._ -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{FiniteDuration, MILLISECONDS} object InMemoryRecipeInstanceManager { type Store = ConcurrentMap[String, RecipeInstanceStatus[IO]] - def build(instanceTTL: Duration)(implicit timer: Timer[IO]): IO[InMemoryRecipeInstanceManager] = { + def build(idleTimeOut: FiniteDuration, retentionPeriodCheckInterval: FiniteDuration)(implicit timer: Timer[IO]): IO[InMemoryRecipeInstanceManager] = { val cache: ConcurrentMap[String, RecipeInstanceStatus[IO]] = CacheBuilder.newBuilder() - .expireAfterWrite(instanceTTL.toMillis, TimeUnit.MILLISECONDS) .build(new CacheLoader[String, RecipeInstanceStatus[IO]] { override def load(key: String): RecipeInstanceStatus[IO] = throw NoSuchProcessException("key") }).asMap() - Ref.of[IO, Store](cache).map(new InMemoryRecipeInstanceManager(_)) + Ref.of[IO, Store](cache).map(new InMemoryRecipeInstanceManager(_, retentionPeriodCheckInterval, idleTimeOut)) } } -final class InMemoryRecipeInstanceManager(inmem: Ref[IO, InMemoryRecipeInstanceManager.Store])(implicit timer: Timer[IO]) extends RecipeInstanceManager[IO] { +final class InMemoryRecipeInstanceManager(inmem: Ref[IO, InMemoryRecipeInstanceManager.Store], + retentionPeriodCheckInterval: FiniteDuration, + idleTimeOut: FiniteDuration)(implicit timer: Timer[IO]) extends RecipeInstanceManager[IO] { - override def fetch(recipeInstanceId: String): IO[Option[RecipeInstanceStatus[IO]]] = - inmem.get.map(store => Option.apply(store.get(recipeInstanceId))) +// We use this function instead of the startRetentionPeriodStream stream since it performs better + def repeat(io : IO[Unit]) : IO[Nothing] = io >> IO.sleep(retentionPeriodCheckInterval) >> repeat(io) + val repeatEvaluation = repeat(cleanupRecipeInstances(idleTimeOut)).unsafeRunAsyncAndForget() + + override def fetch(recipeInstanceId: String): IO[Option[RecipeInstanceStatus[IO]]] = { + inmem.getAndUpdate(store => { + Option.apply(store.get(recipeInstanceId)) match { + case Some(recipeInstance: Active[IO]) => + store.put(recipeInstanceId, recipeInstance.copy(lastModified = System.currentTimeMillis())) + store + case _ => store + } + }).map(store => Option.apply(store.get(recipeInstanceId)) + ) + } override def store(newRecipeInstance: RecipeInstance[IO])(implicit components: BakerComponents[IO]): IO[Unit] = inmem.update(store => { - store.put(newRecipeInstance.recipeInstanceId, RecipeInstanceStatus.Active(newRecipeInstance)) + store.put(newRecipeInstance.recipeInstanceId, RecipeInstanceStatus.Active(newRecipeInstance, System.currentTimeMillis())) store }) @@ -46,7 +61,7 @@ final class InMemoryRecipeInstanceManager(inmem: Ref[IO, InMemoryRecipeInstanceM @nowarn override def getAllRecipeInstancesMetadata: IO[Set[RecipeInstanceMetadata]] = inmem.get.flatMap(_.asScala.toMap.toList.traverse { - case (recipeInstanceId, RecipeInstanceStatus.Active(recipeInstance)) => + case (recipeInstanceId, RecipeInstanceStatus.Active(recipeInstance, _)) => recipeInstance.state.get.map(currentState => RecipeInstanceMetadata(currentState.recipe.recipeId, recipeInstanceId, currentState.createdOn)) case (recipeInstanceId, RecipeInstanceStatus.Deleted(recipeId, createdOn, _)) => IO.pure(RecipeInstanceMetadata(recipeId, recipeInstanceId, createdOn)) diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala index 2949dd7f4..8a149725f 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/Baker.scala @@ -217,13 +217,13 @@ class Baker(private val baker: scaladsl.Baker) extends common.Baker[CompletableF def getRecipeInstanceState(@Nonnull recipeInstanceId: String): CompletableFuture[RecipeInstanceState] = toCompletableFuture(baker.getRecipeInstanceState(recipeInstanceId)).thenApply(_.asJava) -// /** -// * @param recipeInstanceId The recipeInstance Id. -// * @param name The name of the ingredient. -// * @return The provided ingredients. -// */ -// override def getIngredient(recipeInstanceId: String, name: String): CompletableFuture[Value] = -// toCompletableFuture(baker.getIngredient(recipeInstanceId, name)) + /** + * @param recipeInstanceId The recipeInstance Id. + * @param name The name of the ingredient. + * @return The provided ingredients. + */ + override def getIngredient(recipeInstanceId: String, name: String): CompletableFuture[Value] = + toCompletableFuture(baker.getIngredient(recipeInstanceId, name)) /** * Returns all the ingredients that are accumulated for a given process. diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerConfig.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerConfig.scala new file mode 100644 index 000000000..38fa53715 --- /dev/null +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerConfig.scala @@ -0,0 +1,82 @@ +package com.ing.baker.runtime.javadsl + +import com.ing.baker.runtime.model.BakerF + +import java.time.Duration +import scala.concurrent.duration.{FiniteDuration, NANOSECONDS} + +object BakerConfig { + def defaults(): BakerConfig = { + new BakerConfig( + false, + RecipeInstanceConfig(), + Duration.ofSeconds(60), + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Duration.ofSeconds(60), + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Duration.ofSeconds(60) + ) + } +} + +case class BakerConfig( + val allowAddingRecipeWithoutRequiringInstances: Boolean, + val recipeInstanceConfig: RecipeInstanceConfig, + val idleTimeout: Duration, + val retentionPeriodCheckInterval: Duration, + val bakeTimeout: Duration, + val processEventTimeout: Duration, + val inquireTimeout: Duration, + val shutdownTimeout: Duration, + val addRecipeTimeout: Duration, + val executeSingleInteractionTimeout: Duration) { + def withAllowAddingRecipeWithoutRequiringInstances(allowAddingRecipeWithoutRequiringInstances: Boolean): BakerConfig = + copy(allowAddingRecipeWithoutRequiringInstances = allowAddingRecipeWithoutRequiringInstances) + + def withRecipeInstanceConfig(recipeInstanceConfig: RecipeInstanceConfig): BakerConfig = + copy(recipeInstanceConfig = recipeInstanceConfig) + + def withIdleTimeout(idleTimeout: Duration): BakerConfig = + copy(idleTimeout = idleTimeout) + + def withRetentionPeriodCheckInterval(retentionPeriodCheckInterval: Duration): BakerConfig = + copy(retentionPeriodCheckInterval = retentionPeriodCheckInterval) + + def withBakeTimeout(bakeTimeout: Duration): BakerConfig = + copy(bakeTimeout = bakeTimeout) + + def withProcessEventTimeout(processEventTimeout: Duration): BakerConfig = + copy(processEventTimeout = processEventTimeout) + + def withInquireTimeout(inquireTimeout: Duration): BakerConfig = + copy(inquireTimeout = inquireTimeout) + + def withShutdownTimeout(shutdownTimeout: Duration): BakerConfig = + copy(shutdownTimeout = shutdownTimeout) + + def withAddRecipeTimeout(addRecipeTimeout: Duration): BakerConfig = + copy(addRecipeTimeout = addRecipeTimeout) + + def withExecuteSingleInteractionTimeout(executeSingleInteractionTimeout: Duration): BakerConfig = + copy(executeSingleInteractionTimeout = executeSingleInteractionTimeout) + + def toBakerFConfig(): BakerF.Config = { + implicit def toScalaDuration(duration: Duration): FiniteDuration = FiniteDuration.apply(duration.toNanos, NANOSECONDS) + + BakerF.Config( + allowAddingRecipeWithoutRequiringInstances, + recipeInstanceConfig.toBakerFRecipeInstanceConfig(), + idleTimeout, + retentionPeriodCheckInterval, + bakeTimeout, + processEventTimeout, + inquireTimeout, + shutdownTimeout, + addRecipeTimeout, + executeSingleInteractionTimeout + ) + } +} diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerEvent.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerEvent.scala index f24d352a7..0ebed081f 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerEvent.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/BakerEvent.scala @@ -71,6 +71,34 @@ case class EventRejected(timeStamp: Long, def getReason: RejectReason = reason } + +/** + * Event describing the fact that an interaction outcome event was fired for a process + * + * @param timeStamp The time that the event was received + * @param recipeName The name of the recipe that interaction is part of + * @param recipeId The recipe id + * @param recipeInstanceId The id of the process + * @param correlationId The (optional) correlation id of the event + * @param event The event + */ +case class EventFired(timeStamp: Long, + recipeName: String, + recipeId: String, + recipeInstanceId: String, + event: EventInstance) extends BakerEvent with common.EventFired { + + def getTimeStamp: Long = timeStamp + + def getRecipeName: String = recipeName + + def getRecipeId: String = recipeId + + def getRecipeInstanceId: String = recipeInstanceId + + def getEvent: EventInstance = event +} + /** * Event describing the fact that an interaction failed during execution * diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceConfig.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceConfig.scala new file mode 100644 index 000000000..526089e86 --- /dev/null +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceConfig.scala @@ -0,0 +1,24 @@ +package com.ing.baker.runtime.javadsl + +import com.ing.baker.runtime.model.recipeinstance.RecipeInstance + +import java.time.Duration +import java.util.Optional +import scala.concurrent.duration.{FiniteDuration, NANOSECONDS} +import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.jdk.OptionConverters.RichOptional + +case class RecipeInstanceConfig(idleTTL: Optional[Duration] = Optional.of(Duration.ofSeconds(5)), + ingredientsFilter: java.util.List[String] = java.util.List.of()) { + + def withIdleTTL(idleTTL: Optional[Duration]) = copy(idleTTL = idleTTL) + + def withIngredientsFilter(ingredientsFilter: java.util.List[String]) = ingredientsFilter + + def toBakerFRecipeInstanceConfig(): RecipeInstance.Config = { + RecipeInstance.Config( + idleTTL.toScala.map(duration => FiniteDuration.apply(duration.toNanos, NANOSECONDS)), + ingredientsFilter.asScala.toSeq + ) + } +} diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceState.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceState.scala index 5ae49302d..2fc3ad95f 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceState.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/javadsl/RecipeInstanceState.scala @@ -19,6 +19,7 @@ case class RecipeInstanceState( recipeId: String, recipeInstanceId: String, ingredients: java.util.Map[String, Value], + recipeInstanceMetadata: java.util.Map[String, String], events: java.util.List[EventMoment] ) extends common.RecipeInstanceState with JavaApi { @@ -31,6 +32,13 @@ case class RecipeInstanceState( */ def getIngredients: java.util.Map[String, Value] = ingredients + /** + * Returns the accumulated ingredients. + * + * @return The accumulated ingredients + */ + def getRecipeInstanceMetadata: java.util.Map[String, String] = recipeInstanceMetadata + /** * Returns the RuntimeEvents * @@ -54,5 +62,5 @@ case class RecipeInstanceState( @nowarn def asScala: scaladsl.RecipeInstanceState = - scaladsl.RecipeInstanceState(recipeId, recipeInstanceId, ingredients.asScala.toMap, events.asScala.map(_.asScala).toIndexedSeq) + scaladsl.RecipeInstanceState(recipeId, recipeInstanceId, ingredients.asScala.toMap, recipeInstanceMetadata.asScala.toMap, events.asScala.map(_.asScala).toIndexedSeq) } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala index 5080e8145..7858ae7ef 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala @@ -305,19 +305,19 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con .timeout(config.inquireTimeout) .recoverWith(javaTimeoutToBakerTimeout("getRecipeInstanceState")) -// /** -// * Returns all provided ingredients for a given RecipeInstance id. -// * -// * @param recipeInstanceId The process id. -// * @return The provided ingredients. -// */ -// override def getIngredient(recipeInstanceId: String, name: String): F[Value] = { -// getRecipeInstanceState(recipeInstanceId).map(_.ingredients) -// .map(x => x.get(name) match { -// case Some(value) => value -// case None => throw NoSuchIngredientException(name) -// }) -// } + /** + * Returns all provided ingredients for a given RecipeInstance id. + * + * @param recipeInstanceId The process id. + * @return The provided ingredients. + */ + override def getIngredient(recipeInstanceId: String, name: String): F[Value] = { + getRecipeInstanceState(recipeInstanceId).map(_.ingredients) + .map(x => x.get(name) match { + case Some(value) => value + case None => throw NoSuchIngredientException(name) + }) + } /** * Returns all provided ingredients for a given RecipeInstance id. @@ -463,8 +463,8 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con mapK(self.getAllRecipeInstancesMetadata) override def getRecipeInstanceState(recipeInstanceId: String): G[RecipeInstanceState] = mapK(self.getRecipeInstanceState(recipeInstanceId)) -// override def getIngredient(recipeInstanceId: String, name: String): G[Value] = -// mapK(self.getIngredient(recipeInstanceId, name)) + override def getIngredient(recipeInstanceId: String, name: String): G[Value] = + mapK(self.getIngredient(recipeInstanceId, name)) override def getIngredients(recipeInstanceId: String): G[Map[String, Value]] = mapK(self.getIngredients(recipeInstanceId)) override def getEvents(recipeInstanceId: String): G[Seq[EventMoment]] = @@ -523,8 +523,8 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con mapK(self.getAllRecipeInstancesMetadata) override def getRecipeInstanceState(recipeInstanceId: String): Future[RecipeInstanceState] = mapK(self.getRecipeInstanceState(recipeInstanceId)) -// override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = -// mapK(self.getIngredient(recipeInstanceId, name)) + override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = + mapK(self.getIngredient(recipeInstanceId, name)) override def getIngredients(recipeInstanceId: String): Future[Map[String, Value]] = mapK(self.getIngredients(recipeInstanceId)) override def getEvents(recipeInstanceId: String): Future[Seq[EventMoment]] = diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerLogging.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerLogging.scala index 1012621fa..61910c364 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerLogging.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerLogging.scala @@ -75,7 +75,9 @@ case class BakerLogging(logger: Logger = BakerLogging.defaultLogger) { "recipeInstanceId" -> interactionCompleted.recipeInstanceId, "interactionName" -> interactionCompleted.interactionName, "duration" -> interactionCompleted.duration.toString, - "timeFinished" -> interactionCompleted.timeStamp.toString + "timeFinished" -> interactionCompleted.timeStamp.toString, + "recipeId" -> interactionCompleted.recipeId, + "recipeName" -> interactionCompleted.recipeName ) withMDC(mdc, _.info(msg)) } @@ -94,10 +96,17 @@ case class BakerLogging(logger: Logger = BakerLogging.defaultLogger) { withMDC(mdc, _.error(msg, interactionFailed.throwable)) } - def firingEvent(recipeInstanceId: String, executionId: Long, transition: Transition, timeStarted: Long): Unit = { + def firingEvent(recipeInstanceId: String, + recipeId: String, + recipeName: String, + executionId: Long, + transition: Transition, + timeStarted: Long): Unit = { val msg = s"Firing event '${transition.label}'" val mdc = Map( "recipeInstanceId" -> recipeInstanceId, + "recipeId" -> recipeId, + "recipeName" -> recipeName, "eventName" -> transition.label, "runtimeTimestamp" -> timeStarted.toString, "executionId" -> executionId.toString @@ -105,6 +114,18 @@ case class BakerLogging(logger: Logger = BakerLogging.defaultLogger) { withMDC(mdc, _.info(msg)) } + def eventFired(eventFired: EventFired): Unit = { + val msg = s"Firing event '${eventFired.event.name}'" + val mdc = Map( + "recipeInstanceId" -> eventFired.recipeInstanceId, + "recipeId" -> eventFired.recipeId, + "recipeName" -> eventFired.recipeName, + "eventName" -> eventFired.event.name, + "runtimeTimestamp" -> eventFired.timeStamp.toString, + ) + withMDC(mdc, _.info(msg)) + } + def eventReceived(eventReceived: EventReceived): Unit = { val msg = s"Event received '${eventReceived.event.name}'" val mdc = Map( diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/InteractionManager.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/InteractionManager.scala index 5635a3285..70ef6caff 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/InteractionManager.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/InteractionManager.scala @@ -43,7 +43,7 @@ trait InteractionManager[F[_]] { def execute(interaction: InteractionTransition, input: Seq[IngredientInstance], metadata: Option[Map[String, String]])(implicit sync: Sync[F], effect: MonadError[F, Throwable]): F[Option[EventInstance]] = { if(interaction.interactionName.startsWith(checkpointEventInteractionPrefix)){ effect.pure(Some(EventInstance(interaction.interactionName.stripPrefix(checkpointEventInteractionPrefix)))) - }else{ + } else{ findFor(interaction) .flatMap { case Some(implementation) => implementation.execute(input, metadata.getOrElse(Map())) diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeInstanceManager.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeInstanceManager.scala index be1fe44e2..94b031306 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeInstanceManager.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeInstanceManager.scala @@ -6,7 +6,7 @@ import cats.effect.{ConcurrentEffect, Effect, Resource, Timer} import cats.implicits._ import com.ing.baker.il.{RecipeVisualStyle, RecipeVisualizer} import com.ing.baker.runtime.common.BakerException.{ProcessAlreadyExistsException, ProcessDeletedException} -import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetaDataName +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName import com.ing.baker.runtime.common.{BakerException, SensoryEventStatus} import com.ing.baker.runtime.model.RecipeInstanceManager.RecipeInstanceStatus import com.ing.baker.runtime.model.recipeinstance.RecipeInstance @@ -24,7 +24,7 @@ object RecipeInstanceManager { object RecipeInstanceStatus { - case class Active[F[_]](recipeInstance: RecipeInstance[F]) extends RecipeInstanceStatus[F] + case class Active[F[_]](recipeInstance: RecipeInstance[F], lastModified: Long) extends RecipeInstanceStatus[F] case class Deleted[F[_]](recipeId: String, createdOn: Long, deletedOn: Long) extends RecipeInstanceStatus[F] } @@ -44,21 +44,23 @@ trait RecipeInstanceManager[F[_]] { def getAllRecipeInstancesMetadata: F[Set[RecipeInstanceMetadata]] - def startRetentionPeriodStream(interval: FiniteDuration)(implicit effect: Effect[F], timer: Timer[F]): Resource[F, Unit] = - Stream.awakeEvery[F](interval).evalMap { _ => - for { - allRecipeInstances <- fetchAll - _ <- allRecipeInstances.toList.traverse { case (recipeInstanceId, instance) => - computeShouldDelete(instance).flatMap(shouldDelete => - if (shouldDelete) remove(recipeInstanceId) else effect.unit) - } - } yield () - }.compile.resource.drain + //This is not used since the direct usage of IO seems to perform better + def startRetentionPeriodStream(interval: FiniteDuration, idleTTL: FiniteDuration)(implicit effect: Effect[F], timer: Timer[F]) = + Stream.awakeEvery[F](interval).evalMap { _ => cleanupRecipeInstances(idleTTL) }.compile.resource.drain + + protected def cleanupRecipeInstances(idleTimeOut: FiniteDuration)(implicit effect: Effect[F], timer: Timer[F]): F[Unit] = + for { + allRecipeInstances <- fetchAll + _ <- allRecipeInstances.toList.traverse { case (recipeInstanceId, instance) => + computeShouldDelete(instance, idleTimeOut).flatMap(shouldDelete => + if (shouldDelete) remove(recipeInstanceId) else effect.unit) + } + } yield () def bake(recipeId: String, recipeInstanceId: String, config: RecipeInstance.Config)(implicit components: BakerComponents[F], effect: Effect[F], timer: Timer[F]): F[Unit] = for { _ <- fetch(recipeInstanceId).flatMap[Unit] { - case Some(RecipeInstanceStatus.Active(_)) => + case Some(RecipeInstanceStatus.Active(_, _)) => effect.raiseError(ProcessAlreadyExistsException(recipeInstanceId)) case Some(RecipeInstanceStatus.Deleted(_, _, _)) => effect.raiseError(ProcessDeletedException(recipeInstanceId)) @@ -77,6 +79,7 @@ trait RecipeInstanceManager[F[_]] { currentState.recipe.recipeId, recipeInstanceId, currentState.ingredients, + currentState.recipeInstanceMetadata, currentState.events ) }) @@ -98,7 +101,7 @@ trait RecipeInstanceManager[F[_]] { Left(FireSensoryEventRejection.NoSuchRecipeInstance(recipeInstanceId)) case Some(RecipeInstanceStatus.Deleted(_, _, _)) => Left(FireSensoryEventRejection.RecipeInstanceDeleted(recipeInstanceId)) - case Some(RecipeInstanceStatus.Active(recipeInstance)) => + case Some(RecipeInstanceStatus.Active(recipeInstance, _)) => Right(recipeInstance) }).flatMap(_.fireEventStream(event, correlationId)) } @@ -149,16 +152,10 @@ trait RecipeInstanceManager[F[_]] { def addMetaData(recipeInstanceId: String, metadata: Map[String, String])(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): F[Unit] = { getExistent(recipeInstanceId).map((recipeInstance: RecipeInstance[F]) => { recipeInstance.state.update(currentState => { - val newBakerMetaData = currentState.ingredients.get(RecipeInstanceMetaDataName) match { - case Some(value) => - if (value.isInstanceOf(MapType(com.ing.baker.types.CharArray))) { - val oldMetaData: Map[String, String] = value.asMap[String, String](classOf[String], classOf[String]).asScala.toMap - oldMetaData ++ metadata - } - else metadata - case None => metadata - } - currentState.copy(ingredients = currentState.ingredients + (RecipeInstanceMetaDataName -> com.ing.baker.types.Converters.toValue(newBakerMetaData))) + val newRecipeInstanceMetaData = currentState.recipeInstanceMetadata ++ metadata + currentState.copy( + ingredients = currentState.ingredients + (RecipeInstanceMetadataName -> com.ing.baker.types.Converters.toValue(newRecipeInstanceMetaData)), + recipeInstanceMetadata = newRecipeInstanceMetaData) }) }).flatten } @@ -175,7 +172,7 @@ trait RecipeInstanceManager[F[_]] { private def getExistent(recipeInstanceId: String)(implicit effect: Effect[F]): F[RecipeInstance[F]] = fetch(recipeInstanceId).flatMap { - case Some(RecipeInstanceStatus.Active(recipeInstance)) => effect.pure(recipeInstance) + case Some(RecipeInstanceStatus.Active(recipeInstance, _)) => effect.pure(recipeInstance) case Some(RecipeInstanceStatus.Deleted(_, _, _)) => effect.raiseError(BakerException.ProcessDeletedException(recipeInstanceId)) case None => effect.raiseError(BakerException.NoSuchProcessException(recipeInstanceId)) } @@ -225,13 +222,17 @@ trait RecipeInstanceManager[F[_]] { f(a) } - private def computeShouldDelete(status: RecipeInstanceStatus[F])(implicit effect: Effect[F], timer: Timer[F]): F[Boolean] = + private def computeShouldDelete(status: RecipeInstanceStatus[F], idleTimeOut: FiniteDuration)(implicit effect: Effect[F], timer: Timer[F]): F[Boolean] = for { currentTime <- timer.clock.realTime(MILLISECONDS) result <- status match { - case RecipeInstanceStatus.Active(recipeInstance) => + case RecipeInstanceStatus.Active(recipeInstance, lastModified) => recipeInstance.state.get.map { currentState => - currentState.recipe.retentionPeriod.exists(_.toMillis + currentState.createdOn < currentTime) + //If the process is Inactive validate on the idleTTL + val shouldPassivateOnIdleTTL = currentState.isInactive && currentTime > (lastModified + idleTimeOut.toMillis) + //If the retentionPeriod is defined always delete after this time + val shouldPassivateOnRetentionPeriod = currentState.recipe.retentionPeriod.exists(_.toMillis + currentState.createdOn < currentTime) + shouldPassivateOnIdleTTL || shouldPassivateOnRetentionPeriod } case RecipeInstanceStatus.Deleted(_, _, _) => effect.pure(false) diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeManager.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeManager.scala index 002dd6e68..4c54ad94e 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeManager.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/RecipeManager.scala @@ -38,9 +38,9 @@ trait RecipeManager[F[_]] extends LazyLogging { for { timestamp <- timer.clock.realTime(duration.MILLISECONDS) _ <- store(compiledRecipe, timestamp) - event = RecipeAdded(compiledRecipe.name, compiledRecipe.recipeId, timestamp, compiledRecipe) - _ <- effect.delay(components.logging.addedRecipe(event)) - _ <- components.eventStream.publish(event) + recipeAdded = RecipeAdded(compiledRecipe.name, compiledRecipe.recipeId, timestamp, compiledRecipe) + _ <- effect.delay(components.logging.addedRecipe(recipeAdded)) + _ <- components.eventStream.publish(recipeAdded) } yield () } yield compiledRecipe.recipeId diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstance.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstance.scala index 915502b91..23b551c34 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstance.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstance.scala @@ -8,7 +8,7 @@ import com.ing.baker.il.CompiledRecipe import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome import com.ing.baker.runtime.model.{BakerComponents, FireSensoryEventRejection} import com.ing.baker.runtime.model.recipeinstance.RecipeInstance.FatalInteractionException -import com.ing.baker.runtime.scaladsl.{EventInstance, EventReceived, EventRejected, RecipeInstanceCreated} +import com.ing.baker.runtime.scaladsl.{EventFired, EventInstance, EventReceived, EventRejected, RecipeInstanceCreated} import com.typesafe.scalalogging.LazyLogging import fs2.Stream @@ -41,12 +41,13 @@ case class RecipeInstance[F[_]](recipeInstanceId: String, config: RecipeInstance initialExecution <- EitherT.fromEither[F](currentState.validateExecution(input, correlationId, currentTime)) .leftSemiflatMap { case (rejection, reason) => for { - event <- effect.delay(EventRejected(currentTime, recipeInstanceId, correlationId, input, rejection.asReason)) - _ <- effect.delay(components.logging.eventRejected(event)) - _ <- components.eventStream.publish(event) + eventRejected <- effect.delay(EventRejected(currentTime, recipeInstanceId, correlationId, input, rejection.asReason)) + _ <- effect.delay(components.logging.eventRejected(eventRejected)) + _ <- components.eventStream.publish(eventRejected) } yield rejection } _ <- EitherT.liftF(components.eventStream.publish(EventReceived(currentTime, currentState.recipe.name, currentState.recipe.recipeId, recipeInstanceId, correlationId, input))) + _ <- EitherT.liftF(components.eventStream.publish(EventFired(currentTime, currentState.recipe.name, currentState.recipe.recipeId, recipeInstanceId, input))) } yield baseCase(initialExecution) .collect { case Some(output) => output.filterNot(config.ingredientsFilter) } @@ -106,7 +107,11 @@ case class RecipeInstance[F[_]](recipeInstanceId: String, config: RecipeInstance } yield output -> enabledExecutions case Left(ExceptionStrategyOutcome.Continue(eventName)) => - handleExecutionOutcome(finishedExecution)(Right(Some(EventInstance(eventName, Map.empty)))) + val output: EventInstance = EventInstance(eventName, Map.empty) + for { + enabledExecutions <- state.modify(_.recordFailedWithOutputExecution(finishedExecution, Some(output))) + _ <- scheduleIdleStop + } yield Some(output) -> enabledExecutions case Left(strategy @ ExceptionStrategyOutcome.BlockTransition) => state diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceEventValidation.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceEventValidation.scala index a62c22426..58e6be034 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceEventValidation.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceEventValidation.scala @@ -33,7 +33,10 @@ trait RecipeInstanceEventValidation { recipeInstance: RecipeInstanceState => consume = params.head, input = Some(input), ingredients = recipeInstance.ingredients, - correlationId = correlationId + recipeInstanceMetadata = recipeInstance.recipeInstanceMetadata, + eventList = events, + correlationId = correlationId, + isReprovider = false ) } yield execution } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceState.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceState.scala index daae399ef..91cf01d9a 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceState.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/RecipeInstanceState.scala @@ -1,21 +1,19 @@ package com.ing.baker.runtime.model.recipeinstance +import com.ing.baker.il.CompiledRecipe import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome -import com.ing.baker.il.{CompiledRecipe, EventDescriptor} -import com.ing.baker.il.petrinet.Place -import com.ing.baker.petrinet.api.{Marking, MultiSet} -import com.ing.baker.runtime.scaladsl.{EventInstance, EventMoment} +import com.ing.baker.il.petrinet.Place.IngredientPlace import com.ing.baker.il.petrinet._ import com.ing.baker.petrinet.api._ -import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetaDataName +import com.ing.baker.runtime.common.IngredientInstance +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName +import com.ing.baker.runtime.scaladsl.{EventInstance, EventMoment} import com.ing.baker.types.{CharArray, MapType, Value} -import scala.collection.immutable - object RecipeInstanceState { def getMetaDataFromIngredients(ingredients: Map[String, Value]): Option[Map[String, String]] = { - ingredients.get(RecipeInstanceMetaDataName).flatMap(value => { + ingredients.get(RecipeInstanceMetadataName).flatMap(value => { if (value.isInstanceOf(MapType(CharArray))) Some(value.as[Map[String, String]]) else @@ -23,6 +21,9 @@ object RecipeInstanceState { }) } + def getMetaDataFromIngredients(ingredients: Seq[IngredientInstance]): Option[Map[String, String]] = + getMetaDataFromIngredients(ingredients.map(i => i.name -> i.value).toMap) + def empty(recipeInstanceId: String, recipe: CompiledRecipe, createdOn: Long): RecipeInstanceState = RecipeInstanceState( recipeInstanceId, @@ -31,6 +32,7 @@ object RecipeInstanceState { sequenceNumber = 0, marking = recipe.initialMarking, ingredients = Map.empty, + recipeInstanceMetadata = Map.empty, events = List.empty, completedCorrelationIds = Set.empty, executions = Map.empty, @@ -45,6 +47,7 @@ case class RecipeInstanceState( sequenceNumber: Long, marking: Marking[Place], ingredients: Map[String, Value], + recipeInstanceMetadata: Map[String, String], events: List[EventMoment], completedCorrelationIds: Set[String], executions: Map[Long, TransitionExecution], @@ -70,6 +73,14 @@ case class RecipeInstanceState( def recordFailedExecution(transitionExecution: TransitionExecution, exceptionStrategy: ExceptionStrategyOutcome): RecipeInstanceState = addExecution(transitionExecution.toFailedState(exceptionStrategy)) + def recordFailedWithOutputExecution(transitionExecution: TransitionExecution, output: Option[EventInstance]): (RecipeInstanceState, Set[TransitionExecution]) = + aggregateOutputEvent(output) + .increaseSequenceNumber + .aggregatePetriNetChanges(transitionExecution, output) + .addCompletedCorrelationId(transitionExecution) + .addExecution(transitionExecution.copy(state = TransitionExecution.State.Failed(transitionExecution.failureCount, ExceptionStrategyOutcome.BlockTransition))) + .allEnabledExecutions + def recordCompletedExecution(transitionExecution: TransitionExecution, output: Option[EventInstance]): (RecipeInstanceState, Set[TransitionExecution]) = aggregateOutputEvent(output) .increaseSequenceNumber @@ -97,6 +108,19 @@ case class RecipeInstanceState( !hasFailed(transition) && canBeFiredAutomatically(transition) } val executions = canFire.map { + case (transition: InteractionTransition, markings) => + TransitionExecution( + recipeInstanceId = recipeInstanceId, + recipe = recipe, + transition = transition, + consume = markings.head, + input = None, + ingredients = ingredients, + recipeInstanceMetadata = recipeInstanceMetadata, + eventList = events, + correlationId = None, + isReprovider = transition.isReprovider + ) case (transition, markings) => TransitionExecution( recipeInstanceId = recipeInstanceId, @@ -105,7 +129,10 @@ case class RecipeInstanceState( consume = markings.head, input = None, ingredients = ingredients, - correlationId = None + recipeInstanceMetadata = recipeInstanceMetadata, + eventList = events, + correlationId = None, + isReprovider = false ) }.toSeq @@ -125,12 +152,20 @@ case class RecipeInstanceState( copy(sequenceNumber = sequenceNumber + 1) private def aggregatePetriNetChanges(transitionExecution: TransitionExecution, output: Option[EventInstance]): RecipeInstanceState = { - val producedMarking = - recipe.petriNet.outMarking(transitionExecution.transition).keys.map { place => + val outputMarkings: MultiSet[Place] = recipe.petriNet.outMarking(transitionExecution.transition) + + val producedMarking: Marking[Place] = { + outputMarkings.keys.map { place: Place => val value: Any = output.map(_.name).orNull place -> MultiSet.copyOff(Seq(value)) }.toMarking - copy(marking = (marking |-| transitionExecution.consume) |+| producedMarking) + } + + val reproviderMarkings: Marking[Place] = if (transitionExecution.isReprovider) { + outputMarkings.toMarking.filter((input: (Place, MultiSet[Any])) => input._1.placeType == IngredientPlace) + } else Map.empty + + copy(marking = (marking |-| transitionExecution.consume) |+| producedMarking |+| reproviderMarkings) } private def addCompletedCorrelationId(transitionExecution: TransitionExecution): RecipeInstanceState = diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/TransitionExecution.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/TransitionExecution.scala index b23a02415..a5223cc21 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/TransitionExecution.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/recipeinstance/TransitionExecution.scala @@ -1,8 +1,7 @@ package com.ing.baker.runtime.model.recipeinstance import java.lang.reflect.InvocationTargetException - -import cats.effect.{Effect, Timer} +import cats.effect.{Effect, IO, Timer} import cats.implicits._ import com.ing.baker.il import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome @@ -12,11 +11,11 @@ import com.ing.baker.petrinet.api._ import com.ing.baker.runtime.model.BakerComponents import com.ing.baker.runtime.model.recipeinstance.RecipeInstance.FatalInteractionException import com.ing.baker.runtime.scaladsl._ -import com.ing.baker.types.{PrimitiveValue, Value} +import com.ing.baker.types.{ListValue, PrimitiveValue, RecordValue, Value} import com.typesafe.scalalogging.LazyLogging import org.slf4j.MDC -import scala.collection.immutable.Seq +import scala.collection.immutable.{List, Seq} import scala.concurrent.duration.MILLISECONDS import scala.util.Random @@ -59,8 +58,11 @@ private[recipeinstance] case class TransitionExecution( consume: Marking[Place], input: Option[EventInstance], ingredients: Map[String, Value], + recipeInstanceMetadata: Map[String, String], + eventList: List[EventMoment], correlationId: Option[String], - state: TransitionExecution.State = TransitionExecution.State.Active + state: TransitionExecution.State = TransitionExecution.State.Active, + isReprovider: Boolean ) extends LazyLogging { def isInactive: Boolean = @@ -100,7 +102,7 @@ private[recipeinstance] case class TransitionExecution( case _: EventTransition => for { timerstamp <- timer.clock.realTime(MILLISECONDS) - _ <- effect.delay(components.logging.firingEvent(recipeInstanceId, id, transition, timerstamp)) + _ <- effect.delay(components.logging.firingEvent(recipeInstanceId, "UNKNOWN", "UNKNOWN", id, transition, timerstamp)) } yield input case _ => effect.pure(None) @@ -127,7 +129,21 @@ private[recipeinstance] case class TransitionExecution( def buildInteractionInput: Seq[IngredientInstance] = { val recipeInstanceIdIngredient: (String, Value) = il.recipeInstanceIdName -> PrimitiveValue(recipeInstanceId) val processIdIngredient: (String, Value) = il.processIdName -> PrimitiveValue(recipeInstanceId) - val allIngredients: Map[String, Value] = ingredients ++ interactionTransition.predefinedParameters + recipeInstanceIdIngredient + processIdIngredient + + // Only map the recipeInstanceEventList if is it required, otherwise give an empty list + val recipeInstanceEventList: (String, Value) = + if(interactionTransition.requiredIngredients.exists(_.name == il.recipeInstanceEventListName)) + il.recipeInstanceEventListName -> ListValue(eventList.map(e => PrimitiveValue(e.name))) + else + il.recipeInstanceEventListName -> ListValue(List()) + + val allIngredients: Map[String, Value] = + ingredients ++ + interactionTransition.predefinedParameters + + recipeInstanceIdIngredient + + processIdIngredient + + recipeInstanceEventList + interactionTransition.requiredIngredients.map { case IngredientDescriptor(name, _) => IngredientInstance(name, allIngredients.getOrElse(name, throw new FatalInteractionException(s"Missing parameter '$name'"))) @@ -148,27 +164,37 @@ private[recipeinstance] case class TransitionExecution( components.interactions.execute( interactionTransition, buildInteractionInput, - com.ing.baker.runtime.model.recipeinstance.RecipeInstanceState.getMetaDataFromIngredients(ingredients)) + Some(recipeInstanceMetadata)) + for { startTime <- timer.clock.realTime(MILLISECONDS) outcome <- { for { - event <- effect.delay(InteractionStarted(startTime, recipe.name, recipe.recipeId, recipeInstanceId, interactionTransition.interactionName)) - _ <- effect.delay(components.logging.interactionStarted(event)) - _ <- components.eventStream.publish(event) + interactionStarted <- effect.delay(InteractionStarted(startTime, recipe.name, recipe.recipeId, recipeInstanceId, interactionTransition.interactionName)) + _ <- effect.delay(components.logging.interactionStarted(interactionStarted)) + _ <- components.eventStream.publish(interactionStarted) interactionOutput <- effect.bracket(setupMdc)(_ => execute)(_ => cleanMdc) _ <- validateInteractionOutput(interactionTransition, interactionOutput) transformedOutput = interactionOutput.map(_.transformWith(interactionTransition)) endTime <- timer.clock.realTime(MILLISECONDS) - event = InteractionCompleted( + + interactionCompleted = InteractionCompleted( endTime, endTime - startTime, recipe.name, recipe.recipeId, recipeInstanceId, interactionTransition.interactionName, transformedOutput) - _ <- effect.delay(components.logging.interactionFinished(event)) - _ <- components.eventStream.publish(event) + _ <- effect.delay(components.logging.interactionFinished(interactionCompleted)) + _ <- components.eventStream.publish(interactionCompleted) + + _ <- transformedOutput match { + case Some(event) => + val eventFired = EventFired(endTime, recipe.name, recipe.recipeId, recipeInstanceId, event) + components.logging.eventFired(eventFired) + components.eventStream.publish(eventFired) + case None => effect.pure() + } } yield transformedOutput }.onError { case e: Throwable => @@ -179,11 +205,11 @@ private[recipeinstance] case class TransitionExecution( } for { endTime <- timer.clock.realTime(MILLISECONDS) - event = InteractionFailed( + interactionFailed = InteractionFailed( endTime, endTime - startTime, recipe.name, recipe.recipeId, recipeInstanceId, transition.label, failureCount, throwable, interactionTransition.failureStrategy.apply(failureCount + 1)) - _ <- effect.delay(components.logging.interactionFailed(event)) - _ <- components.eventStream.publish(event) + _ <- effect.delay(components.logging.interactionFailed(interactionFailed)) + _ <- components.eventStream.publish(interactionFailed) } yield () } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/BakerEvent.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/BakerEvent.scala index f2422f326..bc67b5ef3 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/BakerEvent.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/BakerEvent.scala @@ -17,6 +17,8 @@ sealed trait BakerEvent extends common.BakerEvent with ScalaApi { javadsl.EventReceived(timeStamp, recipeName, recipeId, recipeInstanceId, Optional.ofNullable(correlationId.orNull), event.asJava) case EventRejected(timeStamp, recipeInstanceId, correlationId, event, reason) => javadsl.EventRejected(timeStamp, recipeInstanceId, Optional.ofNullable(correlationId.orNull), event.asJava, reason) + case EventFired(timeStamp, recipeName, recipeId, recipeInstanceId, event) => + javadsl.EventFired(timeStamp, recipeName, recipeId, recipeInstanceId, event.asJava) case InteractionFailed(timeStamp, duration, recipeName, recipeId, recipeInstanceId, interactionName, failureCount, throwable, exceptionStrategyOutcome) => javadsl.InteractionFailed(timeStamp, duration, recipeName, recipeId, recipeInstanceId, interactionName, failureCount, throwable, exceptionStrategyOutcome) case InteractionStarted(timeStamp, recipeName, recipeId, recipeInstanceId, interactionName) => @@ -61,6 +63,23 @@ case class EventRejected(timeStamp: Long, correlationId: Option[String], event: EventInstance, reason: RejectReason) extends BakerEvent with common.EventRejected + +/** + * Event describing the fact that an interaction outcome event was fired for a process + * + * @param timeStamp The time that the event was received + * @param recipeName The name of the recipe that interaction is part of + * @param recipeId The recipe id + * @param recipeInstanceId The id of the process + * @param correlationId The (optional) correlation id of the event + * @param event The event + */ +case class EventFired(timeStamp: Long, + recipeName: String, + recipeId: String, + recipeInstanceId: String, + event: EventInstance) extends BakerEvent with common.EventFired + /** * Event describing the fact that an interaction failed during execution * diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/RecipeInstanceState.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/RecipeInstanceState.scala index 6724eaf72..dc763ba54 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/RecipeInstanceState.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/scaladsl/RecipeInstanceState.scala @@ -19,6 +19,7 @@ case class RecipeInstanceState( recipeId: String, recipeInstanceId: String, ingredients: Map[String, Value], + recipeInstanceMetadata: Map[String, String], events: Seq[EventMoment]) extends common.RecipeInstanceState with ScalaApi { @@ -28,5 +29,5 @@ case class RecipeInstanceState( @nowarn def asJava: javadsl.RecipeInstanceState = - new javadsl.RecipeInstanceState(recipeId, recipeInstanceId, ingredients.asJava, events.map(_.asJava()).asJava) + new javadsl.RecipeInstanceState(recipeId, recipeInstanceId, ingredients.asJava, recipeInstanceMetadata.asJava, events.map(_.asJava()).asJava) } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/BakerEventMapping.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/BakerEventMapping.scala index d36b92e83..26a9d4c66 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/BakerEventMapping.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/BakerEventMapping.scala @@ -20,6 +20,7 @@ class BakerEventMapping extends ProtoMap[BakerEvent, protobuf.BakerEvent] { protobuf.BakerEvent(a match { case event: EventReceived => protobuf.BakerEvent.OneofBakerEvent.EventReceived(ctxToProto(event)(EventReceivedMapping)) case event: EventRejected => protobuf.BakerEvent.OneofBakerEvent.EventRejected(ctxToProto(event)(EventRejectedMapping)) + case event: EventFired => protobuf.BakerEvent.OneofBakerEvent.EventFired(ctxToProto(event)(EventFiredMapping)) case event: InteractionCompleted => protobuf.BakerEvent.OneofBakerEvent.InteractionCompleted(ctxToProto(event)(InteractionCompletedMapping)) case event: InteractionFailed => protobuf.BakerEvent.OneofBakerEvent.InteractionFailed(ctxToProto(event)(InteractionFailedMapping)) case event: InteractionStarted => protobuf.BakerEvent.OneofBakerEvent.InteractionStarted(ctxToProto(event)(InteractionStartedMapping)) @@ -31,6 +32,7 @@ class BakerEventMapping extends ProtoMap[BakerEvent, protobuf.BakerEvent] { message.oneofBakerEvent match { case event: protobuf.BakerEvent.OneofBakerEvent.EventReceived => ctxFromProto(event.value)(EventReceivedMapping) case event: protobuf.BakerEvent.OneofBakerEvent.EventRejected=> ctxFromProto(event.value)(EventRejectedMapping) + case event: protobuf.BakerEvent.OneofBakerEvent.EventFired => ctxFromProto(event.value)(EventFiredMapping) case event: protobuf.BakerEvent.OneofBakerEvent.InteractionCompleted=> ctxFromProto(event.value)(InteractionCompletedMapping) case event: protobuf.BakerEvent.OneofBakerEvent.InteractionFailed => ctxFromProto(event.value)(InteractionFailedMapping) case event: protobuf.BakerEvent.OneofBakerEvent.InteractionStarted => ctxFromProto(event.value)(InteractionStartedMapping) @@ -121,6 +123,36 @@ object BakerEventMapping { ) } + object EventFiredMapping extends ProtoMap[EventFired, protobuf.EventFiredBakerEvent] { + + override def companion: GeneratedMessageCompanion[protobuf.EventFiredBakerEvent] = protobuf.EventFiredBakerEvent + + override def toProto(a: EventFired): protobuf.EventFiredBakerEvent = + protobuf.EventFiredBakerEvent( + timeStamp = Some(a.timeStamp), + recipeName = Some(a.recipeName), + recipeId = Some(a.recipeId), + recipeInstanceId = Some(a.recipeInstanceId), + event = Some(ctxToProto(a.event)) + ) + + override def fromProto(message: protobuf.EventFiredBakerEvent): Try[EventFired] = + for { + timeStamp <- versioned(message.timeStamp, "timeStamp") + recipeName <- versioned(message.recipeName, "recipeName") + recipeId <- versioned(message.recipeId, "recipeId") + recipeInstanceId <- versioned(message.recipeInstanceId, "recipeInstanceId") + eventProto <- versioned(message.event, "event") + event <- ctxFromProto(eventProto) + } yield EventFired( + timeStamp = timeStamp, + recipeName = recipeName, + recipeId = recipeId, + recipeInstanceId = recipeInstanceId, + event = event + ) + } + object InteractionFailedMapping extends ProtoMap[InteractionFailed, protobuf.InteractionFailedBakerEvent] { override def companion: GeneratedMessageCompanion[protobuf.InteractionFailedBakerEvent] = protobuf.InteractionFailedBakerEvent diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/CompiledRecipeMapping.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/CompiledRecipeMapping.scala index 735c95283..92c7e07ca 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/CompiledRecipeMapping.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/CompiledRecipeMapping.scala @@ -6,7 +6,7 @@ import com.ing.baker.il.CompiledRecipe.Scala212CompatibleJava import com.ing.baker.il.petrinet.{Node, Place, RecipePetriNet, Transition} import com.ing.baker.petrinet.api.{Marking, _} import com.ing.baker.runtime.akka.actor.protobuf -import com.ing.baker.runtime.serialization.ProtoMap.{ctxFromProto, ctxToProto, versioned} +import com.ing.baker.runtime.serialization.ProtoMap.{ctxFromProto, ctxToProto, versioned, versionedOptional} import com.ing.baker.runtime.serialization.{ProtoMap, TokenIdentifier} import com.ing.baker.types.Value import scalax.collection.GraphEdge @@ -98,7 +98,8 @@ class CompiledRecipeMapping extends ProtoMap[il.CompiledRecipe, protobuf.Compile predefinedParameters = t.predefinedParameters.view.map { case (key, value) => (key, ctxToProto(value))}.toMap, maximumInteractionCount = t.maximumInteractionCount, failureStrategy = Option(ctxToProto(t.failureStrategy)), - eventOutputTransformers = t.eventOutputTransformers.view.map { case (key, value) => (key, ctxToProto(value))}.toMap + eventOutputTransformers = t.eventOutputTransformers.view.map { case (key, value) => (key, ctxToProto(value))}.toMap, + isReprovider = Some(t.isReprovider) ) protobuf.Node(protobuf.Node.OneofNode.InteractionTransition(pt)) @@ -195,6 +196,7 @@ class CompiledRecipeMapping extends ProtoMap[il.CompiledRecipe, protobuf.Compile .traverse[Try, (String, il.EventOutputTransformer)] { case (k, v) => ctxFromProto(v).map(k -> _) } .map(_.toMap) + isReprovider = versionedOptional(transition.value.isReprovider, false) } yield Right(il.petrinet.InteractionTransition( eventsToFire = eventDescriptor ++ providedIngredientEvent, @@ -205,7 +207,8 @@ class CompiledRecipeMapping extends ProtoMap[il.CompiledRecipe, protobuf.Compile predefinedParameters = predefinedparameters, maximumInteractionCount = transition.value.maximumInteractionCount, failureStrategy = failureStrategy, - eventOutputTransformers = eventOutputTransformers + eventOutputTransformers = eventOutputTransformers, + isReprovider = isReprovider )) case other => diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/ProcessStateMapping.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/ProcessStateMapping.scala index 61499209b..ecc70c7c8 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/ProcessStateMapping.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/protomappings/ProcessStateMapping.scala @@ -18,7 +18,7 @@ class ProcessStateMapping extends ProtoMap[RecipeInstanceState, proto.ProcessSta val protoIngredients = a.ingredients.toSeq.map { case (name, value) => proto.Ingredient(Some(name), None, Some(ctxToProto(value))) } - proto.ProcessState(Some(a.recipeId), Some(a.recipeInstanceId), protoIngredients, a.events.map(ctxToProto(_))) + proto.ProcessState(Some(a.recipeId), Some(a.recipeInstanceId), protoIngredients, a.recipeInstanceMetadata, a.events.map(ctxToProto(_))) } def fromProto(message: proto.ProcessState): Try[RecipeInstanceState] = @@ -32,7 +32,8 @@ class ProcessStateMapping extends ProtoMap[RecipeInstanceState, proto.ProcessSta value <- ctxFromProto(protoValue) } yield (name, value) } + recipeInstanceMetaData = message.recipeInstanceMetadata events <- message.events.toList.traverse (ctxFromProto(_)) - } yield RecipeInstanceState(recipeId, recipeInstanceId, ingredients.toMap, events) + } yield RecipeInstanceState(recipeId, recipeInstanceId, ingredients.toMap, recipeInstanceMetaData, events) } diff --git a/core/baker-interface/src/test/scala/com/ing/baker/runtime/inmemory/InMemoryMemoryCleanupSpec.scala b/core/baker-interface/src/test/scala/com/ing/baker/runtime/inmemory/InMemoryMemoryCleanupSpec.scala index fb40fb34a..a33527d29 100644 --- a/core/baker-interface/src/test/scala/com/ing/baker/runtime/inmemory/InMemoryMemoryCleanupSpec.scala +++ b/core/baker-interface/src/test/scala/com/ing/baker/runtime/inmemory/InMemoryMemoryCleanupSpec.scala @@ -1,18 +1,20 @@ package com.ing.baker.runtime.inmemory -import java.util.UUID - import cats.effect.{ContextShift, IO, Timer} import com.ing.baker.compiler.RecipeCompiler import com.ing.baker.recipe.TestRecipe +import com.ing.baker.recipe.TestRecipe._ +import com.ing.baker.recipe.common.InteractionFailureStrategy +import com.ing.baker.recipe.scaladsl.Recipe import com.ing.baker.runtime.common.BakerException.NoSuchProcessException -import com.ing.baker.runtime.model.BakerF -import com.ing.baker.runtime.scaladsl.RecipeInstanceState +import com.ing.baker.runtime.model.{BakerF, InteractionInstance} +import com.ing.baker.runtime.scaladsl.{EventInstance, RecipeInstanceState} import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -import scala.concurrent.ExecutionContext +import java.util.UUID import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} class InMemoryMemoryCleanupSpec extends AnyFunSpec with Matchers { describe("InMemoryRecipeInstanceManager") { @@ -31,18 +33,175 @@ class InMemoryMemoryCleanupSpec extends AnyFunSpec with Matchers { result.unsafeRunSync() } - it("should delete a process after the timeout") { + it("should delete a process after the RetentionPeriod if RetentionPeriod is defined") { + val recipe = Recipe("tempRecipe1") + .withInteractions( + interactionOne + ) + .withSensoryEvents(initialEvent) + .withRetentionPeriod(100 milliseconds) + + class InteractionOneInterfaceImplementation() extends TestRecipe.InteractionOne { + override def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] = { + println("Interaction executing") + Future.successful(new InteractionOneSuccessful("output")) + } + } + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) val recipeInstanceId = UUID.randomUUID().toString val result: IO[RecipeInstanceState] = for { - baker <- InMemoryBaker.build(BakerF.Config(idleTimeout = 100.milliseconds, allowAddingRecipeWithoutRequiringInstances = true), List.empty) - recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(TestRecipe.getRecipe("InMemory")), validate = false) + baker <- InMemoryBaker.build(BakerF.Config( + idleTimeout = 10.milliseconds, + retentionPeriodCheckInterval = 10.milliseconds, + allowAddingRecipeWithoutRequiringInstances = true), + List(InteractionInstance.unsafeFrom(new InteractionOneInterfaceImplementation()))) + recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(recipe), validate = false) + _ <- baker.bake(recipeId, recipeInstanceId) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(120.milliseconds) + result <- baker.getRecipeInstanceState(recipeInstanceId) + } yield (result) + assertThrows[NoSuchProcessException](result.unsafeRunSync()) + } + + it("should delete a process after the idleTimeOut if the process is inactive") { + val recipe = Recipe("tempRecipe2") + .withInteractions( + interactionOne + ) + .withSensoryEvents(initialEvent) + + class InteractionOneInterfaceImplementation() extends TestRecipe.InteractionOne { + override def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] = { + Future.successful(new InteractionOneSuccessful("output")) + } + } + + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val recipeInstanceId = UUID.randomUUID().toString + + val result: IO[RecipeInstanceState] = for { + baker <- InMemoryBaker.build(BakerF.Config( + idleTimeout = 100.milliseconds, + retentionPeriodCheckInterval = 10.milliseconds, + allowAddingRecipeWithoutRequiringInstances = true), + List(InteractionInstance.unsafeFrom(new InteractionOneInterfaceImplementation()))) + recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(recipe), validate = false) + _ <- baker.bake(recipeId, recipeInstanceId) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(200.milliseconds) + result <- baker.getRecipeInstanceState(recipeInstanceId) + } yield (result) + assertThrows[NoSuchProcessException](result.unsafeRunSync()) + } + + it("should not delete a process after the Idle Timeout if it is still executing") { + val recipe = Recipe("tempRecipe3") + .withInteractions( + interactionOne + .withFailureStrategy(InteractionFailureStrategy.RetryWithIncrementalBackoff( + initialDelay = 5 millisecond, maximumRetries = 100, maxTimeBetweenRetries = Some(5 milliseconds))), + ) + .withSensoryEvents(initialEvent) + + class InteractionOneInterfaceImplementation() extends TestRecipe.InteractionOne { + override def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] = { + Future.failed(new RuntimeException("Failing interaction")) + } + } + + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val recipeInstanceId = UUID.randomUUID().toString + + val result: IO[RecipeInstanceState] = for { + baker <- InMemoryBaker.build(BakerF.Config( + idleTimeout = 100.milliseconds, + retentionPeriodCheckInterval = 10.milliseconds, + allowAddingRecipeWithoutRequiringInstances = true), + List(InteractionInstance.unsafeFrom(new InteractionOneInterfaceImplementation()))) + recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(recipe), validate = false) + _ <- baker.bake(recipeId, recipeInstanceId) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(120.milliseconds) + result <- baker.getRecipeInstanceState(recipeInstanceId) + } yield (result) + result.unsafeRunSync() + } + + it("should not delete a process if the idle timeout is reset due to activity") { + val recipe = Recipe("tempRecipe3") + .withInteractions( + interactionOne + ) + .withSensoryEvents(initialEvent) + + class InteractionOneInterfaceImplementation() extends TestRecipe.InteractionOne { + override def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] = { + Future.failed(new RuntimeException("Failing interaction")) + } + } + + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val recipeInstanceId = UUID.randomUUID().toString + + val result: IO[RecipeInstanceState] = for { + baker <- InMemoryBaker.build(BakerF.Config( + idleTimeout = 100.milliseconds, + retentionPeriodCheckInterval = 10.milliseconds, + allowAddingRecipeWithoutRequiringInstances = true), + List(InteractionInstance.unsafeFrom(new InteractionOneInterfaceImplementation()))) + recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(recipe), validate = false) + _ <- baker.bake(recipeId, recipeInstanceId) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(80.milliseconds) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(80.milliseconds) + result <- baker.getRecipeInstanceState(recipeInstanceId) + } yield (result) + result.unsafeRunSync() + } + + it("should delete a process after the RetentionPeriod if it is still executing") { + val recipe = Recipe("tempRecipe4") + .withInteractions( + interactionOne + .withFailureStrategy(InteractionFailureStrategy.RetryWithIncrementalBackoff( + initialDelay = 5 millisecond, maximumRetries = 100, maxTimeBetweenRetries = Some(5 milliseconds))), + ) + .withSensoryEvents(initialEvent) + .withRetentionPeriod(100 milliseconds) + + class InteractionOneInterfaceImplementation() extends TestRecipe.InteractionOne { + override def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] = { + Future.failed(new RuntimeException("Failing interaction")) + } + } + + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val recipeInstanceId = UUID.randomUUID().toString + + val result: IO[RecipeInstanceState] = for { + baker <- InMemoryBaker.build(BakerF.Config( + idleTimeout = 100.milliseconds, + retentionPeriodCheckInterval = 10.milliseconds, + allowAddingRecipeWithoutRequiringInstances = true), + List(InteractionInstance.unsafeFrom(new InteractionOneInterfaceImplementation()))) + recipeId <- baker.addRecipe(RecipeCompiler.compileRecipe(recipe), validate = false) _ <- baker.bake(recipeId, recipeInstanceId) - _ <- baker.getRecipeInstanceState(recipeInstanceId) - _ <- IO.sleep(110.milliseconds) + _ = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent("initialIngredient"))).unsafeRunAsyncAndForget() + _ <- IO.sleep(120.milliseconds) result <- baker.getRecipeInstanceState(recipeInstanceId) } yield (result) assertThrows[NoSuchProcessException](result.unsafeRunSync()) diff --git a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelFixtures.scala b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelFixtures.scala index 024b946e1..318666d18 100644 --- a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelFixtures.scala +++ b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelFixtures.scala @@ -50,6 +50,8 @@ trait BakerModelFixtures[F[_]] extends TestRecipe[F] with MockitoSugar { ) val testInteractionOneMock: InteractionOne = mock[InteractionOne] + val testInteractionOneWithMetaDataMock: InteractionOneWithMetaData = mock[InteractionOneWithMetaData] + val testInteractionOneWithEventListMock: InteractionOneWithEventList = mock[InteractionOneWithEventList] val testInteractionTwoMock: InteractionTwo = mock[InteractionTwo] val testInteractionThreeMock: InteractionThree = mock[InteractionThree] val testInteractionFourMock: InteractionFour = mock[InteractionFour] @@ -67,6 +69,8 @@ trait BakerModelFixtures[F[_]] extends TestRecipe[F] with MockitoSugar { def mockImplementations(implicit effect: Applicative[F], classTag: ClassTag[F[Any]]): List[InteractionInstance[F]] = List( testInteractionOneMock, + testInteractionOneWithMetaDataMock, + testInteractionOneWithEventListMock, testInteractionTwoMock, testInteractionThreeMock, testInteractionFourMock, diff --git a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelSpecExecutionSemanticsTests.scala b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelSpecExecutionSemanticsTests.scala index 2552da8f7..f71a2baab 100644 --- a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelSpecExecutionSemanticsTests.scala +++ b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/BakerModelSpecExecutionSemanticsTests.scala @@ -4,7 +4,7 @@ import cats.effect.ConcurrentEffect import cats.implicits._ import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction, Recipe} import com.ing.baker.runtime.common.BakerException.{IllegalEventException, NoSuchProcessException, ProcessAlreadyExistsException} -import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetaDataName +import com.ing.baker.runtime.common.RecipeInstanceState.RecipeInstanceMetadataName import com.ing.baker.runtime.common.SensoryEventStatus import com.ing.baker.runtime.scaladsl.{EventInstance, InteractionInstanceInput, RecipeEventMetadata} import com.ing.baker.types.{CharArray, Int32, PrimitiveValue, Value} @@ -37,7 +37,7 @@ trait BakerModelSpecExecutionSemanticsTests[F[_]] { self: BakerModelSpec[F] => _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value")) _ <- baker.addMetaData(id, Map.apply[String, String]("key2" -> "value2")) ingredients <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.containsKey("key") && metaData.get("key") == "value" && metaData.containsKey("key2") && metaData.get("key2") == "value2") @@ -53,7 +53,7 @@ trait BakerModelSpecExecutionSemanticsTests[F[_]] { self: BakerModelSpec[F] => _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value")) _ <- baker.addMetaData(id, Map.apply[String, String]("key" -> "value2")) ingredients <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.containsKey("key") && metaData.get("key") == "value2") @@ -67,7 +67,7 @@ trait BakerModelSpecExecutionSemanticsTests[F[_]] { self: BakerModelSpec[F] => _ <- baker.bake(recipeId, id) _ <- baker.addMetaData(id, Map.empty) ingredients <- baker.getIngredients(id) - metaData = ingredients(RecipeInstanceMetaDataName).asMap(classOf[String], classOf[String]) + metaData = ingredients(RecipeInstanceMetadataName).asMap(classOf[String], classOf[String]) } yield assert( metaData.size() == 0) @@ -80,7 +80,7 @@ trait BakerModelSpecExecutionSemanticsTests[F[_]] { self: BakerModelSpec[F] => id = UUID.randomUUID().toString _ <- baker.bake(recipeId, id) ingredients <- baker.getIngredients(id) - } yield assert(!ingredients.contains(RecipeInstanceMetaDataName)) + } yield assert(!ingredients.contains(RecipeInstanceMetadataName)) } test("throw an ProcessAlreadyExistsException if baking a process with the same identifier twice") { context => @@ -160,6 +160,53 @@ trait BakerModelSpecExecutionSemanticsTests[F[_]] { self: BakerModelSpec[F] => "interactionOneOriginalIngredient" -> interactionOneIngredientValue) } + test("execute an interaction when its ingredient is provided with MetaData requirement") { context => + val recipe = + Recipe("IngredientProvidedRecipeWithMetaData") + .withInteraction(interactionOneWithMetaData) + .withSensoryEvent(initialEvent) + + for { + bakerWithRecipe <- context.setupBakerWithRecipe(recipe, mockImplementations) + (baker, recipeId) = bakerWithRecipe + _ = when(testInteractionOneWithMetaDataMock.apply(anyString(), anyString(), any())).thenReturn(effect.pure(InteractionOneSuccessful(interactionOneIngredientValue))) + recipeInstanceId = UUID.randomUUID().toString + metaData = Map("MetaDataKey" -> "MetaDataValue") + _ <- baker.bake(recipeId, recipeInstanceId, metaData) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent(initialIngredientValue))) + _ = verify(testInteractionOneWithMetaDataMock).apply(recipeInstanceId, "initialIngredient", metaData) + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield + state.ingredients shouldBe + ingredientMap( + "RecipeInstanceMetaData" -> metaData, + "initialIngredient" -> initialIngredientValue, + "interactionOneOriginalIngredient" -> interactionOneIngredientValue) + } + + test("execute an interaction when its ingredient is provided with EventList requirement") { context => + val recipe = + Recipe("IngredientProvidedRecipeWithEventList") + .withInteraction(interactionOneWithEventList) + .withSensoryEvent(initialEvent) + + for { + bakerWithRecipe <- context.setupBakerWithRecipe(recipe, mockImplementations) + (baker, recipeId) = bakerWithRecipe + _ = when(testInteractionOneWithEventListMock.apply(anyString(), anyString(), any())).thenReturn(effect.pure(InteractionOneSuccessful(interactionOneIngredientValue))) + recipeInstanceId = UUID.randomUUID().toString + eventList = List("InitialEvent") + _ <- baker.bake(recipeId, recipeInstanceId) + _ <- baker.fireEventAndResolveWhenCompleted(recipeInstanceId, EventInstance.unsafeFrom(InitialEvent(initialIngredientValue))) + _ = verify(testInteractionOneWithEventListMock).apply(recipeInstanceId, "initialIngredient", eventList) + state <- baker.getRecipeInstanceState(recipeInstanceId) + } yield + state.ingredients shouldBe + ingredientMap( + "initialIngredient" -> initialIngredientValue, + "interactionOneOriginalIngredient" -> interactionOneIngredientValue) + } + test("Correctly notify on event") { context => val sensoryEvent = Event( diff --git a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/TestRecipe.scala b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/TestRecipe.scala index 0ac667318..cbbbba88b 100644 --- a/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/TestRecipe.scala +++ b/core/baker-interface/src/test/scala/com/ing/baker/runtime/model/TestRecipe.scala @@ -123,6 +123,30 @@ trait TestRecipe[F[_]] { def apply(recipeInstanceId: String, initialIngredient: String): F[InteractionOneSuccessful] } + val interactionOneWithMetaData = + Interaction( + name = "InteractionOneWithMetaData", + inputIngredients = Seq(recipeInstanceId, initialIngredient, recipeInstanceMetaData), + output = Seq(interactionOneSuccessful)) + + trait InteractionOneWithMetaData { + def name: String = "InteractionOneWithMetaData" + + def apply(recipeInstanceId: String, initialIngredient: String, bakerMetaData: Map[String, String]): F[InteractionOneSuccessful] + } + + val interactionOneWithEventList = + Interaction( + name = "InteractionOneWithEventList", + inputIngredients = Seq(recipeInstanceId, initialIngredient, recipeInstanceEventList), + output = Seq(interactionOneSuccessful)) + + trait InteractionOneWithEventList { + def name: String = "InteractionOneWithEventList" + + def apply(recipeInstanceId: String, initialIngredient: String, recipeInstanceEventList: List[String]): F[InteractionOneSuccessful] + } + val interactionTwo = Interaction( name = "InteractionTwo", diff --git a/core/baker-types/src/main/resources/reference.conf b/core/baker-types/src/main/resources/reference.conf index 6cde0cc2c..f8d85fa9c 100644 --- a/core/baker-types/src/main/resources/reference.conf +++ b/core/baker-types/src/main/resources/reference.conf @@ -3,7 +3,7 @@ baker.types { "java.util.Set" = "com.ing.baker.types.modules.JavaModules$SetModule" "java.util.Map" = "com.ing.baker.types.modules.JavaModules$MapModule" "java.util.Optional" = "com.ing.baker.types.modules.JavaModules$OptionalModule" - + "java.util.UUID" = "com.ing.baker.types.modules.UUIDModule" "java.lang.Enum" = "com.ing.baker.types.modules.EnumModule" "scala.collection.immutable.List" = "com.ing.baker.types.modules.ScalaModules$ListModule" @@ -11,8 +11,19 @@ baker.types { "scala.collection.immutable.Map" = "com.ing.baker.types.modules.ScalaModules$MapModule" "scala.Option" = "com.ing.baker.types.modules.ScalaModules$OptionModule" + "java.lang.Record" = "com.ing.baker.types.modules.RecordModule" "java.lang.Object" = "com.ing.baker.types.modules.PojoModule" + "java.util.Currency" = "com.ing.baker.types.modules.CurrencyModule" + + "java.util.Date" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.LocalDate" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.LocalDateTime" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.OffsetDateTime" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.ZonedDateTime" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.Instant" = "com.ing.baker.types.modules.JavaTimeModule" + "java.time.Duration" = "com.ing.baker.types.modules.DurationModule" + "org.joda.time.DateTime" = "com.ing.baker.types.modules.JodaTimeModule" "org.joda.time.LocalDateTime" = "com.ing.baker.types.modules.JodaTimeModule" "org.joda.time.LocalDate" = "com.ing.baker.types.modules.JodaTimeModule" diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/Value.scala b/core/baker-types/src/main/scala/com/ing/baker/types/Value.scala index 92e3d4e68..f844a02c0 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/Value.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/Value.scala @@ -217,3 +217,14 @@ case class ListValue(entries: List[Value]) extends Value { override def toString: String = entries.mkString("[", ",", "]") } + +object FromValue { + class FromValueExtractor[T: universe.TypeTag]() { + def unapply(v: Value) = Try { + v.as[T] + }.toOption + } + + def apply[T: universe.TypeTag]() = new FromValueExtractor[T] +} + diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/ClassModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/ClassModule.scala index 1b8828a28..b1a9b8f8b 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/modules/ClassModule.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/ClassModule.scala @@ -1,6 +1,6 @@ package com.ing.baker.types.modules -import java.lang.reflect +import java.lang.reflect.Type import com.ing.baker.types._ @@ -8,7 +8,7 @@ import scala.reflect.runtime.universe.TypeTag abstract class ClassModule[T : TypeTag] extends TypeModule { - protected val clazz = mirror.runtimeClass(mirror.typeOf[T]) + protected val clazz: Class[_] = mirror.runtimeClass(mirror.typeOf[T]) - override def isApplicable(javaType: reflect.Type): Boolean = isAssignableToBaseClass(javaType, clazz) + override def isApplicable(javaType: Type): Boolean = isAssignableToBaseClass(javaType, clazz) } diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/CurrencyModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/CurrencyModule.scala new file mode 100644 index 000000000..13f0b2f04 --- /dev/null +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/CurrencyModule.scala @@ -0,0 +1,46 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types._ + +import java.lang.reflect.{Type => JType} +import java.util.Currency + +class CurrencyModule extends TypeModule { + + override def isApplicable(javaType: JType): Boolean = javaType match { + case clazz: Class[_] if clazz.isAssignableFrom(classOf[java.util.Currency]) => true + case _ => false + } + + override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = { + RecordType(Seq( + RecordField("currencyCode", CharArray), + RecordField("defaultFractionDigits", Int32), + RecordField("numericCode", Int32))) + } + + override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = { + (value, javaType) match { + case (NullValue, _) => null + case (RecordValue(records), clazz: Class[_]) if clazz.isAssignableFrom(classOf[java.util.Currency]) && records.contains("currencyCode") => + java.util.Currency.getInstance(records("currencyCode").as[String]) + case _ => + throw new IllegalArgumentException(s"Unsupported record: $value") + } + } + + override def fromJava(context: TypeAdapter, obj: Any): Value = { + obj match { + case currency: Currency => + val currencyCode: String = currency.getCurrencyCode + val defaultFractionDigits: Int = currency.getDefaultFractionDigits + val numericCode: Int = currency.getNumericCode + RecordValue(Map( + "currencyCode" -> Converters.toValue(currencyCode), + "defaultFractionDigits" -> Converters.toValue(defaultFractionDigits), + "numericCode" -> Converters.toValue(numericCode))) + case _ => + throw new IllegalArgumentException(s"Could not translate from Java object") + } + } +} diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/DurationModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/DurationModule.scala new file mode 100644 index 000000000..b3814557f --- /dev/null +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/DurationModule.scala @@ -0,0 +1,43 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types._ + +import java.lang.reflect.{Type => JType} +import java.time.Duration + +/** + * This module is using POJO representation for the Duration instead of the Date. + * This is done to ensure backwards compatibility. + * In older versions Durations are translated to our POJO type but in Java 17 the initialization from POJO to Duration does not work anymore. + */ +class DurationModule extends TypeModule { + + override def isApplicable(javaType: JType): Boolean = + isAssignableToBaseClass(javaType, classOf[Duration]) + + override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = { + RecordType(Seq(RecordField("seconds", Int64), RecordField("nanos", Int32))) + } + + override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = + (value, javaType) match { + case (NullValue, _) => null + case (RecordValue(entries), clazz: Class[_]) if classOf[Duration].isAssignableFrom(clazz) => + val seconds: Long = entries("seconds").as[Long] + val nanos: Int = entries("nanos").as[Int] + Duration.ofSeconds(seconds).withNanos(nanos) + case _=> + throw new IllegalArgumentException(s"Unsupported value: $value") + } + + override def fromJava(context: TypeAdapter, obj: Any): Value = { + obj match { + case duration: Duration => + val seconds: Long = duration.getSeconds + val nanos: Int = duration.getNano + RecordValue(Map("seconds" -> Converters.toValue(seconds), "nanos" -> Converters.toValue(nanos))) + case _ => + throw new IllegalArgumentException(s"Could not translate from Java object") + } + } +} diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaModules.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaModules.scala index ea744bd42..b525960f6 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaModules.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaModules.scala @@ -9,7 +9,7 @@ import scala.collection.JavaConverters._ object JavaModules { - class ListModule extends ClassModule[java.util.List[_]] { + class ListModule extends ClassModule[util.List[_]] { override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): ListType = { val entryType = context.readType(getTypeParameter(javaType, 0)) diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaTimeModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaTimeModule.scala new file mode 100644 index 000000000..52c79a2e5 --- /dev/null +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/JavaTimeModule.scala @@ -0,0 +1,50 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types._ + +import java.time._ + +/** + * Add support for Java time objects + **/ +class JavaTimeModule extends TypeModule { + + override def isApplicable(javaType: java.lang.reflect.Type): Boolean = + isAssignableToBaseClass(javaType, classOf[java.util.Date]) || + isAssignableToBaseClass(javaType, classOf[LocalDate]) || + isAssignableToBaseClass(javaType, classOf[LocalDateTime]) || + isAssignableToBaseClass(javaType, classOf[OffsetDateTime]) || + isAssignableToBaseClass(javaType, classOf[ZonedDateTime]) || + isAssignableToBaseClass(javaType, classOf[Instant]) + + override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = Date + + override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = + (value, javaType) match { + case (NullValue, _) => null + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[java.util.Date].isAssignableFrom(clazz) => + java.util.Date.from(Instant.ofEpochMilli(millis)) + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[LocalDate].isAssignableFrom(clazz) => + LocalDate.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()) + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[LocalDateTime].isAssignableFrom(clazz) => + LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()) + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[OffsetDateTime].isAssignableFrom(clazz) => + OffsetDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()) + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[ZonedDateTime].isAssignableFrom(clazz) => + ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()) + case (PrimitiveValue(millis: Long), clazz: Class[_]) if classOf[Instant].isAssignableFrom(clazz) => + Instant.ofEpochMilli(millis) + case unsupportedType => + throw new IllegalArgumentException(s"UnsupportedType: $unsupportedType") + } + + override def fromJava(context: TypeAdapter, obj: Any): Value = + obj match { + case date: java.util.Date => PrimitiveValue(date.toInstant.toEpochMilli) + case localDate: LocalDate => PrimitiveValue(localDate.atStartOfDay.atZone(ZoneId.systemDefault()).toInstant.toEpochMilli) + case localDateTime: LocalDateTime => PrimitiveValue(localDateTime.atZone(ZoneId.systemDefault()).toInstant.toEpochMilli) + case offsetDateTime: OffsetDateTime => PrimitiveValue(offsetDateTime.toInstant.toEpochMilli) + case zonedDateTime: ZonedDateTime => PrimitiveValue(zonedDateTime.toInstant.toEpochMilli) + case instant: Instant => PrimitiveValue(instant.toEpochMilli) + } +} diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/PojoModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/PojoModule.scala index a89126551..942229c85 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/modules/PojoModule.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/PojoModule.scala @@ -10,7 +10,6 @@ class PojoModule extends TypeModule { override def isApplicable(javaType: java.lang.reflect.Type): Boolean = true override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = { - val pojoClass = getBaseClass(javaType) val fields = pojoClass.getDeclaredFields.toIndexedSeq.filterNot(f => f.isSynthetic || Modifier.isStatic(f.getModifiers)) val ingredients = fields.map(f => RecordField(f.getName, context.readType(f.getGenericType))) @@ -35,8 +34,9 @@ class PojoModule extends TypeModule { fields.foreach { f => entries.get(f.getName).foreach { entryValue => - val fieldType = f.getGenericType() + val fieldType = f.getGenericType try { + //this is an illegal action with Java 17 when a record is used, or any final fields val value = context.toJava(entryValue, fieldType) f.setAccessible(true) f.set(pojoInstance, value) diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/PrimitiveModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/PrimitiveModule.scala index e9c904d22..cb03dbfae 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/modules/PrimitiveModule.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/PrimitiveModule.scala @@ -4,24 +4,25 @@ import java.lang.reflect.ParameterizedType import com.ing.baker.types import com.ing.baker.types._ +import java.lang.reflect.{Type => JType} class PrimitiveModule extends TypeModule { - def isByteArray(javaType: java.lang.reflect.Type) = javaType match { + private def isByteArray(javaType: JType) = javaType match { case clazz: Class[_] => clazz == classOf[Array[Byte]] case t: ParameterizedType => classOf[scala.Array[_]].isAssignableFrom(getBaseClass(t)) && classOf[Byte].isAssignableFrom(getBaseClass(t.getActualTypeArguments()(0))) } - override def isApplicable(javaType: java.lang.reflect.Type): Boolean = { + override def isApplicable(javaType: JType): Boolean = { javaType match { case clazz: Class[_] => primitiveMappings.contains(clazz) case other => isByteArray(other) } } - override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = { + override def readType(context: TypeAdapter, javaType: JType): Type = { javaType match { case clazz: Class[_] => primitiveMappings(clazz) case other if isByteArray(other) => types.ByteArray @@ -31,7 +32,7 @@ class PrimitiveModule extends TypeModule { override def fromJava(context: TypeAdapter, obj: Any): Value = PrimitiveValue(obj) - override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = { + override def toJava(context: TypeAdapter, value: Value, javaType: JType): Any = { (value, javaType) match { case (p @ PrimitiveValue(obj), clazz: Class[_]) if p.isAssignableTo(clazz) => obj diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/RecordModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/RecordModule.scala new file mode 100644 index 000000000..dcc4ef6b8 --- /dev/null +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/RecordModule.scala @@ -0,0 +1,65 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types._ + +import java.lang.reflect.{Type => JType} + +class RecordModule extends TypeModule { + + override def isApplicable(javaType: JType): Boolean = { + try { + getBaseClass(javaType).isRecord + } + catch { + //if this happens, we don't use Java 16+ + case _: Exception => false + } + } + + override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = { + val recordType = getBaseClass(javaType) + val ingredients: Seq[RecordField] = recordType.getRecordComponents.toIndexedSeq.map(f => RecordField(f.getName, context.readType(f.getGenericType))) + RecordType(ingredients) + } + + override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = value match { + case NullValue => null + case RecordValue(entries) => + val recordType = getBaseClass(javaType) + + val recordValues: Array[Any] = recordType.getRecordComponents + .map(r => { + val value = entries(r.getName) + val fieldType = r.getGenericType + try { + context.toJava(value, fieldType) + } catch { + case e: Exception => + throw new IllegalStateException(s"Failed parse field '${r.getName}' as type: $fieldType", e) + } + }) + + try { + val paramTypes: Array[Class[_]] = recordType.getRecordComponents.map(r => r.getType) + recordType.getDeclaredConstructors.find(constructor => { + constructor.getParameterCount == paramTypes.length + }) match { + case Some(constructor) => + constructor.newInstance(recordValues: _*) + case None => + throw new NoSuchMethodException("No constructor found for record") + } + } catch { + case _: Exception => + throw new RuntimeException("Could not construct type (" + recordType.getName + ")") + } + case _=> + throw new IllegalArgumentException(s"Unsupported value: $value") + } + + override def fromJava(context: TypeAdapter, pojo: Any): Value = { + val entries: Map[String, Value] = pojo.getClass.getRecordComponents + .map(f => f.getName -> context.fromJava(f.getAccessor.invoke(pojo))).toMap //is order guaranteed? + RecordValue(entries) + } +} diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/ScalaModules.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/ScalaModules.scala index d6195ac8d..edae12bf6 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/modules/ScalaModules.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/ScalaModules.scala @@ -4,19 +4,19 @@ import com.ing.baker.types._ import java.lang.reflect.ParameterizedType import scala.annotation.nowarn -import java.lang.reflect +import java.lang.reflect.{Type => JType} object ScalaModules { class ListModule extends ClassModule[List[_]] { - override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type) = { + override def readType(context: TypeAdapter, javaType: JType): ListType = { val entryType = context.readType(getTypeParameter(javaType, 0)) ListType(entryType) } @nowarn - override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type) = value match { + override def toJava(context: TypeAdapter, value: Value, javaType: JType): List[Any] = value match { case NullValue => null case ListValue(entries) if isApplicable(javaType) => val entryType = getTypeParameter(javaType, 0) @@ -30,13 +30,13 @@ object ScalaModules { class SetModule extends ClassModule[Set[_]] { - override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type) = { + override def readType(context: TypeAdapter, javaType: JType): ListType = { val entryType = context.readType(getTypeParameter(javaType, 0)) ListType(entryType) } @nowarn - override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type) = value match { + override def toJava(context: TypeAdapter, value: Value, javaType: JType): Set[Any] = value match { case NullValue => null case ListValue(entries) if isApplicable(javaType) => val entryType = getTypeParameter(javaType, 0) @@ -50,13 +50,13 @@ object ScalaModules { class MapModule extends ClassModule[Map[_, _]] { - override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type) = { + override def readType(context: TypeAdapter, javaType: JType): MapType = { val entryType = context.readType(getTypeParameter(javaType, 1)) MapType(entryType) } @nowarn - override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type) = value match { + override def toJava(context: TypeAdapter, value: Value, javaType: JType): Map[String, Any] = value match { case NullValue => null case RecordValue(entries) if classOf[Map[_,_]].isAssignableFrom(getBaseClass(javaType)) => val keyType = getTypeParameter(javaType, 0) @@ -71,21 +71,21 @@ object ScalaModules { def fromJava(context: TypeAdapter, obj: Any): Value = obj match { case map: Map[_, _] => - val entries: Map[String, Value] = map.map { case (key, obj) => key.asInstanceOf[String] -> context.fromJava(obj) }.toMap + val entries: Map[String, Value] = map.map { case (key, obj) => key.asInstanceOf[String] -> context.fromJava(obj) } RecordValue(entries) } } class OptionModule extends ClassModule[Option[_]] { - override def readType(context: TypeAdapter, javaType: reflect.Type): Type = javaType match { + override def readType(context: TypeAdapter, javaType: JType): Type = javaType match { case clazz: ParameterizedType if classOf[scala.Option[_]].isAssignableFrom(getBaseClass(clazz)) => val entryType = context.readType(clazz.getActualTypeArguments()(0)) OptionType(entryType) } @nowarn - override def toJava(context: TypeAdapter, value: Value, javaType: reflect.Type): Any = (value, javaType) match { + override def toJava(context: TypeAdapter, value: Value, javaType: JType): Any = (value, javaType) match { case (_, generic: ParameterizedType) if classOf[Option[_]].isAssignableFrom(getBaseClass(generic.getRawType)) => val optionType = generic.getActualTypeArguments()(0) value match { @@ -104,6 +104,3 @@ object ScalaModules { } } } - - - diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/modules/UUIDModule.scala b/core/baker-types/src/main/scala/com/ing/baker/types/modules/UUIDModule.scala new file mode 100644 index 000000000..0d5a92aa2 --- /dev/null +++ b/core/baker-types/src/main/scala/com/ing/baker/types/modules/UUIDModule.scala @@ -0,0 +1,38 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types._ + +import java.lang.reflect + +import scala.annotation.nowarn + +class UUIDModule extends TypeModule { + + override def isApplicable(javaType: reflect.Type): Boolean = javaType match { + case clazz: Class[_] if clazz.isAssignableFrom(classOf[java.util.UUID]) => true + case _ => false + } + + /** + * Attempts to convert a value to a desired java type. + * + * @param value The value + * @param javaType The desired java type. + * + * @return An instance of the java type. + */ + @nowarn + override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = { + (value, javaType) match { + case (NullValue, _) => null + case (PrimitiveValue(uuid: String), clazz: Class[_]) if clazz.isAssignableFrom(classOf[java.util.UUID]) => + java.util.UUID.fromString(uuid) + } + } + + override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = CharArray + + + override def fromJava(context: TypeAdapter, obj: Any): Value = PrimitiveValue(obj.toString) + +} diff --git a/core/baker-types/src/main/scala/com/ing/baker/types/package.scala b/core/baker-types/src/main/scala/com/ing/baker/types/package.scala index afdaae0c0..6b24dd8dd 100644 --- a/core/baker-types/src/main/scala/com/ing/baker/types/package.scala +++ b/core/baker-types/src/main/scala/com/ing/baker/types/package.scala @@ -1,7 +1,7 @@ package com.ing.baker -import java.lang.reflect.ParameterizedType - +import java.lang.reflect.{ParameterizedType, WildcardType} +import scala.annotation.tailrec import scala.reflect.runtime.universe package object types { @@ -15,17 +15,28 @@ package object types { * List[String] -> List * Map[String, Int] -> Map */ + @tailrec def getBaseClass(javaType: java.lang.reflect.Type): Class[_] = javaType match { case c: Class[_] => c case t: ParameterizedType => getBaseClass(t.getRawType) - case t @ _ => throw new IllegalArgumentException(s"Unsupported type: $javaType") + case _ => throw new IllegalArgumentException(s"Unsupported type: $javaType") } def getTypeParameter(javaType: java.lang.reflect.Type, index: Int): java.lang.reflect.Type = { - javaType.asInstanceOf[ParameterizedType].getActualTypeArguments()(index) + val actualTypeArguments = javaType.asInstanceOf[ParameterizedType].getActualTypeArguments()(index) + + actualTypeArguments match { + case wildcardType: WildcardType => + val upperBounds = wildcardType.getUpperBounds + upperBounds.size match { + case 1 => upperBounds.apply(0) + case _ => throw new IllegalArgumentException(s"Multiple upper bounds are not supported. Found multiple upper bounds for type: $javaType") + } + case _ => actualTypeArguments + } } - def isAssignableToBaseClass(javaType: java.lang.reflect.Type, base: Class[_]) = base.isAssignableFrom(getBaseClass(javaType)) + def isAssignableToBaseClass(javaType: java.lang.reflect.Type, base: Class[_]): Boolean = base.isAssignableFrom(getBaseClass(javaType)) def createJavaType(paramType: universe.Type): java.lang.reflect.Type = { val typeConstructor = mirror.runtimeClass(paramType) @@ -38,7 +49,7 @@ package object types { override def getRawType: java.lang.reflect.Type = typeConstructor override def getActualTypeArguments: Array[java.lang.reflect.Type] = innerTypes override def getOwnerType: java.lang.reflect.Type = null - override def toString() = s"ParameterizedType: $typeConstructor[${getActualTypeArguments.mkString(",")}]" + override def toString = s"ParameterizedType: $typeConstructor[${getActualTypeArguments.mkString(",")}]" } } } @@ -102,5 +113,5 @@ package object types { // TYPES - def isPrimitiveValue(obj: Any) = supportedPrimitiveClasses.exists(clazz => clazz.isInstance(obj)) + def isPrimitiveValue(obj: Any): Boolean = supportedPrimitiveClasses.exists(clazz => clazz.isInstance(obj)) } diff --git a/core/baker-types/src/test/scala/com/ing/baker/types/modules/CurrencyModulesSpec.scala b/core/baker-types/src/test/scala/com/ing/baker/types/modules/CurrencyModulesSpec.scala new file mode 100644 index 000000000..8717d187c --- /dev/null +++ b/core/baker-types/src/test/scala/com/ing/baker/types/modules/CurrencyModulesSpec.scala @@ -0,0 +1,30 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types.Converters.{readJavaType, toJava, toValue} +import com.ing.baker.types._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatestplus.scalacheck.Checkers + +import java.util.Currency + +class CurrencyModulesSpec extends AnyWordSpecLike with Matchers with Checkers { + + + "The Currency modules" should { + + "Have the expected type" in { + readJavaType[Currency] shouldBe RecordType(Seq( + RecordField("currencyCode", CharArray), + RecordField("defaultFractionDigits", Int32), + RecordField("numericCode", Int32))) + } + + "Pare from and to Currency" in { + val euro = Currency.getInstance("EUR") + val euroValue = toValue(euro) + val recreatedEuro = toJava[Currency](euroValue) + recreatedEuro shouldBe euro + } + } +} diff --git a/core/baker-types/src/test/scala/com/ing/baker/types/modules/DurationModuleSpec.scala b/core/baker-types/src/test/scala/com/ing/baker/types/modules/DurationModuleSpec.scala new file mode 100644 index 000000000..cc90c8e3d --- /dev/null +++ b/core/baker-types/src/test/scala/com/ing/baker/types/modules/DurationModuleSpec.scala @@ -0,0 +1,50 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types +import com.ing.baker.types._ +import org.scalacheck.Gen +import org.scalacheck.Test.Parameters.defaultVerbose +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatestplus.scalacheck.Checkers + +import java.time._ + +class DurationModuleSpec extends AnyWordSpecLike with Matchers with Checkers { + + private val minSuccessfulTests = 100 + + // Long.MaxValue is not supported by joda time for local dates, resulting in a integer overflow + // This shifts the long max value 1 bit to the right (divides by 2) + // This translates to the date: Fri Apr 24 17:36:27 CEST 146140482 + private val maxMillis = Long.MaxValue >> 1 + + private val numGen: Gen[Long] = Gen.chooseNum[Long]( + 0L, maxMillis, 0, maxMillis + ) + + "The DurationModule" should { + + "be able to parse the types of Duration" in { + Converters.readJavaType[Duration] shouldBe types.RecordType(Seq(RecordField("seconds", Int64), RecordField("nanos", Int32))) + } + + "be able to read/write all Duration instances" in { + + val durationGen: Gen[Duration] = numGen.map(millis => Duration.ofMillis(millis)) + + check(transitivityProperty[Duration](durationGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + "be able to read/write original POJO Duration instances" in { + val duration: Duration = Duration.ofNanos(11000000001L) + val value: Value = Converters.toValue(duration) + val converted = value.as(classOf[Duration]) + duration shouldBe converted + + val durationValue: RecordValue = RecordValue(Map("seconds" -> Converters.toValue(11L), "nanos" -> Converters.toValue(1))) + val newDuration = durationValue.as(classOf[Duration]) + newDuration shouldBe duration + } + } +} diff --git a/core/baker-types/src/test/scala/com/ing/baker/types/modules/JavaTimeModuleSpec.scala b/core/baker-types/src/test/scala/com/ing/baker/types/modules/JavaTimeModuleSpec.scala new file mode 100644 index 000000000..af786dfc7 --- /dev/null +++ b/core/baker-types/src/test/scala/com/ing/baker/types/modules/JavaTimeModuleSpec.scala @@ -0,0 +1,80 @@ +package com.ing.baker.types.modules + +import com.ing.baker.types +import com.ing.baker.types.{Converters, Int32, Int64, RecordField, RecordValue, Value} + +import java.time.{Duration, Instant, LocalDate, LocalDateTime, OffsetDateTime, ZoneId, ZonedDateTime} +import org.scalacheck.Gen +import org.scalacheck.Test.Parameters.defaultVerbose +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatestplus.scalacheck.Checkers + +class JavaTimeModuleSpec extends AnyWordSpecLike with Matchers with Checkers { + + private val minSuccessfulTests = 100 + + // Long.MaxValue is not supported by joda time for local dates, resulting in a integer overflow + // This shifts the long max value 1 bit to the right (divides by 2) + // This translates to the date: Fri Apr 24 17:36:27 CEST 146140482 + private val maxMillis = Long.MaxValue >> 1 + + private val numGen: Gen[Long] = Gen.chooseNum[Long]( + 0L, maxMillis, 0, maxMillis + ) + + "The JavaTimeModule" should { + + "be able to parse the types of DateTime, LocalDateTime and LocalDate" in { + Converters.readJavaType[java.util.Date] shouldBe types.Date + Converters.readJavaType[LocalDate] shouldBe types.Date + Converters.readJavaType[LocalDateTime] shouldBe types.Date + Converters.readJavaType[OffsetDateTime] shouldBe types.Date + Converters.readJavaType[ZonedDateTime] shouldBe types.Date + Converters.readJavaType[Instant] shouldBe types.Date + } + + "be able to read/write all java.util.Date instances" in { + + val dateGen: Gen[java.util.Date] = numGen.map(millis => java.util.Date.from(Instant.ofEpochMilli(millis))) + + check(transitivityProperty[java.util.Date](dateGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + + "be able to read/write all LocalDate instances" in { + + val localDateGen: Gen[LocalDate] = numGen.map(millis => LocalDate.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())) + + check(transitivityProperty[LocalDate](localDateGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + "be able to read/write all LocalDateTime instances" in { + + val localDateTimeGen: Gen[LocalDateTime] = numGen.map(millis => LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())) + + check(transitivityProperty[LocalDateTime](localDateTimeGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + "be able to read/write all OffsetDateTime instances" in { + + val offsetDateTimeGen: Gen[OffsetDateTime] = numGen.map(millis => OffsetDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())) + + check(transitivityProperty[OffsetDateTime](offsetDateTimeGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + "be able to read/write all ZonedDateTime instances" in { + + val offsetDateTimeGen: Gen[ZonedDateTime] = numGen.map(millis => ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())) + + check(transitivityProperty[ZonedDateTime](offsetDateTimeGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + + "be able to read/write all Instant instances" in { + + val instantGen: Gen[Instant] = numGen.map(millis => Instant.ofEpochMilli(millis)) + + check(transitivityProperty[Instant](instantGen), defaultVerbose.withMinSuccessfulTests(minSuccessfulTests)) + } + } +} diff --git a/core/baker-types/src/test/scala/com/ing/baker/types/modules/PrimitiveModuleSpec.scala b/core/baker-types/src/test/scala/com/ing/baker/types/modules/PrimitiveModuleSpec.scala index 73117c81e..f6f95b6af 100644 --- a/core/baker-types/src/test/scala/com/ing/baker/types/modules/PrimitiveModuleSpec.scala +++ b/core/baker-types/src/test/scala/com/ing/baker/types/modules/PrimitiveModuleSpec.scala @@ -12,6 +12,7 @@ import org.scalatest.wordspec.AnyWordSpecLike import org.scalatestplus.scalacheck.Checkers import java.lang +import java.util.UUID import scala.annotation.nowarn import scala.reflect.runtime.universe.TypeTag @@ -37,7 +38,7 @@ object PrimitiveModuleSpec { class PrimitiveModuleSpec extends AnyWordSpecLike with Matchers with Checkers { - "The primivite module" should { + "The primitive module" should { "correctly parse all the supported primitive types" in { @@ -57,6 +58,7 @@ class PrimitiveModuleSpec extends AnyWordSpecLike with Matchers with Checkers { readJavaType[BigDecimal] shouldBe types.FloatBig readJavaType[java.math.BigDecimal] shouldBe types.FloatBig readJavaType[Array[Byte]] shouldBe types.ByteArray + readJavaType[UUID] shouldBe types.CharArray } "read/write all supported primitive types" in { diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeValidations.scala b/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeValidations.scala index db9cb5557..1b5bb2913 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeValidations.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeValidations.scala @@ -14,11 +14,31 @@ object RecipeValidations { // check if the process id argument type is correct val processIdArgumentTypeValidation : Seq[String] = - interactionTransition.requiredIngredients.filter(id => id.name.equals(recipeInstanceIdName)).flatMap { + interactionTransition.requiredIngredients.filter(id => + id.name.equals(recipeInstanceIdName) + ).flatMap { case IngredientDescriptor(_, types.CharArray) => None case IngredientDescriptor(_, incompatibleType) => Some(s"Non supported process id type: ${incompatibleType} on interaction: '${interactionTransition.interactionName}'") } + //Check if MetaData is correct type + val bakerMetaDataTypeValidation: Seq[String] = + interactionTransition.requiredIngredients.filter(id => + id.name.equals(recipeInstanceMetadataName) + ).flatMap { + case IngredientDescriptor(_, types.MapType(types.CharArray)) => None + case IngredientDescriptor(_, incompatibleType) => Some(s"Non supported MetaData type: ${incompatibleType} on interaction: '${interactionTransition.interactionName}'") + } + + //Check if BakerEventList is correct type + val bakerEventListTypeValidation: Seq[String] = + interactionTransition.requiredIngredients.filter(id => + id.name.equals(recipeInstanceEventListName) + ).flatMap { + case IngredientDescriptor(_, types.ListType(types.CharArray)) => None + case IngredientDescriptor(_, incompatibleType) => Some(s"Non supported EventList type: ${incompatibleType} on interaction: '${interactionTransition.interactionName}'") + } + // check if the predefined ingredient is of the expected type val predefinedIngredientOfExpectedTypeValidation : Iterable[String] = interactionTransition.predefinedParameters.flatMap { @@ -33,7 +53,11 @@ object RecipeValidations { } } - interactionWithNoRequirementsValidation ++ processIdArgumentTypeValidation ++ predefinedIngredientOfExpectedTypeValidation + interactionWithNoRequirementsValidation ++ + processIdArgumentTypeValidation ++ + bakerMetaDataTypeValidation ++ + bakerEventListTypeValidation ++ + predefinedIngredientOfExpectedTypeValidation } def validateInteractions(compiledRecipe: CompiledRecipe): Seq[String] = { @@ -61,6 +85,15 @@ object RecipeValidations { } } + /** + * Validates that provided ingredients do not contain reserved names for Baker + */ + def validateSpecialIngredientsNotProvided(compiledRecipe: CompiledRecipe): Seq[String] = { + compiledRecipe.allIngredients.filter(i => + i.name == recipeInstanceIdName || i.name == recipeInstanceMetadataName || i.name == recipeInstanceEventListName + ).map(i => s"Ingredient '${i.name}' is provided and this is a reserved name for internal use in Baker") + }.toSeq + def validateNoCycles(compiledRecipe: CompiledRecipe): Seq[String] = { val cycle: Option[compiledRecipe.petriNet.innerGraph.Cycle] = compiledRecipe.petriNet.innerGraph.findCycle cycle.map(c => s"The petrinet topology contains a cycle: $c").toList @@ -90,6 +123,7 @@ object RecipeValidations { val postCompileValidationErrors : Seq[String] = Seq( validateInteractionIngredients(compiledRecipe), + validateSpecialIngredientsNotProvided(compiledRecipe), validateInteractions(compiledRecipe), if (!validationSettings.allowCycles) validateNoCycles(compiledRecipe) else Seq(), if (!validationSettings.allowDisconnectedness && !compiledRecipe.petriNet.innerGraph.isConnected) Seq("The petrinet topology is not completely connected") else Seq(), diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeVisualizer.scala b/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeVisualizer.scala index d2899bf46..cb6474a72 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeVisualizer.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/il/RecipeVisualizer.scala @@ -102,7 +102,10 @@ object RecipeVisualizer { // specifies which transitions to compact (remove) val transitionsToCompact = (node: RecipePetriNetGraph#NodeT) => node.value match { - case Right(transition: Transition) => transition.isInstanceOf[IntermediateTransition] || transition.isInstanceOf[MultiFacilitatorTransition] + case Right(transition: Transition) => + transition.isInstanceOf[IntermediateTransition] || + transition.isInstanceOf[MultiFacilitatorTransition] || + transition.label.startsWith(checkpointEventInteractionPrefix) case _ => false } @@ -128,14 +131,14 @@ object RecipeVisualizer { generateDot(recipe.petriNet.innerGraph, style, filter, eventNames, ingredientNames) - def visualizePetriNet[P, T](graph: PetriNetGraph[P, T], placeLabelFn: P => String = (p: P) => p.toString, transitionLabelFn: T => String = (t: T) => t.toString): String = { + def visualizePetriNet(graph: PetriNetGraph, placeLabelFn: Place => String = (p: Place) => p.toString, transitionLabelFn: Transition => String = (t: Transition) => t.toString): String = { - val nodeLabelFn: Either[P, T] => String = { + val nodeLabelFn: Either[Place, Transition] => String = { case Left(p) => placeLabelFn(p) case Right(t) => transitionLabelFn(t) } - val nodeDotAttrFn: Either[P, T] => List[DotAttr] = { + val nodeDotAttrFn: Either[Place, Transition] => List[DotAttr] = { case Left(_) => List(DotAttr("shape", "circle")) case Right(_) => List(DotAttr("shape", "square")) } @@ -146,11 +149,11 @@ object RecipeVisualizer { attrStmts = List.empty, attrList = List.empty) - def myNodeTransformer(innerNode: PetriNetGraph[P, T]#NodeT): Option[(DotGraph, DotNodeStmt)] = { + def myNodeTransformer(innerNode: PetriNetGraph#NodeT): Option[(DotGraph, DotNodeStmt)] = { Some((myRoot, DotNodeStmt(nodeLabelFn(innerNode.value), nodeDotAttrFn(innerNode.value)))) } - def myEdgeTransformer(innerEdge: PetriNetGraph[P, T]#EdgeT): Option[(DotGraph, DotEdgeStmt)] = { + def myEdgeTransformer(innerEdge: PetriNetGraph#EdgeT): Option[(DotGraph, DotEdgeStmt)] = { val source = innerEdge.edge.sources.head.value val target = innerEdge.edge.targets.head.value diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/il/package.scala b/core/intermediate-language/src/main/scala/com/ing/baker/il/package.scala index 4c6b4bdba..be00ae961 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/il/package.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/il/package.scala @@ -10,6 +10,8 @@ import scala.collection.immutable.Seq package object il { val recipeInstanceIdName = "recipeInstanceId" + val recipeInstanceMetadataName = "RecipeInstanceMetaData" //Cannot rename to RecipeInstanceMetadata since this will break backwards compatibility + val recipeInstanceEventListName = "RecipeInstanceEventList" val processIdName = "$ProcessID$" //needed for backwards compatibility with V1 and V2 val exhaustedEventAppend = "RetryExhausted" val checkpointEventInteractionPrefix = "$CheckpointEventInteraction$" diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/InteractionTransition.scala b/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/InteractionTransition.scala index 7d972726e..162a77d90 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/InteractionTransition.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/InteractionTransition.scala @@ -20,7 +20,8 @@ case class InteractionTransition(eventsToFire: Seq[EventDescriptor], predefinedParameters: Map[String, Value], maximumInteractionCount: Option[Int], failureStrategy: InteractionFailureStrategy, - eventOutputTransformers: Map[String, EventOutputTransformer] = Map.empty) + eventOutputTransformers: Map[String, EventOutputTransformer] = Map.empty, + isReprovider: Boolean) extends Transition with HasCustomToStringForRecipeId { @@ -32,10 +33,19 @@ case class InteractionTransition(eventsToFire: Seq[EventDescriptor], * These are the ingredients that are not pre-defined or recipeInstanceId */ val nonProvidedIngredients: Seq[IngredientDescriptor] = - requiredIngredients.filterNot(i => i.name == recipeInstanceIdName || predefinedParameters.keySet.contains(i.name)) - - override def toStringForRecipeId(recipeIdVariant: RecipeIdVariant): String = + requiredIngredients.filterNot(i => + i.name == recipeInstanceIdName || + i.name == recipeInstanceMetadataName || + i.name == recipeInstanceEventListName || + predefinedParameters.keySet.contains(i.name)) + + override def toStringForRecipeId(recipeIdVariant: RecipeIdVariant): String = { + val originalId = s"InteractionTransition(${eventsToFire.toRecipeIdStringTypeA(recipeIdVariant)},${originalEvents.toRecipeIdStringTypeA(recipeIdVariant)}," + s"${requiredIngredients.toRecipeIdStringTypeB(recipeIdVariant)},$interactionName,$originalInteractionName," + s"$predefinedParameters,$maximumInteractionCount,$failureStrategy,$eventOutputTransformers)" + + if(isReprovider) originalId + ",isReprovider" + else originalId + } } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/package.scala b/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/package.scala index 0c3302606..9ea9f1fbe 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/package.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/il/petrinet/package.scala @@ -8,7 +8,7 @@ package object petrinet { /** * Type alias for a petri net with recipe Place and Transition types */ - type RecipePetriNet = PetriNet[Place, Transition] + type RecipePetriNet = PetriNet /** * Type alias for the node type of the scalax.collection.Graph backing the petri net. diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/Marking.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/Marking.scala index d6070a9f4..1fbaecaea 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/Marking.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/Marking.scala @@ -7,5 +7,5 @@ object Marking { * * @return The empty marking. */ - def empty[P]: Marking[P] = Map.empty[P, MultiSet[Any]] + def empty[X]: Marking[X] = Map.empty[X, MultiSet[Any]] } \ No newline at end of file diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MarkingOps.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MarkingOps.scala index c923c0e87..b8d6fff15 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MarkingOps.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MarkingOps.scala @@ -5,16 +5,16 @@ trait MarkingOps { /** * Some convenience method additions to work with Markings. */ - implicit class MarkingFunctions[P](marking: Marking[P]) { + implicit class MarkingFunctions[X](marking: Marking[X]) { - def multiplicities: MultiSet[P] = marking.view.map { case (key, value) => (key, value.multisetSize)}.toMap + def multiplicities: MultiSet[X] = marking.view.map { case (key, value) => (key, value.multisetSize)}.toMap - def add(p: P, value: Any, count: Int = 1): Marking[P] = { + def add(p: X, value: Any, count: Int = 1): Marking[X] = { val newTokens = marking.getOrElse(p, MultiSet.empty).multisetIncrement(value, count) marking.+(p -> newTokens) } - def |-|(other: Marking[P]): Marking[P] = other.keySet.foldLeft(marking) { + def |-|(other: Marking[X]): Marking[X] = other.keySet.foldLeft(marking) { case (result, place) => @@ -29,7 +29,7 @@ trait MarkingOps { } } - def |+|(other: Marking[P]): Marking[P] = other.keySet.foldLeft(marking) { + def |+|(other: Marking[X]): Marking[X] = other.keySet.foldLeft(marking) { case (result, place) => val newTokens = marking.get(place) match { case None => other(place) @@ -40,8 +40,8 @@ trait MarkingOps { } } - implicit class IterableToMarking[P](i: Iterable[(P, MultiSet[Any])]) { - def toMarking: Marking[P] = i.toMap[P, MultiSet[Any]] + implicit class IterableToMarking[X](i: Iterable[(X, MultiSet[Any])]) { + def toMarking: Marking[X] = i.toMap[X, MultiSet[Any]] } /** @@ -49,10 +49,10 @@ trait MarkingOps { * Having null values here is due to the design of petrinet library which uses a ColoredPetriNet which supports tokens with values.
* Here we only know the Place and the number of tokens, but not the values, so the value is initialized to null. * @param mset MultiSet of Places - * @tparam P Place type parameter + * @tparam X Place type parameter * @return Marking (state of the petrinet) with 'null' token values */ - implicit class MultiSetToMarking[P](mset: MultiSet[P]) { - def toMarking: Marking[P] = mset.map { case (p, n) => p -> Map[Any, Int](Tuple2(null, n)) }.toMarking + implicit class MultiSetToMarking[X](mset: MultiSet[X]) { + def toMarking: Marking[X] = mset.map { case (p, n) => p -> Map[Any, Int](Tuple2(null, n)) }.toMarking } } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSet.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSet.scala index 9de4e7819..1606b3d67 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSet.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSet.scala @@ -5,19 +5,19 @@ object MultiSet { /** * The empty multi set. */ - def empty[T]: MultiSet[T] = Map.empty[T, Int] + def empty[X]: MultiSet[X] = Map.empty[X, Int] /** * Copies a the given elements into a multi set. * * Equal elements in the sequence will increase the multiplicity of that element in multi set. */ - def copyOff[T](elements: Iterable[T]): MultiSet[T] = elements.foldLeft(empty[T]) { case (mset, e) => mset.multisetIncrement(e, 1) } + def copyOff[X](elements: Iterable[X]): MultiSet[X] = elements.foldLeft(empty[X]) { case (mset, e) => mset.multisetIncrement(e, 1) } /** * Creates a multiset of the provided elements. * * Equal elements in the arguments will increase the multiplicity of that element in multi set. */ - def apply[T](elements: T*): MultiSet[T] = copyOff[T](elements) + def apply[X](elements: X*): MultiSet[X] = copyOff[X](elements) } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSetOps.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSetOps.scala index bed7f6e8e..3d2039e63 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSetOps.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/MultiSetOps.scala @@ -2,8 +2,8 @@ package com.ing.baker.petrinet.api trait MultiSetOps { - implicit class MultiSetFunctions[T](mset: MultiSet[T]) { - def multisetDifference(other: MultiSet[T]): MultiSet[T] = + implicit class MultiSetFunctions[X](mset: MultiSet[X]) { + def multisetDifference(other: MultiSet[X]): MultiSet[X] = other.foldLeft(mset) { case (result, (p, count)) => result.get(p) match { case None => result @@ -12,7 +12,7 @@ trait MultiSetOps { } } - def multisetSum(other: MultiSet[T]): MultiSet[T] = + def multisetSum(other: MultiSet[X]): MultiSet[X] = other.foldLeft(mset) { case (m, (p, count)) => m.get(p) match { case None => m + (p -> count) @@ -26,7 +26,7 @@ trait MultiSetOps { * @param other * @return */ - def isSubSet(other: MultiSet[T]): Boolean = + def isSubSet(other: MultiSet[X]): Boolean = !other.exists { case (element, count) => mset.get(element) match { case None => true @@ -37,22 +37,22 @@ trait MultiSetOps { def multisetSize: Int = mset.values.sum - def setMultiplicity(map: Map[T, Int])(element: T, m: Int) = map + (element -> m) + def setMultiplicity(map: Map[X, Int])(element: X, m: Int) = map + (element -> m) - def allElements: Iterable[T] = mset.foldLeft(List.empty[T]) { - case (list, (e, count)) => List.fill[T](count)(e) ::: list + def allElements: Iterable[X] = mset.foldLeft(List.empty[X]) { + case (list, (e, count)) => List.fill[X](count)(e) ::: list } - def multisetDecrement(element: T, count: Int): MultiSet[T] = + def multisetDecrement(element: X, count: Int): MultiSet[X] = mset.get(element) match { case None => mset case Some(n) if n <= count => mset - element case Some(n) => mset + (element -> (n - count)) } - def multisetIncrement(element: T, count: Int): MultiSet[T] = mset + (element -> (count + mset.getOrElse(element, 0))) + def multisetIncrement(element: X, count: Int): MultiSet[X] = mset + (element -> (count + mset.getOrElse(element, 0))) - def multisetIntersects(other: MultiSet[T]): Boolean = { + def multisetIntersects(other: MultiSet[X]): Boolean = { mset.exists { case (p, n) => other.getOrElse(p, 0) > 0 } } } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNet.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNet.scala index 717a69910..545506e2d 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNet.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNet.scala @@ -1,25 +1,27 @@ package com.ing.baker.petrinet.api +import com.ing.baker.il.petrinet.{Place, Transition} + /** * Petri net class. * * Backed by a graph object from scala-graph (https://github.com/scala-graph/scala-graph) */ -class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { +class PetriNet(val innerGraph: PetriNetGraph) { /** * The set of places of the petri net * * @return The set of places */ - val places: Set[P] = innerGraph.nodes.collect { case n if n.isPlace => n.asPlace }.toSet + val places: Set[Place] = innerGraph.nodes.collect { case n if n.isPlace => n.asPlace }.toSet /** * The set of transitions of the petri net * * @return The set of transitions. */ - val transitions: Set[T] = innerGraph.nodes.collect { case n if n.isTransition => n.asTransition }.toSet + val transitions: Set[Transition] = innerGraph.nodes.collect { case n if n.isTransition => n.asTransition }.toSet /** * The out-adjecent places of a transition. @@ -27,7 +29,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param t transition * @return */ - def outgoingPlaces(t: T): Set[P] = innerGraph.get(Right(t)).outgoingPlaces + def outgoingPlaces(t: Transition): Set[Place] = innerGraph.get(Right(t)).outgoingPlaces /** * The out-adjacent transitions of a place. @@ -35,7 +37,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param p place * @return */ - def outgoingTransitions(p: P): Set[T] = innerGraph.get(Left(p)).outgoingTransitions + def outgoingTransitions(p: Place): Set[Transition] = innerGraph.get(Left(p)).outgoingTransitions /** * The in-adjacent places of a transition. @@ -43,7 +45,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param t transition * @return */ - def incomingPlaces(t: T): Set[P] = innerGraph.get(Right(t)).incomingPlaces + def incomingPlaces(t: Transition): Set[Place] = innerGraph.get(Right(t)).incomingPlaces /** * The in-adjacent transitions of a place. @@ -51,14 +53,14 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param p place * @return */ - def incomingTransitions(p: P): Set[T] = innerGraph.get(Left(p)).incomingTransitions + def incomingTransitions(p: Place): Set[Transition] = innerGraph.get(Left(p)).incomingTransitions /** * The set of nodes (places + transitions) in the petri net. * * @return The set of nodes. */ - def nodes: scala.collection.Set[Either[P, T]] = innerGraph.nodes.map(_.value) + def nodes: scala.collection.Set[Either[Place, Transition]] = innerGraph.nodes.map(_.value) /** * Returns the in-marking of a transition. That is; a map of place -> arc weight @@ -66,7 +68,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param t transition * @return */ - def inMarking(t: T): MultiSet[P] = innerGraph.get(Right(t)).incoming.map(e => e.source.asPlace -> e.weight.toInt).toMap + def inMarking(t: Transition): MultiSet[Place] = innerGraph.get(Right(t)).incoming.map(e => e.source.asPlace -> e.weight.toInt).toMap /** * The out marking of a transition. That is; a map of place -> arc weight @@ -74,7 +76,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param t transition * @return */ - def outMarking(t: T): MultiSet[P] = innerGraph.get(Right(t)).outgoing.map(e => e.target.asPlace -> e.weight.toInt).toMap + def outMarking(t: Transition): MultiSet[Place] = innerGraph.get(Right(t)).outgoing.map(e => e.target.asPlace -> e.weight.toInt).toMap /** * Returns the (optional) edge for a given place -> transition pair. @@ -83,7 +85,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param to The target transition. * @return */ - def findPTEdge(from: P, to: T): Option[Any] = + def findPTEdge(from: Place, to: Transition): Option[Any] = innerGraph.get(Left(from)).outgoing.find(_.target.value == Right(to)).map(_.toOuter.label) /** @@ -93,7 +95,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { * @param to The target place. * @return */ - def findTPEdge(from: T, to: P): Option[Any] = + def findTPEdge(from: Transition, to: Place): Option[Any] = innerGraph.get(Right(from)).outgoing.find(_.target.value == Left(to)).map(_.toOuter.label) /** @@ -108,7 +110,7 @@ class PetriNet[P, T](val innerGraph: PetriNetGraph[P, T]) { obj match { case null => false - case pn: PetriNet[_, _] => pn.innerGraph.equals(innerGraph) + case pn: PetriNet => pn.innerGraph.equals(innerGraph) case _ => false } } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNetAnalysis.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNetAnalysis.scala index 74b87eec1..802ebfbbc 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNetAnalysis.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/PetriNetAnalysis.scala @@ -1,5 +1,7 @@ package com.ing.baker.petrinet.api +import com.ing.baker.il.petrinet.{Place, Transition} + object PetriNetAnalysis { // indicates an unbounded token count in a place @@ -16,8 +18,8 @@ object PetriNetAnalysis { } } - implicit class PetriNetOps[P, T](petriNet: PetriNet[P, T]) { - def removeTransitions(transitions: Iterable[T]): PetriNet[P, T] = { + implicit class PetriNetOps(petriNet: PetriNet) { + def removeTransitions(transitions: Iterable[Transition]): PetriNet = { val graph = transitions.foldLeft(petriNet.innerGraph) { case (acc, t) => acc.-(Right(t)) } @@ -28,13 +30,13 @@ object PetriNetAnalysis { /** * A node in the coverability tree */ - class Node[P, T]( - var marking: MultiSet[P], - var isNew: Boolean, - var children: Map[T, Node[P, T]]) { + class Node( + var marking: MultiSet[Place], + var isNew: Boolean, + var children: Map[Transition, Node]) { // returns the path to a new node - def newNode: Option[List[Node[P, T]]] = + def newNode: Option[List[Node]] = if (isNew) Some(List(this)) else @@ -45,7 +47,7 @@ object PetriNetAnalysis { s"marking: $markingString, children: $children" } - def isCoverable(target: MultiSet[P]): Boolean = { + def isCoverable(target: MultiSet[Place]): Boolean = { if (marking >= target) true else @@ -55,7 +57,7 @@ object PetriNetAnalysis { def maxNrTokens: Int = (marking.values ++ children.values.map(_.maxNrTokens)).max } - def optimize[P, T](petrinet: PetriNet[P, T], m0: MultiSet[P]): (PetriNet[P, T], MultiSet[P]) = { + def optimize(petrinet: PetriNet, m0: MultiSet[Place]): (PetriNet, MultiSet[Place]) = { val unboundedTransitions = unboundedEnabled(petrinet, m0) @@ -65,7 +67,7 @@ object PetriNetAnalysis { // remove the cold transitions to simplify things val updatedPetriNet = petrinet.removeTransitions(unboundedTransitions) - val unboundedOut = unboundedTransitions.foldLeft(MultiSet.empty[P]) { + val unboundedOut = unboundedTransitions.foldLeft(MultiSet.empty[Place]) { case (acc, t) => acc.multisetSum(petrinet.outMarking(t)) }.map { case (p, _) => p -> W @@ -75,7 +77,7 @@ object PetriNetAnalysis { } } - def unboundedEnabled[P, T](petrinet: PetriNet[P, T], m0: MultiSet[P]): Iterable[T] = { + def unboundedEnabled(petrinet: PetriNet, m0: MultiSet[Place]): Iterable[Transition] = { val coldTransitions = petrinet.transitions.filter(t => petrinet.incomingPlaces(t).isEmpty) val unboundedMarking = m0.filter { case (_, n) => n == W } @@ -88,7 +90,7 @@ object PetriNetAnalysis { /** * Implements page 47 of http://cpntools.org/_media/book/covgraph.pdf */ - def calculateCoverabilityTree[P, T](petrinet: PetriNet[P, T], m0: MultiSet[P]): Node[P, T] = { + def calculateCoverabilityTree(petrinet: PetriNet, m0: MultiSet[Place]): Node = { val (pn, initialMarking) = optimize(petrinet, m0) @@ -96,7 +98,7 @@ object PetriNetAnalysis { val inMarking = pn.transitions.map(t => t -> petrinet.inMarking(t)).toMap val outMarking = pn.transitions.map(t => t -> petrinet.outMarking(t)).toMap - def fire(m0: MultiSet[P], t: T): MultiSet[P] = { + def fire(m0: MultiSet[Place], t: Transition): MultiSet[Place] = { // unbounded places stay unchanged val (unbounded, bounded) = m0.partition { case (_, n) => n == W } @@ -105,7 +107,7 @@ object PetriNetAnalysis { .multisetSum(outMarking(t)) ++ unbounded } - def enabledTransitions(m0: MultiSet[P]): Iterator[T] = { + def enabledTransitions(m0: MultiSet[Place]): Iterator[Transition] = { val outAdjacent = m0.keys.map(pn.outgoingTransitions).reduceOption(_ ++ _).getOrElse(Set.empty). filter(t => m0 >= inMarking(t)) @@ -114,7 +116,7 @@ object PetriNetAnalysis { } // 1. Label the initial marking M0 as the root and tag it 'new' - val root = new Node[P, T](initialMarking, true, Map.empty) + val root = new Node(initialMarking, true, Map.empty) var newNode = root.newNode @@ -136,13 +138,13 @@ object PetriNetAnalysis { enabledTransitions(M).foreach { t => // i. obtain the marking that results from firing t at M - val postT: MultiSet[P] = fire(M, t) + val postT: MultiSet[Place] = fire(M, t) // ii. if on the path to m there exists a marking that is covered by M1 - val coverableM: Option[MultiSet[P]] = + val coverableM: Option[MultiSet[Place]] = pathToM.map(_.marking).find(M11 => postT >= M11 && postT != M11) - val M1: MultiSet[P] = coverableM.map { M11 => + val M1: MultiSet[Place] = coverableM.map { M11 => postT.map { case (p, n) if n > M11.getOrElse(p, 0) => p -> W case (p, n) => p -> n @@ -150,7 +152,7 @@ object PetriNetAnalysis { }.getOrElse(postT) // iii. introduce M1 as a node - val newNode: Node[P, T] = new Node[P, T](M1, true, Map.empty) + val newNode: Node = new Node(M1, true, Map.empty) node.children += t -> newNode } } diff --git a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/package.scala b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/package.scala index 4f321cb66..810b4891c 100644 --- a/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/package.scala +++ b/core/intermediate-language/src/main/scala/com/ing/baker/petrinet/api/package.scala @@ -1,5 +1,6 @@ package com.ing.baker.petrinet +import com.ing.baker.il.petrinet.{Place, Transition} import scalax.collection.Graph import scalax.collection.GraphPredef._ import scalax.collection.edge.WLDiEdge @@ -14,28 +15,28 @@ package object api extends MultiSetOps with MarkingOps { /** * Type alias for something that is identifiable. */ - type Identifiable[T] = T => Id + type Identifiable[X] = X => Id /** * Type alias for a multi set. */ - type MultiSet[T] = Map[T, Int] + type MultiSet[X] = Map[X, Int] /** * Type alias for a marking. */ - type Marking[P] = Map[P, MultiSet[Any]] + type Marking[X] = Map[X, MultiSet[Any]] /** * Type alias for a petri net graph. * * See also: scala-graph (https://github.com/scala-graph/scala-graph */ - type PetriNetGraph[P, T] = Graph[Either[P, T], WLDiEdge] + type PetriNetGraph = Graph[Either[Place, Transition], WLDiEdge] - implicit class IdentifiableOps[T : Identifiable](e: T) { + implicit class IdentifiableOps[X : Identifiable](e: X) { - def getId: Long = implicitly[Identifiable[T]].apply(e) + def getId: Long = implicitly[Identifiable[X]].apply(e) } implicit class IdentifiableSeqOps[T : Identifiable](seq: Iterable[T]) { @@ -43,37 +44,37 @@ package object api extends MultiSetOps with MarkingOps { def getById(id: Long, name: String = "element"): T = findById(id).getOrElse { throw new IllegalStateException(s"No $name found with id: $id") } } - implicit class MarkingMarshall[P : Identifiable](marking: Marking[P]) { + implicit class MarkingMarshall(marking: Marking[Place]) { - def marshall: Marking[Id] = translateKeys(marking, (p: P) => implicitly[Identifiable[P]].apply(p)) + def marshall: Marking[Id] = translateKeys(marking, (p: Place) => implicitly[Identifiable[Place]].apply(p)) } implicit class MarkingUnMarshall(marking: Marking[Id]) { - def unmarshall[P : Identifiable](places: Iterable[P]): Marking[P] = translateKeys(marking, (id: Long) => places.getById(id, "place in petrinet")) + def unmarshall(places: Iterable[Place]): Marking[Place] = translateKeys(marking, (id: Long) => places.getById(id, "place in petrinet")) } def translateKeys[K1, K2, V](map: Map[K1, V], fn: K1 => K2): Map[K2, V] = map.map { case (key, value) => fn(key) -> value } - implicit class PetriNetGraphNodeOps[P, T](val node: PetriNetGraph[P, T]#NodeT) { + implicit class PetriNetGraphNodeOps(val node: PetriNetGraph#NodeT) { - def asPlace: P = node.value match { + def asPlace: Place = node.value match { case Left(p) => p case _ => throw new IllegalStateException(s"node $node is not a place!") } - def asTransition: T = node.value match { + def asTransition: Transition = node.value match { case Right(t) => t case _ => throw new IllegalStateException(s"node $node is not a transition!") } - def incomingNodes: Set[Either[P, T]] = node.incoming.map(_.source.value) - def incomingPlaces: Set[P] = incomingNodes.collect { case Left(place) => place } - def incomingTransitions: Set[T] = incomingNodes.collect { case Right(transition) => transition } + def incomingNodes: Set[Either[Place, Transition]] = node.incoming.map(_.source.value) + def incomingPlaces: Set[Place] = incomingNodes.collect { case Left(place) => place } + def incomingTransitions: Set[Transition] = incomingNodes.collect { case Right(transition) => transition } - def outgoingNodes: Set[Either[P, T]] = node.outgoing.map(_.target.value) - def outgoingPlaces: Set[P] = outgoingNodes.collect { case Left(place) => place } - def outgoingTransitions: Set[T] = outgoingNodes.collect { case Right(transition) => transition } + def outgoingNodes: Set[Either[Place, Transition]] = node.outgoing.map(_.target.value) + def outgoingPlaces: Set[Place] = outgoingNodes.collect { case Left(place) => place } + def outgoingTransitions: Set[Transition] = outgoingNodes.collect { case Right(transition) => transition } def isPlace: Boolean = node.value.isLeft def isTransition: Boolean = node.value.isRight diff --git a/core/intermediate-language/src/test/scala/com/ing/baker/petrinet/api/PetriNetAnalysisSpec.scala b/core/intermediate-language/src/test/scala/com/ing/baker/petrinet/api/PetriNetAnalysisSpec.scala index c7f005019..605256ee7 100644 --- a/core/intermediate-language/src/test/scala/com/ing/baker/petrinet/api/PetriNetAnalysisSpec.scala +++ b/core/intermediate-language/src/test/scala/com/ing/baker/petrinet/api/PetriNetAnalysisSpec.scala @@ -1,5 +1,7 @@ package com.ing.baker.petrinet.api +import com.ing.baker.il.petrinet.{IntermediateTransition, Place, Transition} +import com.ing.baker.il.petrinet.Place.IngredientPlace import com.ing.baker.petrinet.api.DSL._ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -17,21 +19,20 @@ object DSL { */ type Arc = WLDiEdge[Node] - type Place = Int - - type Transition = Int - type MarkingLike[T] = T => SimpleMarking - type SimpleMarking = MultiSet[Int] + type SimpleMarking = MultiSet[Place] case class TransitionAdjacency(in: SimpleMarking, out: SimpleMarking) - implicit def toSimpleMarking1(p: Int): SimpleMarking = Map(p -> 1) - implicit def toSimpleMarking2(p: (Int, Int)): SimpleMarking = Map(p._1 -> 1, p._2 -> 1) - implicit def toSimpleMarking3(p: (Int, Int, Int)): SimpleMarking = Map(p._1 -> 1, p._2 -> 1, p._3 -> 1) - implicit def toSimpleMarkingSeq(p: Seq[Int]): SimpleMarking = p.map(_ -> 1).toMap - + implicit def toPlace(p:Int): Place = Place(p.toString, IngredientPlace) + implicit def toPlace(m: (Int, Int)): (Place, Int) = m match { case (k,v) => toPlace(k) -> v } + implicit def toTransition(p:Int): Transition = new IntermediateTransition(p.toString) + implicit def toSimpleMarking1(p: Place): SimpleMarking = Map(p -> 1) + implicit def toSimpleMarking1(p: Int): SimpleMarking = Map(toPlace(p) -> 1) + implicit def toSimpleMarking2(p: (Int, Int)): SimpleMarking = Map(toPlace(p._1) -> 1, toPlace(p._2) -> 1) + implicit def toSimpleMarking3(p: (Int, Int, Int)): SimpleMarking = Map(toPlace(p._1) -> 1, toPlace(p._2) -> 1, toPlace(p._3) -> 1) + implicit def toSimpleMarkingSeq(p: Seq[Int]): SimpleMarking = p.map(toPlace(_) -> 1).toMap implicit class ADJ[In: MarkingLike](in: In) { def ~|~>[Out: MarkingLike](out: Out): TransitionAdjacency = TransitionAdjacency(implicitly[MarkingLike[In]].apply(in), implicitly[MarkingLike[Out]].apply(out)) } @@ -50,7 +51,7 @@ object DSL { val b = branch(branchFactor, start) b.out.keys.foldLeft(Seq(b)) { case (accTree, n) => - val subTreeRoot = accTree.flatMap(a => a.in.keys ++ a.out.keys).max + 1 + val subTreeRoot = accTree.flatMap(a => a.in.keys ++ a.out.keys).map(_.getId).max.toInt + 1 val subTree = tree(branchFactor, depth - 1, subTreeRoot) val connection = n ~|~> subTreeRoot accTree ++ subTree :+ connection @@ -58,11 +59,11 @@ object DSL { } } - def createPetriNet(adjacencies: TransitionAdjacency*): PetriNet[Place, Transition] = { + def createPetriNet(adjacencies: TransitionAdjacency*): PetriNet = { val params: Seq[Arc] = adjacencies.toSeq.zipWithIndex.flatMap { - case (a, t) => - a.in.map { case (p, weight) => WLDiEdge[Node, String](Left(p), Right(t + 1))(weight, "") }.toSeq ++ - a.out.map { case (p, weight) => WLDiEdge[Node, String](Right(t + 1), Left(p))(weight, "") }.toSeq + case (a: TransitionAdjacency, t) => + a.in.map { case (p, weight) => WLDiEdge[Node, String](Left(p), Right(toTransition(t + 1)))(weight, "") }.toSeq ++ + a.out.map { case (p, weight) => WLDiEdge[Node, String](Right(toTransition(t + 1)), Left(p))(weight, "") }.toSeq } new PetriNet(Graph(params: _*)) @@ -83,7 +84,7 @@ class PetriNetAnalysisSpec extends AnyWordSpec with Matchers { 1 ~|~> 7 ) - val initialMarking = Map(1 -> 1) + val initialMarking: SimpleMarking = Map(1 -> 1) val tree = PetriNetAnalysis.calculateCoverabilityTree(boundedNet, initialMarking) diff --git a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/Assertions.scala b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/Assertions.scala index 2340bac24..50aa879dc 100644 --- a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/Assertions.scala +++ b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/Assertions.scala @@ -19,6 +19,12 @@ object Assertions { Some("No interactions found.").filter(_ => recipe.interactions.isEmpty) ).flatten + private def assertRequiredEventForReprovider(recipe: Recipe): Seq[String] = { + recipe.interactions.filter(interaction => { + interaction.isReprovider && interaction.requiredEvents.isEmpty && interaction.requiredOneOfEvents.isEmpty + }).map(interaction => s"Reprovider interaction ${interaction.name} needs to have a event precondition") + } + private def assertSensoryEventsNegativeFiringLimits(recipe: Recipe): Seq[String] = Seq( Some("MaxFiringLimit should be greater than 0").filter(_ => @@ -34,6 +40,6 @@ object Assertions { assertValidNames[Ingredient](_.name, allIngredients, "Ingredient") assertNoDuplicateElementsExist[InteractionDescriptor](_.name, recipe.interactions.toSet) assertNoDuplicateElementsExist[Event](_.name, recipe.sensoryEvents) - assertNonEmptyRecipe(recipe) ++ assertSensoryEventsNegativeFiringLimits(recipe) + assertNonEmptyRecipe(recipe) ++ assertSensoryEventsNegativeFiringLimits(recipe) ++ assertRequiredEventForReprovider(recipe) } } diff --git a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/RecipeCompiler.scala b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/RecipeCompiler.scala index e308cfbdc..edec23a05 100644 --- a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/RecipeCompiler.scala +++ b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/RecipeCompiler.scala @@ -8,7 +8,8 @@ import com.ing.baker.il.petrinet._ import com.ing.baker.il.{CompiledRecipe, EventDescriptor, ValidationSettings, checkpointEventInteractionPrefix} import com.ing.baker.petrinet.api._ import com.ing.baker.recipe.common._ -import com.ing.baker.recipe.scaladsl.{Interaction, Event} +import com.ing.baker.recipe.{javadsl, kotlindsl} +import com.ing.baker.recipe.scaladsl.{Event, Interaction} import scalax.collection.edge.WLDiEdge import scalax.collection.immutable.Graph @@ -126,6 +127,7 @@ object RecipeCompiler { else Seq.empty } + /** * Draws an arc from all required ingredients for an interaction * If the ingredient has multiple consumers create a multi transition place and create both arcs for it @@ -138,7 +140,7 @@ object RecipeCompiler { t.nonProvidedIngredients.map(_.name).partition(ingredientsWithMultipleConsumers.contains) // the extra arcs to model multiple output transitions from one place - val internalDataInputArcs = fieldNamesWithPrefixMulti flatMap { fieldName => + val internalDataInputArcs: Seq[Arc] = fieldNamesWithPrefixMulti flatMap { fieldName => val multiTransitionPlace = createPlace(s"${t.label}-$fieldName", placeType = MultiTransitionPlace) Seq( // one arc from multiplier place to the transition @@ -148,23 +150,37 @@ object RecipeCompiler { // one arc from extra added place to transition arc(multiTransitionPlace, t, 1)) } + + // the data input arcs / places - val dataInputArcs = fieldNamesWithoutPrefix.map(fieldName => arc(createPlace(fieldName, IngredientPlace), t, 1)) + val dataInputArcs: Seq[Arc] = fieldNamesWithoutPrefix.map(fieldName => arc(createPlace(fieldName, IngredientPlace), t, 1)) + + val dataOutputArcs: Seq[Arc] = + if(t.isReprovider) + fieldNamesWithoutPrefix.map(fieldName => arc(t, createPlace(fieldName, IngredientPlace), 1)) ++ + fieldNamesWithPrefixMulti.map(fieldName => arc(t, createPlace(s"${t.label}-$fieldName", placeType = MultiTransitionPlace), 1)) + else + Seq.empty - val limitInteractionCountArc = + val limitInteractionCountArc: Option[Arc] = t.maximumInteractionCount.map(n => arc(createPlace(s"limit:${t.label}", FiringLimiterPlace(n)), t, 1)) - dataInputArcs ++ internalDataInputArcs ++ limitInteractionCountArc + dataInputArcs ++ dataOutputArcs ++ internalDataInputArcs ++ limitInteractionCountArc } private def buildInteractionArcs(multipleOutputFacilitatorTransitions: Seq[Transition], placeNameWithDuplicateTransitions: Map[String, Seq[InteractionTransition]], eventTransitions: Seq[EventTransition]) (t: InteractionTransition): Seq[Arc] = { - buildInteractionInputArcs( + + val inputArcs: Seq[Arc] = buildInteractionInputArcs( t, multipleOutputFacilitatorTransitions, - placeNameWithDuplicateTransitions) ++ buildInteractionOutputArcs(t, eventTransitions) + placeNameWithDuplicateTransitions) + + val outputArcs: Seq[Arc] = buildInteractionOutputArcs(t, eventTransitions) + + inputArcs ++ outputArcs } /** @@ -306,7 +322,7 @@ object RecipeCompiler { ++ internalEventArcs ++ multipleOutputFacilitatorArcs) - val petriNet: PetriNet[Place, Transition] = new PetriNet(Graph(arcs: _*)) + val petriNet: PetriNet = new PetriNet(Graph(arcs: _*)) val initialMarking: Marking[Place] = petriNet.places.collect { case p @ Place(_, FiringLimiterPlace(n)) => p -> Map[Any, Int]((null, n)) @@ -315,9 +331,11 @@ object RecipeCompiler { val errors = preconditionORErrors ++ preconditionANDErrors ++ precompileErrors val oldRecipeIdVariant : OldRecipeIdVariant = - if (recipe.isInstanceOf[com.ing.baker.recipe.javadsl.Recipe]) Scala212CompatibleJava - else if (recipe.isInstanceOf[com.ing.baker.recipe.kotlindsl.Recipe]) Scala212CompatibleKotlin - else Scala212CompatibleScala + recipe match { + case _: javadsl.Recipe => Scala212CompatibleJava + case _: kotlindsl.Recipe => Scala212CompatibleKotlin + case _ => Scala212CompatibleScala + } val compiledRecipe = CompiledRecipe.build( name = recipe.name, diff --git a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/package.scala b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/package.scala index b79fa23ab..7794e41dc 100644 --- a/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/package.scala +++ b/core/recipe-compiler/src/main/scala/com/ing/baker/compiler/package.scala @@ -47,11 +47,15 @@ package object compiler { event.providedIngredients.map(ingredientToCompiledIngredient)) } - //Replace RecipeInstanceId to recipeInstanceIdName tag as know in compiledRecipe- - //Replace ingredient tags with overridden tags + // Replace RecipeInstanceId to recipeInstanceIdName tag as know in compiledRecipe + // Replace BakerMetaData to BakerMetaData tag as know in compiledRecipe + // Replace BakerEventList to BakerEventList tag as know in compiledRecipe + // Replace ingredient tags with overridden tags val inputFields: Seq[(String, Type)] = interactionDescriptor.inputIngredients .map { ingredient => if (ingredient.name == common.recipeInstanceIdName) il.recipeInstanceIdName -> ingredient.ingredientType + else if(ingredient.name == common.recipeInstanceMetadataName) il.recipeInstanceMetadataName -> ingredient.ingredientType + else if(ingredient.name == common.recipeInstanceEventListName) il.recipeInstanceEventListName -> ingredient.ingredientType else interactionDescriptor.overriddenIngredientNames.getOrElse(ingredient.name, ingredient.name) -> ingredient.ingredientType } @@ -100,7 +104,8 @@ package object compiler { maximumInteractionCount = interactionDescriptor.maximumInteractionCount, failureStrategy = failureStrategy, eventOutputTransformers = interactionDescriptor.eventOutputTransformers.map { - case (event, transformer) => event.name -> transformEventOutputTransformer(transformer) }) + case (event, transformer) => event.name -> transformEventOutputTransformer(transformer) }, + isReprovider = interactionDescriptor.isReprovider) } } } diff --git a/core/recipe-compiler/src/test/scala/com/ing/baker/compiler/RecipeCompilerSpec.scala b/core/recipe-compiler/src/test/scala/com/ing/baker/compiler/RecipeCompilerSpec.scala index 26d6d11e0..37c934dad 100644 --- a/core/recipe-compiler/src/test/scala/com/ing/baker/compiler/RecipeCompilerSpec.scala +++ b/core/recipe-compiler/src/test/scala/com/ing/baker/compiler/RecipeCompilerSpec.scala @@ -87,6 +87,50 @@ class RecipeCompilerSpec extends AnyWordSpecLike with Matchers { compiledRecipe.validationErrors should contain("Non supported process id type: Int32 on interaction: 'wrongrecipeInstanceIdInteraction'") } + "give an error if the MetaData is required and is not of the Map type" in { + val wrongMetaDataInteraction = + Interaction( + name = "wrongMetaDataInteraction", + inputIngredients = Seq(new Ingredient[Int](common.recipeInstanceMetadataName), initialIngredient), + output = Seq.empty) + + val recipe = Recipe("NonProvidedIngredient") + .withSensoryEvent(initialEvent) + .withInteractions(wrongMetaDataInteraction) + + val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(recipe) + compiledRecipe.validationErrors should contain("Non supported MetaData type: Int32 on interaction: 'wrongMetaDataInteraction'") + } + + "give an error if the baker internal ingredients are provided" in { + val wrongDateEvent = Event("WrongDataEvent", + Seq( + Ingredient[String]("recipeInstanceId"), + Ingredient[String]("RecipeInstanceMetaData")), + maxFiringLimit = None) + + val wrongDateEvent2 = Event("WrongDataEvent2", + Seq(Ingredient[String]("RecipeInstanceEventList")), + maxFiringLimit = None) + + val wrongMetaDataInteraction = + Interaction( + name = "wrongDataProvidedInteraction", + inputIngredients = Seq(new Ingredient[String](common.recipeInstanceIdName), initialIngredient), + output = Seq(wrongDateEvent2)) + + val recipe = Recipe("WrongDataRecipe") + .withSensoryEvents(initialEvent, wrongDateEvent) + .withInteractions(wrongMetaDataInteraction) + + val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(recipe) + compiledRecipe.validationErrors shouldBe List( + "Ingredient 'recipeInstanceId' is provided and this is a reserved name for internal use in Baker", + "Ingredient 'RecipeInstanceMetaData' is provided and this is a reserved name for internal use in Baker", + "Ingredient 'RecipeInstanceEventList' is provided and this is a reserved name for internal use in Baker" + ) + } + "give a list of wrong ingredients" when { "an ingredient is of the wrong type" in { val initialIngredientInt = new common.Ingredient("initialIngredient", RecordType(Seq(RecordField("data", Int32)))) @@ -220,6 +264,14 @@ class RecipeCompilerSpec extends AnyWordSpecLike with Matchers { intercept[IllegalArgumentException](RecipeCompiler.compileRecipe(recipe)).getMessage.shouldBe("Recipe with a null or empty name found") } } + + "an Interaction is reprovider, but has no required events" in { + val recipe: Recipe = Recipe("LoopingWithReprovider") + .withInteraction(interactionOne.isReprovider(true)) + .withSensoryEvents(initialEvent) + val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(recipe) + compiledRecipe.validationErrors shouldBe List("Reprovider interaction InteractionOne needs to have a event precondition") + } } "give the interaction an optional ingredient as empty" when { @@ -335,6 +387,13 @@ class RecipeCompilerSpec extends AnyWordSpecLike with Matchers { } } + "give the interaction with Reprovider enabled" when { + "it compiles a java recipe and changes recipeId" in { + val recipe = TestRecipeJava.getRecipeReprovider("id-test-recipe") + val compiledRecipe = RecipeCompiler.compileRecipe(recipe) + compiledRecipe.recipeId shouldBe "416e8abc02abcbee" + } + } "give the interaction for checkpoint-events" when { "it compiles a java recipe" in { diff --git a/core/recipe-dsl-kotlin/src/main/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDsl.kt b/core/recipe-dsl-kotlin/src/main/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDsl.kt index 905474392..748c283a6 100644 --- a/core/recipe-dsl-kotlin/src/main/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDsl.kt +++ b/core/recipe-dsl-kotlin/src/main/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDsl.kt @@ -146,6 +146,16 @@ class InteractionBuilder(private val interactionClass: KClass = mutableSetOf() @@ -154,6 +164,8 @@ class InteractionBuilder(private val interactionClass: KClass = mutableSetOf() private val requiredOneOfEvents: MutableSet> = mutableSetOf() + + /** * All events specified in this block have to be available for the interaction to be executed (AND precondition). * @@ -244,7 +256,8 @@ class InteractionBuilder(private val interactionClass: KClass return with(classifier) { if (sealedSubclasses.isNotEmpty()) { - sealedSubclasses.map { it.toEvent() }.toSet() + flattenSealedSubclasses().map { it.toEvent() }.toSet() } else { setOf(classifier.toEvent()) } } } + + private fun KClass<*>.flattenSealedSubclasses(): List> { + return if (sealedSubclasses.isNotEmpty()) { + sealedSubclasses.flatMap { it.flattenSealedSubclasses() } + } else { + listOf(this) + } + } } @RecipeDslMarker @@ -464,22 +486,9 @@ private fun KClass.interaction private fun KClass<*>.toEvent(maxFiringLimit: Int? = null): Event { return Event( simpleName, - primaryConstructor?.parameters?.map { Ingredient(it.name, it.type.javaType) }, + primaryConstructor?.parameters?.map { Ingredient(it.name, it.type.javaType) } ?: emptyList(), Optional.ofNullable(maxFiringLimit) ) } private fun KFunction<*>.hasFiresEventAnnotation() = annotations.any { it.annotationClass == FiresEvent::class } - -private fun KParameter.toJavaType(): Type { - return if (type.arguments.isEmpty()) { - type.javaType - } else { - return object : ParameterizedType { - // Not null assertions are 'safe' here. We already validated we are dealing with a generic type. - override fun getRawType(): Type = type.classifier?.starProjectedType?.javaType!! - override fun getActualTypeArguments(): Array = type.arguments.map { it.type?.javaType!! }.toTypedArray() - override fun getOwnerType(): Type? = null - } - } -} diff --git a/core/recipe-dsl-kotlin/src/test/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDslTest.kt b/core/recipe-dsl-kotlin/src/test/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDslTest.kt index 9eccb0184..6a2babd08 100644 --- a/core/recipe-dsl-kotlin/src/test/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDslTest.kt +++ b/core/recipe-dsl-kotlin/src/test/kotlin/com/ing/baker/recipe/kotlindsl/KotlinDslTest.kt @@ -138,6 +138,9 @@ class KotlinDslTest { with(recipe.interactions().toList().apply(2)) { assertEquals("ShipItems", name()) + assertEquals("ShippingConfirmed", output().apply(0).name()) + assertEquals(0, output().apply(0).providedIngredients().size()) + assertEquals(1, requiredEvents().size()) assertEquals("PaymentSuccessful", requiredEvents().toList().apply(0)) @@ -388,11 +391,39 @@ class KotlinDslTest { } with(recipe.interactions().apply(0)) { - assertEquals(2, inputIngredients().size()) - assertEquals(inputIngredients().toList().apply(0).name(), "metaData") - assertEquals(inputIngredients().toList().apply(0).ingredientType().toString(), "CharArray") - assertEquals(inputIngredients().toList().apply(1).name(), "reservedItems") - assertEquals(inputIngredients().toList().apply(1).ingredientType().toString(), "List[Record(name: CharArray, id: Int64)]") + assertEquals(4, inputIngredients().size()) + assertEquals("metaData", inputIngredients().toList().apply(0).name()) + assertEquals("CharArray", inputIngredients().toList().apply(0).ingredientType().toString()) + assertEquals("agreements", inputIngredients().toList().apply(1).name()) + assertEquals("List[Record(name: CharArray, id: Int64)]", inputIngredients().toList().apply(1).ingredientType().toString()) + assertEquals("uniqueAgreements", inputIngredients().toList().apply(2).name()) + assertEquals("List[Record(name: CharArray, id: Int64)]", inputIngredients().toList().apply(2).ingredientType().toString()) + assertEquals("mapOfAgreements", inputIngredients().toList().apply(3).name()) + assertEquals("Map[Record(name: CharArray, id: Int64)]", inputIngredients().toList().apply(3).ingredientType().toString()) + } + } + + @Test + fun `nested sealed classes`() { + + val recipe = recipe("RecipeWithNestedSealedClasses") { + interaction() + } + + with(recipe) { + assertEquals("RecipeWithNestedSealedClasses", name()) + assertEquals(1, interactions().size()) + } + + with(recipe.interactions().toList().apply(0)) { + assertEquals("ApiInteraction", name()) + + assertEquals(1, inputIngredients().size()) + assertEquals("reservedItems", inputIngredients().toList().apply(0).name()) + + assertEquals(2, output().size()) + assertEquals("ResponseSuccessful", output().toList().apply(0).name()) + assertEquals("ResponseFailed", output().toList().apply(1).name()) } } @@ -417,8 +448,8 @@ class KotlinDslTest { interface MakePayment : Interaction { sealed interface MakePaymentOutcome - class PaymentSuccessful : MakePaymentOutcome - class PaymentFailed : MakePaymentOutcome + object PaymentSuccessful : MakePaymentOutcome + object PaymentFailed : MakePaymentOutcome fun apply( reservedItems: Ingredients.ReservedItems, @@ -427,7 +458,7 @@ class KotlinDslTest { } interface ShipItems : Interaction { - class ShippingConfirmed + object ShippingConfirmed fun apply( shippingAddress: Ingredients.ShippingAddress, @@ -447,7 +478,24 @@ class KotlinDslTest { interface GenericIngredientContainerWithJavaElements : Interaction { class Result - fun apply(metaData: String, reservedItems: List): Result + fun apply( + metaData: String, + agreements: List, + uniqueAgreements: kotlin.collections.Set, + mapOfAgreements: Map + ): Result + } + + interface ApiInteraction: Interaction { + sealed interface Response + sealed interface Response200: Response + sealed interface Response500: Response + class ResponseSuccessful : Response200 + class ResponseFailed : Response500 + + fun apply( + reservedItems: Ingredients.ReservedItems, + ): Response } interface KotlinInteractionWithAnnotations : Interaction { diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/InteractionDescriptor.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/InteractionDescriptor.scala index af03723b8..84fffddbc 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/InteractionDescriptor.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/InteractionDescriptor.scala @@ -75,4 +75,9 @@ trait InteractionDescriptor { * An optional strategy how to deal with failures. Falls back to the default strategy specified in the recipe. */ val failureStrategy: Option[InteractionFailureStrategy] + + /** + * If this is set to true all incoming ingredient place will be provided for this interaction after it was executed + */ + val isReprovider: Boolean } \ No newline at end of file diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/package.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/package.scala index 30f8e7b21..e0b13666e 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/package.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/common/package.scala @@ -4,4 +4,6 @@ package object common { val recipeInstanceIdName = "RecipeInstanceId" val exhaustedEventAppend = "RetryExhausted" + val recipeInstanceMetadataName = "RecipeInstanceMetadata" + val recipeInstanceEventListName = "RecipeInstanceEventList" } diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/InteractionDescriptor.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/InteractionDescriptor.scala index c2c21a8a6..d1fac9610 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/InteractionDescriptor.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/InteractionDescriptor.scala @@ -19,6 +19,7 @@ case class InteractionDescriptor private( override val maximumInteractionCount: Option[Int], override val failureStrategy: Option[common.InteractionFailureStrategy] = None, override val eventOutputTransformers: Map[common.Event, common.EventOutputTransformer] = Map.empty, + override val isReprovider: Boolean = false, newName: Option[String] = None) extends common.InteractionDescriptor { @@ -239,6 +240,16 @@ case class InteractionDescriptor private( */ def withMaximumInteractionCount(times: Int): InteractionDescriptor = this.copy(maximumInteractionCount = Some(times)) + + /** + * When this interaction is executed it will fill its own interaction places. + * This is usefull if you want to execute this interaction multiple times without providing the ingredient again. + * To use this the InteractionDescriptor requires a prerequisite event. + * @param isReprovider + * @return + */ + def isReprovider(isReprovider: Boolean): InteractionDescriptor = + this.copy(isReprovider = isReprovider) } object InteractionDescriptor { diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/ReflectionHelpers.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/ReflectionHelpers.scala index 30ec9476c..12c5a4e24 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/ReflectionHelpers.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/ReflectionHelpers.scala @@ -2,8 +2,7 @@ package com.ing.baker.recipe.javadsl import java.lang.annotation.Annotation import java.lang.reflect.{Method, Type} - -import com.ing.baker.recipe.annotations.{ProcessId, RecipeInstanceId} +import com.ing.baker.recipe.annotations.{RecipeInstanceEventList, RecipeInstanceMetadata, ProcessId, RecipeInstanceId} import com.ing.baker.recipe.{annotations, common} import com.thoughtworks.paranamer.AnnotationParanamer @@ -33,6 +32,10 @@ object ReflectionHelpers { common.recipeInstanceIdName else if (annotationType.equals(classOf[ProcessId])) common.recipeInstanceIdName + else if (annotationType.equals(classOf[RecipeInstanceMetadata])) + common.recipeInstanceMetadataName + else if (annotationType.equals(classOf[RecipeInstanceEventList])) + common.recipeInstanceEventListName else annotationType.getSimpleName } diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/package.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/package.scala index 7baa677e4..3c496fe04 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/package.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/javadsl/package.scala @@ -56,8 +56,12 @@ package object javadsl { if (!method.getReturnType.isAssignableFrom(implicitly[ClassTag[CompletableFuture[_]]].runtimeClass)) throw new common.RecipeValidationException(s"Interaction $name is declared to be async, but it doesn't return a CompletableFuture") } else { - if (!method.getReturnType.isAssignableFrom(eventClass)) - throw new common.RecipeValidationException(s"Interaction $name provides event '${eventClass.getName}' that is incompatible with it's return type") + if (!method.getReturnType.isAssignableFrom(eventClass)) { + if(method.getReturnType.isAssignableFrom(implicitly[ClassTag[CompletableFuture[_]]].runtimeClass)) { + throw new common.RecipeValidationException(s"Interaction $name is declared to be sync but returns a CompletableFuture. If you have written an Async interaction remember to add @AsyncInteraction annotation") + } + throw new common.RecipeValidationException(s"Interaction $name provides event '${eventClass.getName}' that is incompatible with it's return type.") + } } } @@ -66,6 +70,6 @@ package object javadsl { else Seq.empty } - InteractionDescriptor(name, inputIngredients, output, Set.empty, Set.empty, Map.empty, Map.empty, None, None, None, Map.empty, newName) + InteractionDescriptor(name, inputIngredients, output, Set.empty, Set.empty, Map.empty, Map.empty, None, None, None, Map.empty, isReprovider = false, newName) } } diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/kotlindsl/Interaction.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/kotlindsl/Interaction.scala index 34415fc88..c415448d2 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/kotlindsl/Interaction.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/kotlindsl/Interaction.scala @@ -23,7 +23,8 @@ class Interaction private( overriddenIngredientNamesInput: Map[String, String], eventOutputTransformersInput: Map[common.Event, common.EventOutputTransformer], maximumInteractionCountInput: Option[Int], - failureStrategyInput: Option[common.InteractionFailureStrategy] + failureStrategyInput: Option[common.InteractionFailureStrategy], + isReproviderInput: Boolean ) extends common.InteractionDescriptor { override val name: String = nameInput @@ -38,6 +39,7 @@ class Interaction private( override val eventOutputTransformers: Map[common.Event, common.EventOutputTransformer] = eventOutputTransformersInput override val maximumInteractionCount: Option[Int] = maximumInteractionCountInput override val failureStrategy: Option[common.InteractionFailureStrategy] = failureStrategyInput + override val isReprovider: Boolean = isReproviderInput } object Interaction { @@ -49,7 +51,8 @@ object Interaction { overriddenIngredientNames: java.util.Map[String, String], eventOutputTransformers: java.util.Map[javadsl.Event, EventOutputTransformer], maximumInteractionCount: java.util.Optional[Int], - failureStrategy: java.util.Optional[common.InteractionFailureStrategy]) = new Interaction( + failureStrategy: java.util.Optional[common.InteractionFailureStrategy], + isReprovider: Boolean) = new Interaction( name, interactionDescriptor.originalName, interactionDescriptor.inputIngredients, @@ -60,7 +63,8 @@ object Interaction { overriddenIngredientNames.asScala.toMap, eventOutputTransformers.asScala.toMap.map { case (k, v) => k -> v }, maximumInteractionCount.map[Option[Int]](Option.apply(_)).orElse(None), - failureStrategy.map[Option[InteractionFailureStrategy]](Option.apply(_)).orElse(None) + failureStrategy.map[Option[InteractionFailureStrategy]](Option.apply(_)).orElse(None), + isReprovider ) def of(name: String, @@ -73,7 +77,8 @@ object Interaction { overriddenIngredientNames: java.util.Map[String, String], eventOutputTransformers: java.util.Map[Event, EventOutputTransformer], maximumInteractionCount: java.util.Optional[Int], - failureStrategy: java.util.Optional[common.InteractionFailureStrategy]) = new Interaction( + failureStrategy: java.util.Optional[common.InteractionFailureStrategy], + isReprovider: Boolean) = new Interaction( name, originalName, new ArraySeq.ofRef(inputIngredients.asScala.toArray), @@ -84,6 +89,7 @@ object Interaction { overriddenIngredientNames.asScala.toMap, eventOutputTransformers.asScala.toMap.map { case (k, v) => k -> v }, maximumInteractionCount.map[Option[Int]](Option.apply(_)).orElse(None), - failureStrategy.map[Option[InteractionFailureStrategy]](Option.apply(_)).orElse(None) + failureStrategy.map[Option[InteractionFailureStrategy]](Option.apply(_)).orElse(None), + isReprovider ) } diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/Interaction.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/Interaction.scala index 6c418beed..5829061e5 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/Interaction.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/Interaction.scala @@ -30,7 +30,9 @@ case class Interaction private(override val name: String, override val maximumInteractionCount: Option[Int] = None, override val failureStrategy: Option[InteractionFailureStrategy] = None, override val eventOutputTransformers: Map[common.Event, common.EventOutputTransformer] = Map.empty, - oldName: Option[String] = None) + override val isReprovider: Boolean = false, + oldName: Option[String] = None, + ) extends common.InteractionDescriptor { override val originalName: String = oldName.getOrElse(name) @@ -72,4 +74,15 @@ case class Interaction private(override val name: String, def withEventOutputTransformer(event: common.Event, newEventName: String, ingredientRenames: Map[String, String]): Interaction = copy(eventOutputTransformers = eventOutputTransformers + (event -> EventOutputTransformer(newEventName, ingredientRenames))) + + /** + * When this interaction is executed it will fill its own interaction places. + * This is usefull if you want to execute this interaction multiple times without providing the ingredient again. + * To use this the InteractionDescriptor requires a prerequisite event. + * + * @param isReprovider + * @return + */ + def isReprovider(isReprovider: Boolean): Interaction = + this.copy(isReprovider = isReprovider) } diff --git a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/package.scala b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/package.scala index 2186d2ba5..2b9901a99 100644 --- a/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/package.scala +++ b/core/recipe-dsl/src/main/scala/com/ing/baker/recipe/scaladsl/package.scala @@ -3,6 +3,8 @@ package com.ing.baker.recipe package object scaladsl { val recipeInstanceId: Ingredient[String] = new Ingredient[String](common.recipeInstanceIdName) + val recipeInstanceMetaData: Ingredient[Map[String, String]] = new Ingredient[Map[String, String]](common.recipeInstanceMetadataName) + val recipeInstanceEventList: Ingredient[List[String]] = new Ingredient[List[String]](common.recipeInstanceEventListName) } diff --git a/core/recipe-dsl/src/test/java/com/ing/baker/recipe/TestRecipeJava.java b/core/recipe-dsl/src/test/java/com/ing/baker/recipe/TestRecipeJava.java index aef1d22b5..123108f90 100644 --- a/core/recipe-dsl/src/test/java/com/ing/baker/recipe/TestRecipeJava.java +++ b/core/recipe-dsl/src/test/java/com/ing/baker/recipe/TestRecipeJava.java @@ -42,7 +42,7 @@ InteractionOneSuccessful apply(@ProcessId final String processId, } } - private static final class EventFromInteractionTwo { + public static final class EventFromInteractionTwo { private String interactionTwoIngredient; public EventFromInteractionTwo(String interactionTwoIngredient) { @@ -58,7 +58,7 @@ public void setInteractionTwoIngredient(String interactionTwoIngredient) { } } - private static class InteractionTwo implements Interaction { + public static class InteractionTwo implements Interaction { @FiresEvent(oneOf = EventFromInteractionTwo.class) EventFromInteractionTwo apply( @RequiresIngredient("initialIngredient") final String initialIngredient) { @@ -66,7 +66,7 @@ EventFromInteractionTwo apply( } } - private static final class InteractionThreeSuccessful { + public static final class InteractionThreeSuccessful { private String interactionThreeIngredientA; private String interactionThreeIngredientB; @@ -141,4 +141,24 @@ public static Recipe getRecipe(String name) { .withSensoryEvent(InitialEvent.class) .withSensoryEvent(EmptyEvent.class); } + + public static Recipe getRecipeReprovider(String name) { + return new Recipe(name) + .withInteractions( + of(InteractionOne.class) + .withFailureStrategy(new InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder() + .withInitialDelay(Duration.ofMillis(10)) + .withMaximumRetries(3) + .build() + ), + of(InteractionTwo.class) + .isReprovider(true), + of(SupplierInteraction.class) + .withRequiredEvent(EmptyEvent.class), + of(InteractionThree.class) + .withRequiredEvent(SupplierInteractionSuccessful.class) + ) + .withSensoryEvent(InitialEvent.class) + .withSensoryEvent(EmptyEvent.class); + } } diff --git a/core/recipe-dsl/src/test/scala/com/ing/baker/recipe/TestRecipe.scala b/core/recipe-dsl/src/test/scala/com/ing/baker/recipe/TestRecipe.scala index 7a0fcfde0..adf18c8ea 100644 --- a/core/recipe-dsl/src/test/scala/com/ing/baker/recipe/TestRecipe.scala +++ b/core/recipe-dsl/src/test/scala/com/ing/baker/recipe/TestRecipe.scala @@ -102,6 +102,30 @@ object TestRecipe { def apply(recipeInstanceId: String, initialIngredient: String): Future[InteractionOneSuccessful] } + val interactionOneWithMetaData = + Interaction( + name = "InteractionOneWithMetaData", + inputIngredients = Seq(recipeInstanceId, initialIngredient, recipeInstanceMetaData), + output = Seq(interactionOneSuccessful)) + + trait InteractionOneWithMetaData { + def name: String = "InteractionOneWithMetaData" + + def apply(recipeInstanceId: String, initialIngredient: String, bakerMetaData: Map[String, String]): Future[InteractionOneSuccessful] + } + + val interactionOneWithEventList = + Interaction( + name = "InteractionOneWithEventList", + inputIngredients = Seq(recipeInstanceId, initialIngredient, recipeInstanceEventList), + output = Seq(interactionOneSuccessful)) + + trait InteractionOneWithEventList { + def name: String = "InteractionOneWithEventList" + + def apply(recipeInstanceId: String, initialIngredient: String, recipeInstanceEventList: List[String]): Future[InteractionOneSuccessful] + } + val interactionTwo = Interaction( name = "InteractionTwo", diff --git a/docs/images/deps.svg b/docs/images/deps.svg deleted file mode 100644 index 94d876059..000000000 --- a/docs/images/deps.svg +++ /dev/null @@ -1,49 +0,0 @@ - - -G - - - -Compiler - -Compiler - - - -DSL - -DSL - - - -Compiler->DSL - - - - - -Intermediate Language - -Intermediate Language - - - -Compiler->Intermediate Language - - - - - -Runtime - -Runtime - - - -Runtime->Intermediate Language - - - - - diff --git a/docs/images/module-dependencies.svg b/docs/images/module-dependencies.svg new file mode 100644 index 000000000..3f0a9a440 --- /dev/null +++ b/docs/images/module-dependencies.svg @@ -0,0 +1,103 @@ + + + + + + + + +compilerrecipe-dslintermediate-languageruntime + + + \ No newline at end of file diff --git a/docs/images/web-shop-visual-state.svg b/docs/images/web-shop-visual-state.svg new file mode 100644 index 000000000..44fa428e1 --- /dev/null +++ b/docs/images/web-shop-visual-state.svg @@ -0,0 +1,187 @@ + + +%0 + + + +orderId + +orderId + + + +CancelOrder + +CancelOrder + + + +orderId->CancelOrder + + + + + +ShipOrder + +ShipOrder + + + +orderId->ShipOrder + + + + + +CheckStock + +CheckStock + + + +orderId->CheckStock + + + + + +OrderCancelled + +OrderCancelled + + + +CancelOrder->OrderCancelled + + + + + +OrderHasUnavailableItems + +OrderHasUnavailableItems + + + +OrderHasUnavailableItems->CancelOrder + + + + + +unavailableProductIds + +unavailableProductIds + + + +OrderHasUnavailableItems->unavailableProductIds + + + + + +address + +address + + + +address->ShipOrder + + + + + +SufficientStock + +SufficientStock + + + +SufficientStock->ShipOrder + + + + + +OrderShipped + +OrderShipped + + + +ShipOrder->OrderShipped + + + + + +OrderPlaced + +OrderPlaced + + + +OrderPlaced->orderId + + + + + +OrderPlaced->address + + + + + +productIds + +productIds + + + +OrderPlaced->productIds + + + + + +customerId + +customerId + + + +OrderPlaced->customerId + + + + + +CheckStock->OrderHasUnavailableItems + + + + + +CheckStock->SufficientStock + + + + + +unavailableProductIds->CancelOrder + + + + + +productIds->CheckStock + + + + + \ No newline at end of file diff --git a/docs/images/web-shop-visualization.svg b/docs/images/web-shop-visualization.svg new file mode 100644 index 000000000..0a5940641 --- /dev/null +++ b/docs/images/web-shop-visualization.svg @@ -0,0 +1,187 @@ + + +%0 + + + +orderId + +orderId + + + +CancelOrder + +CancelOrder + + + +orderId->CancelOrder + + + + + +ShipOrder + +ShipOrder + + + +orderId->ShipOrder + + + + + +CheckStock + +CheckStock + + + +orderId->CheckStock + + + + + +OrderCancelled + +OrderCancelled + + + +CancelOrder->OrderCancelled + + + + + +OrderHasUnavailableItems + +OrderHasUnavailableItems + + + +OrderHasUnavailableItems->CancelOrder + + + + + +unavailableProductIds + +unavailableProductIds + + + +OrderHasUnavailableItems->unavailableProductIds + + + + + +address + +address + + + +address->ShipOrder + + + + + +OrderShipped + +OrderShipped + + + +ShipOrder->OrderShipped + + + + + +CheckStock->OrderHasUnavailableItems + + + + + +SufficientStock + +SufficientStock + + + +CheckStock->SufficientStock + + + + + +OrderPlaced + +OrderPlaced + + + +OrderPlaced->orderId + + + + + +OrderPlaced->address + + + + + +productIds + +productIds + + + +OrderPlaced->productIds + + + + + +customerId + +customerId + + + +OrderPlaced->customerId + + + + + +SufficientStock->ShipOrder + + + + + +unavailableProductIds->CancelOrder + + + + + +productIds->CheckStock + + + + + \ No newline at end of file diff --git a/docs/images/webshop-example-1.svg b/docs/images/webshop-example-1.svg deleted file mode 100644 index d223b182f..000000000 --- a/docs/images/webshop-example-1.svg +++ /dev/null @@ -1,134 +0,0 @@ - - - _anonymous_0 - - - ReserveItems - - - - - - - - - - - - - - ReserveItems - - - OrderHadUnavailableItems - - - - - - - - - - - - - - OrderHadUnavailableItems - - - ReserveItems->OrderHadUnavailableItems - - - - - ItemsReserved - - - - - - - - - - - - - - ItemsReserved - - - ReserveItems->ItemsReserved - - - - - reservedItems - - reservedItems - - - unavailableItems - - unavailableItems - - - OrderHadUnavailableItems->unavailableItems - - - - - orderId - - orderId - - - orderId->ReserveItems - - - - - OrderPlaced - - - - - - - - - - - - - - OrderPlaced - - - OrderPlaced->orderId - - - - - items - - items - - - OrderPlaced->items - - - - - items->ReserveItems - - - - - ItemsReserved->reservedItems - - - - - \ No newline at end of file diff --git a/docs/images/webshop-state-2.svg b/docs/images/webshop-state-2.svg deleted file mode 100644 index 88ec029bc..000000000 --- a/docs/images/webshop-state-2.svg +++ /dev/null @@ -1,205 +0,0 @@ - - -%3 - - - -SendInvoice - -SendInvoice - - - -InvoiceWasSent - -InvoiceWasSent - - - -SendInvoice->InvoiceWasSent - - - - - -ValidateOrder - -ValidateOrder - - - -Valid - -Valid - - - -ValidateOrder->Valid - - - - - -Failed - -Failed - - - -ValidateOrder->Failed - - - - - -ManufactureGoods - -ManufactureGoods - - - -Valid->ManufactureGoods - - - - - -GoodsManufactured - -GoodsManufactured - - - -ManufactureGoods->GoodsManufactured - - - - - -goods - -goods - - - -GoodsManufactured->goods - - - - - -ShipGoods - -ShipGoods - - - -goods->ShipGoods - - - - - -GoodsShipped - -GoodsShipped - - - -ShipGoods->GoodsShipped - - - - - -GoodsShipped->SendInvoice - - - - - -trackingId - -trackingId - - - -GoodsShipped->trackingId - - - - - -CustomerInfoReceived - -CustomerInfoReceived - - - -customerInfo - -customerInfo - - - -CustomerInfoReceived->customerInfo - - - - - -customerInfo->SendInvoice - - - - - -customerInfo->ShipGoods - - - - - -PaymentMade - -PaymentMade - - - -PaymentMade->ManufactureGoods - - - - - -order - -order - - - -order->ValidateOrder - - - - - -order->ManufactureGoods - - - - - -OrderPlaced - -OrderPlaced - - - -OrderPlaced->order - - - - - diff --git a/docs/index.md b/docs/index.md index 184f51c44..e7fe4a039 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,63 +1,39 @@ -# Introduction +# Baker -Baker is a library that reduces the effort to orchestrate (micro)service-based process flows. +Baker is a library that provides a simple and intuitive way to orchestrate microservice-based process flows. -Developers declare the orchestration logic in a `Recipe` (process blueprint). - -A `Recipe` is made out of: - -- `Interactions` (functions) -- `Ingredients` (containers for data) -- `Events` - -The Baker runtime on the other hand runs instances of the `Recipe` across a cluster of nodes in an asynchronous fashion. - -### Baker allows you to - -- *Declaratively* design your business processes using a [recipe Domain Specific Language (DSL)](sections/reference/dsls). -- [Visualize](sections/reference/visualization) your recipe allowing product owners, architects and developers to talk the same language. -- Manage your recipes using the [Baker runtime](sections/reference/runtime). -- [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. +You declare your orchestration logic as a recipe using the Java, Kotlin, or Scala DSL. A recipe consists of +`interactions` (system calls), `ingredients` (data), and `events` (things that have happened in your process). ## Why Baker -Upgrading your business to an agile, adaptive and scalable microservice-based architecture does bring significant advantages, -but also critical challenges that must be resolved: - -- 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: - -![](images/webshop.svg) - -## 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. - +When working with microservice architectures, you encounter various challenges related to distributed systems. Things +like communication models, consistency management, failure handling, scaling approaches, and more. Baker simplifies +the development process by offering out-of-the-box solutions with its clustered runtime. Baker nodes can create and +distribute instances of recipes, handle failures in interactions using different strategies, restore the state of +long-lived processes, and provide additional functionalities to streamline microservice development. + +??? Abstract "Service composition" + Baker allows you to compose complex business processes by combining multiple microservices. It acts as a centralized + control mechanism to define the sequence and dependencies between services. Facilitating the execution of + orchestrated processes. Enabling you to build more robust and sophisticated applications that span multiple services. + +??? Abstract "Decouple business logic from service technologies" + Baker forces you to separate business logic from implementation details. Your business logic is expressed as a recipe + via the Java, Kotlin, or Scala DSL. The implementation details are contained in the interaction implementations. + +??? Abstract "Retry mechanism" + Baker includes a built-in retry mechanism. When a failure occurs in a microservice, Baker can automatically retry + the failed operation. Retrying the operation can help overcome transient errors or temporary network issues. Baker + can be configured with retry policies, including parameters such as the number of retries, delay between retries, + and exponential backoff strategies. + +??? Abstract "Visualize your business process" + Bakers ability to visualize recipes provides a powerful communication tool that helps product owners, architects, and + engineers to have a common understanding of the business process. This feature allows you to easily share your + recipe with others, enabling collaboration and feedback. + +## New to Baker? + +A good first step is to read more about Baker's [core concepts](sections/concepts). Afterward, you can +follow this quick [tutorial](sections/tutorial) to build your first Baker process. diff --git a/docs/sections/concepts.md b/docs/sections/concepts.md index edc95ad3f..bb854739b 100644 --- a/docs/sections/concepts.md +++ b/docs/sections/concepts.md @@ -1,137 +1,60 @@ # Concepts -Baker introduces *interactions*, *ingredients*, and *events* as a model of abstracting. - -With these three components we can create recipes (process blue prints) +In Baker, you declare orchestration logic as a `Recipe`. A recipe consists of three main building blocks: +`Ingredients`, `Interactions`, and `Events`. ## 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](../reference/baker-types-and-values/). - -## Interaction - -An interaction is similar to a function. - -It requires *input* ([ingredients](../reference/main-abstractions/#ingredient-and-ingredientinstance)) and -provides *output* ([events](../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. +An ingredient is a combination of a `name` and `type`. Similar to how a variable declaration in your codebase is a combination +of a name and type. For example, you can have an ingredient with the name `iban` and type `string`. Types are expressed +via the [Baker type system](../reference/baker-types-and-values/). -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 +Ingredients are pure pieces of data. They are immutable, meaning they do not change once they enter the process or +workflow. There is no support for hierarchy. Expressing a relationship like `Animal -> Dog -> Labrador` is not possible. - 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. +Ingredients serve as input for interactions and are carried through the process via +events. ## Event -An event has a *name* and can (optionally) provide ingredients. +Events represent something that has happened in your process. Most of the time, events are outputs of interactions. +We refer to outputs of interactions as `internal events`. Sometimes events come from outside the process. we refer to +events from outside the process as `sensory events`. Sensory events start/trigger the process. -The purpose of events is therefore twofold. +!!! note + Under the hood `internal events` and `sensory events` are identical. The naming distinction exists for practical + reasons only. -1. It signifies that something of interest has happened for a [recipe instance](../reference/main-abstractions/#recipe-and-recipeinstance). +An event has a `name` and (optionally) provides ingredients. In the end, events and ingredients are just data structures +that describe your process data. A good example would be an `OrderPlaced` event which carries two ingredients: +the `orderId` and a `list of products`. - Example, *"the customer placed the order"*, *"terms and conditions were accepted"* - -2. The event may provide ingredients required to continue the process. - - Example, *"OrderPlaced"* -> `` - -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** +## Interaction -*Events*, *Interactions* and *Ingredients* can be composed into recipes. +Interactions resemble functions. They require input (ingredients) and provide output (events). An +interaction can do many things. For example, fetch data from another service, do some complex calculation, +send a message to an event broker, etc. -Recipes are similar to process blueprints. +## Recipe -Baker provides a [recipe DSL](../reference/dsls/) in which you can declaratively describe your recipe. +A recipe is the blueprint of your business process. You define this process by combining ingredients, events, and +interactions. Baker provides a recipe DSL, allowing you to define your recipe in Java, Kotlin, or Scala. The example +below displays a (naive) recipe implementation to ship orders from a web-shop. -A small example: -``` java -new Recipe("webshop") - .withSensoryEvents( - OrderPlaced.class, - CustomerInfoReceived.class - .withInteractions( - of(ValidateOrder.class), - of(ManufactureGoods.class)); -``` +=== "Java" -The main take away is that when declaring your recipe you do not have to think about order. + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/SimpleRecipe.java" + ``` -Everything is automatically linked by the *data* requirements of the interactions. +=== "Kotlin" -## Continuing from here + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/SimpleRecipe.kt" + ``` -After adding the dependencies you can continue to: +=== "Scala" -* Go through the [development life cycle section](../development-life-cycle/design-a-recipe) if you like learning by doing; -* Go through the [reference section](../reference/main-abstractions) if you like learning by description. + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/SimpleRecipe.scala" + ``` diff --git a/docs/sections/cookbook/error-handling.md b/docs/sections/cookbook/error-handling.md new file mode 100644 index 000000000..2fdb409a1 --- /dev/null +++ b/docs/sections/cookbook/error-handling.md @@ -0,0 +1,232 @@ +# Error handling + +An interaction can fail to achieve its intended purpose. Baker categorizes failures in technical +failures and functional failures. + +Technical failures are characterized by the possibility of being retried and +eventually succeeding. Examples of technical failures include timeouts due to an unreliable network, temporary +unavailability of an external system, and receiving an unexpected response from an external system. These failures +are unexpected and are handled by throwing an exception from the interaction. + +Functional failures cannot be resolved by retrying the interaction. +Examples of functional failures include cases where there is insufficient stock to ship the order, or there is +insufficient credit to perform a transfer. These failures are anticipated and considered as potential outcomes of the +interaction. They are handled by returning an event from the interaction. + +## Failure strategies + +Baker offers multiple mitigation strategies for technical failures. + +!!! tip + The examples in this section all set the `defaultFailureStrategy` on `recipe` level. The same strategies are also + available for the `failureStrategy` on `interaction` level. + +### Block interaction + +This is the default failure strategy. When an exception occurs the interaction is blocked. This option is suitable +for non-idempotent interactions that cannot be retried. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeBlockInteraction.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeBlockInteraction.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeBlockInteraction.scala" + ``` + +### Fire event + +This option is the equivalent of a `try-catch` in code. When an exception occurs an event is fired. Instead of failing, +the process continues. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeFireEvent.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeFireEvent.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeFireEvent.scala" + ``` + +### Retry with incremental back-off + +Incremental back-off allows you to configure a retry mechanism that exponentially increases the time between each retry. +You retry quickly at first, but slower over time. Retry with incremental back-off keeps retrying until a set deadline +or the maximum amount of retries is reached. + +#### Until deadline + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilDeadline.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilDeadline.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilDeadline.scala" + ``` + +#### Until maximum retries + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilMaxRetries.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilMaxRetries.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilMaxRetries.scala" + ``` + + +| name | meaning | +|-------------------------|-------------------------------------------------| +| `initialDelay` | The delay before retrying for the first time. | +| `backoffFactor` | The backoff factor for the delay, defaults to 2 | +| `maxTimeBetweenRetries` | The maximum time between retries. | +| `deadLine` | The total amount of time spend retrying. | +| `maximumRetries` | The maximum amount of retries. | + + +Our example results in a retry pattern off: + +`100 millis -> 200 millis -> 400 millis -> ... -> 100 seconds -> 100 seconds`. + +Which can be visualized like this: + +![Visual representation of incremental back-off](../../images/incremental-backoff.png) + +!!! Note + Delays do not include the interaction execution time. If the first retry takes 5 seconds (and fails), the second retry will be triggered after: + + `(100 millis + 5 seconds + 200 millis) = 5.3 seconds` + + The deadline also does not consider interaction execution time. Keep this in mind when setting the deadline value. + +#### Retry exhaustion +If an interaction keeps failing, the retry is exhausted and the interaction becomes blocked. If you don't want the +interaction to block after exhausting all the retries, you can continue the process with a predefined event. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryExhaustedEvent.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryExhaustedEvent.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryExhaustedEvent.scala" + ``` + +## Manual intervention + +Baker allows you to resolve blocked interactions, and to stop retrying interactions via manual intervention. + +### Force a retry + +This method retries a given interaction a single time. If it succeeds, your process continues. If it fails, the interaction +stays blocked. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/ManualRetryInteraction.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualRetryInteraction.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/ManualRetryInteraction.scala" + ``` + +### Resolve interaction + +This method resolves a blocked interaction by firing an event. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/ManualResolveInteraction.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualResolveInteraction.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/ManualResolveInteraction.scala" + ``` + +### Stop retrying + +This method halts the retry process of a failing interaction by blocking the interaction. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/ManualStopInteraction.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualStopInteraction.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/ManualStopInteraction.scala" + ``` diff --git a/docs/sections/cookbook/fire-sensory-events.md b/docs/sections/cookbook/fire-sensory-events.md new file mode 100644 index 000000000..592df70e7 --- /dev/null +++ b/docs/sections/cookbook/fire-sensory-events.md @@ -0,0 +1,136 @@ +# Fire events and inquiry + +This section describes how to fire sensory events into a Baker process, and how to query information from a running +Baker process. + +!!! Note + For the Java API most methods return a `CompletableFuture`, in the Scala API they return `Future`. The Kotlin + API makes use of `suspending` functions, and thus does not wrap the return type. To keep things readable, + the descriptions in this section reason from Java's perspective. + +## Fire sensory events + +To trigger a Baker process you'll need to fire a sensory event. After firing an event, you may want to continue your +asynchronous computation at one of four different moments: + +1. Right after the event was received, but before any interactions are executed. +2. After all interactions have completed. At this point the process is either finished, or requires other sensory events to continue. +3. You want to do something on both the previously mentioned moments. +4. As soon one of the interactions fires a specific event. + +To this end, the Baker interface exposes four different methods to fire events. We'll discuss each of those in more +detail. + +### Fire event and resolve when received +This method returns a `CompletableFuture`. It completes right after the event was received, but +before any interactions are executed. The `SensoryEventStatus` is an enum containing information about the outcome +of the event (`Received`, `Completed`, `FiringLimitMet`, etc). + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenReceived.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenReceived.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenReceived.scala" + ``` + +### Fire event and resolve when completed +Returns a `CompletableFuture`. It completes when additional sensory events are required to continue +the process, or when the process has finished. The `SensoryEventResult` contains the `SensoryEventStatus` and a list +of all events names triggered as a result of this sensory event. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenCompleted.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenCompleted.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenCompleted.scala" + ``` + +### Fire event +This method is useful if you want to do something after the event was received and after all interactions have completed. +The method returns an `EventResolutions` object consisting of a `CompletableFuture` and +`CompletableFuture`. The former completes on receiving the event, the latter on completion of the +interactions. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/FireEvent.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEvent.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/FireEvent.scala" + ``` + +### Fire event and resolve on event +This method returns a `CompletableFuture`. It completes when one of the interactions fires an event +with a specified name. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveOnEvent.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveOnEvent.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveOnEvent.scala" + ``` + +## Inquiry +Baker allows you to query the state of a recipe instance at any given moment. To this end, Baker exposes a couple +of different methods that allow you to fetch information about events, ingredients, and interactions from a running process. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/InquiryExample.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/InquiryExample.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/InquiryExample.scala" + ``` \ No newline at end of file diff --git a/docs/sections/cookbook/interaction-execution.md b/docs/sections/cookbook/interaction-execution.md new file mode 100644 index 000000000..04410ab3f --- /dev/null +++ b/docs/sections/cookbook/interaction-execution.md @@ -0,0 +1,44 @@ +# Interaction execution + +This section will give a basic explanation when baker will execute interactions in your Recipe. +For a more in depth information please see the [execution-semantics page](../reference/execution-semantics.md). + +## When does Baker execute an interaction +Baker will execute an interaction when: + +1. All incoming ingredients are available +2. All event preconditions are met. + +After an interaction is executed it will 'consume' the ingredients and events. +Interactions get their own copies of events/ingredients to consume. +You do not have to worry about one interaction taking the ingredients away from another. + +To execute an interaction again all incoming ingredients need to be provided again and the event preconditions need to be met again. + +## Reprovider interactions +Interactions can be configured to be reprovider interactions. +This means that after execution they will provide their own ingredients again. +Reprovider interactions will only need to have their ingredients to be provided once. +They will execute everytime their event preconditions are met. + +Reprovider interactions do not update the ingredient data and will always use the latest ingredient data that is available. + +If there are no event preconditions on a reprovider interaction, it will automatically loop its execution. Therefore this is made mandatory in the RecipeCompiler. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithReproviderInteraction.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithReproviderInteraction.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithReproviderInteraction.scala" + ``` \ No newline at end of file diff --git a/docs/sections/cookbook/monitoring.md b/docs/sections/cookbook/monitoring.md new file mode 100644 index 000000000..de4c8a9e6 --- /dev/null +++ b/docs/sections/cookbook/monitoring.md @@ -0,0 +1,54 @@ +# Monitoring + +Baker allows you to register a couple of different event listeners. These are especially useful for monitoring and +logging purposes. + +!!! Warning + Each event is guaranteed to be delivered `AT MOST ONCE`. You should not use an event listener for critical functionality. + +!!! Note + The delivery is local (JVM) only. You will not receive events from other nodes when running in cluster mode. + +## Recipe instance events +The `registerEventListener` method allows you to declare an event listener which listens to all events of a specific +recipe instance. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/RegisterEventListener.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterEventListener.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/RegisterEventListener.scala" + ``` + +## All Baker events +The `registerBakerEventListener` method allows you to declare an event listener which listens to all Baker events. These +are events that notify what Baker is doing. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/RegisterBakerEventListener.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterBakerEventListener.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/RegisterBakerEventListener.scala" + ``` \ No newline at end of file diff --git a/docs/sections/cookbook/recipe-dsl.md b/docs/sections/cookbook/recipe-dsl.md new file mode 100644 index 000000000..7e259fc91 --- /dev/null +++ b/docs/sections/cookbook/recipe-dsl.md @@ -0,0 +1,289 @@ +# Recipe DSL + +!!! Warning + The examples in this section are set up to demonstrate specific features of the recipe DSL. The snippets are not + complete recipes. For example, some snippets only specify sensory events without any interactions, + and vice versa. + +## Sensory events + +### Firing limit + +Sensory events can be declared with or without firing limit. The firing limit determines how many times the event is +allowed to be fired into the process. If you don't want to impose any limits, you have to explicitly declare the event +as such. If left unspecified, the firing limit defaults to 1. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithSensoryEvents.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithSensoryEvents.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithSensoryEvents.scala" + ``` + + !!! Note + For Scala events the `maxFiringLimit` is set in the `Event` definition. An example event definition can be + found in [this section](../../tutorial#define-the-sensory-event) of the tutorial. + +### Event receive period +The period during which the process accepts sensory events. This is an optional parameter, defaults to accepting +sensory events forever. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventReceivePeriod.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventReceivePeriod.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventReceivePeriod.scala" + ``` + +## Interactions + +### Custom name +By default, the name of the interaction matches the name of the interaction class. Optionally, you can specify your +own name. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/InteractionWithCustomName.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/InteractionWithCustomName.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/InteractionWithCustomName.scala" + ``` + + +### Maximum interaction count +By default, an interaction can be invoked an unlimited amount of times. Optionally, you can specify a limit. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithMaxInteractionCount.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithMaxInteractionCount.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithMaxInteractionCount.scala" + ``` + +### Predefined ingredients +It's possible to register static ingredients to the interaction via predefined ingredients. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithPreDefinedIngredients.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithPreDefinedIngredients.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithPreDefinedIngredients.scala" + ``` + +### Required events +Sometimes an interaction should only execute after a certain event has happened. To achieve this you can specify +`requiredEvents` or `requiredOneOfEvents`. + +All required events have to be available for the interaction to be executed. It works as a logical AND. In contrast, +required one of events works as a logical OR. At least one of those events should be available before the interaction +is executed. + +In this example, the `ShipOrder` interaction will only execute if the `FraudCheckCompleted` and one of (or both) the +`PaymentReceived` or `UsedCouponCode` events are available. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRequiredEvents.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRequiredEvents.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRequiredEvents.scala" + ``` + +### Transform output events + +It's possible to change the name of an output event. Optionally, you can also change names of +ingredients. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventTransformation.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventTransformation.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventTransformation.scala" + ``` + +### Override input ingredients + +It's possible to change the names the input ingredients. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithIngredientNameOverrides.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithIngredientNameOverrides.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithIngredientNameOverrides.scala" + ``` + +### Failure strategy +It's possible to define a failure strategy on interaction level. This failure strategy will only be used for this +specific interaction. Interaction specific failure strategies take precedence over the default failure strategy. +For all available failure strategies, see the [error handling](/sections/cookbook/error-handling) section. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithFailureStrategy.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithFailureStrategy.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithFailureStrategy.scala" + ``` + +## Recipe + +### Retention period +The period during which the process keeps running, the recipe will be deleted AFTER the retention period has passed (measured from the creation time of the Recipe instance. This is an optional parameter, defaults to keep the process forever. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRetentionPeriod.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRetentionPeriod.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRetentionPeriod.scala" + ``` + +### Checkpoint events +Checkpoints are used to fire an event with a given name whenever certain preconditions are met. The preconditions +are specified via `requiredEvents` or `requiredOneOfEvents`. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithCheckpointEvent.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithCheckpointEvent.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithCheckpointEvent.scala" + ``` + +### Default failure strategy +The default failure strategy allows you to set a failure strategy for interactions that don't specify +one explicitly. For all available failure strategies, see the [error handling](/sections/cookbook/error-handling) section. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithDefaultFailureStrategy.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithDefaultFailureStrategy.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithDefaultFailureStrategy.scala" + ``` diff --git a/docs/sections/cookbook/testing.md b/docs/sections/cookbook/testing.md new file mode 100644 index 000000000..e104af836 --- /dev/null +++ b/docs/sections/cookbook/testing.md @@ -0,0 +1,303 @@ +# Testing + +In general, Baker applications are easy to test because the Baker model enforces separation and decoupling between parts +of +your system. It provides a clear distinction between business logic (the recipe) and implementation details +(interaction implementations). Furthermore, interactions are independent of each other, since every interaction only +depends on its inputs. + +This section describes testing strategies for the different layers of a Baker application. And demonstrates how to +simplify testing by using the `baker-test` library. + +## Recipe validation tests + +A recipe is validated when it's compiled by the `RecipeCompiler`. Any validation errors that might occur during this +compilation are available through the resulting `CompiledRecipe` instance. A simple unit test that checks for +validation errors in your recipe is essential. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/test/java/examples/java/recipes/WebShopRecipeTest.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/test/kotlin/examples/kotlin/recipes/WebShopRecipeTest.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/test/scala/examples/scala/recipes/WebShopRecipeTest.scala" + ``` + +## Business logic tests + +The next layer is to test that the recipe actually does what you expect it to do at the business logic level. You can +achieve this providing dummy or mock interactions that behave in an expected manner. The objective here is to test that +the Recipe ends on the expected state given a certain order of firing sensory events. + +The full test flow looks like this: + +1. Setup dummy or mock interactions that behave according to the test scenario. +2. Create a new in memory Baker with the interactions from step 1. +3. Add and bake the recipe +4. Fire sensory events +5. Assert expectations by [querying the state of the recipe](../fire-sensory-events/#inquiry). + +## Implementation tests + +The final layer is to individually test your implementations, which will resemble your normal e2e tests, +interconnectivity tests, or unit tests. What these tests look like will strongly depend on your teams testing and +coding conventions. + +## Baker test library +The `baker-test` library simplifies the testing of baker-based logic. Using this library makes the test code +concise and readable in both Java and Scala. It also simplifies the testing of the cases when asynchronous recipe +execution is involved. + +!!! Warning + At the moment `baker-test` is not compatible with the suspending Kotlin Baker APIs. + +### Adding the dependency + +=== "Maven" + + ```xml + + com.ing.baker + baker-test_2.13 + ${baker.version} + test + + ``` + +=== "Sbt" + + ```scala + libraryDependencies += "com.ing.baker" %% "baker-test_2.13" % bakerVersion + ``` + +### EventsFlow + +`EventsFlow` is made to simplify the work with the baker events while testing. `EventsFlow` is immutable. + +You create a new events flow from events classes: + +=== "Scala" + + ```scala + val flow: EventsFlow = + classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow + ``` + +=== "Java" + + ```java + EventsFlow flow = EventsFlow.of( + SomeSensoryEvent.class, + InteractionSucceeded.class + ); + ``` + +There is also an option to create a new event flow from the existing one: + +=== "Scala" + + ```scala + val anotherFlow: EventsFlow = + flow -- classOf[SomeSensoryEvent] ++ classOf[AnotherSensoryEvent] + ``` + +=== "Java" + + ```java + EventsFlow anotherFlow = flow + .remove(SomeSensoryEvent.class) + .add(AnotherSensoryEvent.class); + ``` + +It is also possible to combine classes, strings and other events flows: + +=== "Scala" + + ```scala + val unhappyFlow: EventsFlow = + happyFlow -- classOf[InteractionSucceeded] ++ "InteractionExhausted" +++ someErrorFlow + ``` + +=== "Java" + + ```java + EventsFlow unhappyFlow = happyFlow + .remove(InteractionSucceeded.class) + .add("InteractionExhausted") + .add(someErrorFlow); + ``` + +Events flows are compared ignoring the order of the events: + +=== "Scala" + + ```scala + "EventOne" :: "EventTwo" :: EmptyFlow == "EventTwo" :: "EventOne" :: EmptyFlow // true + ``` + +=== "Java" + + ```java + EventsFlow.of("EventOne","EventTwo").equals(EventsFlow.of("EventTwo","EventOne")); // true + ``` + +While comparing events flows it does not matter if an event is provided as a class or as a string: + +=== "Scala" + + ```scala + classOf[EventOne] :: EmptyFlow == "EventOne" :: EmptyFlow // true + ``` + +=== "Java" + + ```java + EventsFlow.of(EventOne.class).equals(EventsFlow.of("EventOne")); // true + ``` + +### RecipeAssert + +`RecipeAssert` is the starting point of all your assertions for the recipe instance. + +To create a `RecipeAssert` instance a baker instance and a recipe instance id are required: + +=== "Scala" + + ```scala + val recipeAssert: RecipeAssert = RecipeAssert(baker, recipeInstanceId) + ``` + +=== "Java" + + ```java + RecipeAssert recipeAssert = RecipeAssert.of(baker, recipeInstanceId); + ``` + +There is a simple way to assert if the events flow for this recipe instance is exactly the same as expected: + +=== "Scala" + + ```scala + val happyFlow: EventsFlow = + classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow + RecipeAssert(baker, recipeInstanceId).assertEventsFlow(happyFlow) + ``` + +=== "Java" + + ```java + EventsFlow happyFlow = EventsFlow.of( + SomeSensoryEvent.class, + InteractionSucceeded.class + ); + RecipeAssert.of(baker, recipeInstanceId).assertEventsFlow(happyFlow); + ``` + +If the assertion fails a clear error message with the difference is provided: + +``` +Events are not equal: + actual: OrderPlaced, ItemsReserved + expected: OrderPlaced, ItemsNotReserved +difference: ++ ItemsNotReserved + -- ItemsReserved +``` + +There are multiple methods to assert ingredient values. + +=== "Scala" + + ```scala + RecipeAssert(baker, recipeInstanceId) + .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value + .assertIngredient("nullishIngredient").isNull // exists and has `null` value + .assertIngredient("not-existing").isAbsent // ingredient is not a part of the recipe + .assertIngredient("someListOfStrings").is(value => Assertions.assert(value.asList(classOf[String]).size == 2)) // custom + ``` + +=== "Java" + + ```java + RecipeAssert.of(baker, recipeInstanceId) + .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value + .assertIngredient("nullishIngredient").isNull() // exists and has `null` value + .assertIngredient("not-existing").isAbsent() // ingredient is not a part of the recipe + .assertIngredient("someListOfStrings").is(val => Assert.assertEquals(2, val.asList(String.class).size())); // custom + ``` + +You can log some information from the baker recipe instance. + +_Note: But in most cases you probably should not have to do it because the current state is logged when any of the assertions fail._ + + +=== "Scala" + + ```scala + RecipeAssert(baker, recipeInstanceId) + .logIngredients() // logs ingredients + .logEventNames() // logs event names + .logVisualState() // logs visual state in dot language + .logCurrentState() // logs all the information available + ``` + +=== "Java" + + ```java + RecipeAssert.of(baker, recipeInstanceId) + .logIngredients() // logs ingredients + .logEventNames() // logs event names + .logVisualState() // logs visual state in dot language + .logCurrentState(); // logs all the information available + ``` + +Quite a common task is to wait for a baker process to finish or specific event to fire. +Therefore, the blocking method was implemented: + +=== "Scala" + + ```scala + RecipeAssert(baker, recipeInstanceId).waitFor(happyFlow) + // on this line all the events within happyFlow have happened + // otherwise timeout occurs and an assertion error is thrown + ``` + +=== "Java" + + ```java + RecipeAssert.of(baker, recipeInstanceId).waitFor(happyFlow); + // on this line all the events within happyFlow have happened + // otherwise timeout occurs and an assertion error is thrown + ``` + +As you have probably already noticed `RecipeAssert` is chainable +so the typical usage would probably be something like the following: + +=== "Scala" + + ```scala + RecipeAssert(baker, recipeInstanceId) + .waitFor(happyFlow) + .assertEventsFlow(happyFlow) + .assertIngredient("ingredientA").isEqual(ingredientValueA) + .assertIngredient("ingredientB").isEqual(ingredientValueB) + ``` + +=== "Java" + + ```java + RecipeAssert.of(baker, recipeInstanceId) + .waitFor(happyFlow) + .assertEventsFlow(happyFlow) + .assertIngredient("ingredientA").isEqual(ingredientValueA) + .assertIngredient("ingredientB").isEqual(ingredientValueB); + ``` diff --git a/docs/sections/cookbook/visualizations.md b/docs/sections/cookbook/visualizations.md new file mode 100644 index 000000000..79807027f --- /dev/null +++ b/docs/sections/cookbook/visualizations.md @@ -0,0 +1,89 @@ +# Visualizations + +Bakers ability to visualize recipes provides a powerful communication tool that helps product owners, architects, +and engineers to have a common understanding of the business process. This feature allows you to easily share your +recipe with others, enabling collaboration and feedback. + +## Visualize a recipe + +Baker uses [Graphviz](https://www.graphviz.org/) to visualize recipes. You can generate a Graphviz String from the +compiled recipe. + +=== "Java" + + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/visualization/WebShopVisualization.java" + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/visualization/WebShopVisualization.kt" + ``` + +=== "Scala" + + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/visualization/WebShopVisualization.scala" + ``` + +Running the method above, with the `WebShopRecipe` from the [tutorial](../../tutorial) results in the following Graphviz +String: + +??? example "Graphviz String" + ``` + digraph { + node [fontname = "ING Me", fontsize = 22, fontcolor = white] + pad = 0.2 + orderId -> CancelOrder + CancelOrder [shape = rect, style = "rounded, filled", color = "#525199", penwidth = 2, margin = 0.5] + OrderHasUnavailableItems -> CancelOrder + address -> ShipOrder + CheckStock [shape = rect, style = "rounded, filled", color = "#525199", penwidth = 2, margin = 0.5] + OrderPlaced -> address + CancelOrder -> OrderCancelled + CheckStock -> SufficientStock + orderId [shape = circle, style = filled, color = "#FF6200"] + orderId -> ShipOrder + OrderPlaced [shape = diamond, style = "rounded, filled", color = "#767676", fillcolor = "#D5D5D5", fontcolor = black, penwidth = 2, margin = 0.3] + OrderCancelled [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3] + OrderHasUnavailableItems [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3] + unavailableProductIds -> CancelOrder + ShipOrder [shape = rect, style = "rounded, filled", color = "#525199", penwidth = 2, margin = 0.5] + productIds -> CheckStock + customerId [shape = circle, style = filled, color = "#FF6200"] + SufficientStock -> ShipOrder + OrderPlaced -> orderId + OrderShipped [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3] + orderId -> CheckStock + OrderPlaced -> customerId + CheckStock -> OrderHasUnavailableItems + unavailableProductIds [shape = circle, style = filled, color = "#FF6200"] + OrderPlaced -> productIds + ShipOrder -> OrderShipped + SufficientStock [shape = diamond, style = "rounded, filled", color = "#767676", margin = 0.3] + productIds [shape = circle, style = filled, color = "#FF6200"] + address [shape = circle, style = filled, color = "#FF6200"] + OrderHasUnavailableItems -> unavailableProductIds + } + ``` + +The easiest way to convert the Graphviz string into an image is by using an [online converter](http://www.webgraphviz.com/). + +??? example "Web-shop Recipe Visualization" + ![WebShop Recipe Visualization](../../images/web-shop-visualization.svg) + +The diamonds are events. Light gray for sensory events, dark gray for output events. Ingredients are shown as orange +circles, and the purple rectangles are interactions. + +!!! tip + Don't like the default style? It's possible to override the default style by passing an optional `RecipeVisualStyle` + argument to `recipe.getRecipeVisualization`. + +## Visualize recipe state + +Visualizing the state of a running recipe can also be useful. You can fetch the state visualization of a running (or finished) +recipe via `baker.getVisualState(recipeInstanceId)`. + +??? example "Web-shop recipe instance state visualization" + ![WebShop Recipe Visualization](../../images/web-shop-visual-state.svg) \ No newline at end of file diff --git a/docs/sections/development-life-cycle/bake-fire-events-and-inquiry.md b/docs/sections/development-life-cycle/bake-fire-events-and-inquiry.md deleted file mode 100644 index 360cdb057..000000000 --- a/docs/sections/development-life-cycle/bake-fire-events-and-inquiry.md +++ /dev/null @@ -1,281 +0,0 @@ -# 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` 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" - - ```scala - import akka.actor.ActorSystem - import com.ing.baker.compiler.RecipeCompiler - import com.ing.baker.il.CompiledRecipe - import com.ing.baker.runtime.scaladsl.EventInstance - import com.ing.baker.runtime.akka.AkkaBaker - - import scala.concurrent.{Await, Future} - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - - - implicit val actorSystem: ActorSystem = - ActorSystem("WebshopSystem") - - val baker: Baker = AkkaBaker.akkaLocalDefault(actorSystem) - - val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - - val program: Future[Unit] = for { - _ <- baker.addInteractionInstance(WebshopInstancesReflection.reserveItemsInstance) - recipeId <- baker.addRecipe(RecipeRecord.of(compiledRecipe)) - } yield () - - ``` - -=== "Java" - - ```java - import akka.actor.ActorSystem; - import com.ing.baker.compiler.RecipeCompiler; - import com.ing.baker.il.CompiledRecipe; - import com.ing.baker.runtime.akka.AkkaBaker; - 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"); - Baker baker = AkkaBaker.javaLocalDefault(actorSystem); - - InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems()); - CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe); - - CompletableFuture asyncRecipeId = baker.addInteractionInstance(reserveItemsInstance) - .thenCompose(ignore -> baker.addRecipe(RecipeRecord.of(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(RecipeRecord.of(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](../../development-life-cycle/configure/) and the [runtime section](../../reference/runtime/)._ - -=== "Scala" - - ```scala - import akka.actor.ActorSystem - import com.ing.baker.compiler.RecipeCompiler - import com.ing.baker.il.CompiledRecipe - import com.ing.baker.runtime.scaladsl.EventInstance - import com.ing.baker.runtime.akka.AkkaBaker - - import scala.concurrent.{Await, Future} - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - - - implicit val actorSystem: ActorSystem = - ActorSystem("WebshopSystem") - val baker: Baker = AkkaBaker.localDefault(actorSystem) - - val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - - val program: Future[Unit] = for { - _ <- baker.addInteractionInstance(WebshopInstancesReflection.reserveItemsInstance) - recipeId <- baker.addRecipe(RecipeRecord.of(compiledRecipe)) - _ <- baker.bake(recipeId, "first-instance-id") - } yield () - - ``` - -=== "Java" - - ```java - import akka.actor.ActorSystem; - import com.ing.baker.compiler.RecipeCompiler; - import com.ing.baker.il.CompiledRecipe; - import com.ing.baker.runtime.akka.AkkaBaker; - 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"); - Baker baker = AkkaBaker.javaLocalDefault(actorSystem); - - InteractionInstance reserveItemsInstance = InteractionInstance.from(new ReserveItems()); - CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(JWebshopRecipe.recipe); - - CompletableFuture asyncRecipeId = baker.addInteractionInstance(reserveItemsInstance) - .thenCompose(ignore -> baker.addRecipe(RecipeRecord.of(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` 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" - - ```scala - import akka.actor.ActorSystem - import com.ing.baker.compiler.RecipeCompiler - import com.ing.baker.il.CompiledRecipe - import com.ing.baker.runtime.scaladsl.EventInstance - import com.ing.baker.runtime.akka.AkkaBaker - - import scala.concurrent.{Await, Future} - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - - - implicit val actorSystem: ActorSystem = - ActorSystem("WebshopSystem") - val baker: Baker = AkkaBaker.localDefault(actorSystem) - - val compiledRecipe: CompiledRecipe = RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - - val program: Future[Unit] = for { - _ <- baker.addInteractionInstance(WebshopInstancesReflection.reserveItemsInstance) - recipeId <- baker.addRecipe(RecipeRecord.of(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" - - ```java - import akka.actor.ActorSystem; - import com.ing.baker.compiler.RecipeCompiler; - import com.ing.baker.il.CompiledRecipe; - import com.ing.baker.runtime.akka.AkkaBaker; - 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"); - Baker baker = AkkaBaker.javaLocalDefault(actorSystem); - - List 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> result = baker.addInteractionInstance(reserveItemsInstance) - .thenCompose(ignore -> baker.addRecipe(RecipeRecord.of(compiledRecipe))) - .thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId)) - .thenCompose(ignore -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, firstOrderPlaced)) - .thenApply(EventResult::events); - - List 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](../../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. - -![](../../images/webshop-state-1.svg) diff --git a/docs/sections/development-life-cycle/configure.md b/docs/sections/development-life-cycle/configure.md deleted file mode 100644 index 3656439ab..000000000 --- a/docs/sections/development-life-cycle/configure.md +++ /dev/null @@ -1,95 +0,0 @@ -# Configure - -## Minimal configuration - -When creating a baker instance using the constructor `Baker.akka(config, actorSystem)` 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 graceful shutdown - shutdown-timeout = 30 seconds - - # the timeout when calling executeSingleInteraction - execute-single-interaction-timeout = 60 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" -} - -``` diff --git a/docs/sections/development-life-cycle/design-a-recipe.md b/docs/sections/development-life-cycle/design-a-recipe.md deleted file mode 100644 index ae49fbe14..000000000 --- a/docs/sections/development-life-cycle/design-a-recipe.md +++ /dev/null @@ -1,305 +0,0 @@ -# Design a Recipe - -Before creating a recipe, we have to translate the business requirements into Baker's three essential building blocks: -`Ingredients`, `Events`, and `Interactions`. - -Ingredients are immutable containers for the data in your process. In this example, `orderId` and the list of -`productIds` both qualify as an ingredient. Ingredients serve as input for interactions and are carried through -the process via events. - -Events represent something that has happened in your process. Most of the time, events are outputs of interactions. -Sometimes events come from outside the process. Those events are called `SensoryEvents`. SensoryEvents are used -to start/trigger the process. - -An interaction resembles a function. It requires inputs (ingredients) and provides output (events). This means an -interaction can do many things. For example, you can have interactions that fetch data from another service, or -an interaction that sends a message to an event broker, etc. - -## Ingredients and Events - -A recipe is always triggered by a sensory event. In this web-shop example we can model the placing of the order -as the sensory event. This event will provide the two required ingredients: `orderId` and a list of `productIds`. - -=== "Java" - - ```java - - public class OrderPlaced { - public final String orderId; - public final List items; - - public OrderPlaced(String orderId, List items) { - this.orderId = orderId; - this.items = items; - } - } - ``` - -=== "Kotlin" - - ```kotlin - - data class OrderPlaced( - val orderId: String, - val productIds: List - ) - ``` - -=== "Scala" - - ```scala - - import com.ing.baker.recipe.scaladsl._ - - object Ingredients { - - val OrderId: Ingredient[String] = - Ingredient[String]("orderId") - - val ProductIds: Ingredient[List[String]] = - Ingredient[List[String]]("items") - } - - object Events { - - val OrderPlaced: Event = Event( - name = "OrderPlaced", - providedIngredients = Seq( - Ingredients.OrderId, - Ingredients.ProductIds - ), - maxFiringLimit = Some(1) - ) - } - ``` - -Ingredients and events are just data structures that describe your process data. In this example `OrderPlaced` is an -event which carries two ingredients: `orderId` and `productIds`. An ingredient consists of a name and -type information. For example for the field `String orderId`, Baker creates an ingredient with the name of "orderId" -and a type of `CharArray`. More information about Baker types can be found in [this section](../../reference/baker-types-and-values/). - -As shown in the Scala example, events have a maximum amount of times they are allowed to fire. The Baker runtime will -enforce this limit. For more information about this and other features of events please refer to [this section](../../reference/dsls/#events). - -## Interactions - -Next, it's time to model our interaction. In our example it's just a simple call to the warehouse service to reserve -the items. - -_Note: Notice that when using the reflection API, the Java/Kotlin 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._ - -=== "Java" - - ```java - - 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 unavailableItems; - - public OrderHadUnavailableItems(List unavailableItems) { - this.unavailableItems = unavailableItems; - } - } - - class ItemsReserved implements ReserveItemsOutcome { - - public final List reservedItems; - - public ItemsReserved(List 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("productIds") List productIds); - } - - } - - ``` - -=== "Kotlin" - - ```kotlin - interface ReserveItems : Interaction { - - sealed interface ReserveItemsOutcome - data class OrderHadUnavailableItems(val unavailableItems: List) : ReserveItemsOutcome - data class ItemsReserved(val reservedItems: List) : ReserveItemsOutcome - - fun apply(orderId: String, productIds: List): ReserveItemsOutcome - } - - ``` - -=== "Scala" - - ```scala - - object Interactions { - - val ReserveItems: Interaction = Interaction( - name = "ReserveItems", - inputIngredients = Seq( - Ingredients.OrderId, - Ingredients.ProductIds, - ), - output = Seq( - Events.OrderHadUnavailableItems, - 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) - ) - } - - ``` - -## The Recipe - -The final step is to create the "blueprint" of our process: the recipe. To define a recipe you can use the Java, -Kotlin, or Scala DSL. - -=== "Java" - - ```java - - public class JWebshopRecipe { - - // ... previous events and interactions. - - public final static Recipe recipe = new Recipe("WebshopRecipe") - .withSensoryEvents(OrderPlaced.class) - .withInteractions(of(ReserveItems.class)); - } - ``` - -=== "Kotlin" - ```kotlin - - object WebShopRecipe { - val recipe = recipe("web-shop reserve items recipe") { - sensoryEvents { - event() - } - interaction() - } - } - - ``` - -=== "Scala" - - ```scala - - object WebshopRecipe { - val recipe: Recipe = Recipe("Webshop") - .withSensoryEvents( - Events.OrderPlaced - ) - .withInteractions( - Interactions.ReserveItems, - ) - } - ``` - -Despite this simple example, you might have realised that this can be further composed into bigger processes by -making new interactions that require events and ingredients which are outputs of previous interactions. You can also -create interactions which take no input ingredients but are executed after certain events are fired. For this and other -features please refer to the full DSL documentation [here](../../reference/dsls/#events). - -It should come as no surprise that this setup allows `Ingredients`, `Events` and `Interactions` to be reused in different -Recipes. Giving common business verbs that your programs and organisation can use across teams, the same way different -cooking recipes share processes (simmering, boiling, cutting). - -## Scala reflection API - -As you went through the examples you might have found the Scala API quite verbose. This is because the Java and Kotlin -APIs lean on reflection. We also have a Scala reflection API available, resulting in more concise Scala recipe -definitions. - -=== "Scala (Reflection API)" - - ```scala - - 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) - } - - ``` \ No newline at end of file diff --git a/docs/sections/development-life-cycle/implement-interactions.md b/docs/sections/development-life-cycle/implement-interactions.md deleted file mode 100644 index be3486418..000000000 --- a/docs/sections/development-life-cycle/implement-interactions.md +++ /dev/null @@ -1,67 +0,0 @@ -# Implement Interactions - -Before we can run our recipe, we need to create `InteractionInstances` that the Baker runtime will use to execute the -interactions. In other words, we need to provide implementations for the interactions. - -=== "Java" - - ```java - public class ReserveItemsInstance implements 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 productIds) { - // Add your implementation here. - // The body of this function is going to be executed by the Baker runtime when the ingredients are available. - } - } - ``` - -=== "Kotlin" - - ```kotlin - class ReserveItemsInstance : ReserveItems { - override fun apply(orderId: String, productIds: List): ReserveItemsOutcome { - // Add your implementation here. - // The body of this function is going to be executed by the Baker runtime when the ingredients are available. - } - } - ``` - -=== "Scala Reflection API" - - ```scala - import com.ing.baker.runtime.scaladsl.InteractionInstance - - import scala.concurrent.Future - import scala.concurrent.ExecutionContext.Implicits.global - - class ReserveItemsInstance extends ReserveItems { - - override def apply(orderId: String, productIds: List[String]): Future[WebshopRecipeReflection.ReserveItemsOutput] = { - // Add your implementation here. - // The body of this function is going to be executed by the Baker runtime when the ingredients are available. - } - } - ``` - -=== "Scala" - - ```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]] = { - // Add your implementation here. - // The body of this function is going to be executed by the Baker runtime when the ingredients are available. - } - ``` diff --git a/docs/sections/development-life-cycle/index.md b/docs/sections/development-life-cycle/index.md deleted file mode 100644 index 11c639608..000000000 --- a/docs/sections/development-life-cycle/index.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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: diff --git a/docs/sections/development-life-cycle/introduction.md b/docs/sections/development-life-cycle/introduction.md deleted file mode 100644 index 0e6db197a..000000000 --- a/docs/sections/development-life-cycle/introduction.md +++ /dev/null @@ -1,18 +0,0 @@ -# Introduction - -The "Development Life Cycle" serves as a "hands-on" tutorial of Baker. In this tutorial you will build a simple web-shop -recipe one step at a time. In other words, it's a practical approach to learning Baker. - -## Setting the stage -To be practical, we need some kind of context in which we can be practical. To that end, imagine you are working -as a software engineer for a modern e-commerce company. They are building a web-shop made up of different microservices. -You are responsible for building the order reservation process. The requirements read: - -> An `order` consists of an `orderId` and a list of `productIds`. After placing an order we need to verify if the -> warehouse has sufficient stock. We can do this by calling the warehouse service with the `orderId` and `ProductIds`. -> If all products are in stock, the warehouse service returns the ids of the reserved items. If (at least) one of the -> items isn't in stock, the warehouse service returns a list of the unavailable items. If there are unavailable items -> the process should stop. - -Evaluating business requirements is always the first step towards creating a Baker process. In the next section you -will learn how to translate these requirements to a Baker recipe. diff --git a/docs/sections/development-life-cycle/monitor.md b/docs/sections/development-life-cycle/monitor.md deleted file mode 100644 index 1cf8ab11a..000000000 --- a/docs/sections/development-life-cycle/monitor.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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 (Recipe Events)" - - ```scala - baker.registerEventListener((recipeInstanceId: String, event: EventInstance) => { - println(s"Recipe instance : $recipeInstanceId processed event ${event.name}") - }) - ``` - -=== "Java (Recipe Events)" - - ```java - BiConsumer handler = (String recipeInstanceId, EventInstance event) -> - System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name()); - - baker.registerEventListener(handler); - ``` - -=== "Scala (Baker Events)" - - ```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 (Baker Events)" - - ```java - import com.ing.baker.runtime.javadsl.BakerEvent; - - baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event)); - ``` - diff --git a/docs/sections/development-life-cycle/resolve-failed-recipe-instances.md b/docs/sections/development-life-cycle/resolve-failed-recipe-instances.md deleted file mode 100644 index f5cf33f84..000000000 --- a/docs/sections/development-life-cycle/resolve-failed-recipe-instances.md +++ /dev/null @@ -1,181 +0,0 @@ -# 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) - .withDefaultFailureStrategy(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: - -![](/images/incremental-backoff.png) - -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: - -```java -.withDefaultFailureStrategy(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" - - ```scala - val program: Future[Unit] = - baker.retryInteraction(recipeInstanceId, "ReserveItems") - ``` - -=== "Java" - - ```java - CompletableFuture 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" - - ```scala - val program: Future[Unit] = - baker.resolveInteraction(recipeInstanceId, "ReserveItems", ItemsReserved(List("item1"))) - ``` - -=== "Java" - - ```java - CompletableFuture 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" - - ```scala - val program: Future[Unit] = - baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems") - ``` - -=== "Java" - - ```java - CompletableFuture program = - baker.stopRetryingInteraction(recipeInstanceId, "ReserveItems"); - ``` diff --git a/docs/sections/development-life-cycle/test.md b/docs/sections/development-life-cycle/test.md deleted file mode 100644 index 1f5520072..000000000 --- a/docs/sections/development-life-cycle/test.md +++ /dev/null @@ -1,547 +0,0 @@ -# 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. -We also will explain how to simplify your testing logic using our `baker-test` library. - -## 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" - - ```scala - class WebshopRecipeSpec extends FlatSpec with Matchers { - - "The WebshopRecipe" should "compile the recipe without errors" in { - RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - } - } - ``` - -=== "Java" - - ```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. -Or the other way is to use `BakerAssert.waitFor(eventsFlow)` method from the `baker-test` library._ - -=== "Scala" - - ```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 baker: Baker = AkkaBaker.localDefaul(system) - - 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(RecipeRecord.of(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" - - ```java - static public class HappyFlowReserveItems implements JWebshopRecipe.ReserveItems { - - @Override - public ReserveItemsOutcome apply(String id, List items) { - return new ItemsReserved(items); - } - } - - @Test - public void shouldRunSimpleInstance() { - - ActorSystem actorSystem = ActorSystem.create("WebshopSystem"); - Baker baker = AkkaBaker.javaLocalDefault(actorSystem); - - List 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> result = baker.addInteractionInstace(reserveItemsInstance) - .thenCompose(ignore -> baker.addRecipe(RecipeRecord.of(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 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" - - ```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 baker: Baker = AkkaBaker.localDefault(system) - - 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(RecipeRecord.of(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" - - ```java - import static org.mockito.Mockito.*; - - @Test - public void shouldRunSimpleInstanceMockitoSample() { - - ActorSystem actorSystem = ActorSystem.create("WebshopSystem"); - Baker baker = AkkaBaker.javaLocalDefault(actorSystem); - - List 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> result = baker.addInteractionInstace(reserveItemsInstance) - .thenCompose(ignore -> baker.addRecipe(RecipeRecord.of(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 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. - -## Baker Test library - -This library contains the tooling to help with the testing of the baker-based logic. -The usage of this library makes the test code concise and readable in both java and scala. -It also simplifies the testing of the cases when asynchronous recipe execution is involved. - -=== "Scala" - - ```scala - RecipeAssert(baker, recipeInstanceId) - .waitFor(classOf[SensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow) - .assertIngredient("testIngredient").isEqual("foo") - ``` - -=== "Java" - - ```java - RecipeAssert.of(baker, recipeInstanceId) - .waitFor(EventsFlow.of(SensoryEvent.class, InteractionSucceeded.class)) - .assertIngredient("testIngredient").isEqual("foo") - ``` - -You can include it to your project by adding `baker-test` artifact: - -=== "Sbt" - - ```scala - libraryDependencies += "com.ing.baker" %% "baker-test_2.12" % bakerVersion - ``` - -=== "Maven" - - ```xml - - com.ing.baker - baker-test_2.12 - ${baker.version} - test - - ``` - -### EventsFlow - -`EventsFlow` is made to simplify the work with the baker events while testing. `EventsFlow` is immutable. - -You create a new events flow from events classes: - -=== "Scala" - - ```scala - val flow: EventsFlow = - classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow - ``` - -=== "Java" - - ```java - EventsFlow flow = EventsFlow.of( - SomeSensoryEvent.class, - InteractionSucceeded.class - ); - ``` - -There is also an option to create a new event flow from the existing one: - -=== "Scala" - - ```scala - val anotherFlow: EventsFlow = - flow -- classOf[SomeSensoryEvent] ++ classOf[AnotherSensoryEvent] - ``` - -=== "Java" - - ```java - EventsFlow anotherFlow = flow - .remove(SomeSensoryEvent.class) - .add(AnotherSensoryEvent.class); - ``` - -It is also possible to combine classes, strings and other events flows: - -=== "Scala" - - ```scala - val unhappyFlow: EventsFlow = - happyFlow -- classOf[InteractionSucceeded] ++ "InteractionExhausted" +++ someErrorFlow - ``` - -=== "Java" - - ```java - EventsFlow unhappyFlow = happyFlow - .remove(InteractionSucceeded.class) - .add("InteractionExhausted") - .add(someErrorFlow); - ``` - -Events flows are compared ignoring the order of the events: - -=== "Scala" - - ```scala - "EventOne" :: "EventTwo" :: EmptyFlow == "EventTwo" :: "EventOne" :: EmptyFlow // true - ``` - -=== "Java" - - ```java - EventsFlow.of("EventOne","EventTwo").equals(EventsFlow.of("EventTwo","EventOne")); // true - ``` - -While comparing events flows it does not matter if an event is provided as a class or as a string: - -=== "Scala" - - ```scala - classOf[EventOne] :: EmptyFlow == "EventOne" :: EmptyFlow // true - ``` - -=== "Java" - - ```java - EventsFlow.of(EventOne.class).equals(EventsFlow.of("EventOne")); // true - ``` - -### RecipeAssert - -`RecipeAssert` is the starting point of all your assertions for the recipe instance. - -To create a `RecipeAssert` instance a baker instance and a recipe instance id are required: - -=== "Scala" - - ```scala - val recipeAssert: RecipeAssert = RecipeAssert(baker, recipeInstanceId) - ``` - -=== "Java" - - ```java - RecipeAssert recipeAssert = RecipeAssert.of(baker, recipeInstanceId); - ``` - -There is a simple way to assert if the events flow for this recipe instance is exactly the same as expected: - -=== "Scala" - - ```scala - val happyFlow: EventsFlow = - classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow - RecipeAssert(baker, recipeInstanceId).assertEventsFlow(happyFlow) - ``` - -=== "Java" - - ```java - EventsFlow happyFlow = EventsFlow.of( - SomeSensoryEvent.class, - InteractionSucceeded.class - ); - RecipeAssert.of(baker, recipeInstanceId).assertEventsFlow(happyFlow); - ``` - -If the assertion fails a clear error message with the difference is provided: - -``` -Events are not equal: - actual: OrderPlaced, ItemsReserved - expected: OrderPlaced, ItemsNotReserved -difference: ++ ItemsNotReserved - -- ItemsReserved -``` - -There are multiple methods to assert ingredient values. - -=== "Scala" - - ```scala - RecipeAssert(baker, recipeInstanceId) - .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value - .assertIngredient("nullishIngredient").isNull // exists and has `null` value - .assertIngredient("not-existing").isAbsent // ingredient is not a part of the recipe - .assertIngredient("someListOfStrings").is(value => Assertions.assert(value.asList(classOf[String]).size == 2)) // custom - ``` - -=== "Java" - - ```java - RecipeAssert.of(baker, recipeInstanceId) - .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value - .assertIngredient("nullishIngredient").isNull() // exists and has `null` value - .assertIngredient("not-existing").isAbsent() // ingredient is not a part of the recipe - .assertIngredient("someListOfStrings").is(val => Assert.assertEquals(2, val.asList(String.class).size())); // custom - ``` - -You can log some information from the baker recipe instance. - -_Note: But in most cases you probably should not have to do it because the current state is logged when any of the assertions fail._ - - -=== "Scala" - - ```scala - RecipeAssert(baker, recipeInstanceId) - .logIngredients() // logs ingredients - .logEventNames() // logs event names - .logVisualState() // logs visual state in dot language - .logCurrentState() // logs all the information available - ``` - -=== "Java" - - ```java - RecipeAssert.of(baker, recipeInstanceId) - .logIngredients() // logs ingredients - .logEventNames() // logs event names - .logVisualState() // logs visual state in dot language - .logCurrentState(); // logs all the information available - ``` - -Quite a common task is to wait for a baker process to finish or specific event to fire. -Therefore, the blocking method was implemented: - -=== "Scala" - - ```scala - RecipeAssert(baker, recipeInstanceId).waitFor(happyFlow) - // on this line all the events within happyFlow have happened - // otherwise timeout occurs and an assertion error is thrown - ``` - -=== "Java" - - ```java - RecipeAssert.of(baker, recipeInstanceId).waitFor(happyFlow); - // on this line all the events within happyFlow have happened - // otherwise timeout occurs and an assertion error is thrown - ``` - -As you have probably already noticed `RecipeAssert` is chainable -so the typical usage would probably be something like the following: - -=== "Scala" - - ```scala - RecipeAssert(baker, recipeInstanceId) - .waitFor(happyFlow) - .assertEventsFlow(happyFlow) - .assertIngredient("ingredientA").isEqual(ingredientValueA) - .assertIngredient("ingredientB").isEqual(ingredientValueB) - ``` - -=== "Java" - - ```java - RecipeAssert.of(baker, recipeInstanceId) - .waitFor(happyFlow) - .assertEventsFlow(happyFlow) - .assertIngredient("ingredientA").isEqual(ingredientValueA) - .assertIngredient("ingredientB").isEqual(ingredientValueB); - ``` \ No newline at end of file diff --git a/docs/sections/development-life-cycle/use-visualizations.md b/docs/sections/development-life-cycle/use-visualizations.md deleted file mode 100644 index e41418556..000000000 --- a/docs/sections/development-life-cycle/use-visualizations.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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" - - ```scala - - import com.ing.baker.il.CompiledRecipe - import com.ing.baker.compiler.RecipeCompiler - - val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - val visualization: String = compiled.getRecipeVisualization - - ``` - -=== "Java" - - ```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: - -![](../../images/webshop-example-1.svg) - -For complete documentation of how to configure the visualization, refer to [this section](../../reference/visualization/). \ No newline at end of file diff --git a/docs/sections/getting-started.md b/docs/sections/getting-started.md deleted file mode 100644 index 8c3ae03ea..000000000 --- a/docs/sections/getting-started.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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: - -=== "Sbt" - -```scala -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" - -```xml - - com.ing.baker - baker-recipe-dsl_2.12 - 3.0.0 - - - com.ing.baker - baker-compiler_2.12 - 3.0.0 - - - com.ing.baker - baker-runtime_2.12 - 3.0.0 - - -``` - -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](../reference/dsls) to describe your recipes (process blueprints) *declaritively* | -| runtime | [Runtime](../reference/runtime/) based on [akka](https://www.akka.io) to manage and execute your recipes | -| compiler | [Compiles your recipe](../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. - -![](../images/deps.svg) - -## Continuing from here - -After adding the dependencies you can continue to: - -1. Understand the [high level concepts](../concepts). -2. If you like learning by doing, go through the [development life cycle section](../development-life-cycle/design-a-recipe). -3. If you like learning by description, go through the [reference section](../reference/main-abstractions). diff --git a/docs/sections/quickstart-guide.md b/docs/sections/quickstart-guide.md new file mode 100644 index 000000000..c218f4f51 --- /dev/null +++ b/docs/sections/quickstart-guide.md @@ -0,0 +1,105 @@ +# Quickstart guide + +## Enable Maven Central repository + +Baker is published in [Maven Central](https://mvnrepository.com/artifact/com.ing.baker). So you will need to enable +Maven Central repository as a source of dependencies in your build. + +=== "Maven" + + ``` + Maven includes the Maven Central repository by default. + ``` + +=== "Gradle (Kotlin)" + + ```kotlin + repositories { + mavenCentral() + } + ``` + +=== "Gradle (Groovy)" + + ```groovy + repositories { + mavenCentral() + } + ``` + +=== "Sbt" + + ``` + Most of the time Sbt includes the Maven Central repository by default. + ``` + +## Include dependencies + +Baker is composed of different modules. For most projects you need to include the three dependencies listed below. +If you don't require all functionality, simply select the ones you need for your project. + +=== "Maven" + + ```xml + + com.ing.baker + baker-recipe-dsl_2.13 + ${baker.version} + + + com.ing.baker + baker-compiler_2.13 + ${baker.version} + + + com.ing.baker + baker-runtime_2.13 + ${baker.version} + + ``` + +=== "Gradle (Kotlin)" + + ```kotlin + implementation("com.ing.baker:baker-recipe-dsl-kotlin_2.13:$bakerVersion") + implementation("com.ing.baker:baker-compiler_2.13:$bakerVersion") + implementation("com.ing.baker:baker-runtime_2.13:$bakerVersion") + ``` + +=== "Gradle (Groovy)" + + ```groovy + implementation 'com.ing.baker:baker-recipe-dsl_2.13:$bakerVersion' + implementation 'com.ing.baker:baker-compiler_2.13:$bakerVersion' + implementation 'com.ing.baker:baker-runtime_2.13:$bakerVersion' + ``` + +=== "Sbt" + + ```scala + dependencies += "com.ing.baker" %% "baker-recipe-dsl" % bakerVersion + dependencies += "com.ing.baker" %% "baker-compiler" % bakerVersion + dependencies += "com.ing.baker" %% "baker-runtime" % bakerVersion + ``` + +!!! note + + Kotlin users should include `baker-recipe-dsl-kotlin_2.13` instead of `baker-recipe-dsl_2.13`. + +!!! note + + Replace the version placeholders with the actual version you want to use. The latest stable version can be + found on [Maven Central](https://mvnrepository.com/artifact/com.ing.baker). + +## Module overview + +| Module | Description | +|-----------------------|------------------------------------------------------------------------------------------------------------| +| recipe-dsl | A declarative DSL to describe your recipes. | +| runtime | The Baker runtime to manage and execute your recipes. | +| compiler | A compiler that compiles recipes into a model that the runtime can execute. | +| intermediate-language | Recipe and Petri Net model used by the compiler and runtime. You don't interact with this module directly. | + +## Dependency graph + +![Dependency graph](../images/module-dependencies.svg) diff --git a/docs/sections/reference/dsls.md b/docs/sections/reference/dsls.md deleted file mode 100644 index 82792ec2f..000000000 --- a/docs/sections/reference/dsls.md +++ /dev/null @@ -1,671 +0,0 @@ -# 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. - -=== "Java" - - ```java - - 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 items; - - public OrderPlaced(String orderId, List 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 unavailableItems; - - public OrderHadUnavailableItems(List unavailableItems) { - this.unavailableItems = unavailableItems; - } - } - - class ItemsReserved implements ReserveItemsOutcome { - - public final List reservedItems; - - public ItemsReserved(List reservedItems) { - this.reservedItems = reservedItems; - } - } - - @FiresEvent(oneOf = {OrderHadUnavailableItems.class, ItemsReserved.class}) - ReserveItemsOutcome apply(@RequiresIngredient("orderId") String id, @RequiresIngredient("items") List 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()); - } - ``` - -=== "Kotlin" - ```kotlin - - import com.ing.baker.recipe.kotlindsl.recipe - - import com.ing.baker.recipe.javadsl.Interaction - import kotlin.time.Duration.Companion.hours - import kotlin.time.Duration.Companion.milliseconds - import kotlin.time.Duration.Companion.minutes - - data class OrderPlaced( - val orderId: String, - val productIds: List - ) - - object PaymentMade - - interface ReserveItems : Interaction { - sealed interface ReserveItemsOutcome - data class OrderHadUnavailableItems(val unavailableItems: List) : ReserveItemsOutcome - data class ItemsReserved(val reservedItems: List) : ReserveItemsOutcome - - fun apply(orderId: String, productIds: List): ReserveItemsOutcome - } - - - object WebShopRecipe { - val recipe = recipe("web-shop recipe") { - sensoryEvents { - event() - event() - } - interaction { - requiredEvents { - event() - } - } - defaultFailureStrategy = retryWithIncrementalBackoff { - until = deadline(24.hours) - initialDelay = 100.milliseconds - maxTimeBetweenRetries = 10.minutes - } - } - } - ``` - -=== "Scala" - - ```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 (Reflection API)" - - ```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 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()) - } - - ``` - -## Events - -[Events](../../reference/main-abstractions/#event-and-eventinstance) are simple `POJO` classes. For example: - -=== "Java" - - ```java - public class CustomerInfoReceived { - public final CustomerInfo customerInfo; - - public CustomerInfoReceived(CustomerInfo customerInfo) { - this.customerInfo = customerInfo; - } - } - ``` - -=== "Kotlin" - - ```kotlin - data class CustomerInfoReceived(val customerInfo: CustomerInfo) - ``` - -=== "Scala" - - ```scala - case class CustomerInfoReceived(customerInfo: CustomerInfo) - ``` - -The field types of the `POJO` class must be compatible with the baker type system. - -See the [supported types](../../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](../../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" - ```java - .withSensoryEventsNoFiringLimit(CustomerInfoReceived.class) - ``` - -=== "Kotlin" - ```kotlin - sensoryEvents { - event() // max firing limit defaults to 1 - event(maxFiringLimit = 5) - eventWithoutFiringLimit() - } - ``` - -In this example the `CustomerInfoReceived` can now be received multiple times by a process instance. - -## Interactions - -Interactions are interfaces with some requirements. See [here](../../development-life-cycle/design-a-recipe/#interactions) how to define them. - -You can include interactions in your recipe using the static `of(..)` method. - -=== "Java" - ```java - - import static com.ing.baker.recipe.javadsl.InteractionDescriptor.of; - - final Recipe webshopRecipe = new Recipe("webshop") - .withInteractions( - of(ValidateOrder.class) - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction() - } - ``` - -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" - ```java - .withInteractions( - of(ShipGoods.class).withMaximumInteractionCount(1) - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - maximumInteractionCount = 1 - } - } - ``` - -### Predefining ingredients - -An interaction normally requires all its input ingredients to be provided from [Events](../../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" - ```java - .withInteractions( - of(SendEmail.class) - .withPredefinedIngredient("emailTemplate", "Welcome to ING!") - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - preDefinedIngredients { - "titlePlaceHolder" to "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" - - ```java - .withInteractions( - of(ManufactureGoods.class) - .withEventTransformation( - GoodsManufactured.class, "ManufacturingDone", - ImmutableMap.of("goods", "manufacturedGoods") - ) - ) - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - transformEvent(newName = "ManufacturingDone") { - "goods" to "manufacturedGoods" // renames the 'goods' ingredient to '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" - ```java - of(SendInvoice.class) - .withRequiredEvents(ShipGoods.GoodsShipped.class) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - requiredEvents { - event() - } - } - } - ``` - -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" - ```java - of(SendInvoice.class) - .withRequiredOneOfEvents(EventA.class, EventB.class) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - requiredOneOfEvents { - event() - event() - } - } - } - ``` - -In this case the interaction may fire if *either* `EventA` OR `EventB` has occurred. - -### 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](../../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" - ```java - .withInteractions( - of(ValidateOrder.class) - .withInteractionFailureStrategy( - InteractionFailureStrategy.FireEvent("ValidateOrderFailed") - ) - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - failureStrategy = fireEventAfterFailure() - } - } - ``` - -#### 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" - ```java - .withInteractions( - of(ValidateOrder.class) - .withDefaultFailureStrategy(new RetryWithIncrementalBackoffBuilder() - .withInitialDelay(Duration.ofMillis(100)) - .withBackoffFactor(2.0) - .withMaxTimeBetweenRetries(Duration.ofSeconds(100)) - .withDeadline(Duration.ofHours(24)) - .build()) - ) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - failureStrategy = retryWithIncrementalBackoff { - until = deadline(24.hours) - initialDelay = 100.milliseconds - backoffFactor = 2.0 - maxTimeBetweenRetries = 100.seconds - } - } - } - ``` - -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: - -![](/images/incremental-backoff.png) - -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: - -=== "Java" - ```java - .withDefaultFailureStrategy(new RetryWithIncrementalBackoffBuilder() - .withFireRetryExhaustedEvent(SomeEvent.class)) - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction { - failureStrategy = retryWithIncrementalBackoff { - fireRetryExhaustedEvent = "SomeEvent" - } - } - } - ``` - -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" - ```java - final Recipe webshopRecipe = new Recipe("webshop") - .withDefaultFailureStrategy( - new RetryWithIncrementalBackoffBuilder() - .withInitialDelay(Duration.ofMillis(100)) - .withDeadline(Duration.ofHours(24)) - .withMaxTimeBetweenRetries(Duration.ofMinutes(10)) - .build()); - ``` - -=== "Kotlin" - ```kotlin - val recipe = recipe("web-shop recipe") { - interaction() - defaultFailureStrategy = retryWithIncrementalBackoff { - until = deadline(24.hours) - initialDelay = 100.milliseconds - backoffFactor = 2.0 - maxTimeBetweenRetries = 100.seconds - } - } - ``` diff --git a/docs/sections/reference/event-listener.md b/docs/sections/reference/event-listener.md deleted file mode 100644 index f73074887..000000000 --- a/docs/sections/reference/event-listener.md +++ /dev/null @@ -1,76 +0,0 @@ -# Event Listener - -After creating a [baker runtime](../../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" - - ```scala - baker.registerEventListener((recipeInstanceId: String, event: EventInstance) => { - println(s"Recipe instance : $recipeInstanceId processed event ${event.name}") - }) - ``` -=== "Java" - - ```java - BiConsumer 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" - - ```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" - - ```java - import com.ing.baker.runtime.javadsl.BakerEvent; - - baker.registerBakerEventListener((BakerEvent event) -> System.out.println(event)); - ``` - diff --git a/docs/sections/reference/runtime.md b/docs/sections/reference/runtime.md index 62adec4df..a977fe8b7 100644 --- a/docs/sections/reference/runtime.md +++ b/docs/sections/reference/runtime.md @@ -1,9 +1,13 @@ -# The Akka Based Runtime +# The Baker Runtime -## Baker.akka(config, actorSystem) +Baker provider several constructors to build a runtime to run your Recipes on. The current implementations are an in memory implementation and an +[Akka](https://akka.io/) based implementation. -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. +## Akka Runtime +The Akka based implementation can be configured to run in local mode or in cluster mode. +We advised to use the in memory Baker instead of the Akka Baker in local mode if you do not require state. + +### Baker.akka(config, actorSystem) _Note: We recommend reviewing also Akka configuration._ @@ -37,7 +41,7 @@ _Note: We recommend reviewing also Akka configuration._ ``` 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). +in the resources directory. You can see more about configuration on [this section](../stores). Alternatively there is a constructor that will provide the default configuration for a local mode Baker, this is recommended for tests. @@ -58,7 +62,7 @@ is recommended for tests. ``` -### Advantages of the Cluster Mode +#### Advantages of the Cluster Mode The capabilities gained when in cluster mode are: @@ -209,7 +213,7 @@ except it comes in a `CompletableFuture` that will help you handle async program `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). +into a [visualization](../../cookbook/visualizations). === "Scala" diff --git a/docs/sections/reference/visualization.md b/docs/sections/reference/visualization.md deleted file mode 100644 index 80da50a83..000000000 --- a/docs/sections/reference/visualization.md +++ /dev/null @@ -1,164 +0,0 @@ -# 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" - - ```scala - - import com.ing.baker.il.CompiledRecipe - import com.ing.baker.compiler.RecipeCompiler - - val compiled = RecipeCompiler.compileRecipe(WebshopRecipe.recipe) - val visualization: String = compiled.getRecipeVisualization - - ``` - -=== "Java" - - ```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" - - ```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. - -![](../../images/webshop-state-1.svg) diff --git a/docs/sections/tutorial.md b/docs/sections/tutorial.md new file mode 100644 index 000000000..38e259892 --- /dev/null +++ b/docs/sections/tutorial.md @@ -0,0 +1,285 @@ +# Tutorial + +This guide walks you through the process of creating a Baker orchestration workflow one step at a time. Completing +this tutorial takes around 20 minutes. + +!!! note + To follow this tutorial you will need a Java, Kotlin, or Scala project that includes the dependencies mentioned in + the [quickstart guide](../quickstart-guide). + +!!! note + This tutorial assumes you have a basic understanding of `ingredients`, `events`, `interactions`, and `recipes`. If + not, please read the [concepts section](../concepts) before starting the tutorial. + +## Setting the stage + +Imagine you are working as a software engineer for a modern e-commerce company. They are building a web-shop made up of +different microservices. You are responsible for orchestrating the order-flow. The requirements read: + +!!! quote "" + Once a customer places an order, we need to verify if the + products are in stock. Stock levels are available via the `StockService`. If there is enough stock, ship the order + by calling the `ShippingService`. If there is insufficient stock, cancel the order by calling the + `CancellationService`. + +## Define the sensory event + +A recipe is always triggered by a `sensory event`. In this example, our sensory event is the customer placing an order. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/events/OrderPlaced.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/events/OrderPlaced.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/events/OrderPlaced.scala" + ``` + +The `OrderPlaced` event carries four ingredients. The `address` ingredient is of type `Address`, which is just a simple +data class. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/ingredients/Address.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/ingredients/Address.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/ingredients/Address.scala" + ``` + +## Define the interactions +Next, it's time to model our interactions. We need to create a total of three interactions. One to validate if the products +are in stock, one to ship the order, and one to cancel the order. + +In this step we will just declare our interaction blueprints as interfaces. That's all we need to be able to declare +a recipe. The implementation for these interactions will follow at a later stage. + +### Check stock + +Our stock validation interaction requires two ingredients as input. The `orderId` and a list of `productIds`. The +interaction won't execute unless these ingredients are available in the process. The interaction will either emit +a `SufficientStock` event, if all products are in stock. Or an `OrderHasUnavailableItems` event otherwise. +The `OrderHasUnavailableItems` event carries a list of `unavailableProductIds` as ingredient. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/CheckStock.java" + ``` + + !!! Note + The `@FiresEvent` annotation is used to define the possible outcome events. + + !!! Note + The `@RequiresIngredient` annotation is used to define the ingredient names that this interaction needs for its + execution. + + !!! warning + The Java implementation makes use of Bakers reflection API. For this to work, the method in the + interaction must be named `apply`. Other names won't work. + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStock.kt" + ``` + + !!! Note + Kotlin's reflection API is more powerful than Java's. There is no need for any annotations when you model the + possible outcome events as a `sealed` hierarchy. + + !!! warning + The Kotlin implementation makes use of Bakers reflection API. For this to work, the method in the + interaction must be named `apply`. Other names won't work. + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStock.scala" + ``` + +### Ship Order + +To ship an order we'll need the `orderId` and an `address`. For the sake of simplicity, this interaction will always +result in a `OrderShipped` event. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/ShipOrder.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrder.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrder.scala" + ``` + +### Cancel order + +To cancel the order we'll need the `orderId` and a list of `unavailableProductIds`. Of course, `unavailableProductIds` +will only be available if the stock validation failed. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/CancelOrder.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrder.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrder.scala" + ``` + +## Define the recipe + +At this point we can compose our sensory event and three interactions into a recipe. The `OrderPlaced` event is declared +as a sensory event. `OrderPlaced` carries all the ingredients required by the `CheckStock` interaction, so once the +sensory event fires the `CheckStock` interaction will execute. + +`CheckStock` will output either a `SufficientStock` or `OrderHasUnavailableItems` event. `ShipOrder` will only execute if the +process contains an event of `SufficientStock` and `CancelOrder` will only execute if the process contains an event +of `OrderHasUnavailableItems`. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/recipes/WebShopRecipe.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/WebShopRecipe.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/recipes/WebShopRecipe.scala" + ``` + +## Implement the interactions + +Before we can run our recipe, we need to create InteractionInstances that the Baker runtime will use to execute the +interactions. In other words, we need to provide implementations for the interactions. + +Since this is a tutorial, we'll just create some dummy implementations. In a real solution, this is the part where you +would implement your actual logic. + +!!! tip + In these examples we use a `Impl` suffix for the implementation classes. In your real solution you might want to + use a more meaningful name. + +### Check stock implementation + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/CheckStockImpl.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStockImpl.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStockImpl.scala" + ``` + +### Ship order implementation + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/ShipOrderImpl.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrderImpl.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrderImpl.scala" + ``` + +### Cancel order implementation + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/interactions/CancelOrderImpl.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrderImpl.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrderImpl.scala" + ``` + +## Execute the recipe + +To execute the recipe we first need an instance of the `InMemoryBaker`. You can create one by providing the interaction +implementations to the `InMemoryBaker` static factory. + +The next step is to add the recipe to Baker. You can do this via the `addRecipe` method. If the `validate` flag is set +to `true`, Baker checks if all required interaction implementations are available. Adding the recipe is something you +only need to do once for each unique recipe. + +Before we can fire the sensory event, we need to create a new process instance of the recipe. We do this via the `bake` +method. You are required to specify a `recipeInstanceId`. Here we use `UUID`, but it can be anything as long as it's +unique within your processes. + +Finally, we can fire the sensory event via `fireEventAndResolveWhenCompleted`. The moment the event arrives our process +will start. + +=== "Java" + ```java + --8<-- "docs-code-snippets/src/main/java/examples/java/application/WebShopApp.java" + ``` + +=== "Kotlin" + ```kotlin + --8<-- "docs-code-snippets/src/main/kotlin/examples/kotlin/application/WebShopApp.kt" + ``` + +=== "Scala" + ```scala + --8<-- "docs-code-snippets/src/main/scala/examples/scala/application/WebShopApp.scala" + ``` + +Run the main function and observe the results. Depending on the outcome of the `CheckStock` interaction you will see +one of these messages in the console: + +!!! quote "" + Checking stock for order: 123 and products: [iPhone, PlayStation5] + + Shipping order 123 to Address(street=Hoofdstraat, city=Amsterdam, zipCode=1234AA, country=The Netherlands) + +!!! quote "" + Checking stock for order: 123 and products: [iPhone, PlayStation5] + + Canceling order 123. The following products are unavailable: [iPhone, PlayStation5] + +## Wrap-up +Congratulations! You just build your first Baker process. Of course, this is just a simplified example. To learn more +about what you can do with Baker, please refer to the cookbook section. There you will find information on things like +error handling, testing recipes, creating visualizations, and more. diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/FireEvent.java b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEvent.java new file mode 100644 index 000000000..3a241d0e7 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEvent.java @@ -0,0 +1,22 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; +import examples.java.events.OrderPlaced; + +public class FireEvent { + + private final Baker baker; + + public FireEvent(Baker baker) { + this.baker = baker; + } + + public void example(String recipeInstanceId, OrderPlaced orderPlaced) { + var eventInstance = EventInstance.from(orderPlaced); + + var eventResolutions = baker.fireEvent(recipeInstanceId, eventInstance); + var sensoryEventStatus = eventResolutions.getResolveWhenReceived().join(); + var sensoryEventResult = eventResolutions.getResolveWhenCompleted().join(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveOnEvent.java b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveOnEvent.java new file mode 100644 index 000000000..e2e47751d --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveOnEvent.java @@ -0,0 +1,19 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; +import examples.java.events.OrderPlaced; + +public class FireEventAndResolveOnEvent { + + private final Baker baker; + + public FireEventAndResolveOnEvent(Baker baker) { + this.baker = baker; + } + + public void example(String recipeInstanceId, OrderPlaced orderPlaced) { + var eventInstance = EventInstance.from(orderPlaced); + var sensoryEventResult = baker.fireEventAndResolveOnEvent(recipeInstanceId, eventInstance, "ExpectedEvent").join(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenCompleted.java b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenCompleted.java new file mode 100644 index 000000000..9c9653b43 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenCompleted.java @@ -0,0 +1,19 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; +import examples.java.events.OrderPlaced; + +public class FireEventAndResolveWhenCompleted { + + private final Baker baker; + + public FireEventAndResolveWhenCompleted(Baker baker) { + this.baker = baker; + } + + public void example(String recipeInstanceId, OrderPlaced orderPlaced) { + var eventInstance = EventInstance.from(orderPlaced); + var sensoryEventResult = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, eventInstance).join(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenReceived.java b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenReceived.java new file mode 100644 index 000000000..beb044901 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/FireEventAndResolveWhenReceived.java @@ -0,0 +1,19 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; +import examples.java.events.OrderPlaced; + +public class FireEventAndResolveWhenReceived { + + private final Baker baker; + + public FireEventAndResolveWhenReceived(Baker baker) { + this.baker = baker; + } + + public void example(String recipeInstanceId, OrderPlaced orderPlaced) { + var eventInstance = EventInstance.from(orderPlaced); + var sensoryEventStatus = baker.fireEventAndResolveWhenReceived(recipeInstanceId, eventInstance).join(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/InquiryExample.java b/examples/docs-code-snippets/src/main/java/examples/java/application/InquiryExample.java new file mode 100644 index 000000000..ab4cf851f --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/InquiryExample.java @@ -0,0 +1,24 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; + +public class InquiryExample { + + private final Baker baker; + + public InquiryExample(Baker baker) { + this.baker = baker; + } + + public void example(String recipeInstanceId) { + var ingredient = baker.getIngredient(recipeInstanceId, "orderId").join(); + + var ingredients = baker.getIngredients(recipeInstanceId).join(); + + var events = baker.getEvents(recipeInstanceId).join(); + + var eventNames = baker.getEventNames(recipeInstanceId).join(); + + var recipeInstanceState = baker.getRecipeInstanceState(recipeInstanceId).join(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/ManualResolveInteraction.java b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualResolveInteraction.java new file mode 100644 index 000000000..79077b47d --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualResolveInteraction.java @@ -0,0 +1,15 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; + +public class ManualResolveInteraction { + + public void resolveExample(Baker baker, String recipeInstanceId) { + baker.resolveInteraction( + recipeInstanceId, + "ShipOrder", + EventInstance.from("ShippingFailed") + ); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/ManualRetryInteraction.java b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualRetryInteraction.java new file mode 100644 index 000000000..ca1a930fe --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualRetryInteraction.java @@ -0,0 +1,10 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; + +public class ManualRetryInteraction { + + public void retryExample(Baker baker, String recipeInstanceId) { + baker.retryInteraction(recipeInstanceId, "ShipOrder"); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/ManualStopInteraction.java b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualStopInteraction.java new file mode 100644 index 000000000..ff103d94a --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/ManualStopInteraction.java @@ -0,0 +1,11 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; + +public class ManualStopInteraction { + + public void stopExample(Baker baker, String recipeInstanceId) { + baker.stopRetryingInteraction(recipeInstanceId, "ShipOrder"); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterBakerEventListener.java b/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterBakerEventListener.java new file mode 100644 index 000000000..4c2c0b32c --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterBakerEventListener.java @@ -0,0 +1,19 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.BakerEvent; + +public class RegisterBakerEventListener { + + private final Baker baker; + + public RegisterBakerEventListener(Baker baker) { + this.baker = baker; + } + + public void example() { + baker.registerBakerEventListener((BakerEvent event) -> + System.out.println("Received event: " + event) + ); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterEventListener.java b/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterEventListener.java new file mode 100644 index 000000000..690513dec --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/RegisterEventListener.java @@ -0,0 +1,19 @@ +package examples.java.application; + +import com.ing.baker.runtime.javadsl.Baker; +import com.ing.baker.runtime.javadsl.EventInstance; + +public class RegisterEventListener { + + private final Baker baker; + + public RegisterEventListener(Baker baker) { + this.baker = baker; + } + + public void example() { + baker.registerEventListener((String recipeInstanceId, EventInstance event) -> + System.out.println("Recipe Instance " + recipeInstanceId + " processed event " + event.name()) + ); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/application/WebShopApp.java b/examples/docs-code-snippets/src/main/java/examples/java/application/WebShopApp.java new file mode 100644 index 000000000..c0c33cdaf --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/application/WebShopApp.java @@ -0,0 +1,36 @@ +package examples.java.application; + +import com.ing.baker.compiler.RecipeCompiler; +import com.ing.baker.runtime.inmemory.InMemoryBaker; +import com.ing.baker.runtime.javadsl.EventInstance; +import examples.java.events.OrderPlaced; +import examples.java.ingredients.Address; +import examples.java.interactions.CancelOrderImpl; +import examples.java.interactions.CheckStockImpl; +import examples.java.interactions.ShipOrderImpl; +import examples.java.recipes.WebShopRecipe; + +import java.util.List; +import java.util.UUID; + +public class WebShopApp { + + public static void main(String[] args) { + var baker = InMemoryBaker.java( + List.of(new CheckStockImpl(), new CancelOrderImpl(), new ShipOrderImpl()) + ); + + var recipeInstanceId = UUID.randomUUID().toString(); + var sensoryEvent = EventInstance.from(createOrderPlaced()); + + baker.addRecipe(RecipeCompiler.compileRecipe(WebShopRecipe.recipe), true) + .thenCompose(recipeId -> baker.bake(recipeId, recipeInstanceId)) + .thenCompose(ignored -> baker.fireEventAndResolveWhenCompleted(recipeInstanceId, sensoryEvent)) + .join(); + } + + private static OrderPlaced createOrderPlaced() { + var address = new Address("Hoofdstraat", "Amsterdam", "1234AA", "The Netherlands"); + return new OrderPlaced("123", "456", address, List.of("iPhone", "Playstation5")); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/events/FraudCheckCompleted.java b/examples/docs-code-snippets/src/main/java/examples/java/events/FraudCheckCompleted.java new file mode 100644 index 000000000..3971c8e78 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/events/FraudCheckCompleted.java @@ -0,0 +1,4 @@ +package examples.java.events; + +public record FraudCheckCompleted() { +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/events/OrderPlaced.java b/examples/docs-code-snippets/src/main/java/examples/java/events/OrderPlaced.java new file mode 100644 index 000000000..7f0a4888f --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/events/OrderPlaced.java @@ -0,0 +1,12 @@ +package examples.java.events; + +import examples.java.ingredients.Address; + +import java.util.List; + +public record OrderPlaced( + String orderId, + String customerId, + Address address, + List productIds +) { } diff --git a/examples/docs-code-snippets/src/main/java/examples/java/events/PaymentReceived.java b/examples/docs-code-snippets/src/main/java/examples/java/events/PaymentReceived.java new file mode 100644 index 000000000..8bc96d934 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/events/PaymentReceived.java @@ -0,0 +1,10 @@ +package examples.java.events; + +import java.math.BigDecimal; + +public record PaymentReceived( + String orderId, + BigDecimal amount, + String currency +) { +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/ingredients/Address.java b/examples/docs-code-snippets/src/main/java/examples/java/ingredients/Address.java new file mode 100644 index 000000000..81333e74d --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/ingredients/Address.java @@ -0,0 +1,8 @@ +package examples.java.ingredients; + +public record Address( + String street, + String city, + String zipCode, + String country +) { } diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrder.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrder.java new file mode 100644 index 000000000..e7ccfd004 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrder.java @@ -0,0 +1,18 @@ +package examples.java.interactions; + +import com.ing.baker.recipe.annotations.FiresEvent; +import com.ing.baker.recipe.annotations.RequiresIngredient; +import com.ing.baker.recipe.javadsl.Interaction; + +import java.util.List; + +public interface CancelOrder extends Interaction { + + record OrderCancelled() { + } + + @FiresEvent(oneOf = {OrderCancelled.class}) + OrderCancelled apply(@RequiresIngredient("orderId") String orderId, + @RequiresIngredient("unavailableProductIds") List unavailableProductIds + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrderImpl.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrderImpl.java new file mode 100644 index 000000000..8f62aaa8d --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CancelOrderImpl.java @@ -0,0 +1,11 @@ +package examples.java.interactions; + +import java.util.List; + +public class CancelOrderImpl implements CancelOrder { + @Override + public OrderCancelled apply(String orderId, List unavailableProductIds) { + System.out.printf("Canceling order %s. The following products are unavailable: %s", orderId, unavailableProductIds); + return new OrderCancelled(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStock.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStock.java new file mode 100644 index 000000000..adf575bb1 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStock.java @@ -0,0 +1,24 @@ +package examples.java.interactions; + +import com.ing.baker.recipe.annotations.FiresEvent; +import com.ing.baker.recipe.annotations.RequiresIngredient; +import com.ing.baker.recipe.javadsl.Interaction; + +import java.util.List; + +public interface CheckStock extends Interaction { + + interface Outcome { + } + + record OrderHasUnavailableItems(List unavailableProductIds) implements Outcome { + } + + record SufficientStock() implements Outcome { + } + + @FiresEvent(oneOf = {SufficientStock.class, OrderHasUnavailableItems.class}) + Outcome apply(@RequiresIngredient("orderId") String orderId, + @RequiresIngredient("productIds") List productIds + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStockImpl.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStockImpl.java new file mode 100644 index 000000000..02379826f --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/CheckStockImpl.java @@ -0,0 +1,18 @@ +package examples.java.interactions; + +import java.util.List; + +public class CheckStockImpl implements CheckStock { + + @Override + public Outcome apply(String orderId, List productIds) { + System.out.printf("Checking stock for order: %s and products: %s%n", orderId, productIds); + + int random = (int) (Math.random() * (1000 - 1)) + 1; + if (random < 500) { + return new SufficientStock(); + } else { + return new OrderHasUnavailableItems(productIds); + } + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrder.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrder.java new file mode 100644 index 000000000..924733358 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrder.java @@ -0,0 +1,17 @@ +package examples.java.interactions; + +import com.ing.baker.recipe.annotations.FiresEvent; +import com.ing.baker.recipe.annotations.RequiresIngredient; +import com.ing.baker.recipe.javadsl.Interaction; +import examples.java.ingredients.Address; + +public interface ShipOrder extends Interaction { + + record OrderShipped() { + } + + @FiresEvent(oneOf = {OrderShipped.class}) + OrderShipped apply(@RequiresIngredient("orderId") String orderId, + @RequiresIngredient("address") Address address + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrderImpl.java b/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrderImpl.java new file mode 100644 index 000000000..a25e0a052 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/interactions/ShipOrderImpl.java @@ -0,0 +1,11 @@ +package examples.java.interactions; + +import examples.java.ingredients.Address; + +public class ShipOrderImpl implements ShipOrder { + @Override + public OrderShipped apply(String orderId, Address address) { + System.out.printf("Shipping order %s to %s", orderId, address); + return new OrderShipped(); + } +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/InteractionWithCustomName.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/InteractionWithCustomName.java new file mode 100644 index 000000000..b3d272c8c --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/InteractionWithCustomName.java @@ -0,0 +1,13 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.interactions.ShipOrder; + +public class InteractionWithCustomName { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class, "ship-order") + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeBlockInteraction.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeBlockInteraction.java new file mode 100644 index 000000000..e9522b876 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeBlockInteraction.java @@ -0,0 +1,12 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +public class RecipeBlockInteraction { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.BlockInteraction() + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeFireEvent.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeFireEvent.java new file mode 100644 index 000000000..e2c114f39 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeFireEvent.java @@ -0,0 +1,12 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +public class RecipeFireEvent { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.FireEvent("MyEvent") + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryExhaustedEvent.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryExhaustedEvent.java new file mode 100644 index 000000000..d1ae34882 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryExhaustedEvent.java @@ -0,0 +1,20 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +import java.time.Duration; + +public class RecipeRetryExhaustedEvent { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + new InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder() + .withInitialDelay(Duration.ofMillis(100)) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Duration.ofSeconds(100)) + .withDeadline(Duration.ofHours(24)) + .withFireRetryExhaustedEvent("RetriesExhausted") + .build() + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilDeadline.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilDeadline.java new file mode 100644 index 000000000..59240958f --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilDeadline.java @@ -0,0 +1,19 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +import java.time.Duration; + +public class RecipeRetryWithBackOffUntilDeadline { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + new InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder() + .withInitialDelay(Duration.ofMillis(100)) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Duration.ofSeconds(100)) + .withDeadline(Duration.ofHours(24)) + .build() + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilMaxRetries.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilMaxRetries.java new file mode 100644 index 000000000..6aa3c2bcb --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeRetryWithBackOffUntilMaxRetries.java @@ -0,0 +1,19 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +import java.time.Duration; + +public class RecipeRetryWithBackOffUntilMaxRetries { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + new InteractionFailureStrategy.RetryWithIncrementalBackoffBuilder() + .withInitialDelay(Duration.ofMillis(100)) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Duration.ofSeconds(100)) + .withMaximumRetries(200) + .build() + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithCheckpointEvent.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithCheckpointEvent.java new file mode 100644 index 000000000..a684ce552 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithCheckpointEvent.java @@ -0,0 +1,18 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.CheckPointEvent; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.FraudCheckCompleted; +import examples.java.events.PaymentReceived; + +public class RecipeWithCheckpointEvent { + + public final static Recipe recipe = new Recipe("example") + .withCheckpointEvent( + new CheckPointEvent("CheckpointReached") + .withRequiredEvents( + PaymentReceived.class, + FraudCheckCompleted.class + ) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithDefaultFailureStrategy.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithDefaultFailureStrategy.java new file mode 100644 index 000000000..72069f679 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithDefaultFailureStrategy.java @@ -0,0 +1,12 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; + +public class RecipeWithDefaultFailureStrategy { + + public final static Recipe recipe = new Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.FireEvent("recipeFailed") + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventReceivePeriod.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventReceivePeriod.java new file mode 100644 index 000000000..2d72baf94 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventReceivePeriod.java @@ -0,0 +1,11 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.Recipe; + +import java.time.Duration; + +public class RecipeWithEventReceivePeriod { + + public final static Recipe recipe = new Recipe("example") + .withEventReceivePeriod(Duration.ofHours(5)); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventTransformation.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventTransformation.java new file mode 100644 index 000000000..27f64eb0c --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithEventTransformation.java @@ -0,0 +1,20 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.OrderPlaced; +import examples.java.interactions.ShipOrder; + +import java.util.Map; + +public class RecipeWithEventTransformation { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .withEventTransformation(OrderPlaced.class, "OrderCreated", + Map.of("customerId", "userId", + "productIds", "skus") + ) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithFailureStrategy.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithFailureStrategy.java new file mode 100644 index 000000000..e3bc46dff --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithFailureStrategy.java @@ -0,0 +1,17 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.InteractionFailureStrategy; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.interactions.ShipOrder; + +public class RecipeWithFailureStrategy { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .withFailureStrategy( + InteractionFailureStrategy.FireEvent("shippingFailed") + ) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithIngredientNameOverrides.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithIngredientNameOverrides.java new file mode 100644 index 000000000..7b4217730 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithIngredientNameOverrides.java @@ -0,0 +1,14 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.interactions.ShipOrder; + +public class RecipeWithIngredientNameOverrides { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .renameRequiredIngredient("orderId", "orderNumber") + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithMaxInteractionCount.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithMaxInteractionCount.java new file mode 100644 index 000000000..7af6a4522 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithMaxInteractionCount.java @@ -0,0 +1,14 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.interactions.ShipOrder; + +public class RecipeWithMaxInteractionCount { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .withMaximumInteractionCount(1) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithPredefinedIngredients.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithPredefinedIngredients.java new file mode 100644 index 000000000..093478b4a --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithPredefinedIngredients.java @@ -0,0 +1,22 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.interactions.ShipOrder; + +import java.math.BigDecimal; +import java.util.Map; + +public class RecipeWithPredefinedIngredients { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .withPredefinedIngredients( + Map.of( + "shippingCostAmount", new BigDecimal("5.75"), + "shippingCostCurrency", "EUR" + ) + ) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithReproviderInteraction.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithReproviderInteraction.java new file mode 100644 index 000000000..a5572f149 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithReproviderInteraction.java @@ -0,0 +1,17 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.OrderPlaced; +import examples.java.interactions.ShipOrder; + +public class RecipeWithReproviderInteraction { + + public final static Recipe recipe = new Recipe("Reprovider recipe") + .withSensoryEvent(OrderPlaced.class) + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .isReprovider(true) + .withRequiredEvent(OrderPlaced.class) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRequiredEvents.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRequiredEvents.java new file mode 100644 index 000000000..01feaec3a --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRequiredEvents.java @@ -0,0 +1,16 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.FraudCheckCompleted; +import examples.java.interactions.ShipOrder; + +public class RecipeWithRequiredEvents { + + public final static Recipe recipe = new Recipe("example") + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + .withRequiredEvent(FraudCheckCompleted.class) + .withRequiredOneOfEventsFromName("PaymentReceived", "UsedCouponCode") + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRetentionPeriod.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRetentionPeriod.java new file mode 100644 index 000000000..7f7fb1751 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithRetentionPeriod.java @@ -0,0 +1,11 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.Recipe; + +import java.time.Duration; + +public class RecipeWithRetentionPeriod { + + public final static Recipe recipe = new Recipe("example") + .withRetentionPeriod(Duration.ofDays(3)); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithSensoryEvents.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithSensoryEvents.java new file mode 100644 index 000000000..c1682a42b --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/RecipeWithSensoryEvents.java @@ -0,0 +1,14 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.FraudCheckCompleted; +import examples.java.events.OrderPlaced; +import examples.java.events.PaymentReceived; + +public class RecipeWithSensoryEvents { + + public final static Recipe recipe = new Recipe("example") + .withSensoryEventNoFiringLimit(OrderPlaced.class) + .withSensoryEvent(PaymentReceived.class) + .withSensoryEvent(FraudCheckCompleted.class, 5); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/SimpleRecipe.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/SimpleRecipe.java new file mode 100644 index 000000000..27a7cd3be --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/SimpleRecipe.java @@ -0,0 +1,15 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.OrderPlaced; +import examples.java.interactions.ShipOrder; + +public class SimpleRecipe { + + public final static Recipe recipe = new Recipe("example recipe") + .withSensoryEvent(OrderPlaced.class) + .withInteractions( + InteractionDescriptor.of(ShipOrder.class) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/recipes/WebShopRecipe.java b/examples/docs-code-snippets/src/main/java/examples/java/recipes/WebShopRecipe.java new file mode 100644 index 000000000..0d92a85af --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/recipes/WebShopRecipe.java @@ -0,0 +1,21 @@ +package examples.java.recipes; + +import com.ing.baker.recipe.javadsl.InteractionDescriptor; +import com.ing.baker.recipe.javadsl.Recipe; +import examples.java.events.OrderPlaced; +import examples.java.interactions.CancelOrder; +import examples.java.interactions.CheckStock; +import examples.java.interactions.ShipOrder; + +public class WebShopRecipe { + + public final static Recipe recipe = new Recipe("web-shop recipe") + .withSensoryEvent(OrderPlaced.class) + .withInteractions( + InteractionDescriptor.of(CheckStock.class), + InteractionDescriptor.of(ShipOrder.class) + .withRequiredEvent(CheckStock.SufficientStock.class), + InteractionDescriptor.of(CancelOrder.class) + .withRequiredEvent(CheckStock.OrderHasUnavailableItems.class) + ); +} diff --git a/examples/docs-code-snippets/src/main/java/examples/java/visualization/WebShopVisualization.java b/examples/docs-code-snippets/src/main/java/examples/java/visualization/WebShopVisualization.java new file mode 100644 index 000000000..57b211029 --- /dev/null +++ b/examples/docs-code-snippets/src/main/java/examples/java/visualization/WebShopVisualization.java @@ -0,0 +1,13 @@ +package examples.java.visualization; + +import com.ing.baker.compiler.RecipeCompiler; +import com.ing.baker.il.CompiledRecipe; +import examples.java.recipes.WebShopRecipe; + +public class WebShopVisualization { + public void printVisualizationString() { + CompiledRecipe compiledRecipe = RecipeCompiler.compileRecipe(WebShopRecipe.recipe); + String graphvizString = compiledRecipe.getRecipeVisualization(); + System.out.println(graphvizString); + } +} diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEvent.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEvent.kt new file mode 100644 index 000000000..ad989df32 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEvent.kt @@ -0,0 +1,16 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.Baker +import examples.kotlin.events.OrderPlaced + +class FireEvent(private val baker: Baker) { + + suspend fun example(recipeInstanceId: String, orderPlaced: OrderPlaced) { + val orderPlacedEvent = EventInstance.from(orderPlaced) + + val eventResolutions = baker.fireEvent(recipeInstanceId, orderPlacedEvent) + val sensoryEventStatus = eventResolutions.resolveWhenReceived.await() + val sensoryEventResult = eventResolutions.resolveWhenCompleted.await() + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveOnEvent.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveOnEvent.kt new file mode 100644 index 000000000..5cc94e20f --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveOnEvent.kt @@ -0,0 +1,13 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.Baker +import examples.kotlin.events.OrderPlaced + +class FireEventAndResolveOnEvent(private val baker: Baker) { + + suspend fun example(recipeInstanceId: String, orderPlaced: OrderPlaced) { + val orderPlacedEvent = EventInstance.from(orderPlaced) + val sensoryEventResult = baker.fireEventAndResolveOnEvent(recipeInstanceId, orderPlacedEvent, "ExpectedEvent") + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenCompleted.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenCompleted.kt new file mode 100644 index 000000000..9fc502b55 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenCompleted.kt @@ -0,0 +1,13 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.Baker +import examples.kotlin.events.OrderPlaced + +class FireEventAndResolveWhenCompleted(private val baker: Baker) { + + suspend fun example(recipeInstanceId: String, orderPlaced: OrderPlaced) { + val orderPlacedEvent = EventInstance.from(orderPlaced) + val sensoryEventResult = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, orderPlacedEvent) + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenReceived.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenReceived.kt new file mode 100644 index 000000000..8162c4214 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/FireEventAndResolveWhenReceived.kt @@ -0,0 +1,13 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.Baker +import examples.kotlin.events.OrderPlaced + +class FireEventAndResolveWhenReceived(private val baker: Baker) { + + suspend fun example(recipeInstanceId: String, orderPlaced: OrderPlaced) { + val orderPlacedEvent = EventInstance.from(orderPlaced) + val sensoryEventStatus = baker.fireEventAndResolveWhenReceived(recipeInstanceId, orderPlacedEvent) + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/InquiryExample.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/InquiryExample.kt new file mode 100644 index 000000000..9a03758e6 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/InquiryExample.kt @@ -0,0 +1,18 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.kotlindsl.Baker + +class InquiryExample(private val baker: Baker) { + + suspend fun example(recipeInstanceId: String) { + val ingredient = baker.getIngredient(recipeInstanceId, "orderId") + + val ingredients = baker.getIngredients(recipeInstanceId) + + val events = baker.getEvents(recipeInstanceId) + + val eventNames = baker.getEventNames(recipeInstanceId) + + val recipeInstanceState = baker.getRecipeInstanceState(recipeInstanceId) + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualResolveInteraction.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualResolveInteraction.kt new file mode 100644 index 000000000..8d2fbea9e --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualResolveInteraction.kt @@ -0,0 +1,12 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.Baker + +suspend fun resolveExample(baker: Baker, recipeInstanceId: String) { + baker.resolveInteraction( + recipeInstanceId, + "ShipOrder", + EventInstance.from("ShippingFailed") + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualRetryInteraction.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualRetryInteraction.kt new file mode 100644 index 000000000..9232e69f7 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualRetryInteraction.kt @@ -0,0 +1,7 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.kotlindsl.Baker + +suspend fun retryExample(baker: Baker, recipeInstanceId: String) { + baker.retryInteraction(recipeInstanceId, "ShipOrder") +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualStopInteraction.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualStopInteraction.kt new file mode 100644 index 000000000..904e181c9 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/ManualStopInteraction.kt @@ -0,0 +1,7 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.kotlindsl.Baker + +suspend fun stopExample(baker: Baker, recipeInstanceId: String) { + baker.stopRetryingInteraction(recipeInstanceId, "ShipOrder") +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterBakerEventListener.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterBakerEventListener.kt new file mode 100644 index 000000000..6b9c9c862 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterBakerEventListener.kt @@ -0,0 +1,12 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.kotlindsl.Baker + +class RegisterBakerEventListener(private val baker: Baker) { + + suspend fun example() { + baker.registerBakerEventListener { bakerEvent -> + println("Received event: $bakerEvent") + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterEventListener.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterEventListener.kt new file mode 100644 index 000000000..b2e5bba5b --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/RegisterEventListener.kt @@ -0,0 +1,12 @@ +package examples.kotlin.application + +import com.ing.baker.runtime.kotlindsl.Baker + +class RegisterEventListener(private val baker: Baker) { + + suspend fun example() { + baker.registerEventListener { recipeInstanceId, eventInstance -> + println("Recipe Instance: $recipeInstanceId, processed event: ${eventInstance.name} ") + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/WebShopApp.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/WebShopApp.kt new file mode 100644 index 000000000..8cc86f693 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/application/WebShopApp.kt @@ -0,0 +1,44 @@ +package examples.kotlin.application + +import com.ing.baker.compiler.RecipeCompiler +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.kotlindsl.InMemoryBaker +import examples.kotlin.events.OrderPlaced +import examples.kotlin.ingredients.Address +import examples.kotlin.interactions.CancelOrderImpl +import examples.kotlin.interactions.CheckStockImpl +import examples.kotlin.interactions.ShipOrderImpl +import examples.kotlin.recipes.WebShopRecipe +import kotlinx.coroutines.runBlocking +import java.util.* + +@ExperimentalDsl +fun main(): Unit = runBlocking { + val baker = InMemoryBaker.kotlin( + implementations = listOf(CheckStockImpl, CancelOrderImpl, ShipOrderImpl) + ) + + val recipeId = baker.addRecipe( + compiledRecipe = RecipeCompiler.compileRecipe(WebShopRecipe.recipe), + validate = true + ) + + val recipeInstanceId = UUID.randomUUID().toString() + val sensoryEvent = EventInstance.from(orderPlaced) + + baker.bake(recipeId, recipeInstanceId) + baker.fireEventAndResolveWhenCompleted(recipeInstanceId, sensoryEvent) +} + +private val orderPlaced = OrderPlaced( + orderId = "123", + customerId = "456", + productIds = listOf("iPhone", "PlayStation5"), + address = Address( + street = "Hoofdstraat", + city = "Amsterdam", + zipCode = "1234AA", + country = "The Netherlands" + ) +) diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/FraudCheckCompleted.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/FraudCheckCompleted.kt new file mode 100644 index 000000000..10abdda18 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/FraudCheckCompleted.kt @@ -0,0 +1,3 @@ +package examples.kotlin.events + +object FraudCheckCompleted \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/OrderPlaced.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/OrderPlaced.kt new file mode 100644 index 000000000..e0a2443f7 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/OrderPlaced.kt @@ -0,0 +1,10 @@ +package examples.kotlin.events + +import examples.kotlin.ingredients.Address + +data class OrderPlaced( + val orderId: String, + val customerId: String, + val address: Address, + val productIds: List +) \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/PaymentReceived.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/PaymentReceived.kt new file mode 100644 index 000000000..2e604e752 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/events/PaymentReceived.kt @@ -0,0 +1,9 @@ +package examples.kotlin.events + +import java.math.BigDecimal + +data class PaymentReceived( + val orderId: String, + val amount: BigDecimal, + val currency: String +) \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/ingredients/Address.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/ingredients/Address.kt new file mode 100644 index 000000000..7b7a98e21 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/ingredients/Address.kt @@ -0,0 +1,8 @@ +package examples.kotlin.ingredients + +data class Address( + val street: String, + val city: String, + val zipCode: String, + val country: String +) \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrder.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrder.kt new file mode 100644 index 000000000..aa8a20e6a --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrder.kt @@ -0,0 +1,9 @@ +package examples.kotlin.interactions + +import com.ing.baker.recipe.javadsl.Interaction + +interface CancelOrder : Interaction { + object OrderCancelled + + fun apply(orderId: String, unavailableProductIds: List): OrderCancelled +} diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrderImpl.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrderImpl.kt new file mode 100644 index 000000000..089b12e8a --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CancelOrderImpl.kt @@ -0,0 +1,8 @@ +package examples.kotlin.interactions + +object CancelOrderImpl : CancelOrder { + override fun apply(orderId: String, unavailableProductIds: List): CancelOrder.OrderCancelled { + println("Canceling order $orderId. The following products are unavailable: $unavailableProductIds") + return CancelOrder.OrderCancelled + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStock.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStock.kt new file mode 100644 index 000000000..bde482185 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStock.kt @@ -0,0 +1,16 @@ +package examples.kotlin.interactions + +import com.ing.baker.recipe.javadsl.Interaction + +interface CheckStock : Interaction { + + sealed interface Outcome + + data class OrderHasUnavailableItems( + val unavailableProductIds: List + ) : Outcome + + object SufficientStock : Outcome + + fun apply(orderId: String, productIds: List): Outcome +} diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStockImpl.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStockImpl.kt new file mode 100644 index 000000000..569f09259 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/CheckStockImpl.kt @@ -0,0 +1,13 @@ +package examples.kotlin.interactions + +object CheckStockImpl : CheckStock { + override fun apply(orderId: String, productIds: List): CheckStock.Outcome { + println("Checking stock for order: $orderId and products: $productIds") + + return if ((1..1000).random() < 500) { + CheckStock.SufficientStock + } else { + CheckStock.OrderHasUnavailableItems(productIds) + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrder.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrder.kt new file mode 100644 index 000000000..d651411af --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrder.kt @@ -0,0 +1,10 @@ +package examples.kotlin.interactions + +import com.ing.baker.recipe.javadsl.Interaction +import examples.kotlin.ingredients.Address + +interface ShipOrder : Interaction { + object OrderShipped + + fun apply(orderId: String, address: Address): OrderShipped +} diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrderImpl.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrderImpl.kt new file mode 100644 index 000000000..295e5a3cd --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/interactions/ShipOrderImpl.kt @@ -0,0 +1,10 @@ +package examples.kotlin.interactions + +import examples.kotlin.ingredients.Address + +object ShipOrderImpl : ShipOrder { + override fun apply(orderId: String, address: Address): ShipOrder.OrderShipped { + println("Shipping order $orderId to $address") + return ShipOrder.OrderShipped + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/InteractionWithCustomName.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/InteractionWithCustomName.kt new file mode 100644 index 000000000..74d1bf14b --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/InteractionWithCustomName.kt @@ -0,0 +1,14 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object InteractionWithCustomName { + val recipe = recipe("example") { + interaction { + name = "ship-order" + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeBlockInteraction.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeBlockInteraction.kt new file mode 100644 index 000000000..d2efac6ce --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeBlockInteraction.kt @@ -0,0 +1,11 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe + +@ExperimentalDsl +object RecipeBlockInteraction { + val recipe = recipe("example") { + defaultFailureStrategy = blockInteraction() + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeFireEvent.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeFireEvent.kt new file mode 100644 index 000000000..886d49ee1 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeFireEvent.kt @@ -0,0 +1,11 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe + +@ExperimentalDsl +object RecipeFireEvent { + val recipe = recipe("example") { + defaultFailureStrategy = fireEventAfterFailure("MyEvent") + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryExhaustedEvent.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryExhaustedEvent.kt new file mode 100644 index 000000000..91bcb8774 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryExhaustedEvent.kt @@ -0,0 +1,20 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@ExperimentalDsl +object RecipeRetryExhaustedEvent { + val recipe = recipe("example") { + defaultFailureStrategy = retryWithIncrementalBackoff { + initialDelay = 100.milliseconds + backoffFactor = 2.0 + maxTimeBetweenRetries = 10.seconds + until = deadline(24.hours) + fireRetryExhaustedEvent = "RetriesExhausted" + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilDeadline.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilDeadline.kt new file mode 100644 index 000000000..718ebe137 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilDeadline.kt @@ -0,0 +1,19 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@ExperimentalDsl +object RecipeRetryWithBackOffUntilDeadline { + val recipe = recipe("example") { + defaultFailureStrategy = retryWithIncrementalBackoff { + initialDelay = 100.milliseconds + backoffFactor = 2.0 + maxTimeBetweenRetries = 10.seconds + until = deadline(24.hours) + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilMaxRetries.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilMaxRetries.kt new file mode 100644 index 000000000..e8745a1b6 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeRetryWithBackOffUntilMaxRetries.kt @@ -0,0 +1,18 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@ExperimentalDsl +object RecipeRetryWithBackOffUntilMaxRetries { + val recipe = recipe("example") { + defaultFailureStrategy = retryWithIncrementalBackoff { + initialDelay = 100.milliseconds + backoffFactor = 2.0 + maxTimeBetweenRetries = 10.seconds + until = maximumRetries(200) + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithCheckpointEvent.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithCheckpointEvent.kt new file mode 100644 index 000000000..4a8324af0 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithCheckpointEvent.kt @@ -0,0 +1,18 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.FraudCheckCompleted +import examples.kotlin.events.PaymentReceived + +@ExperimentalDsl +object RecipeWithCheckpointEvent { + val recipe = recipe("example") { + checkpointEvent(eventName = "CheckpointReached") { + requiredEvents { + event() + event() + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithDefaultFailureStrategy.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithDefaultFailureStrategy.kt new file mode 100644 index 000000000..5718fe4fa --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithDefaultFailureStrategy.kt @@ -0,0 +1,12 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithDefaultFailureStrategy { + val recipe = recipe("example") { + defaultFailureStrategy = fireEventAfterFailure("recipeFailed") + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventReceivePeriod.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventReceivePeriod.kt new file mode 100644 index 000000000..999f6bc8f --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventReceivePeriod.kt @@ -0,0 +1,12 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import kotlin.time.Duration.Companion.hours + +@ExperimentalDsl +object RecipeWithEventReceivePeriod { + val recipe = recipe(name = "example") { + eventReceivePeriod = 5.hours + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventTransformation.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventTransformation.kt new file mode 100644 index 000000000..1e3fc128d --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithEventTransformation.kt @@ -0,0 +1,18 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.OrderPlaced +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithEventTransformation { + val recipe = recipe("example") { + interaction { + transformEvent(newName = "OrderCreated") { + "customerId" to "userId" + "productIds" to "skus" + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithFailureStrategy.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithFailureStrategy.kt new file mode 100644 index 000000000..06bc146fe --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithFailureStrategy.kt @@ -0,0 +1,14 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithFailureStrategy { + val recipe = recipe("example") { + interaction { + failureStrategy = fireEventAfterFailure("shippingFailed") + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithIngredientNameOverrides.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithIngredientNameOverrides.kt new file mode 100644 index 000000000..c13ad0447 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithIngredientNameOverrides.kt @@ -0,0 +1,16 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithIngredientNameOverrides { + val recipe = recipe("example") { + interaction { + ingredientNameOverrides { + "orderId" to "orderNumber" + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithMaxInteractionCount.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithMaxInteractionCount.kt new file mode 100644 index 000000000..b1f4024fb --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithMaxInteractionCount.kt @@ -0,0 +1,14 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithMaxInteractionCount { + val recipe = recipe("example") { + interaction { + maximumInteractionCount = 1 + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithPredefinedIngredients.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithPredefinedIngredients.kt new file mode 100644 index 000000000..a5079c056 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithPredefinedIngredients.kt @@ -0,0 +1,17 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithPredefinedIngredients { + val recipe = recipe("example") { + interaction { + preDefinedIngredients { + "shippingCostAmount" to "5.75".toBigDecimal() + "shippingCostCurrency" to "EUR" + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithReproviderInteraction.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithReproviderInteraction.kt new file mode 100644 index 000000000..dd5422fa1 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithReproviderInteraction.kt @@ -0,0 +1,20 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.OrderPlaced +import examples.kotlin.events.PaymentReceived +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithReproviderInteraction { + val recipe = recipe("Reprovider recipe") { + sensoryEvents { + event() + } + interaction { + isReprovider = true + requiredEvents { event() } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRequiredEvents.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRequiredEvents.kt new file mode 100644 index 000000000..76234e651 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRequiredEvents.kt @@ -0,0 +1,22 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.FraudCheckCompleted +import examples.kotlin.events.PaymentReceived +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object RecipeWithRequiredEvents { + val recipe = recipe("example") { + interaction { + requiredEvents { + event() + } + requiredOneOfEvents { + event() + event(name = "UsedCouponCode") + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRetentionPeriod.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRetentionPeriod.kt new file mode 100644 index 000000000..b2e850a0b --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithRetentionPeriod.kt @@ -0,0 +1,13 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours + +@ExperimentalDsl +object RecipeWithRetentionPeriod { + val recipe = recipe(name = "example") { + retentionPeriod = 3.days + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithSensoryEvents.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithSensoryEvents.kt new file mode 100644 index 000000000..e6d17b40f --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/RecipeWithSensoryEvents.kt @@ -0,0 +1,18 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.FraudCheckCompleted +import examples.kotlin.events.OrderPlaced +import examples.kotlin.events.PaymentReceived + +@ExperimentalDsl +object RecipeWithSensoryEvents { + val recipe = recipe(name = "example") { + sensoryEvents { + eventWithoutFiringLimit() + event() + event(maxFiringLimit = 5) + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/SimpleRecipe.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/SimpleRecipe.kt new file mode 100644 index 000000000..0024c2b34 --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/SimpleRecipe.kt @@ -0,0 +1,16 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.OrderPlaced +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object SimpleRecipe { + val recipe = recipe("example recipe") { + sensoryEvents { + event() + } + interaction() + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/WebShopRecipe.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/WebShopRecipe.kt new file mode 100644 index 000000000..ec99caa4e --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/recipes/WebShopRecipe.kt @@ -0,0 +1,29 @@ +package examples.kotlin.recipes + +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import com.ing.baker.recipe.kotlindsl.recipe +import examples.kotlin.events.OrderPlaced +import examples.kotlin.interactions.CancelOrder +import examples.kotlin.interactions.CheckStock +import examples.kotlin.interactions.CheckStock.* +import examples.kotlin.interactions.ShipOrder + +@ExperimentalDsl +object WebShopRecipe { + val recipe = recipe("web-shop recipe") { + sensoryEvents { + event() + } + interaction() + interaction { + requiredEvents { + event() + } + } + interaction { + requiredEvents { + event() + } + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/visualization/WebShopVisualization.kt b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/visualization/WebShopVisualization.kt new file mode 100644 index 000000000..a4014e1ab --- /dev/null +++ b/examples/docs-code-snippets/src/main/kotlin/examples/kotlin/visualization/WebShopVisualization.kt @@ -0,0 +1,12 @@ +package examples.kotlin.visualization + +import com.ing.baker.compiler.RecipeCompiler +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import examples.kotlin.recipes.WebShopRecipe + +@ExperimentalDsl +fun printVisualizationString() { + val compileRecipe = RecipeCompiler.compileRecipe(WebShopRecipe.recipe) + val graphvizString = compileRecipe.recipeVisualization + println(graphvizString) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEvent.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEvent.scala new file mode 100644 index 000000000..65103e39d --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEvent.scala @@ -0,0 +1,15 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance} +import examples.scala.events.OrderPlaced + +class FireEvent(val baker: Baker) { + + def example(recipeInstanceId: String, orderPlaced: OrderPlaced): Unit = { + val eventInstance = EventInstance.unsafeFrom(orderPlaced) + + val eventResolutions = baker.fireEvent(recipeInstanceId, eventInstance) + val sensoryEventStatus = eventResolutions.resolveWhenReceived + val sensoryEventResult = eventResolutions.resolveWhenCompleted + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveOnEvent.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveOnEvent.scala new file mode 100644 index 000000000..aea70aefa --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveOnEvent.scala @@ -0,0 +1,12 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance} +import examples.scala.events.OrderPlaced + +class FireEventAndResolveOnEvent(val baker: Baker) { + + def example(recipeInstanceId: String, orderPlaced: OrderPlaced): Unit = { + val eventInstance = EventInstance.unsafeFrom(orderPlaced) + val sensoryEventResult = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, eventInstance) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenCompleted.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenCompleted.scala new file mode 100644 index 000000000..2203c4bbc --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenCompleted.scala @@ -0,0 +1,12 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance} +import examples.scala.events.OrderPlaced + +class FireEventAndResolveWhenCompleted(val baker: Baker) { + + def example(recipeInstanceId: String, orderPlaced: OrderPlaced): Unit = { + val eventInstance = EventInstance.unsafeFrom(orderPlaced) + val sensoryEventResult = baker.fireEventAndResolveWhenCompleted(recipeInstanceId, eventInstance) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenReceived.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenReceived.scala new file mode 100644 index 000000000..0f48a02db --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/FireEventAndResolveWhenReceived.scala @@ -0,0 +1,12 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance} +import examples.scala.events.OrderPlaced + +class FireEventAndResolveWhenReceived(val baker: Baker) { + + def example(recipeInstanceId: String, orderPlaced: OrderPlaced): Unit = { + val eventInstance = EventInstance.unsafeFrom(orderPlaced) + val sensoryEventStatus = baker.fireEventAndResolveWhenReceived(recipeInstanceId, eventInstance) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/InquiryExample.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/InquiryExample.scala new file mode 100644 index 000000000..60d667c92 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/InquiryExample.scala @@ -0,0 +1,18 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.Baker + +class InquiryExample(val baker: Baker) { + + def example(recipeInstanceId: String): Unit = { + val ingredient = baker.getIngredient(recipeInstanceId, "orderId") + + val ingredients = baker.getIngredients(recipeInstanceId) + + val events = baker.getEvents(recipeInstanceId) + + val eventNames = baker.getEventNames(recipeInstanceId) + + val recipeInstanceState = baker.getRecipeInstanceState(recipeInstanceId) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualResolveInteraction.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualResolveInteraction.scala new file mode 100644 index 000000000..e65e1d0ca --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualResolveInteraction.scala @@ -0,0 +1,14 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance} + +class ManualResolveInteraction { + def resolveExample(baker: Baker, recipeInstanceId: String): Unit = { + baker.resolveInteraction( + recipeInstanceId, + "ShipOrder", + EventInstance("ShippingFailed") + ) + } +} + diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualRetryInteraction.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualRetryInteraction.scala new file mode 100644 index 000000000..65dddb721 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualRetryInteraction.scala @@ -0,0 +1,9 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.Baker + +class ManualRetryInteraction { + def retryExample(baker: Baker, recipeInstanceId: String): Unit = { + baker.retryInteraction(recipeInstanceId, "ShipOrder") + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualStopInteraction.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualStopInteraction.scala new file mode 100644 index 000000000..9d10b8fa7 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/ManualStopInteraction.scala @@ -0,0 +1,9 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.Baker + +class ManualStopInteraction { + def retryExample(baker: Baker, recipeInstanceId: String): Unit = { + baker.stopRetryingInteraction(recipeInstanceId, "ShipOrder") + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterBakerEventListener.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterBakerEventListener.scala new file mode 100644 index 000000000..f16aa41e4 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterBakerEventListener.scala @@ -0,0 +1,12 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.Baker + +class RegisterBakerEventListener(val baker: Baker) { + + def example(): Unit = { + baker.registerBakerEventListener((bakerEvent) => + println(s"Received event: $bakerEvent") + ) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterEventListener.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterEventListener.scala new file mode 100644 index 000000000..3ecb0f95b --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/RegisterEventListener.scala @@ -0,0 +1,12 @@ +package examples.scala.application + +import com.ing.baker.runtime.scaladsl.Baker + +class RegisterEventListener(val baker: Baker) { + + def example(): Unit = { + baker.registerEventListener((recipeInstanceId, event) => + println(s"Recipe instance: $recipeInstanceId, processed event: ${event.name}") + ) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/application/WebShopApp.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/application/WebShopApp.scala new file mode 100644 index 000000000..423b1b21a --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/application/WebShopApp.scala @@ -0,0 +1,53 @@ +package examples.scala.application + +import cats.effect.{ContextShift, IO, Timer} +import com.ing.baker.compiler.RecipeCompiler +import examples.scala.events.OrderPlaced +import com.ing.baker.runtime.inmemory.InMemoryBaker +import com.ing.baker.runtime.model.InteractionInstance +import com.ing.baker.runtime.scaladsl.EventInstance +import examples.scala.ingredients.Address +import examples.scala.interactions.{CancelOrderImpl, CheckStockImpl, ShipOrderImpl} +import examples.scala.recipes.WebShopRecipe + +import java.util.UUID +import scala.concurrent.ExecutionContext + +class WebShopApp { + def main(args: Array[String]): Unit = { + + implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + + val interactions = List( + new CancelOrderImpl(), + new ShipOrderImpl(), + new CheckStockImpl(), + ) + + val bakerF = InMemoryBaker.build(implementations = interactions.map(InteractionInstance.unsafeFrom[IO])) + .unsafeRunSync() + + val recipeInstanceId = UUID.randomUUID().toString + val sensoryEvent = EventInstance.unsafeFrom(orderPlaced) + + for { + recipeId <- bakerF.addRecipe(RecipeCompiler.compileRecipe(recipe = WebShopRecipe.recipe), validate = true) + _ <- bakerF.bake(recipeId, recipeInstanceId) + _ <- bakerF.fireEventAndResolveWhenCompleted(recipeInstanceId, sensoryEvent) + + } yield recipeId + } + + private val orderPlaced = OrderPlaced( + orderId = "123", + customerId = "456", + productIds = List("iPhone", "PlayStation5"), + address = Address( + street = "Hoofdstraat", + city = "Amsterdam", + zipCode = "1234AA", + country = "The Netherlands" + ) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/events/FraudCheckCompleted.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/events/FraudCheckCompleted.scala new file mode 100644 index 000000000..45e40aa29 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/events/FraudCheckCompleted.scala @@ -0,0 +1,11 @@ +package examples.scala.events + +import com.ing.baker.recipe.scaladsl.Event + +object FraudCheckCompleted { + val event: Event = Event( + name = "FraudCheckCompleted", + providedIngredients = Seq.empty, + maxFiringLimit = Some(5) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/events/OrderPlaced.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/events/OrderPlaced.scala new file mode 100644 index 000000000..623e08ac6 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/events/OrderPlaced.scala @@ -0,0 +1,10 @@ +package examples.scala.events + +import examples.scala.ingredients.Address + +case class OrderPlaced( + orderId: String, + customerId: String, + address: Address, + productIds: List[String] +) diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/events/PaymentReceived.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/events/PaymentReceived.scala new file mode 100644 index 000000000..e54d74d6e --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/events/PaymentReceived.scala @@ -0,0 +1,16 @@ +package examples.scala.events + +import com.ing.baker.recipe.scaladsl.{Event, Ingredient} +import examples.scala.ingredients.Address + +object PaymentReceived { + val event: Event = Event( + name = "PaymentReceived", + providedIngredients = Seq( + Ingredient[String](name = "orderId"), + Ingredient[BigDecimal](name = "amount"), + Ingredient[String](name = "currency") + ), + maxFiringLimit = Some(1) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/ingredients/Address.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/ingredients/Address.scala new file mode 100644 index 000000000..ab921b2b2 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/ingredients/Address.scala @@ -0,0 +1,8 @@ +package examples.scala.ingredients + +case class Address( + street: String, + city: String, + zipCode: String, + country: String +) diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrder.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrder.scala new file mode 100644 index 000000000..46c507af8 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrder.scala @@ -0,0 +1,20 @@ +package examples.scala.interactions + +import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction} +import examples.scala.ingredients.Address + +object CancelOrder { + + case class OrderCancelled() + + val interaction: Interaction = Interaction( + name = "CancelOrder", + inputIngredients = Seq( + Ingredient[String](name = "orderId"), + Ingredient[List[String]](name = "unavailableProductIds") + ), + output = Seq( + Event[OrderCancelled] + ) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrderImpl.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrderImpl.scala new file mode 100644 index 000000000..e37daa75c --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CancelOrderImpl.scala @@ -0,0 +1,14 @@ +package examples.scala.interactions + +import scala.concurrent.Future + +trait CancelOrderTrait { + def apply(orderId: String, unavailableProductIds: List[String]): Future[CancelOrder.OrderCancelled] +} + +class CancelOrderImpl extends CancelOrderTrait { + override def apply(orderId: String, unavailableProductIds: List[String]): Future[CancelOrder.OrderCancelled] = { + println(s"Canceling order $orderId. The following products are unavailable: $unavailableProductIds") + Future.successful(CancelOrder.OrderCancelled()) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStock.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStock.scala new file mode 100644 index 000000000..c6a0d23cb --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStock.scala @@ -0,0 +1,23 @@ +package examples.scala.interactions + +import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction} + +object CheckStock { + + sealed trait Outcome + + case class SufficientStock() extends Outcome + case class OrderHasUnavailableItems(unavailableProductIds: List[String]) extends Outcome + + val interaction: Interaction = Interaction( + name = "CheckStock", + inputIngredients = Seq( + Ingredient[String](name = "orderId"), + Ingredient[List[String]](name = "productIds") + ), + output = Seq( + Event[SufficientStock], + Event[OrderHasUnavailableItems] + ) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStockImpl.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStockImpl.scala new file mode 100644 index 000000000..f5bf847da --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/CheckStockImpl.scala @@ -0,0 +1,21 @@ +package examples.scala.interactions + +import scala.concurrent.Future +import scala.util.Random + +trait CheckStockTrait { + def apply(orderId: String, productIds: List[String]): Future[CheckStock.Outcome] +} + +class CheckStockImpl extends CheckStockTrait { + override def apply(orderId: String, productIds: List[String]): Future[CheckStock.Outcome] = { + println(s"Checking stock for order: $orderId and products: $productIds") + + val randomNumber = new Random().nextInt(1000) + 1 + if (randomNumber < 500) { + Future.successful(CheckStock.SufficientStock()) + } else { + Future.successful(CheckStock.OrderHasUnavailableItems(productIds)) + } + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrder.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrder.scala new file mode 100644 index 000000000..4436bf407 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrder.scala @@ -0,0 +1,20 @@ +package examples.scala.interactions + +import com.ing.baker.recipe.scaladsl.{Event, Ingredient, Interaction} +import examples.scala.ingredients.Address + +object ShipOrder { + + case class OrderShipped() + + val interaction: Interaction = Interaction( + name = "ShipOrder", + inputIngredients = Seq( + Ingredient[String](name = "orderId"), + Ingredient[Address](name = "address") + ), + output = Seq( + Event[OrderShipped] + ) + ) +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrderImpl.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrderImpl.scala new file mode 100644 index 000000000..41bce4ff7 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/interactions/ShipOrderImpl.scala @@ -0,0 +1,16 @@ +package examples.scala.interactions + +import examples.scala.ingredients.Address + +import scala.concurrent.Future + +trait ShipOrderTrait { + def apply(orderId: String, address: Address): Future[ShipOrder.OrderShipped] +} + +class ShipOrderImpl extends ShipOrderTrait { + override def apply(orderId: String, address: Address): Future[ShipOrder.OrderShipped] = { + println(s"Shipping order $orderId to $address") + Future.successful(ShipOrder.OrderShipped()) + } +} diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/InteractionWithCustomName.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/InteractionWithCustomName.scala new file mode 100644 index 000000000..924b928a2 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/InteractionWithCustomName.scala @@ -0,0 +1,12 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.interactions.ShipOrder + +object InteractionWithCustomName { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withName("ship-order") + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeBlockInteraction.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeBlockInteraction.scala new file mode 100644 index 000000000..e3b198865 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeBlockInteraction.scala @@ -0,0 +1,11 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy +import com.ing.baker.recipe.scaladsl.Recipe + +object RecipeBlockInteraction { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.BlockInteraction() + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeFireEvent.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeFireEvent.scala new file mode 100644 index 000000000..16d27e46f --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeFireEvent.scala @@ -0,0 +1,11 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy +import com.ing.baker.recipe.scaladsl.Recipe + +object RecipeFireEvent { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.FireEventAfterFailure(Some("MyEvent")) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryExhaustedEvent.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryExhaustedEvent.scala new file mode 100644 index 000000000..a0dc66251 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryExhaustedEvent.scala @@ -0,0 +1,21 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff.UntilDeadline +import com.ing.baker.recipe.scaladsl.Recipe + +import scala.concurrent.duration.DurationInt + +object RecipeRetryExhaustedEvent { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + RetryWithIncrementalBackoff + .builder() + .withInitialDelay(100.milliseconds) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Some(10.minutes)) + .withUntil(Some(UntilDeadline(24.hours))) + .withFireRetryExhaustedEvent(Some("RetriesExhausted")) + .build() + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilDeadline.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilDeadline.scala new file mode 100644 index 000000000..b3ae120db --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilDeadline.scala @@ -0,0 +1,20 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff.UntilDeadline +import com.ing.baker.recipe.scaladsl.Recipe + +import scala.concurrent.duration.DurationInt + +object RecipeRetryWithBackOffUntilDeadline { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + RetryWithIncrementalBackoff + .builder() + .withInitialDelay(100.milliseconds) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Some(10.minutes)) + .withUntil(Some(UntilDeadline(24.hours))) + .build() + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilMaxRetries.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilMaxRetries.scala new file mode 100644 index 000000000..81f0cc351 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeRetryWithBackOffUntilMaxRetries.scala @@ -0,0 +1,20 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff +import com.ing.baker.recipe.common.InteractionFailureStrategy.RetryWithIncrementalBackoff.UntilMaximumRetries +import com.ing.baker.recipe.scaladsl.Recipe + +import scala.concurrent.duration.DurationInt + +object RecipeRetryWithBackOffUntilMaxRetries { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + RetryWithIncrementalBackoff + .builder() + .withInitialDelay(100.milliseconds) + .withBackoffFactor(2.0) + .withMaxTimeBetweenRetries(Some(10.minutes)) + .withUntil(Some(UntilMaximumRetries(200))) + .build() + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithCheckpointEvent.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithCheckpointEvent.scala new file mode 100644 index 000000000..e866ac8e5 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithCheckpointEvent.scala @@ -0,0 +1,18 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.events.{FraudCheckCompleted, PaymentReceived} + +object RecipeWithCheckpointEvent { + val recipe: Recipe = Recipe("example") + .withCheckpointEvent( + scaladsl.CheckPointEvent( + name = "CheckpointReached", + requiredEvents = Set.apply( + PaymentReceived.event.name, + FraudCheckCompleted.event.name + ) + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithDefaultFailureStrategy.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithDefaultFailureStrategy.scala new file mode 100644 index 000000000..9bd66d17e --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithDefaultFailureStrategy.scala @@ -0,0 +1,11 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy +import com.ing.baker.recipe.scaladsl.Recipe + +object RecipeWithDefaultFailureStrategy { + val recipe: Recipe = Recipe("example") + .withDefaultFailureStrategy( + InteractionFailureStrategy.FireEventAfterFailure(Some("recipeFailed")) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventReceivePeriod.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventReceivePeriod.scala new file mode 100644 index 000000000..b42a4accc --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventReceivePeriod.scala @@ -0,0 +1,10 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe + +import scala.concurrent.duration.{FiniteDuration, HOURS} + +object RecipeWithEventReceivePeriod { + val recipe: Recipe = Recipe(name = "example") + .withEventReceivePeriod(FiniteDuration.apply(5, HOURS)) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventTransformation.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventTransformation.scala new file mode 100644 index 000000000..07e0990a5 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithEventTransformation.scala @@ -0,0 +1,20 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.events.PaymentReceived +import examples.scala.interactions.ShipOrder + +object RecipeWithEventTransformation { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withEventOutputTransformer( + event = PaymentReceived.event, + newEventName = "OrderCreated", + ingredientRenames = Map.apply( + ("customerId", "userId"), + ("productIds", "skus") + ) + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithFailureStrategy.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithFailureStrategy.scala new file mode 100644 index 000000000..102345e5a --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithFailureStrategy.scala @@ -0,0 +1,15 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.common.InteractionFailureStrategy +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.interactions.ShipOrder + +object RecipeWithFailureStrategy { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withFailureStrategy( + InteractionFailureStrategy.FireEventAfterFailure(Some("shippingFailed")) + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithIngredientNameOverrides.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithIngredientNameOverrides.scala new file mode 100644 index 000000000..b2746f15a --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithIngredientNameOverrides.scala @@ -0,0 +1,12 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.interactions.ShipOrder + +object RecipeWithIngredientNameOverrides { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withOverriddenIngredientName("orderId", "orderNumber") + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithMaxInteractionCount.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithMaxInteractionCount.scala new file mode 100644 index 000000000..b87d17e54 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithMaxInteractionCount.scala @@ -0,0 +1,12 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.interactions.ShipOrder + +object RecipeWithMaxInteractionCount { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withMaximumInteractionCount(1) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithPredefinedIngredients.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithPredefinedIngredients.scala new file mode 100644 index 000000000..5f3e6d844 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithPredefinedIngredients.scala @@ -0,0 +1,15 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe +import examples.scala.interactions.ShipOrder + +object RecipeWithPredefinedIngredients { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withPredefinedIngredients( + ("shippingCostAmount", BigDecimal("5.75")), + ("shippingCostCurrency", "EUR") + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithReproviderInteraction.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithReproviderInteraction.scala new file mode 100644 index 000000000..d659dc177 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithReproviderInteraction.scala @@ -0,0 +1,15 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.{Event, Recipe} +import examples.scala.events.OrderPlaced +import examples.scala.interactions.ShipOrder + +object RecipeWithReproviderInteraction { + val recipe: Recipe = Recipe("Reprovider recipe") + .withSensoryEvent(Event[OrderPlaced]) + .withInteraction( + ShipOrder.interaction + .isReprovider(true) + .withRequiredEvent(Event[OrderPlaced]) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRequiredEvents.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRequiredEvents.scala new file mode 100644 index 000000000..d52051089 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRequiredEvents.scala @@ -0,0 +1,19 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.{Event, Recipe} +import examples.scala.events.{FraudCheckCompleted, PaymentReceived} +import examples.scala.interactions.ShipOrder + +object RecipeWithRequiredEvents { + val recipe: Recipe = Recipe("example") + .withInteraction( + ShipOrder.interaction + .withRequiredEvent( + FraudCheckCompleted.event + ) + .withRequiredOneOfEvents( + PaymentReceived.event, + Event("UsedCouponCode") + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRetentionPeriod.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRetentionPeriod.scala new file mode 100644 index 000000000..2f5a5536e --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithRetentionPeriod.scala @@ -0,0 +1,10 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.Recipe + +import scala.concurrent.duration.{DAYS, FiniteDuration, HOURS} + +object RecipeWithRetentionPeriod { + val recipe: Recipe = Recipe(name = "example") + .withRetentionPeriod(FiniteDuration.apply(3, DAYS)) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithSensoryEvents.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithSensoryEvents.scala new file mode 100644 index 000000000..7d0002eef --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/RecipeWithSensoryEvents.scala @@ -0,0 +1,13 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.{Event, Recipe} +import examples.scala.events.{FraudCheckCompleted, OrderPlaced, PaymentReceived} + +object RecipeWithSensoryEvents { + val recipe: Recipe = Recipe(name = "example") + .withSensoryEvents( + Event[OrderPlaced], + PaymentReceived.event, + FraudCheckCompleted.event + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/SimpleRecipe.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/SimpleRecipe.scala new file mode 100644 index 000000000..ebe800bda --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/SimpleRecipe.scala @@ -0,0 +1,11 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.{Event, Recipe} +import examples.scala.events.OrderPlaced +import examples.scala.interactions.ShipOrder + +object SimpleRecipe { + val recipe: Recipe = Recipe("example recipe") + .withSensoryEvent(Event[OrderPlaced]) + .withInteraction(ShipOrder.interaction) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/WebShopRecipe.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/WebShopRecipe.scala new file mode 100644 index 000000000..07925ea53 --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/recipes/WebShopRecipe.scala @@ -0,0 +1,23 @@ +package examples.scala.recipes + +import com.ing.baker.recipe.scaladsl.{Event, Recipe} +import examples.scala.events.OrderPlaced +import examples.scala.interactions.{CancelOrder, CheckStock, ShipOrder} + +object WebShopRecipe { + val recipe: Recipe = Recipe("web-shop recipe") + .withSensoryEvent( + Event[OrderPlaced] + ) + .withInteractions( + CheckStock.interaction, + ShipOrder.interaction + .withRequiredEvent( + Event[CheckStock.SufficientStock] + ), + CancelOrder.interaction + .withRequiredEvent( + Event[CheckStock.OrderHasUnavailableItems] + ) + ) +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/main/scala/examples/scala/visualization/WebShopVisualization.scala b/examples/docs-code-snippets/src/main/scala/examples/scala/visualization/WebShopVisualization.scala new file mode 100644 index 000000000..e37da969f --- /dev/null +++ b/examples/docs-code-snippets/src/main/scala/examples/scala/visualization/WebShopVisualization.scala @@ -0,0 +1,12 @@ +package examples.scala.visualization + +import com.ing.baker.compiler.RecipeCompiler +import examples.scala.recipes.WebShopRecipe + +class WebShopVisualization { + def printVisualizationString(): Unit = { + val compiledRecipe = RecipeCompiler.compileRecipe(WebShopRecipe.recipe) + val visualization = compiledRecipe.getRecipeVisualization + println(visualization) + } +} diff --git a/examples/docs-code-snippets/src/test/java/examples/java/recipes/WebShopRecipeTest.java b/examples/docs-code-snippets/src/test/java/examples/java/recipes/WebShopRecipeTest.java new file mode 100644 index 000000000..660416861 --- /dev/null +++ b/examples/docs-code-snippets/src/test/java/examples/java/recipes/WebShopRecipeTest.java @@ -0,0 +1,18 @@ +package examples.java.recipes; + +import com.ing.baker.compiler.RecipeCompiler; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class WebShopRecipeTest { + + @Test + public void recipeShouldCompileWithoutValidationErrors() { + var validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).getValidationErrors(); + assertTrue( + String.format("Recipe compilation resulted in validation errors: \n%s", validationErrors), + validationErrors.isEmpty() + ); + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/test/kotlin/examples/kotlin/recipes/WebShopRecipeTest.kt b/examples/docs-code-snippets/src/test/kotlin/examples/kotlin/recipes/WebShopRecipeTest.kt new file mode 100644 index 000000000..388842a94 --- /dev/null +++ b/examples/docs-code-snippets/src/test/kotlin/examples/kotlin/recipes/WebShopRecipeTest.kt @@ -0,0 +1,16 @@ +package examples.kotlin.recipes + +import com.ing.baker.compiler.RecipeCompiler +import com.ing.baker.recipe.kotlindsl.ExperimentalDsl +import org.junit.Test + +@ExperimentalDsl +class WebShopRecipeTest { + @Test + fun `recipe should compile without validation errors`() { + val validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).validationErrors + assert(validationErrors.isEmpty()) { + "Recipe compilation resulted in validation errors: \n${validationErrors.joinToString(separator = "\n")}" + } + } +} \ No newline at end of file diff --git a/examples/docs-code-snippets/src/test/scala/examples/scala/recipes/WebShopRecipeTest.scala b/examples/docs-code-snippets/src/test/scala/examples/scala/recipes/WebShopRecipeTest.scala new file mode 100644 index 000000000..ef1220e99 --- /dev/null +++ b/examples/docs-code-snippets/src/test/scala/examples/scala/recipes/WebShopRecipeTest.scala @@ -0,0 +1,12 @@ +package examples.scala.recipes + +import com.ing.baker.compiler.RecipeCompiler +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class WebShopRecipeTest extends AnyFlatSpec with Matchers { + "Recipe" should "compile without validation errors" in { + val validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).validationErrors + assert(validationErrors.isEmpty) + } +} diff --git a/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala index 54379f4ad..8f1da3e03 100644 --- a/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala +++ b/http/baker-http-client/src/main/scala/com/ing/baker/http/client/scaladsl/BakerClient.scala @@ -228,7 +228,6 @@ final class BakerClient( client: Client[IO], (host, prefix) => POST(event, (root(host, prefix) / "instances" / recipeInstanceId / "fire-and-resolve-when-completed") .withOptionQueryParam("correlationId", correlationId)), fallbackEndpoint).map { result => logger.info(s"For recipe instance '$recipeInstanceId', fired and completed event '${event.name}', resulting status ${result.sensoryEventStatus}") - logger.debug(s"Resulting ingredients ${result.ingredients.map { case (ingredient, value) => s"$ingredient=$value" }.mkString(", ")}") result } @@ -273,7 +272,6 @@ final class BakerClient( client: Client[IO], (host, prefix) => POST(event, (root(host, prefix) / "instances" / recipeInstanceId / "fire-and-resolve-on-event" / onEvent) .withOptionQueryParam("correlationId", correlationId)), fallbackEndpoint).map { result => logger.info(s"For recipe instance '$recipeInstanceId', fired event '${event.name}', and resolved on event '$onEvent', resulting status ${result.sensoryEventStatus}") - logger.debug(s"Resulting ingredients ${result.ingredients.map { case (ingredient, value) => s"$ingredient=$value" }.mkString(", ")}") result } @@ -321,13 +319,13 @@ final class BakerClient( client: Client[IO], override def getRecipeInstanceState(recipeInstanceId: String): Future[RecipeInstanceState] = callRemoteBakerServiceFallbackAware[RecipeInstanceState]((host, prefix) => GET(root(host, prefix) / "instances" / recipeInstanceId), fallbackEndpoint) -// /** -// * @param recipeInstanceId -// * @param name -// * @return -// */ -// override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = -// callRemoteBakerServiceFallbackAware[Value]((host, prefix) => GET(root(host, prefix) / "instances" / recipeInstanceId / "ingredient" / name), fallbackEndpoint) + /** + * @param recipeInstanceId + * @param name + * @return + */ + override def getIngredient(recipeInstanceId: String, name: String): Future[Value] = + callRemoteBakerServiceFallbackAware[Value]((host, prefix) => GET(root(host, prefix) / "instances" / recipeInstanceId / "ingredient" / name), fallbackEndpoint) /** * Returns all provided ingredients for a given RecipeInstance id. diff --git a/http/baker-http-dashboard/package.json b/http/baker-http-dashboard/package.json index 41c023ec0..0587cd242 100644 --- a/http/baker-http-dashboard/package.json +++ b/http/baker-http-dashboard/package.json @@ -36,7 +36,7 @@ "@angular-eslint/eslint-plugin-template": "14.0.2", "@angular-eslint/schematics": "14.0.2", "@angular-eslint/template-parser": "14.0.2", - "@angular/cli": "^14.2.11", + "@angular/cli": "^14.2.13", "@angular/compiler-cli": "^14.0.6", "@angular/language-service": "^14.0.6", "@angular/localize": "^14.0.6", diff --git a/http/baker-http-dashboard/src/app/app-routing.module.ts b/http/baker-http-dashboard/src/app/app-routing.module.ts index ca7b49749..17fe6f00b 100644 --- a/http/baker-http-dashboard/src/app/app-routing.module.ts +++ b/http/baker-http-dashboard/src/app/app-routing.module.ts @@ -29,26 +29,6 @@ const routes: Routes = [ "component": InstancesComponent, "path": "instances/:recipeInstanceId", }, - { - "component": HomeComponent, - "path": ":prefix" - }, - { - "component": RecipesComponent, - "path": ":prefix/recipes" - }, - { - "component": InteractionsComponent, - "path": ":prefix/interactions", - }, - { - "component": InstancesComponent, - "path": ":prefix/instances", - }, - { - "component": InstancesComponent, - "path": ":prefix/instances/:recipeInstanceId", - }, { "component": NotFoundComponent, "path": "**", diff --git a/http/baker-http-dashboard/src/app/app.component.html b/http/baker-http-dashboard/src/app/app.component.html index bc60e1d24..0833fdd40 100644 --- a/http/baker-http-dashboard/src/app/app.component.html +++ b/http/baker-http-dashboard/src/app/app.component.html @@ -9,10 +9,10 @@

{{ title }}

-
Home - Recipes - Interactions - Instances + Home + Recipes + Interactions + Instances diff --git a/http/baker-http-dashboard/src/app/app.component.ts b/http/baker-http-dashboard/src/app/app.component.ts index 2bd0882cf..80df3c651 100644 --- a/http/baker-http-dashboard/src/app/app.component.ts +++ b/http/baker-http-dashboard/src/app/app.component.ts @@ -10,7 +10,6 @@ import {wasmFolder} from "@hpcc-js/wasm"; }) export class AppComponent implements OnDestroy, OnInit { title = AppSettingsService.settings.applicationName; - prefix = AppSettingsService.prefix.prefix; mobileQuery: MediaQueryList; private readonly mobileQueryListener: () => void; diff --git a/http/baker-http-dashboard/src/app/app.settings.ts b/http/baker-http-dashboard/src/app/app.settings.ts index f8d33a7d3..676f8c6af 100644 --- a/http/baker-http-dashboard/src/app/app.settings.ts +++ b/http/baker-http-dashboard/src/app/app.settings.ts @@ -4,11 +4,6 @@ import {Value} from "./baker-value.api"; const LOCAL_SETTINGS_LOCATION = "/assets/settings/settings.json"; const SETTINGS_LOCATION = "dashboard_config"; - -export interface PrefixSettings { - prefix: string; -} - export interface AppSettings { applicationName: string; apiPath: string; @@ -17,13 +12,12 @@ export interface AppSettings { @Injectable() export class AppSettingsService { - static prefix: PrefixSettings; static settings: AppSettings; constructor (private http: HttpClient) { } - public getAppSettings(prefix: String):Promise { + public getAppSettings():Promise { return new Promise((resolve, reject) => { // //For testing purposes: // AppSettingsService.settings = { @@ -33,7 +27,7 @@ export class AppSettingsService { // }; // resolve() - this.http.get(prefix + "/" + SETTINGS_LOCATION).toPromise().then(response => { + this.http.get(SETTINGS_LOCATION).toPromise().then(response => { AppSettingsService.settings = response; resolve(); }).catch((response: any) => { @@ -43,32 +37,8 @@ export class AppSettingsService { } load () { - const url = new URL(window.location.href); - - // if there is an empty path or just a / the prefix is empty - if(url.pathname == "/" || url.pathname.length == 0) { - AppSettingsService.prefix = {"prefix": url.pathname.substring(url.pathname.lastIndexOf('/') + 1)} - } - // If there is a path we need to ensure they are not part of the path - else { - var temp = url.pathname; - if(temp.includes('/recipes')) { - temp = temp.substring(0, temp.lastIndexOf('/recipes')) - } - if(temp.includes('/interactions')) { - temp = temp.substring(0, temp.lastIndexOf('/interactions')) - } - if(temp.includes('/instances')) { - temp = temp.substring(0, temp.lastIndexOf('/instances')) - } - if(temp.lastIndexOf("/") > 0) { - temp = temp.substring(0, temp.lastIndexOf('/')) - } - AppSettingsService.prefix = {"prefix": temp} - } - return new Promise((resolve, reject) => { - this.getAppSettings(AppSettingsService.prefix.prefix) + this.getAppSettings() .then(_ => resolve()) }); } diff --git a/http/baker-http-dashboard/src/app/bakery.service.ts b/http/baker-http-dashboard/src/app/bakery.service.ts index 0b9f2a9d5..618736552 100644 --- a/http/baker-http-dashboard/src/app/bakery.service.ts +++ b/http/baker-http-dashboard/src/app/bakery.service.ts @@ -32,7 +32,7 @@ import {Value} from "./baker-value.api"; @Injectable({"providedIn": "root"}) export class BakeryService { - private baseUrl = AppSettingsService.prefix.prefix + AppSettingsService.settings.apiPath; + private baseUrl = AppSettingsService.settings.apiPath; httpOptions = { "headers": new HttpHeaders({"Content-Type": "application/json"}) diff --git a/http/baker-http-dashboard/src/app/instances/instances.component.ts b/http/baker-http-dashboard/src/app/instances/instances.component.ts index 73c06f098..3ce881663 100644 --- a/http/baker-http-dashboard/src/app/instances/instances.component.ts +++ b/http/baker-http-dashboard/src/app/instances/instances.component.ts @@ -59,7 +59,7 @@ export class InstancesComponent implements OnInit { updateInstance(event: Event): void { (event.target as HTMLInputElement)?.blur(); - this.router.navigateByUrl(AppSettingsService.prefix.prefix + `/instances/${this.instanceId}`).then(() => this.instanceChanged()); + this.router.navigateByUrl(`/instances/${this.instanceId}`).then(() => this.instanceChanged()); } instanceChanged (): void { diff --git a/http/baker-http-dashboard/src/app/recipes/recipes.component.ts b/http/baker-http-dashboard/src/app/recipes/recipes.component.ts index d9f16d0da..a51156cf3 100644 --- a/http/baker-http-dashboard/src/app/recipes/recipes.component.ts +++ b/http/baker-http-dashboard/src/app/recipes/recipes.component.ts @@ -47,7 +47,7 @@ export class RecipesComponent implements OnInit { bakeRecipe(recipeId: string): void { const instanceId = this.randomId(8) this.bakeryService.postBake(instanceId, recipeId).subscribe(() => { - window.location.href = AppSettingsService.prefix.prefix + `/instances/${instanceId}` + window.location.href = `/instances/${instanceId}` }); } diff --git a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala index b3e8d8e98..4001753f5 100644 --- a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/javadsl/BakerWithHttpResponse.scala @@ -80,7 +80,7 @@ class BakerWithHttpResponse(val baker: Baker, ec: ExecutionContext) extends Lazy def getEvents: JFuture[String] = baker.getEvents(recipeInstanceId).toBakerResult -// def getIngredient(name: String): JFuture[String] = baker.getIngredient(recipeInstanceId, name).toBakerResult + def getIngredient(name: String): JFuture[String] = baker.getIngredient(recipeInstanceId, name).toBakerResult def getIngredients: JFuture[String] = baker.getIngredients(recipeInstanceId).toBakerResult diff --git a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala index f216a97aa..7f9e3a18d 100644 --- a/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala +++ b/http/baker-http-server/src/main/scala/com/ing/baker/http/server/scaladsl/Http4sBakerServer.scala @@ -208,7 +208,7 @@ final class Http4sBakerServer private(baker: Baker)(implicit cs: ContextShift[IO case GET -> Root / RecipeInstanceId(recipeInstanceId) / "events" => baker.getEvents(recipeInstanceId).toBakerResultResponseIO -// case GET -> Root / RecipeInstanceId(recipeInstanceId) / "ingredient" / IngredientName(name) => baker.getIngredient(recipeInstanceId, name).toBakerResultResponseIO + case GET -> Root / RecipeInstanceId(recipeInstanceId) / "ingredient" / IngredientName(name) => baker.getIngredient(recipeInstanceId, name).toBakerResultResponseIO case GET -> Root / RecipeInstanceId(recipeInstanceId) / "ingredients" => baker.getIngredients(recipeInstanceId).toBakerResultResponseIO diff --git a/mkdocs.yml b/mkdocs.yml index 0047862e4..ce9c862c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,8 @@ repo_url: 'https://github.com/ing-bank/baker' theme: name: 'material' + features: + - content.code.copy extra_css: - css/ing.css @@ -12,32 +14,31 @@ extra_css: markdown_extensions: - codehilite - pymdownx.superfences + - admonition + - pymdownx.details - pymdownx.tabbed: alternate_style: true - - admonition + - pymdownx.snippets: + base_path: examples nav: - Home: 'index.md' - - 'sections/getting-started.md' + - 'sections/quickstart-guide.md' - 'sections/concepts.md' - - Development Life Cycle: - - 'sections/development-life-cycle/introduction.md' - - 'sections/development-life-cycle/design-a-recipe.md' - - 'sections/development-life-cycle/use-visualizations.md' - - 'sections/development-life-cycle/implement-interactions.md' - - 'sections/development-life-cycle/bake-fire-events-and-inquiry.md' - - 'sections/development-life-cycle/test.md' - - 'sections/development-life-cycle/configure.md' - - 'sections/development-life-cycle/monitor.md' - - 'sections/development-life-cycle/resolve-failed-recipe-instances.md' + - 'sections/tutorial.md' + - Cookbook: + - 'sections/cookbook/recipe-dsl.md' + - 'sections/cookbook/error-handling.md' + - 'sections/cookbook/visualizations.md' + - 'sections/cookbook/fire-sensory-events.md' + - 'sections/cookbook/interaction-execution.md' + - 'sections/cookbook/testing.md' + - 'sections/cookbook/monitoring.md' - Reference: - 'sections/reference/main-abstractions.md' - 'sections/reference/execution-semantics.md' - 'sections/reference/baker-types-and-values.md' - - 'sections/reference/dsls.md' - 'sections/reference/runtime.md' - - 'sections/reference/visualization.md' - - 'sections/reference/event-listener.md' - 'sections/reference/stores.md' - Versions: - 'sections/versions/baker-3-release-notes.md' diff --git a/project/BuildInteractionDockerImageSBTPlugin.scala b/project/BuildInteractionDockerImageSBTPlugin.scala deleted file mode 100644 index a2f372a5f..000000000 --- a/project/BuildInteractionDockerImageSBTPlugin.scala +++ /dev/null @@ -1,205 +0,0 @@ -package bakery.sbt - -import java.nio.charset.Charset - -import com.typesafe.sbt.packager.Keys.packageName -import com.typesafe.sbt.packager.archetypes.JavaAppPackaging -import com.typesafe.sbt.packager.docker.{CmdLike, DockerPlugin, ExecCmd} -import com.typesafe.sbt.packager.docker.DockerPlugin.autoImport._ -import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport._ -import kubeyml.deployment.NoProbe -import kubeyml.deployment.plugin.Keys._ -import kubeyml.deployment.plugin.KubeDeploymentPlugin -import sbt.Keys._ -import sbt._ - -object BuildInteractionDockerImageSBTPlugin extends sbt.AutoPlugin { - - case class CommandArgumentsBuilder(name: Option[String], publish: Option[String], artifact: Option[String], interactions: List[String], springEnabled: Option[Boolean]) - case class CommandArguments(name: String, publish: String, artifact: Option[String], interactions: List[String], springEnabled: Boolean) - - override def requires: Plugins = DockerPlugin && JavaAppPackaging && KubeDeploymentPlugin - - override def trigger: PluginTrigger = allRequirements - - object autoImport { - val mainClassBody = settingKey[Option[String]]("Main's class source code") - - /** - * Example: "buildInteractionDockerImage --image-name= --publish= --artifact=net.bytebuddy:byte-buddy:1.10.8 --interaction=path.to.Interaction --interaction=path.to.Interaction2" - */ - def buildDockerCommand: Command = Command.args("buildInteractionDockerImage", "") { (state, args) => - - val NameRegex = """--image-name=(.+)""".r - val PublishRegex = """--publish=(.+)""".r - val ArtifactRegex = """--artifact=(.+)""".r - val InteractionRegex = """--interaction=(.+)""".r - val SpringEnabledRegex = """--springEnabled=(.+)""".r - - val builder = args.foldLeft(CommandArgumentsBuilder(None, None, None, List.empty, None)) { (builder, arg) => - arg match { - case NameRegex(value) => builder.copy(name = Some(value)) - case PublishRegex(value) => builder.copy(publish = Some(value)) - case ArtifactRegex(value) => builder.copy(artifact = Some(value)) - case InteractionRegex(value) => builder.copy(interactions = value :: builder.interactions) - case SpringEnabledRegex(value) => builder.copy(springEnabled = Option.apply(value.toBoolean)) - } - } - - val arguments = builder match { - case cmd@CommandArgumentsBuilder(Some(name), Some("local" | "remote") | None, artifact, interactions, None) if interactions.nonEmpty => - CommandArguments(name, cmd.publish.getOrElse("remote"), artifact, interactions, false) - case cmd@CommandArgumentsBuilder(Some(name), Some("local" | "remote") | None, artifact, interactions, Some(springEnabled)) if interactions.nonEmpty => - CommandArguments(name, cmd.publish.getOrElse("remote"), artifact, interactions, springEnabled) - case CommandArgumentsBuilder(None, _, _, _, _) => - throw new MessageOnlyException(s"Expected name for image (--image-name=)") - case CommandArgumentsBuilder(_, _, _, interactions, _) if interactions.isEmpty => - throw new MessageOnlyException(s"Expected at least one interaction or configuration (--interaction=)") - case _ => - throw new MessageOnlyException(s"Expected publish to be either local or remote or empty (--publish=)") - } - - executeDockerBuild(state, arguments) - } - - private def executeDockerBuild(state: State, arguments: CommandArguments): State = { - val moduleID: Option[ModuleID] = arguments.artifact map { - _.split(":") match { - case Array(organization, name, revision) => organization % name % revision - case other => throw new MessageOnlyException(s"Unexpected dependency declaration $other") - } - } - - val stateWithNewDependency = - Project.extract(state).appendWithSession(Seq( - name := arguments.name, - libraryDependencies ++= moduleID.toSeq, - Docker / packageName := arguments.name, - ThisBuild / version := moduleID.map(_.revision).getOrElse((ThisBuild / version ).value), - Universal / javaOptions += arguments.interactions.mkString(","), - kube / livenessProbe := NoProbe, - dockerBaseImage := "adoptopenjdk/openjdk11", - Compile / sourceGenerators += Def.task { - val mainClassName = - (Compile / mainClass).value.getOrElse(throw new MessageOnlyException("mainClass in Compile is required")) - - val pathList = mainClassName.split("\\.") - - val file = - (pathList.dropRight(1) :+ pathList.last + ".scala") - .foldLeft((Compile / sourceManaged).value) { - case (file, subPath) => file / subPath - } - - val mainClassDefault = if(arguments.springEnabled) mainClassBodySpringDefault else mainClassBodyDefault - val sourceBytes = mainClassBody.value.getOrElse(mainClassDefault).getBytes(Charset.defaultCharset()) - IO.write(file, sourceBytes) - Seq(file) - }.taskValue - ), state) - - val commandName = arguments.publish match { - case "local" => "Docker/publishLocal" - case _ => "Docker/publish" - } - val updatedState = Command.process(commandName, stateWithNewDependency) - Command.process("kubeyml:gen", updatedState) - - state - } - } - - import autoImport._ - - override lazy val projectSettings: Seq[Def.Setting[_]] = Seq( - mainClassBody := None, - Compile / mainClass := Some("com.ing.bakery.Main"), - commands += buildDockerCommand - ) - - private val mainClassBodyDefault = - """ - |package com.ing.bakery - | - |import com.ing.bakery.interaction.RemoteInteractionLoader - |import com.ing.baker.runtime.scaladsl.InteractionInstance - | - |import scala.concurrent.ExecutionContext.Implicits.global - | - |/** - | * Expects single argument containing full classpath entry point for interaction - | */ - |object Main extends App { - | - | private def runApp(classNames: String): Unit = - | try { - | val interactions: List[String] = classNames.split(",").toList - | val implementations = interactions - | .map(entryClassName => Class.forName(entryClassName).getConstructor().newInstance().asInstanceOf[AnyRef]) - | .map(implementation => InteractionInstance.unsafeFrom(implementation)) - | RemoteInteractionLoader.apply(implementations) - | } catch { - | case ex: Exception => - | throw new IllegalStateException(s"Unable to initialize the classes $classNames", ex) - | } - | - | - | args.headOption.map(runApp).getOrElse(throw new IllegalAccessException("Expected class name as a parameter")) - |} - |""".stripMargin - - private val mainClassBodySpringDefault = - """ - |package com.ing.bakery - | - |import java.util - | - |import com.ing.bakery.interaction.RemoteInteractionLoader - |import com.ing.baker.recipe.javadsl.Interaction - |import com.ing.baker.runtime.scaladsl.InteractionInstance - |import com.typesafe.scalalogging.LazyLogging - |import org.springframework.context.annotation.AnnotationConfigApplicationContext - | - |import scala.collection.JavaConverters._ - |import scala.annotation.nowarn - |import scala.concurrent.ExecutionContext.Implicits.global - | - |/** - | * Expects single argument containing Spring configuration - | */ - |object Main extends App with LazyLogging{ - | - | @nowarn - | def getImplementations(configurationClassString: String) : List[InteractionInstance] = { - | val configClass = Class.forName(configurationClassString) - | logger.info("Class found: " + configClass) - | val ctx = new AnnotationConfigApplicationContext(); - | logger.info("Context created") - | ctx.register(configClass) - | logger.info("Context registered") - | ctx.refresh() - | logger.info("Context refreshed") - | val interactions: util.Map[String, Interaction] = - | ctx.getBeansOfType(classOf[com.ing.baker.recipe.javadsl.Interaction]) - | interactions.asScala.values.map(implementation => { - | val instance = InteractionInstance.unsafeFrom(implementation) - | logger.info("Added implementation: " + instance.name) - | instance - | }).toList - | } - | - | private def runApp(configurationClassString: String): Unit = - | try { - | logger.info("Starting for configuration: " + configurationClassString) - | val implementations = getImplementations(configurationClassString) - | logger.info("Starting RemoteInteractionLoader") - | RemoteInteractionLoader.apply(implementations) - | } catch { - | case ex: Exception => - | throw new IllegalStateException(s"Unable to initialize the interaction instances", ex) - | } - | - | args.headOption.map(runApp).getOrElse(throw new IllegalAccessException("Please provide a Spring configuration containing valid interactions")) - |} - |""".stripMargin -} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6a36e14f5..37fc275ed 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -3,18 +3,18 @@ import sbt._ //noinspection TypeAnnotation object Dependencies { - val akkaVersion = "2.6.19" - val akkaManagementVersion = "1.1.3" - val akkaPersistenceCassandraVersion = "1.0.5" + val akkaVersion = "2.6.20" + val akkaManagementVersion = "1.1.4" + val akkaPersistenceCassandraVersion = "1.0.6" val akkaHttpVersion = "10.2.9" val http4sVersion = "0.21.34" val fs2Version = "2.5.10" val circeVersion = "0.14.2" - val mockitoScalaVersion = "1.17.7" + val mockitoScalaVersion = "1.17.14" val catsEffectVersion = "2.5.5" val catsCoreVersion = "2.8.0" val scalapbVersion = scalapb.compiler.Version.scalapbVersion - val springVersion = "5.3.22" + val springVersion = "5.3.27" val springBootVersion = "2.6.1" val akkaInmemoryJournal = ("com.github.dnvriend" %% "akka-persistence-inmemory" % "2.5.15.2") @@ -24,15 +24,14 @@ object Dependencies { .exclude("com.typesafe.akka", "akka-stream") .exclude("com.typesafe.akka", "akka-protobuf") - val scalaJava8Compat100 = "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.0" - val scalaJava8Compat091 = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.1" + val scalaJava8Compat100 = "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2" - val scalaTest = "org.scalatest" %% "scalatest" % "3.2.12" + val scalaTest = "org.scalatest" %% "scalatest" % "3.2.15" val mockitoScala = "org.mockito" %% "mockito-scala" % mockitoScalaVersion val mockitoScalaTest = "org.mockito" %% "mockito-scala-scalatest" % mockitoScalaVersion - val mockServer = "org.mock-server" % "mockserver-netty" % "5.13.2" + val mockServer = "org.mock-server" % "mockserver-netty" % "5.14.0" val junitInterface = "com.github.sbt" % "junit-interface" % "0.13.3" - val junitJupiter = "org.junit.jupiter" % "junit-jupiter-engine" % "5.8.2" + val junitJupiter = "org.junit.jupiter" % "junit-jupiter-engine" % "5.9.3" val akkaActor = "com.typesafe.akka" %% "akka-actor" % akkaVersion @@ -60,7 +59,7 @@ object Dependencies { val akkaClusterBoostrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaManagementVersion val akkaDiscoveryKube = "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % akkaManagementVersion - val kafkaClient = "org.apache.kafka" % "kafka-clients" % "3.2.0" + val kafkaClient = "org.apache.kafka" % "kafka-clients" % "3.4.0" val fs2Core = "co.fs2" %% "fs2-core" % fs2Version val fs2Io = "co.fs2" %% "fs2-io" % fs2Version val fs2kafka = "com.github.fd4s" %% "fs2-kafka" % "1.0.0" @@ -70,21 +69,21 @@ object Dependencies { val ficusConfig = "com.iheart" %% "ficus" % "1.5.2" - val scalaGraph = "org.scala-graph" %% "graph-core" % "1.13.1" - val scalaGraphDot = "org.scala-graph" %% "graph-dot" % "1.13.0" + val scalaGraph = "org.scala-graph" %% "graph-core" % "1.13.6" + val scalaGraphDot = "org.scala-graph" %% "graph-dot" % "1.13.3" val graphvizJava = "guru.nidi" % "graphviz-java" % "0.18.1" val prometheus = "io.prometheus" % "simpleclient_hotspot" % "0.16.0" - val prometheusJmx = "io.prometheus.jmx" % "collector" % "0.17.0" - val sensors = "nl.pragmasoft.sensors" %% "sensors-core" % "0.3.0" + val prometheusJmx = "io.prometheus.jmx" % "collector" % "0.18.0" + val sensors = "nl.pragmasoft.sensors" %% "sensors-core" % "0.4.1" val cassandraUnit = "org.cassandraunit" % "cassandra-unit" % "4.3.1.0" - val cassandraDriverCore = "com.datastax.oss" % "java-driver-core" % "4.14.1" - val cassandraDriverQueryBuilder = "com.datastax.oss" % "java-driver-query-builder" % "4.14.1" - val cassandraDriverMetrics = "io.dropwizard.metrics" % "metrics-jmx" % "4.2.10" + val cassandraDriverCore = "com.datastax.oss" % "java-driver-core" % "4.15.0" + val cassandraDriverQueryBuilder = "com.datastax.oss" % "java-driver-query-builder" % "4.15.0" + val cassandraDriverMetrics = "io.dropwizard.metrics" % "metrics-jmx" % "4.2.18" - val skuber = "io.skuber" %% "skuber" % "2.6.4" - val play = "com.typesafe.play" %% "play-json" % "2.9.2" + val skuber = "io.skuber" %% "skuber" % "2.6.7" + val play = "com.typesafe.play" %% "play-json" % "2.9.4" val http4s = "org.http4s" %% "http4s-core" % http4sVersion val http4sDsl = "org.http4s" %% "http4s-dsl" % http4sVersion @@ -103,7 +102,7 @@ object Dependencies { val console4Cats = "dev.profunktor" %% "console4cats" % "0.8.0" val catsRetry = "com.github.cb372" %% "cats-retry" % "2.1.1" - val jnrConstants = "com.github.jnr" % "jnr-constants" % "0.9.9" + val jnrConstants = "com.github.jnr" % "jnr-constants" % "0.10.3" def scalaReflect(scalaV: String): ModuleID = "org.scala-lang" % "scala-reflect" % scalaV @@ -112,22 +111,22 @@ object Dependencies { val paranamer = "com.thoughtworks.paranamer" % "paranamer" % "2.8" val findbugs = "com.google.code.findbugs" % "jsr305" % "1.3.9" - val scalaCollectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % "2.9.0" + val scalaCollectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % "2.10.0" val scalapbRuntime = "com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf" - val protobufJava = "com.google.protobuf" % "protobuf-java" % "3.21.2" + val protobufJava = "com.google.protobuf" % "protobuf-java" % "3.22.2" - val betterFiles = "com.github.pathikrit" %% "better-files" % "3.9.1" + val betterFiles = "com.github.pathikrit" %% "better-files" % "3.9.2" val typeSafeConfig = "com.typesafe" % "config" % "1.4.2" val objenisis = "org.objenesis" % "objenesis" % "3.2" - val jodaTime = "joda-time" % "joda-time" % "2.10.14" - val slf4jApi = "org.slf4j" % "slf4j-api" % "1.7.36" - val logback = "ch.qos.logback" % "logback-classic" % "1.2.11" - val logstash = "net.logstash.logback" % "logstash-logback-encoder" % "6.4" - val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.16.0" + val jodaTime = "joda-time" % "joda-time" % "2.12.5" + val slf4jApi = "org.slf4j" % "slf4j-api" % "2.0.7" + val logback = "ch.qos.logback" % "logback-classic" % "1.4.6" + val logstash = "net.logstash.logback" % "logstash-logback-encoder" % "7.3" + val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.17.0" val scalaCheckPlus = "org.scalatestplus" %% "scalacheck-1-15" % "3.2.11.0" val scalaCheckPlusMockito = "org.scalatestplus" %% "mockito-3-12" % "3.2.10.0" val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" @@ -137,12 +136,12 @@ object Dependencies { val springCore = "org.springframework" % "spring-core" % springVersion val springBootStarter = "org.springframework.boot" % "spring-boot-starter" % springBootVersion - val snakeYaml = "org.yaml" % "snakeyaml" % "1.31" + val snakeYaml = "org.yaml" % "snakeyaml" % "2.0" - val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % "2.13.3" - val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % "2.13.3" + val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % "2.15.1" + val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % "2.15.1" val jawnParser = "org.typelevel" %% "jawn-parser" % "1.4.0" - val nettyHandler = "io.netty" % "netty-handler" % "4.1.81.Final" + val nettyHandler = "io.netty" % "netty-handler" % "4.1.92.Final" private val bouncycastleVersion = "1.70" diff --git a/project/plugins.sbt b/project/plugins.sbt index e625074a9..bdb2ce0f3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -24,4 +24,4 @@ addSbtPlugin("no.arktekk.sbt" % "aether-deploy" % "0.27.0") addSbtPlugin("community.flock.sbt" % "sbt-kotlin-plugin" % "3.0.1") -libraryDependencies += "org.slf4j" % "slf4j-nop" % "1.7.36" +libraryDependencies += "org.slf4j" % "slf4j-nop" % "2.0.7" diff --git a/version.sbt b/version.sbt index fe96bc832..1f34dcfa5 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.8.0-SNAPSHOT" +ThisBuild / version := "4.0.4-SNAPSHOT"