From f52c18d97e7eeec951837cf548e9ae7fa2617f3b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 28 Oct 2024 19:10:12 +0000 Subject: [PATCH 01/44] chore: save work in progress --- docs/adr/0020-reduce-esb-complexity.md | 2 +- docs/adr/0022-add-a-mediator.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0022-add-a-mediator.md diff --git a/docs/adr/0020-reduce-esb-complexity.md b/docs/adr/0020-reduce-esb-complexity.md index 926c49e7d2..82d91ec747 100644 --- a/docs/adr/0020-reduce-esb-complexity.md +++ b/docs/adr/0020-reduce-esb-complexity.md @@ -1,6 +1,6 @@ # 20. Reduce External Service Bus Complexity -Date: 2019-08-01 +Date: 2024-08-01 ## Status diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md new file mode 100644 index 0000000000..2435db0967 --- /dev/null +++ b/docs/adr/0022-add-a-mediator.md @@ -0,0 +1,20 @@ +# 20. Reduce External Service Bus Complexity + +Date: 2024-10-22 + +## Status + +Proposed + +## Context +We have two approaches to a workflow: orchestration and choreography. In choreography the workflow emerges from the interaction of the participants. In orchestration, one participant executes the workflow, calling other participants as needed. Whilst choreography has low-coupling, it also has low-cohesion. At scale this can lead to the Pinball anti-pattern, where it is difficult to maintain the workflow. + +The [Mediator](https://www.oodesign.com/mediator-pattern) pattern provides an orchestrator that manages a workflow that involves multiple objects. In its simplest form, instead of talking to each other, objects talk to the mediator, which then calls other objects as required. + +Brighter provides `IHandleRequests<>` to provide a handler for an individual request, either a command or an event. It is possible to have an emergent workflow, within Brighter, through the interaction of these handlers. + + +## Decision + + +## Consequences \ No newline at end of file From 36890f5d79f52dab1ba3e59fda91a103e90bf809 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 30 Oct 2024 20:06:41 +0000 Subject: [PATCH 02/44] feat: add ADR for a mediator and assembly --- Brighter.sln | 14 ++++++ docs/adr/0022-add-a-mediator.md | 43 +++++++++++++++++-- .../Paramore.Brighter.Mediator.csproj | 15 +++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj diff --git a/Brighter.sln b/Brighter.sln index 3babe86283..26e7fccafb 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,6 +315,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Mediator", "src\Paramore.Brighter.Mediator\Paramore.Brighter.Mediator.csproj", "{7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1765,6 +1767,18 @@ Global {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|Mixed Platforms.Build.0 = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.ActiveCfg = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.Build.0 = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|x86.Build.0 = Debug|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Any CPU.Build.0 = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|x86.ActiveCfg = Release|Any CPU + {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md index 2435db0967..512224e74d 100644 --- a/docs/adr/0022-add-a-mediator.md +++ b/docs/adr/0022-add-a-mediator.md @@ -1,4 +1,4 @@ -# 20. Reduce External Service Bus Complexity +# 22. Add a Mediator to Brighter Date: 2024-10-22 @@ -9,12 +9,47 @@ Proposed ## Context We have two approaches to a workflow: orchestration and choreography. In choreography the workflow emerges from the interaction of the participants. In orchestration, one participant executes the workflow, calling other participants as needed. Whilst choreography has low-coupling, it also has low-cohesion. At scale this can lead to the Pinball anti-pattern, where it is difficult to maintain the workflow. -The [Mediator](https://www.oodesign.com/mediator-pattern) pattern provides an orchestrator that manages a workflow that involves multiple objects. In its simplest form, instead of talking to each other, objects talk to the mediator, which then calls other objects as required. +The [Mediator](https://www.oodesign.com/mediator-pattern) pattern provides an orchestrator that manages a workflow that involves multiple objects. In its simplest form, instead of talking to each other, objects talk to the mediator, which then calls other objects as required to execute the workflow. -Brighter provides `IHandleRequests<>` to provide a handler for an individual request, either a command or an event. It is possible to have an emergent workflow, within Brighter, through the interaction of these handlers. +Brighter provides `IHandleRequests<>` to provide a handler for an individual request, either a command or an event. It is possible to have an emergent workflow, within Brighter, through the choreography of these handlers. However, Brighter provides no model for an orchestrator that manages a workflow that involves multiple handlers. In particular, Brighter does not support a class that can listen to multiple requests and then call other handlers as required to execute the workflow. + +In principle, nothing stops an end user from implementing a `Mediator` class that listens to multiple requests and then calls other handlers as required to execute the workflow. So orchestration has always been viable, but left as an exercise to the user. However, competing OSS projects provide popular workflow functionality, suggesting there is demand for an off-the-shelf solution. + +Other dotnet messaging platforms erroneously conflate the Saga and Mediator patterns. A Saga is a long-running transaction that spans multiple services. A Mediator is an orchestrator that manages a workflow that involves multiple objects. One aspect of those implementations is typically the ability to store workflow state. + +A particular reference for the requirements for this work is [AWS step functions](https://states-language.net/spec.html). AWS Step functions provide a state machine that mediates calls to AWS Lambda functions. When thinking about Brighter's `IHandleRequests` it is attractive to compare them to Lambda functions in the Step functions model : + + 1. The AWS Step funcions state machine does not hold the business logic, that is located in the functions called; the Step function handles calling the Lambda functions and state transitions (as well as error paths) + 2. We want to use the Mediator to orchestrate both internal bus and external bus hosted workflows. Step functions provide a useful model of requirements for the latter. + +This approach is intended to enable flexible, event-driven workflows that can handle various business processes and requirements, including asynchronous event handling and conditional branching. ## Decision +We will add a `Mediator` class to Brighter that will: + + 1. Manages and tracks a WorkflowState object representing the current step in the workflow. + 2. Supports multiple process states, including: + • StartState: Initiates the workflow. + • FireAndForgetProcessState: Dispatches a `Command` and immediately advances to the next state. + • RequestReactionProcessState: Dispatches a `Command` and waits for an event response before advancing. + • ChoiceProcessState: Evaluates conditions using the `Specification` Pattern and chooses the next `Command` to Dispatch based on the evaluation. + • WaitState: Suspends execution for a specified TimeSpan before advancing. + 3. Uses a CommandProcessor for routing commands and events to appropriate handlers. + 4. Can be passed events, and uses the correlation IDs to match events to specific workflow instances and advance the workflow accordingly. + +The Specification Pattern in ChoiceProcessState allows flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. + +We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, such as process states, UIs for workflows will be a feature of later releases. + +## Consequences + +Positive Consequences + + 1. Simplicity: Providing orchestration for a workflow, which is easier to understand + 2. Modularity: It is possible to extend the `Mediator' relativey easy by adding new process states. + +Negative Consequences -## Consequences \ No newline at end of file + 1. Increased Brighter scope: Previously we had assumed that developers would use an off-the-shelf workflow solution like [Stateless](https://github.com/nblumhardt/stateless) or [Workflow Core]. The decision to provide our own workflow, to orchestrate via CommandProcessor means that we increase our scope to include the complexity of workflow management. \ No newline at end of file diff --git a/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj new file mode 100644 index 0000000000..95a56b3eb3 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj @@ -0,0 +1,15 @@ + + + The Command Dispatcher pattern is an addition to the Command design pattern that decouples the dispatcher for a service from its execution. A Command Dispatcher component maps commands to handlers. A Command Processor pattern provides a framework for handling orthogonal concerns such as logging, timeouts, or circuit breakers + Ian Cooper + netstandard2.0;net6.0;net8.0 + Command;Event;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability + latest + enable + + + + + + + From d92cb84d3d1fd4018659f7c1cac58ca6dbe7aa0f Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 30 Oct 2024 21:39:29 +0000 Subject: [PATCH 03/44] feat: required workflow classes to write setup of rist mediator test --- Brighter.sln | 26 +++--- .../IMediatorState.cs | 30 +++++++ src/Paramore.Brighter.Workflow/IStateStore.cs | 33 ++++++++ .../InMemoryStateStore.cs | 43 ++++++++++ src/Paramore.Brighter.Workflow/Mediator.cs | 31 ++++++++ .../Paramore.Brighter.Workflow.csproj} | 4 + .../WorkflowState.cs | 79 +++++++++++++++++++ .../Paramore.Brighter.Core.Tests.csproj | 2 + .../Workflows/TestDoubles/MyCommand.cs | 40 ++++++++++ .../Workflows/TestDoubles/MyCommandHandler.cs | 52 ++++++++++++ .../When_running_a_single_step_workflow.cs | 31 ++++++++ 11 files changed, 358 insertions(+), 13 deletions(-) create mode 100644 src/Paramore.Brighter.Workflow/IMediatorState.cs create mode 100644 src/Paramore.Brighter.Workflow/IStateStore.cs create mode 100644 src/Paramore.Brighter.Workflow/InMemoryStateStore.cs create mode 100644 src/Paramore.Brighter.Workflow/Mediator.cs rename src/{Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj => Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj} (89%) create mode 100644 src/Paramore.Brighter.Workflow/WorkflowState.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs diff --git a/Brighter.sln b/Brighter.sln index 26e7fccafb..cce77903ba 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,7 +315,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Mediator", "src\Paramore.Brighter.Mediator\Paramore.Brighter.Mediator.csproj", "{7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Workflow", "src\Paramore.Brighter.Workflow\Paramore.Brighter.Workflow.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1767,18 +1767,18 @@ Global {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|Mixed Platforms.Build.0 = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.ActiveCfg = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.Build.0 = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Debug|x86.Build.0 = Debug|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Any CPU.Build.0 = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|x86.ActiveCfg = Release|Any CPU - {7B7FA2D0-0CFB-4044-95C6-2EDBB2AEF5D8}.Release|x86.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|x86.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|x86.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Any CPU.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|x86.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Paramore.Brighter.Workflow/IMediatorState.cs b/src/Paramore.Brighter.Workflow/IMediatorState.cs new file mode 100644 index 0000000000..8a57fd096b --- /dev/null +++ b/src/Paramore.Brighter.Workflow/IMediatorState.cs @@ -0,0 +1,30 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +namespace Paramore.Brighter.Workflow; + +public interface IMediatorState +{ + IMediatorState Handle(WorkflowState state, IAmACommandProcessor commandProcessor); +} diff --git a/src/Paramore.Brighter.Workflow/IStateStore.cs b/src/Paramore.Brighter.Workflow/IStateStore.cs new file mode 100644 index 0000000000..8ba206d7ee --- /dev/null +++ b/src/Paramore.Brighter.Workflow/IStateStore.cs @@ -0,0 +1,33 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Workflow; + +public interface IStateStore +{ + void SaveState(WorkflowState state); + WorkflowState GetState(Guid id); +} diff --git a/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs b/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs new file mode 100644 index 0000000000..9fa8980662 --- /dev/null +++ b/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs @@ -0,0 +1,43 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Workflow; + +public class InMemoryStateStore : IStateStore +{ + private readonly Dictionary _states = new(); + + public void SaveState(WorkflowState state) + { + _states[state.Id] = state; + } + + public WorkflowState GetState(Guid id) + { + return _states.TryGetValue(id, out var state) ? state : new NullWorkflowState(); + } +} diff --git a/src/Paramore.Brighter.Workflow/Mediator.cs b/src/Paramore.Brighter.Workflow/Mediator.cs new file mode 100644 index 0000000000..3e04604602 --- /dev/null +++ b/src/Paramore.Brighter.Workflow/Mediator.cs @@ -0,0 +1,31 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +namespace Paramore.Brighter.Workflow; + +public class Mediator(IAmACommandProcessor commandProcessor, IStateStore stateStore) +{ + private readonly IAmACommandProcessor _commandProcessor = commandProcessor; + private readonly IStateStore _stateStore = stateStore; +} diff --git a/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj b/src/Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj similarity index 89% rename from src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj rename to src/Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj index 95a56b3eb3..12a725bd9d 100644 --- a/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj +++ b/src/Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/src/Paramore.Brighter.Workflow/WorkflowState.cs b/src/Paramore.Brighter.Workflow/WorkflowState.cs new file mode 100644 index 0000000000..91d1a5d40c --- /dev/null +++ b/src/Paramore.Brighter.Workflow/WorkflowState.cs @@ -0,0 +1,79 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Workflow; + +public enum WorkflowStep +{ + Start, + FireAndForget, + RequestReaction, + Publish, + Choice, + Wait, + Failure, + Finish +} + +/// +/// WorkflowState represents the current state of the workflow and tracks if it’s awaiting a response. +/// +public class WorkflowState +{ + /// + /// The id of the workflow, used to save-retrieve it from storage + /// + public Guid Id { get; private set; } = Guid.NewGuid(); + + /// + /// What is the current state of the workflow + /// + public WorkflowStep CurrentStep { get; set; } + + /// + /// Is the workflow currently awaiting an event response + /// + public bool AwaitingResponse { get; set; } = false; + + /// + /// Constructs a new Workflows instance in a give state + /// + /// + public WorkflowState(WorkflowStep initialStep) + { + CurrentStep = initialStep; + } + + /// + /// Default constructor, used for serdes + /// + public WorkflowState() { } +} + +public class NullWorkflowState : WorkflowState +{ + public NullWorkflowState() : base(WorkflowStep.Finish) { } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index 4e7e7214f8..a75d2575a1 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -5,6 +5,8 @@ + + diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs new file mode 100644 index 0000000000..02e3239199 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs @@ -0,0 +1,40 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Mediator.TestDoubles +{ + public class MyCommand : Command + { + public MyCommand() + :base(Guid.NewGuid()) + + {} + + public string Value { get; set; } + public bool WasCancelled { get; set; } + public bool TaskCompleted { get; set; } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs new file mode 100644 index 0000000000..63c344e97c --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs @@ -0,0 +1,52 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +namespace Paramore.Brighter.Core.Tests.Mediator.TestDoubles +{ + internal class MyCommandHandler : RequestHandler + { + private MyCommand _command; + + public MyCommandHandler() + { + _command = null; + } + + public override MyCommand Handle(MyCommand command) + { + LogCommand(command); + return base.Handle(command); + } + + public bool ShouldReceive(MyCommand expectedCommand) + { + return (_command != null) && (expectedCommand.Id == _command.Id); + } + + private void LogCommand(MyCommand request) + { + _command = request; + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs new file mode 100644 index 0000000000..e0aba80da8 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -0,0 +1,31 @@ +using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; +using Paramore.Brighter.Workflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorOneStepFlowTests +{ + private readonly MyCommandHandler _myCommandHandler; + + public MediatorOneStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + _myCommandHandler = new MyCommandHandler(); + var handlerFactory = new SimpleHandlerFactorySync(_ => _myCommandHandler); + + var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var stateStore = new InMemoryStateStore(); + var mediator = new Workflow.Mediator(commandProcessor, stateStore); + } + + [Fact] + public void When_running_a_single_step_workflow() + { + + } +} From 556bb7a08722727cb0ded91f3fdccc2d98cdd539 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 31 Oct 2024 18:21:51 +0000 Subject: [PATCH 04/44] chore: safety check whilst releasing V9 and V10; does not build --- docs/adr/0022-add-a-mediator.md | 2 + src/Paramore.Brighter.Workflow/IStateStore.cs | 15 +++++++- .../InMemoryStateStore.cs | 4 +- src/Paramore.Brighter.Workflow/Mediator.cs | 7 +++- src/Paramore.Brighter.Workflow/Step.cs | 37 +++++++++++++++++++ .../WorkflowState.cs | 29 +-------------- .../When_running_a_single_step_workflow.cs | 3 +- 7 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 src/Paramore.Brighter.Workflow/Step.cs diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md index 512224e74d..79f1cc8227 100644 --- a/docs/adr/0022-add-a-mediator.md +++ b/docs/adr/0022-add-a-mediator.md @@ -24,6 +24,8 @@ A particular reference for the requirements for this work is [AWS step functions This approach is intended to enable flexible, event-driven workflows that can handle various business processes and requirements, including asynchronous event handling and conditional branching. +We are also influenced by the [Arazzo Specification](https://github.com/OAI/Arazzo-Specification/blob/main/versions/1.0.0.md) for defining workflows from AsyncAPI. + ## Decision diff --git a/src/Paramore.Brighter.Workflow/IStateStore.cs b/src/Paramore.Brighter.Workflow/IStateStore.cs index 8ba206d7ee..13184edf9d 100644 --- a/src/Paramore.Brighter.Workflow/IStateStore.cs +++ b/src/Paramore.Brighter.Workflow/IStateStore.cs @@ -26,8 +26,21 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Workflow; +/// +/// Used to store the state of a workflow +/// public interface IStateStore { + /// + /// Saves the workflow state + /// + /// The workflow state void SaveState(WorkflowState state); - WorkflowState GetState(Guid id); + + /// + /// Retrieves a workflow via its Id + /// + /// The id of the workflow + /// if found, the workflow, otherwise null + WorkflowState? GetState(Guid id); } diff --git a/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs b/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs index 9fa8980662..087e4bbe78 100644 --- a/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs +++ b/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs @@ -36,8 +36,8 @@ public void SaveState(WorkflowState state) _states[state.Id] = state; } - public WorkflowState GetState(Guid id) + public WorkflowState? GetState(Guid id) { - return _states.TryGetValue(id, out var state) ? state : new NullWorkflowState(); + return _states.TryGetValue(id, out var state) ? state : null; } } diff --git a/src/Paramore.Brighter.Workflow/Mediator.cs b/src/Paramore.Brighter.Workflow/Mediator.cs index 3e04604602..957dea5b3a 100644 --- a/src/Paramore.Brighter.Workflow/Mediator.cs +++ b/src/Paramore.Brighter.Workflow/Mediator.cs @@ -22,10 +22,15 @@ THE SOFTWARE. */ #endregion +using System.Collections.Generic; + namespace Paramore.Brighter.Workflow; -public class Mediator(IAmACommandProcessor commandProcessor, IStateStore stateStore) +public class Mediator(IList steps, IAmACommandProcessor commandProcessor, IStateStore stateStore) { private readonly IAmACommandProcessor _commandProcessor = commandProcessor; private readonly IStateStore _stateStore = stateStore; + + + } diff --git a/src/Paramore.Brighter.Workflow/Step.cs b/src/Paramore.Brighter.Workflow/Step.cs new file mode 100644 index 0000000000..05ffa7d507 --- /dev/null +++ b/src/Paramore.Brighter.Workflow/Step.cs @@ -0,0 +1,37 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +namespace Paramore.Brighter.Workflow; + +public enum StepType +{ + Choice, + Failure, + FireAndForget, + Publish, + RequestReaction, + Wait +} + +public record struct Step(string Description, bool End, string Name, StepType Type); diff --git a/src/Paramore.Brighter.Workflow/WorkflowState.cs b/src/Paramore.Brighter.Workflow/WorkflowState.cs index 91d1a5d40c..fc0dc21e38 100644 --- a/src/Paramore.Brighter.Workflow/WorkflowState.cs +++ b/src/Paramore.Brighter.Workflow/WorkflowState.cs @@ -26,18 +26,6 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Workflow; -public enum WorkflowStep -{ - Start, - FireAndForget, - RequestReaction, - Publish, - Choice, - Wait, - Failure, - Finish -} - /// /// WorkflowState represents the current state of the workflow and tracks if it’s awaiting a response. /// @@ -51,7 +39,7 @@ public class WorkflowState /// /// What is the current state of the workflow /// - public WorkflowStep CurrentStep { get; set; } + public Step CurrentStep { get; set; } /// /// Is the workflow currently awaiting an event response @@ -59,21 +47,8 @@ public class WorkflowState public bool AwaitingResponse { get; set; } = false; /// - /// Constructs a new Workflows instance in a give state - /// - /// - public WorkflowState(WorkflowStep initialStep) - { - CurrentStep = initialStep; - } - - /// - /// Default constructor, used for serdes + /// Constructs a new WorkflowState /// public WorkflowState() { } } -public class NullWorkflowState : WorkflowState -{ - public NullWorkflowState() : base(WorkflowStep.Finish) { } -} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index e0aba80da8..103f837222 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -8,6 +8,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { private readonly MyCommandHandler _myCommandHandler; + private readonly Workflow.Mediator _mediator; public MediatorOneStepFlowTests() { @@ -20,7 +21,7 @@ public MediatorOneStepFlowTests() PipelineBuilder.ClearPipelineCache(); var stateStore = new InMemoryStateStore(); - var mediator = new Workflow.Mediator(commandProcessor, stateStore); + _mediator = new Workflow.Mediator([new Step("Test of Workflow", true, "Test", StepType.FireAndForget], commandProcessor, stateStore); } [Fact] From 3ed0e7e6672019aeb438c4aa377f04341719f4a0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 1 Nov 2024 17:28:33 +0000 Subject: [PATCH 05/44] chore: safety checkin --- .../FireAndForgetAction.cs | 11 +++++++++ .../{IMediatorState.cs => IWorkflowAction.cs} | 4 ++-- src/Paramore.Brighter.Workflow/Mediator.cs | 24 +++++++++++++++---- src/Paramore.Brighter.Workflow/Step.cs | 12 +--------- .../WorkflowState.cs | 14 +++++++---- .../Paramore.Brighter.Core.Tests.csproj | 1 - .../When_running_a_single_step_workflow.cs | 13 ++++++++-- 7 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 src/Paramore.Brighter.Workflow/FireAndForgetAction.cs rename src/Paramore.Brighter.Workflow/{IMediatorState.cs => IWorkflowAction.cs} (90%) diff --git a/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs b/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs new file mode 100644 index 0000000000..4962668c9e --- /dev/null +++ b/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs @@ -0,0 +1,11 @@ +using System; + +namespace Paramore.Brighter.Workflow; + +public class FireAndForgetAction(Func makeCommand) : IWorkflowAction where TRequest : class, IRequest +{ + public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) + { + commandProcessor.Send(makeCommand(state)); + } +} diff --git a/src/Paramore.Brighter.Workflow/IMediatorState.cs b/src/Paramore.Brighter.Workflow/IWorkflowAction.cs similarity index 90% rename from src/Paramore.Brighter.Workflow/IMediatorState.cs rename to src/Paramore.Brighter.Workflow/IWorkflowAction.cs index 8a57fd096b..9c98197d19 100644 --- a/src/Paramore.Brighter.Workflow/IMediatorState.cs +++ b/src/Paramore.Brighter.Workflow/IWorkflowAction.cs @@ -24,7 +24,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Workflow; -public interface IMediatorState +public interface IWorkflowAction { - IMediatorState Handle(WorkflowState state, IAmACommandProcessor commandProcessor); + void Handle(WorkflowState state, IAmACommandProcessor commandProcessor); } diff --git a/src/Paramore.Brighter.Workflow/Mediator.cs b/src/Paramore.Brighter.Workflow/Mediator.cs index 957dea5b3a..00be191e4b 100644 --- a/src/Paramore.Brighter.Workflow/Mediator.cs +++ b/src/Paramore.Brighter.Workflow/Mediator.cs @@ -22,15 +22,31 @@ THE SOFTWARE. */ #endregion +using System; using System.Collections.Generic; namespace Paramore.Brighter.Workflow; public class Mediator(IList steps, IAmACommandProcessor commandProcessor, IStateStore stateStore) { - private readonly IAmACommandProcessor _commandProcessor = commandProcessor; private readonly IStateStore _stateStore = stateStore; - - - + private WorkflowState? _state; + + + public void RunWorkFlow() + { + if (_state == null) + throw new InvalidOperationException("Workflow has not been initialized"); + + foreach (var step in steps) + { + step.Action.Handle(_state, commandProcessor); + if (step.End) break; + } + } + + public void InitializeWorkflow(WorkflowState workflowState) + { + _state = workflowState; + } } diff --git a/src/Paramore.Brighter.Workflow/Step.cs b/src/Paramore.Brighter.Workflow/Step.cs index 05ffa7d507..0b35b21ae5 100644 --- a/src/Paramore.Brighter.Workflow/Step.cs +++ b/src/Paramore.Brighter.Workflow/Step.cs @@ -24,14 +24,4 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Workflow; -public enum StepType -{ - Choice, - Failure, - FireAndForget, - Publish, - RequestReaction, - Wait -} - -public record struct Step(string Description, bool End, string Name, StepType Type); +public record struct Step(string Name, IWorkflowAction Action, string Description, bool End); diff --git a/src/Paramore.Brighter.Workflow/WorkflowState.cs b/src/Paramore.Brighter.Workflow/WorkflowState.cs index fc0dc21e38..4075820844 100644 --- a/src/Paramore.Brighter.Workflow/WorkflowState.cs +++ b/src/Paramore.Brighter.Workflow/WorkflowState.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Generic; namespace Paramore.Brighter.Workflow; @@ -32,9 +33,9 @@ namespace Paramore.Brighter.Workflow; public class WorkflowState { /// - /// The id of the workflow, used to save-retrieve it from storage + /// Is the workflow currently awaiting an event response /// - public Guid Id { get; private set; } = Guid.NewGuid(); + public bool AwaitingResponse { get; set; } = false; /// /// What is the current state of the workflow @@ -42,9 +43,14 @@ public class WorkflowState public Step CurrentStep { get; set; } /// - /// Is the workflow currently awaiting an event response + /// Used to store data that is passed between steps in the workflow /// - public bool AwaitingResponse { get; set; } = false; + public Dictionary Bag { get; set; } = new(); + + /// + /// The id of the workflow, used to save-retrieve it from storage + /// + public Guid Id { get; private set; } = Guid.NewGuid(); /// /// Constructs a new WorkflowState diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index a75d2575a1..cc17023152 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -5,7 +5,6 @@ - diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 103f837222..2213063b42 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,4 +1,5 @@ -using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; +using System.Collections.Generic; +using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; using Paramore.Brighter.Workflow; using Polly.Registry; using Xunit; @@ -21,12 +22,20 @@ public MediatorOneStepFlowTests() PipelineBuilder.ClearPipelineCache(); var stateStore = new InMemoryStateStore(); - _mediator = new Workflow.Mediator([new Step("Test of Workflow", true, "Test", StepType.FireAndForget], commandProcessor, stateStore); + _mediator = new Workflow.Mediator( + [new Step("Test of Workflow", new FireAndForgetAction((state) => new MyCommand{Value = (state.Bag["MyState"] as string)!}), "Test", false)], + commandProcessor, + stateStore + ); } [Fact] public void When_running_a_single_step_workflow() { + _mediator.InitializeWorkflow(new WorkflowState() {Bag = new Dictionary {{"MyState", "Test"}}}); + _mediator.RunWorkFlow(); + //_should_send_a_command_to_the_command_processor + Assert.True(_myCommandHandler.Handled); } } From e0c9c85f396da3ca65d5b04d49886f5964b5a57c Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 1 Nov 2024 22:20:18 +0000 Subject: [PATCH 06/44] feat: add fire and forget action --- .../FireAndForgetAction.cs | 4 ++-- .../Workflows/TestDoubles/MyCommandHandler.cs | 16 +++------------- .../When_running_a_single_step_workflow.cs | 11 +++++++---- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs b/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs index 4962668c9e..4afae1d7fe 100644 --- a/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs +++ b/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs @@ -2,10 +2,10 @@ namespace Paramore.Brighter.Workflow; -public class FireAndForgetAction(Func makeCommand) : IWorkflowAction where TRequest : class, IRequest +public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest { public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) { - commandProcessor.Send(makeCommand(state)); + commandProcessor.Send(requestFactory(state)); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs index 63c344e97c..044cc3230d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs @@ -26,27 +26,17 @@ namespace Paramore.Brighter.Core.Tests.Mediator.TestDoubles { internal class MyCommandHandler : RequestHandler { - private MyCommand _command; - - public MyCommandHandler() - { - _command = null; - } - + public MyCommand? ReceivedCommand { get; private set; } + public override MyCommand Handle(MyCommand command) { LogCommand(command); return base.Handle(command); } - public bool ShouldReceive(MyCommand expectedCommand) - { - return (_command != null) && (expectedCommand.Id == _command.Id); - } - private void LogCommand(MyCommand request) { - _command = request; + ReceivedCommand = request; } } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 2213063b42..0dec924ba9 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using FluentAssertions; using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; using Paramore.Brighter.Workflow; using Polly.Registry; @@ -23,7 +24,10 @@ public MediatorOneStepFlowTests() var stateStore = new InMemoryStateStore(); _mediator = new Workflow.Mediator( - [new Step("Test of Workflow", new FireAndForgetAction((state) => new MyCommand{Value = (state.Bag["MyState"] as string)!}), "Test", false)], + [new Step("Test of Workflow", + new FireAndForgetAction((state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}), + "Test", + false)], commandProcessor, stateStore ); @@ -32,10 +36,9 @@ public MediatorOneStepFlowTests() [Fact] public void When_running_a_single_step_workflow() { - _mediator.InitializeWorkflow(new WorkflowState() {Bag = new Dictionary {{"MyState", "Test"}}}); + _mediator.InitializeWorkflow(new WorkflowState() {Bag = new Dictionary {{"MyValue", "Test"}}}); _mediator.RunWorkFlow(); - //_should_send_a_command_to_the_command_processor - Assert.True(_myCommandHandler.Handled); + _myCommandHandler.ReceivedCommand?.Value.Should().Be( "Test"); } } From f8609b81200d25eb2012488aeef4c0f600a557a2 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 1 Nov 2024 23:01:36 +0000 Subject: [PATCH 07/44] feat: add requestreply outline --- .../FireAndForgetAction.cs | 11 --- .../WorkflowActions.cs | 27 ++++++++ .../WorkflowState.cs | 10 +-- .../Workflows/TestDoubles/MyEvent.cs | 67 +++++++++++++++++++ .../Workflows/TestDoubles/MyEventHandler.cs | 17 +++-- .../When_running_a_workflow_with_reply.cs | 42 ++++++++++++ 6 files changed, 154 insertions(+), 20 deletions(-) delete mode 100644 src/Paramore.Brighter.Workflow/FireAndForgetAction.cs create mode 100644 src/Paramore.Brighter.Workflow/WorkflowActions.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs rename src/Paramore.Brighter.Workflow/IWorkflowAction.cs => tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs (65%) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs diff --git a/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs b/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs deleted file mode 100644 index 4afae1d7fe..0000000000 --- a/src/Paramore.Brighter.Workflow/FireAndForgetAction.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Paramore.Brighter.Workflow; - -public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest -{ - public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) - { - commandProcessor.Send(requestFactory(state)); - } -} diff --git a/src/Paramore.Brighter.Workflow/WorkflowActions.cs b/src/Paramore.Brighter.Workflow/WorkflowActions.cs new file mode 100644 index 0000000000..7c482805bd --- /dev/null +++ b/src/Paramore.Brighter.Workflow/WorkflowActions.cs @@ -0,0 +1,27 @@ +using System; + +namespace Paramore.Brighter.Workflow; + +public interface IWorkflowAction +{ + void Handle(WorkflowState state, IAmACommandProcessor commandProcessor); +} + +public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest +{ + public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) + { + commandProcessor.Send(requestFactory(state)); + } +} + +public class RequestAndReplyAction(Func requestFactory, Action replyFactory) + : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest +{ + public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) + { + commandProcessor.Send(requestFactory(state)); + + state.PendingResponses.Add(typeof(TReply), (reply, state) => replyFactory(reply, state)); + } +} diff --git a/src/Paramore.Brighter.Workflow/WorkflowState.cs b/src/Paramore.Brighter.Workflow/WorkflowState.cs index 4075820844..62fa605e42 100644 --- a/src/Paramore.Brighter.Workflow/WorkflowState.cs +++ b/src/Paramore.Brighter.Workflow/WorkflowState.cs @@ -38,20 +38,22 @@ public class WorkflowState public bool AwaitingResponse { get; set; } = false; /// - /// What is the current state of the workflow + /// Used to store data that is passed between steps in the workflow /// - public Step CurrentStep { get; set; } + public Dictionary Bag { get; set; } = new(); /// - /// Used to store data that is passed between steps in the workflow + /// What is the current state of the workflow /// - public Dictionary Bag { get; set; } = new(); + public Step CurrentStep { get; set; } /// /// The id of the workflow, used to save-retrieve it from storage /// public Guid Id { get; private set; } = Guid.NewGuid(); + public Dictionary> PendingResponses { get; private set; } = new(); + /// /// Constructs a new WorkflowState /// diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs new file mode 100644 index 0000000000..fe8c2e76be --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs @@ -0,0 +1,67 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyEvent : Event, IEquatable + { + public int Data { get; set; } + + public MyEvent() : base(Guid.NewGuid()) + { + } + + public bool Equals(MyEvent other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Data == other.Data; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MyEvent)obj); + } + + public override int GetHashCode() + { + return Data; + } + + public static bool operator ==(MyEvent left, MyEvent right) + { + return Equals(left, right); + } + + public static bool operator !=(MyEvent left, MyEvent right) + { + return !Equals(left, right); + } + } +} diff --git a/src/Paramore.Brighter.Workflow/IWorkflowAction.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs similarity index 65% rename from src/Paramore.Brighter.Workflow/IWorkflowAction.cs rename to tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs index 9c98197d19..34abd14a15 100644 --- a/src/Paramore.Brighter.Workflow/IWorkflowAction.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs @@ -1,6 +1,6 @@ -#region Licence +#region Licence /* The MIT License (MIT) -Copyright © 2024 Ian Cooper +Copyright © 2014 Ian Cooper Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal @@ -22,9 +22,16 @@ THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.Workflow; +using System.Collections.Generic; -public interface IWorkflowAction +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - void Handle(WorkflowState state, IAmACommandProcessor commandProcessor); + internal class MyEventHandler(IDictionary receivedMessages) : RequestHandler + { + public override CommandProcessors.TestDoubles.MyEvent Handle(CommandProcessors.TestDoubles.MyEvent @event) + { + receivedMessages.Add(nameof(MyEventHandler), @event.Id); + return base.Handle(@event); + } + } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs new file mode 100644 index 0000000000..a2a96add45 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -0,0 +1,42 @@ +using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Workflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorTwoStepFlowTests +{ + private readonly MyCommandHandler _myCommandHandler; + private readonly Workflow.Mediator _mediator; + + public MediatorTwoStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + _myCommandHandler = new MyCommandHandler(); + var handlerFactory = new SimpleHandlerFactorySync(_ => _myCommandHandler); + + var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var stateStore = new InMemoryStateStore(); + _mediator = new Workflow.Mediator( + [new Step("Test of Workflow", + new RequestAndReplyAction( + (state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}, + (reply, state) => state.Bag.Add("MyReply", ((MyEvent) reply).Data)), + "Test", + false)], + commandProcessor, + stateStore + ); + } + + [Fact] + public void When_running_a_workflow_with_reply() + { + + } +} From 255d08655cbe51b3eba1877160e58c3e0dee5f42 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 2 Nov 2024 08:56:31 +0000 Subject: [PATCH 08/44] fix: folder name causing issues on MacOS which believes it is an application --- Brighter.sln | 2 +- .../IStateStore.cs | 2 +- .../InMemoryStateStore.cs | 2 +- .../Mediator.cs | 2 +- .../Paramore.Brighter.MediatorWorkflow.csproj} | 0 .../Step.cs | 2 +- .../WorkflowActions.cs | 2 +- .../WorkflowState.cs | 2 +- .../Paramore.Brighter.Core.Tests.csproj | 2 +- .../Workflows/When_running_a_single_step_workflow.cs | 6 +++--- .../Workflows/When_running_a_workflow_with_reply.cs | 6 +++--- 11 files changed, 14 insertions(+), 14 deletions(-) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/IStateStore.cs (97%) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/InMemoryStateStore.cs (97%) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/Mediator.cs (97%) rename src/{Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj => Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj} (100%) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/Step.cs (96%) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/WorkflowActions.cs (95%) rename src/{Paramore.Brighter.Workflow => Paramore.Brighter.MediatorWorkflow}/WorkflowState.cs (97%) diff --git a/Brighter.sln b/Brighter.sln index cce77903ba..ebeb32aa21 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,7 +315,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Workflow", "src\Paramore.Brighter.Workflow\Paramore.Brighter.Workflow.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MediatorWorkflow", "src\Paramore.Brighter.MediatorWorkflow\Paramore.Brighter.MediatorWorkflow.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Paramore.Brighter.Workflow/IStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs similarity index 97% rename from src/Paramore.Brighter.Workflow/IStateStore.cs rename to src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs index 13184edf9d..c835df5cd6 100644 --- a/src/Paramore.Brighter.Workflow/IStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs @@ -24,7 +24,7 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; /// /// Used to store the state of a workflow diff --git a/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs similarity index 97% rename from src/Paramore.Brighter.Workflow/InMemoryStateStore.cs rename to src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs index 087e4bbe78..12d9bcdbf7 100644 --- a/src/Paramore.Brighter.Workflow/InMemoryStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs @@ -25,7 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; public class InMemoryStateStore : IStateStore { diff --git a/src/Paramore.Brighter.Workflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs similarity index 97% rename from src/Paramore.Brighter.Workflow/Mediator.cs rename to src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index 00be191e4b..e3ebb94e3e 100644 --- a/src/Paramore.Brighter.Workflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -25,7 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; public class Mediator(IList steps, IAmACommandProcessor commandProcessor, IStateStore stateStore) { diff --git a/src/Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj b/src/Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj similarity index 100% rename from src/Paramore.Brighter.Workflow/Paramore.Brighter.Workflow.csproj rename to src/Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj diff --git a/src/Paramore.Brighter.Workflow/Step.cs b/src/Paramore.Brighter.MediatorWorkflow/Step.cs similarity index 96% rename from src/Paramore.Brighter.Workflow/Step.cs rename to src/Paramore.Brighter.MediatorWorkflow/Step.cs index 0b35b21ae5..6031a3a210 100644 --- a/src/Paramore.Brighter.Workflow/Step.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Step.cs @@ -22,6 +22,6 @@ THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; public record struct Step(string Name, IWorkflowAction Action, string Description, bool End); diff --git a/src/Paramore.Brighter.Workflow/WorkflowActions.cs b/src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs similarity index 95% rename from src/Paramore.Brighter.Workflow/WorkflowActions.cs rename to src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs index 7c482805bd..e93d58ad5c 100644 --- a/src/Paramore.Brighter.Workflow/WorkflowActions.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs @@ -1,6 +1,6 @@ using System; -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; public interface IWorkflowAction { diff --git a/src/Paramore.Brighter.Workflow/WorkflowState.cs b/src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs similarity index 97% rename from src/Paramore.Brighter.Workflow/WorkflowState.cs rename to src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs index 62fa605e42..56111f268c 100644 --- a/src/Paramore.Brighter.Workflow/WorkflowState.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs @@ -25,7 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; -namespace Paramore.Brighter.Workflow; +namespace Paramore.Brighter.MediatorWorkflow; /// /// WorkflowState represents the current state of the workflow and tracks if it’s awaiting a response. diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index cc17023152..340320e84a 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 0dec924ba9..60c4faf91d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using FluentAssertions; using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; -using Paramore.Brighter.Workflow; +using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; using Xunit; @@ -10,7 +10,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { private readonly MyCommandHandler _myCommandHandler; - private readonly Workflow.Mediator _mediator; + private readonly MediatorWorkflow.Mediator _mediator; public MediatorOneStepFlowTests() { @@ -23,7 +23,7 @@ public MediatorOneStepFlowTests() PipelineBuilder.ClearPipelineCache(); var stateStore = new InMemoryStateStore(); - _mediator = new Workflow.Mediator( + _mediator = new MediatorWorkflow.Mediator( [new Step("Test of Workflow", new FireAndForgetAction((state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}), "Test", diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index a2a96add45..d22cad54ec 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,6 +1,6 @@ using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.Workflow; +using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; using Xunit; @@ -9,7 +9,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorTwoStepFlowTests { private readonly MyCommandHandler _myCommandHandler; - private readonly Workflow.Mediator _mediator; + private readonly MediatorWorkflow.Mediator _mediator; public MediatorTwoStepFlowTests() { @@ -22,7 +22,7 @@ public MediatorTwoStepFlowTests() PipelineBuilder.ClearPipelineCache(); var stateStore = new InMemoryStateStore(); - _mediator = new Workflow.Mediator( + _mediator = new MediatorWorkflow.Mediator( [new Step("Test of Workflow", new RequestAndReplyAction( (state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}, From a321e30cff965fd7a98946aa0398e232d849c485 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 4 Nov 2024 21:20:43 +0000 Subject: [PATCH 09/44] feat: modifications to step and workflow responsibility --- .../IStateStore.cs | 4 +- .../InMemoryStateStore.cs | 6 +- .../Mediator.cs | 49 +++++++++++----- .../Step.cs | 27 --------- .../{WorkflowState.cs => Workflow.cs} | 35 +++++++----- .../WorkflowActions.cs | 27 --------- .../Workflows.cs | 36 ++++++++++++ src/Paramore.Brighter/Event.cs | 6 ++ .../Workflows/TestDoubles/MyCommand.cs | 2 +- .../Workflows/TestDoubles/MyCommandHandler.cs | 8 ++- .../Workflows/TestDoubles/MyEventHandler.cs | 6 +- .../When_running_a_single_step_workflow.cs | 37 ++++++------ .../When_running_a_two_step_workflow.cs | 56 +++++++++++++++++++ .../When_running_a_workflow_with_reply.cs | 42 +++++++------- 14 files changed, 209 insertions(+), 132 deletions(-) delete mode 100644 src/Paramore.Brighter.MediatorWorkflow/Step.cs rename src/Paramore.Brighter.MediatorWorkflow/{WorkflowState.cs => Workflow.cs} (77%) delete mode 100644 src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs create mode 100644 src/Paramore.Brighter.MediatorWorkflow/Workflows.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs index c835df5cd6..9fbae12228 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs @@ -35,12 +35,12 @@ public interface IStateStore /// Saves the workflow state /// /// The workflow state - void SaveState(WorkflowState state); + void SaveState(Workflow state); /// /// Retrieves a workflow via its Id /// /// The id of the workflow /// if found, the workflow, otherwise null - WorkflowState? GetState(Guid id); + Workflow? GetState(Guid id); } diff --git a/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs index 12d9bcdbf7..492d69f94b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs @@ -29,14 +29,14 @@ namespace Paramore.Brighter.MediatorWorkflow; public class InMemoryStateStore : IStateStore { - private readonly Dictionary _states = new(); + private readonly Dictionary _states = new(); - public void SaveState(WorkflowState state) + public void SaveState(Workflow state) { _states[state.Id] = state; } - public WorkflowState? GetState(Guid id) + public Workflow? GetState(Guid id) { return _states.TryGetValue(id, out var state) ? state : null; } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index e3ebb94e3e..30fd413162 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -27,26 +27,45 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; -public class Mediator(IList steps, IAmACommandProcessor commandProcessor, IStateStore stateStore) +/// +/// The mediator orchestrates a workflow, executing each step in the sequence. +/// +/// +/// +/// +public class Mediator(IAmACommandProcessor commandProcessor) { - private readonly IStateStore _stateStore = stateStore; - private WorkflowState? _state; - - - public void RunWorkFlow() + /// + /// + /// Runs the workflow by executing each step in the sequence. + /// + /// Thrown when the workflow has not been initialized. + public void RunWorkFlow(Step? firstStep) { - if (_state == null) - throw new InvalidOperationException("Workflow has not been initialized"); - - foreach (var step in steps) + var step = firstStep; + while (step is not null) { - step.Action.Handle(_state, commandProcessor); - if (step.End) break; + step.Action.Handle(step.Flow, commandProcessor); + step.OnCompletion(); + step = step.Next; } } - public void InitializeWorkflow(WorkflowState workflowState) - { - _state = workflowState; + /// + /// Receives an event and processes it if there is a pending response for the event type. + /// + /// The event to process. + /// Thrown when the workflow has not been initialized. + public void ReceiveWorklowEvent(Event @event) + { + // var state = stateStore.GetState(@event.CorrelationId); + // + // var eventType = @event.GetType(); + // + // if (!state.PendingResponses.TryGetValue(eventType, out Action replyFactory)) + // return; + // + // replyFactory(@event, state); + // state.PendingResponses.Remove(eventType); } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Step.cs b/src/Paramore.Brighter.MediatorWorkflow/Step.cs deleted file mode 100644 index 6031a3a210..0000000000 --- a/src/Paramore.Brighter.MediatorWorkflow/Step.cs +++ /dev/null @@ -1,27 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2024 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -namespace Paramore.Brighter.MediatorWorkflow; - -public record struct Step(string Name, IWorkflowAction Action, string Description, bool End); diff --git a/src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs similarity index 77% rename from src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs rename to src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 56111f268c..99f6ce470b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/WorkflowState.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -27,36 +27,41 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; +public enum WorkflowState +{ + Ready, + Waiting, + Done +} + /// -/// WorkflowState represents the current state of the workflow and tracks if it’s awaiting a response. +/// Workflow represents the current state of the workflow and tracks if it’s awaiting a response. /// -public class WorkflowState +public class Workflow { - /// - /// Is the workflow currently awaiting an event response - /// - public bool AwaitingResponse { get; set; } = false; - /// /// Used to store data that is passed between steps in the workflow /// public Dictionary Bag { get; set; } = new(); - /// - /// What is the current state of the workflow - /// - public Step CurrentStep { get; set; } - /// /// The id of the workflow, used to save-retrieve it from storage /// public Guid Id { get; private set; } = Guid.NewGuid(); - public Dictionary> PendingResponses { get; private set; } = new(); + /// + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives + /// + public Dictionary> PendingResponses { get; private set; } = new(); + + /// + /// Is the workflow currently awaiting an event response + /// + public WorkflowState State { get; set; } = WorkflowState.Ready; /// - /// Constructs a new WorkflowState + /// Constructs a new Workflow /// - public WorkflowState() { } + public Workflow() { } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs b/src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs deleted file mode 100644 index e93d58ad5c..0000000000 --- a/src/Paramore.Brighter.MediatorWorkflow/WorkflowActions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Paramore.Brighter.MediatorWorkflow; - -public interface IWorkflowAction -{ - void Handle(WorkflowState state, IAmACommandProcessor commandProcessor); -} - -public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest -{ - public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) - { - commandProcessor.Send(requestFactory(state)); - } -} - -public class RequestAndReplyAction(Func requestFactory, Action replyFactory) - : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest -{ - public void Handle(WorkflowState state, IAmACommandProcessor commandProcessor) - { - commandProcessor.Send(requestFactory(state)); - - state.PendingResponses.Add(typeof(TReply), (reply, state) => replyFactory(reply, state)); - } -} diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs new file mode 100644 index 0000000000..7d565761b4 --- /dev/null +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -0,0 +1,36 @@ +using System; + +namespace Paramore.Brighter.MediatorWorkflow; + +/// +/// A step in the worfklow. Steps form a singly linked list. +/// +/// The name of the step +/// The type of action we take with the step +/// The workflow that we belong to +/// What is the next step in sequence +public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Workflow Flow, Step? Next); + +public interface IWorkflowAction +{ + void Handle(Workflow state, IAmACommandProcessor commandProcessor); +} + +public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest +{ + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + commandProcessor.Send(requestFactory()); + } +} + +public class RequestAndReplyAction(Func requestFactory, Action replyFactory) + : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest +{ + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + commandProcessor.Send(requestFactory()); + + state.PendingResponses.Add(typeof(TReply), (reply, state) => replyFactory(reply)); + } +} diff --git a/src/Paramore.Brighter/Event.cs b/src/Paramore.Brighter/Event.cs index 642bf5c85c..37e196ed87 100644 --- a/src/Paramore.Brighter/Event.cs +++ b/src/Paramore.Brighter/Event.cs @@ -33,6 +33,12 @@ namespace Paramore.Brighter /// public class Event : IEvent { + /// + /// An event may be the response to a command, in order to find the command that caused the event, we need to know the correlation id + /// In many cases correlation id is the command id + /// + public Guid CorrelationId { get; set; } + /// /// Gets or sets the identifier. /// diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs index 02e3239199..9d450f7b3f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs @@ -24,7 +24,7 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter.Core.Tests.Mediator.TestDoubles +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { public class MyCommand : Command { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs index 044cc3230d..7f486e34aa 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs @@ -22,11 +22,13 @@ THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.Core.Tests.Mediator.TestDoubles +using System.Collections.Generic; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { internal class MyCommandHandler : RequestHandler { - public MyCommand? ReceivedCommand { get; private set; } + public static List ReceivedCommands { get; } = []; public override MyCommand Handle(MyCommand command) { @@ -36,7 +38,7 @@ public override MyCommand Handle(MyCommand command) private void LogCommand(MyCommand request) { - ReceivedCommand = request; + ReceivedCommands.Add(request); } } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs index 34abd14a15..42b04c8a04 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs @@ -26,11 +26,11 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyEventHandler(IDictionary receivedMessages) : RequestHandler + internal class MyEventHandler(MediatorWorkflow.Mediator mediator) : RequestHandler { - public override CommandProcessors.TestDoubles.MyEvent Handle(CommandProcessors.TestDoubles.MyEvent @event) + public override MyEvent Handle(MyEvent @event) { - receivedMessages.Add(nameof(MyEventHandler), @event.Id); + mediator.ReceiveWorklowEvent(@event); return base.Handle(@event); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 60c4faf91d..2eb7a96c1a 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using System.Linq; using FluentAssertions; -using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; using Xunit; @@ -9,36 +10,40 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { - private readonly MyCommandHandler _myCommandHandler; - private readonly MediatorWorkflow.Mediator _mediator; + private readonly Mediator _mediator; + private readonly Step _step; public MediatorOneStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); - _myCommandHandler = new MyCommandHandler(); - var handlerFactory = new SimpleHandlerFactorySync(_ => _myCommandHandler); + MyCommandHandler myCommandHandler = new(); + var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); - var stateStore = new InMemoryStateStore(); - _mediator = new MediatorWorkflow.Mediator( - [new Step("Test of Workflow", - new FireAndForgetAction((state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}), - "Test", - false)], - commandProcessor, - stateStore + var flow = new Workflow() {Bag = new Dictionary {{"MyValue", "Test"}}}; + + _mediator = new Mediator( + commandProcessor + ); + + _step = new Step("Test of Workflow", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), + () => { }, + flow, + null ); } [Fact] public void When_running_a_single_step_workflow() { - _mediator.InitializeWorkflow(new WorkflowState() {Bag = new Dictionary {{"MyValue", "Test"}}}); - _mediator.RunWorkFlow(); + MyCommandHandler.ReceivedCommands.Clear(); + + _mediator.RunWorkFlow(_step); - _myCommandHandler.ReceivedCommand?.Value.Should().Be( "Test"); + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs new file mode 100644 index 0000000000..8e3167f7ba --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorTwoStepFlowTests +{ + private readonly Mediator _mediator; + private readonly Step _stepOne; + + public MediatorTwoStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + MyCommandHandler myCommandHandler = new(); + var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); + + var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var flow = new Workflow() {Bag = new Dictionary {{"MyValue", "Test"}}}; + + _mediator = new Mediator( + commandProcessor + ); + + + var stepTwo = new Step("Test of Workflow Two", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), + () => { }, + flow, + null + ); + + _stepOne = new Step("Test of Workflow One", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), + () => { flow.Bag["MyValue"] = "TestTwo"; }, + flow, + stepTwo + ); + } + + [Fact] + public void When_running_a_single_step_workflow() + { + _mediator.RunWorkFlow(_stepOne); + + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index d22cad54ec..2f03b1e636 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,36 +1,38 @@ -using Paramore.Brighter.Core.Tests.Mediator.TestDoubles; -using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; using Xunit; namespace Paramore.Brighter.Core.Tests.Workflows; -public class MediatorTwoStepFlowTests +public class MediatorReplyStepFlowTests { - private readonly MyCommandHandler _myCommandHandler; - private readonly MediatorWorkflow.Mediator _mediator; + private readonly Mediator _mediator; - public MediatorTwoStepFlowTests() + public MediatorReplyStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); - _myCommandHandler = new MyCommandHandler(); - var handlerFactory = new SimpleHandlerFactorySync(_ => _myCommandHandler); + registry.Register(); + MyCommandHandler myCommandHandler = new(); + var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); - var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); - PipelineBuilder.ClearPipelineCache(); + var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), + new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var flow = new Workflow(); + + var step = new Step("Test of Workflow", + new RequestAndReplyAction( + () => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }, + (reply) => flow.Bag.Add("MyReply", ((MyEvent)reply).Data)), + () => { }, + flow, + null); - var stateStore = new InMemoryStateStore(); - _mediator = new MediatorWorkflow.Mediator( - [new Step("Test of Workflow", - new RequestAndReplyAction( - (state) => new MyCommand{Value = (state.Bag["MyValue"] as string)!}, - (reply, state) => state.Bag.Add("MyReply", ((MyEvent) reply).Data)), - "Test", - false)], - commandProcessor, - stateStore + _mediator = new Mediator( + commandProcessor ); } From 7a879e63686d1871b73506c6794c874a1bf9b7e4 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 5 Nov 2024 17:59:56 +0000 Subject: [PATCH 10/44] feat: add workflow data, over just using the bag --- .../{IStateStore.cs => IAmAWorkflowStore.cs} | 10 ++-- ...StateStore.cs => InMemoryWorkflowStore.cs} | 12 ++-- .../Mediator.cs | 55 +++++++++++++------ .../Workflow.cs | 34 ++++++++++-- .../Workflows.cs | 18 +++--- .../Workflows/TestDoubles/MyEventHandler.cs | 12 +++- .../Workflows/TestDoubles/WorkflowTestData.cs | 9 +++ .../When_running_a_single_step_workflow.cs | 18 +++--- .../When_running_a_two_step_workflow.cs | 24 ++++---- .../When_running_a_workflow_with_reply.cs | 35 ++++++++---- 10 files changed, 153 insertions(+), 74 deletions(-) rename src/Paramore.Brighter.MediatorWorkflow/{IStateStore.cs => IAmAWorkflowStore.cs} (86%) rename src/Paramore.Brighter.MediatorWorkflow/{InMemoryStateStore.cs => InMemoryWorkflowStore.cs} (77%) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs similarity index 86% rename from src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs rename to src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs index 9fbae12228..22476c24f7 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/IStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs @@ -29,18 +29,18 @@ namespace Paramore.Brighter.MediatorWorkflow; /// /// Used to store the state of a workflow /// -public interface IStateStore +public interface IAmAWorkflowStore { /// - /// Saves the workflow state + /// Saves the workflow /// - /// The workflow state - void SaveState(Workflow state); + /// The workflow + void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData; /// /// Retrieves a workflow via its Id /// /// The id of the workflow /// if found, the workflow, otherwise null - Workflow? GetState(Guid id); + Workflow? GetWorkflow(Guid id) ; } diff --git a/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs similarity index 77% rename from src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs rename to src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs index 492d69f94b..94e5ca24dd 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/InMemoryStateStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs @@ -27,17 +27,17 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; -public class InMemoryStateStore : IStateStore +public class InMemoryWorkflowStore : IAmAWorkflowStore { - private readonly Dictionary _states = new(); + private readonly Dictionary _flows = new(); - public void SaveState(Workflow state) + public void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData { - _states[state.Id] = state; + _flows[workflow.Id] = workflow; } - public Workflow? GetState(Guid id) + public Workflow? GetWorkflow(Guid id) { - return _states.TryGetValue(id, out var state) ? state : null; + return _flows.TryGetValue(id, out var state) ? state : null; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index 30fd413162..bfed58b774 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -23,29 +23,45 @@ THE SOFTWARE. */ #endregion using System; -using System.Collections.Generic; namespace Paramore.Brighter.MediatorWorkflow; /// /// The mediator orchestrates a workflow, executing each step in the sequence. /// -/// -/// -/// -public class Mediator(IAmACommandProcessor commandProcessor) +public class Mediator where TData : IAmTheWorkflowData { + private readonly IAmACommandProcessor _commandProcessor; + private readonly IAmAWorkflowStore _workflowStore; + + /// + /// The mediator orchestrates a workflow, executing each step in the sequence. + /// + /// + /// + public Mediator(IAmACommandProcessor commandProcessor, IAmAWorkflowStore workflowStore) + { + _commandProcessor = commandProcessor; + _workflowStore = workflowStore; + } + /// /// /// Runs the workflow by executing each step in the sequence. /// /// Thrown when the workflow has not been initialized. - public void RunWorkFlow(Step? firstStep) - { + public void RunWorkFlow(Step? firstStep) + { var step = firstStep; while (step is not null) { - step.Action.Handle(step.Flow, commandProcessor); + step.Flow.State = WorkflowState.Running; + step.Action.Handle(step.Flow, _commandProcessor); + if (step.Flow.State == WorkflowState.Waiting) + { + + return; + } step.OnCompletion(); step = step.Next; } @@ -58,14 +74,19 @@ public void RunWorkFlow(Step? firstStep) /// Thrown when the workflow has not been initialized. public void ReceiveWorklowEvent(Event @event) { - // var state = stateStore.GetState(@event.CorrelationId); - // - // var eventType = @event.GetType(); - // - // if (!state.PendingResponses.TryGetValue(eventType, out Action replyFactory)) - // return; - // - // replyFactory(@event, state); - // state.PendingResponses.Remove(eventType); + var w = _workflowStore.GetWorkflow(@event.CorrelationId); + + if (w is not Workflow workflow) + throw new InvalidOperationException("Workflow has not been stored"); + + var eventType = @event.GetType(); + + if (!workflow.PendingResponses.TryGetValue(eventType, out Action>? replyFactory)) + return; + + replyFactory(@event, workflow); + workflow.State = WorkflowState.Running; + workflow.PendingResponses.Remove(eventType); + RunWorkFlow(workflow.CurrentStep); } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 99f6ce470b..5208781a3a 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -27,22 +27,44 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; +/// +/// What state is the workflow in +/// public enum WorkflowState { Ready, + Running, Waiting, - Done + Done, +} + +/// +/// empty class, used as maker for the workflow data +/// +public abstract class Workflow { } + +/// +/// Interface for the data that is passed between steps in the workflow +/// +public interface IAmTheWorkflowData +{ + /// + /// Bucket for data that is passed between steps in the workflow + /// + public Dictionary Bag { get; set; } } /// /// Workflow represents the current state of the workflow and tracks if it’s awaiting a response. /// -public class Workflow +public class Workflow : Workflow where TData : IAmTheWorkflowData { /// - /// Used to store data that is passed between steps in the workflow + /// What step are we currently at in the workflow /// - public Dictionary Bag { get; set; } = new(); + public Step? CurrentStep { get; set; } + + public TData Data { get; set; } /// /// The id of the workflow, used to save-retrieve it from storage @@ -52,7 +74,7 @@ public class Workflow /// /// If we are awaiting a response, we store the type of the response and the action to take when it arrives /// - public Dictionary> PendingResponses { get; private set; } = new(); + public Dictionary>> PendingResponses { get; private set; } = new(); /// /// Is the workflow currently awaiting an event response @@ -62,6 +84,6 @@ public class Workflow /// /// Constructs a new Workflow /// - public Workflow() { } + public Workflow(TData data) { Data = data; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 7d565761b4..25b4563e4b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -9,28 +9,28 @@ namespace Paramore.Brighter.MediatorWorkflow; /// The type of action we take with the step /// The workflow that we belong to /// What is the next step in sequence -public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Workflow Flow, Step? Next); +public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Workflow Flow, Step? Next) where TData : IAmTheWorkflowData; -public interface IWorkflowAction +public interface IWorkflowAction where TData : IAmTheWorkflowData { - void Handle(Workflow state, IAmACommandProcessor commandProcessor); + void Handle(Workflow state, IAmACommandProcessor commandProcessor); } -public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest +public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest where TData : IAmTheWorkflowData { - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { commandProcessor.Send(requestFactory()); } } -public class RequestAndReplyAction(Func requestFactory, Action replyFactory) - : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest +public class RequestAndReplyAction(Func requestFactory, Action replyFactory) + : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest where TData : IAmTheWorkflowData { - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { commandProcessor.Send(requestFactory()); - state.PendingResponses.Add(typeof(TReply), (reply, state) => replyFactory(reply)); + state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply)); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs index 42b04c8a04..fc3340e537 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs @@ -23,15 +23,23 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using Paramore.Brighter.MediatorWorkflow; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyEventHandler(MediatorWorkflow.Mediator mediator) : RequestHandler + internal class MyEventHandler(Mediator? mediator) : RequestHandler { + public static List ReceivedEvents { get; } = []; public override MyEvent Handle(MyEvent @event) { - mediator.ReceiveWorklowEvent(@event); + LogEvent(@event); + mediator?.ReceiveWorklowEvent(@event); return base.Handle(@event); } + + private void LogEvent(MyEvent request) + { + ReceivedEvents.Add(request); + } } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs new file mode 100644 index 0000000000..c39b95c442 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Paramore.Brighter.MediatorWorkflow; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class WorkflowTestData : IAmTheWorkflowData +{ + public Dictionary Bag { get; set; } = new(); +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 2eb7a96c1a..ba217629dd 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -10,8 +10,8 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { - private readonly Mediator _mediator; - private readonly Step _step; + private readonly Mediator _mediator; + private readonly Step _step; public MediatorOneStepFlowTests() { @@ -23,14 +23,18 @@ public MediatorOneStepFlowTests() var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); - var flow = new Workflow() {Bag = new Dictionary {{"MyValue", "Test"}}}; + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); - _mediator = new Mediator( - commandProcessor + var flow = new Workflow(workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() ); - _step = new Step("Test of Workflow", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), + _step = new Step("Test of Workflow", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), () => { }, flow, null diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 8e3167f7ba..982cd13679 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -10,8 +10,8 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorTwoStepFlowTests { - private readonly Mediator _mediator; - private readonly Step _stepOne; + private readonly Mediator _mediator; + private readonly Step _stepOne; public MediatorTwoStepFlowTests() { @@ -23,23 +23,27 @@ public MediatorTwoStepFlowTests() var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); - var flow = new Workflow() {Bag = new Dictionary {{"MyValue", "Test"}}}; + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); - _mediator = new Mediator( - commandProcessor + var flow = new Workflow(workflowData); + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() ); - var stepTwo = new Step("Test of Workflow Two", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), + var stepTwo = new Step("Test of Workflow Two", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), () => { }, flow, null ); - _stepOne = new Step("Test of Workflow One", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }), - () => { flow.Bag["MyValue"] = "TestTwo"; }, + _stepOne = new Step("Test of Workflow One", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), + () => { flow.Data.Bag["MyValue"] = "TestTwo"; }, flow, stepTwo ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 2f03b1e636..770dd9b3c5 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,4 +1,5 @@ -using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using System; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; using Xunit; @@ -7,32 +8,42 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorReplyStepFlowTests { - private readonly Mediator _mediator; + private readonly Mediator _mediator; + private bool _stepCompleted; public MediatorReplyStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); registry.Register(); - MyCommandHandler myCommandHandler = new(); - var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); + var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(), + _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); - var flow = new Workflow(); + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + var flow = new Workflow(workflowData) ; - var step = new Step("Test of Workflow", - new RequestAndReplyAction( - () => new MyCommand { Value = (flow.Bag["MyValue"] as string)! }, - (reply) => flow.Bag.Add("MyReply", ((MyEvent)reply).Data)), - () => { }, + var step = new Step("Test of Workflow", + new RequestAndReplyAction( + () => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }, + (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Data)), + () => { _stepCompleted = true; }, flow, null); - _mediator = new Mediator( - commandProcessor + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() ); } From b8eded108e2e1ed511b44eb75d016783f40572ec Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 6 Nov 2024 15:39:24 +0000 Subject: [PATCH 11/44] feat: need correlation id on event and command to support workflow --- .../Ports/Events/GreetingAsyncEvent.cs | 6 +-- .../Greetings/Ports/Events/GreetingEvent.cs | 6 +-- .../Greetings/Ports/Commands/GreetingEvent.cs | 4 +- .../Greetings/Ports/Commands/GreetingEvent.cs | 4 +- .../Greetings/Ports/Commands/GreetingEvent.cs | 4 +- .../Events/Ports/Commands/GreetingEvent.cs | 4 +- .../Greetings/Ports/Commands/FarewellEvent.cs | 13 +------ .../Greetings/Ports/Commands/GreetingEvent.cs | 6 +-- .../Greetings/Ports/Events/GreetingEvent.cs | 4 +- .../Greetings/Ports/Commands/GreetingEvent.cs | 4 +- .../Greetings/Ports/Commands/GreetingEvent.cs | 4 +- .../GreetingsApp/Requests/GreetingMade.cs | 2 +- .../SalutationApp/Requests/GreetingMade.cs | 2 +- .../Requests/SalutationReceived.cs | 2 +- .../GreetingsApp/Requests/GreetingMade.cs | 2 +- .../SalutationApp/Requests/GreetingMade.cs | 2 +- .../Requests/SalutationReceived.cs | 2 +- .../GreetingsApp/Requests/GreetingMade.cs | 2 +- .../SalutationApp/Requests/GreetingMade.cs | 9 +---- .../Requests/SalutationReceived.cs | 9 +---- .../IAmAWorkflowStore.cs | 4 +- .../InMemoryWorkflowStore.cs | 5 ++- .../Mediator.cs | 14 +++++-- .../Workflow.cs | 2 +- .../Workflows.cs | 10 +++-- .../Events/NodeStatusEvent.cs | 9 +---- src/Paramore.Brighter/Command.cs | 8 +++- src/Paramore.Brighter/Event.cs | 12 +----- src/Paramore.Brighter/IRequest.cs | 6 +++ src/Paramore.Brighter/IResponse.cs | 6 +-- .../Monitoring/Events/MonitorEvent.cs | 2 +- src/Paramore.Brighter/Reply.cs | 6 +-- .../TestDoubles/SuperAwesomeEvent.cs | 2 +- .../TestDoubles/ASBTestEvent.cs | 2 +- .../TestDoubles/MyCommandToFail.cs | 10 +---- .../CommandProcessors/TestDoubles/MyEvent.cs | 10 ++--- .../TestDoubles/MyFailingMapperEvent.cs | 20 ++-------- .../Workflows/TestDoubles/MyCommandHandler.cs | 3 +- .../Workflows/TestDoubles/MyEvent.cs | 38 +------------------ .../Workflows/TestDoubles/MyEventHandler.cs | 2 +- .../When_running_a_single_step_workflow.cs | 7 ++-- .../When_running_a_two_step_workflow.cs | 11 ++++-- .../When_running_a_workflow_with_reply.cs | 22 ++++++++--- .../TestEvent.cs | 9 +---- .../TestDoubles/MyEvent.cs | 2 +- .../MessagingGateway/When_queue_is_Purged.cs | 16 +------- .../TestDoubles/MyEvent.cs | 13 ++----- 47 files changed, 129 insertions(+), 213 deletions(-) diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs index 780c08114b..7693afc61d 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs @@ -5,13 +5,13 @@ namespace Greetings.Ports.Events { public class GreetingAsyncEvent : Event { - public GreetingAsyncEvent() : base(Guid.NewGuid()) { } + public GreetingAsyncEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingAsyncEvent(string greeting) : base(Guid.NewGuid()) + public GreetingAsyncEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; set; } } } diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs index 49d708e43d..ee5b2ed499 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs @@ -5,13 +5,13 @@ namespace Greetings.Ports.Events { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; set; } } } diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs index 8d3d9250f0..0e361dd5b9 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Events.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs index 4cc3cbdf09..ca64bfa2e7 100644 --- a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs +++ b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs @@ -5,17 +5,8 @@ namespace Greetings.Ports.Commands { [MessagePackObject(keyAsPropertyName: true)] - public class FarewellEvent : Event + public class FarewellEvent(string farewell) : Event(Guid.NewGuid().ToString()) { - public FarewellEvent() : base(Guid.NewGuid()) - { - } - - public FarewellEvent(string farewell) : base(Guid.NewGuid()) - { - Farewell = farewell; - } - - public string Farewell { get; set; } + public string Farewell { get; set; } = farewell; } } diff --git a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..7ba19b4a39 100644 --- a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,13 +29,13 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; } } } diff --git a/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs b/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs index 0b5e2de006..77877b3378 100644 --- a/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs +++ b/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Events { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs index e6f1544dc1..94b4816c3f 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs @@ -5,7 +5,7 @@ namespace GreetingsApp.Requests; public class GreetingMade : Event { - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs index baa02d99c1..d5ac0d1d0f 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs @@ -5,7 +5,7 @@ namespace SalutationApp.Requests; public class GreetingMade : Event { - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs index b5bda2faf1..2adea17345 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs @@ -5,7 +5,7 @@ namespace SalutationApp.Requests; public class SalutationReceived : Event { - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) + public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid().ToString()) { ReceivedAt = receivedAt; } diff --git a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs index 56dfc2f215..ba8b0b7ff8 100644 --- a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs index 26ba345c0c..6dc1849a0a 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs index 1c00ad06c1..e6c70d73dd 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs @@ -7,7 +7,7 @@ public class SalutationReceived : Event { public DateTimeOffset ReceivedAt { get; } - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) + public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid().ToString()) { ReceivedAt = receivedAt; } diff --git a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs index 56dfc2f215..ba8b0b7ff8 100644 --- a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs index 26ba345c0c..35ad4bfcac 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs @@ -3,13 +3,8 @@ namespace SalutationApp.Requests { - public class GreetingMade : Event + public class GreetingMade(string greeting) : Event(Guid.NewGuid().ToString()) { - public string Greeting { get; set; } - - public GreetingMade(string greeting) : base(Guid.NewGuid()) - { - Greeting = greeting; - } + public string Greeting { get; init; } = greeting; } } diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs index 1c00ad06c1..355c437f75 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs @@ -3,13 +3,8 @@ namespace SalutationApp.Requests { - public class SalutationReceived : Event + public class SalutationReceived(DateTimeOffset receivedAt) : Event(Guid.NewGuid().ToString()) { - public DateTimeOffset ReceivedAt { get; } - - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) - { - ReceivedAt = receivedAt; - } + public DateTimeOffset ReceivedAt { get; } = receivedAt; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs index 22476c24f7..4623734e9f 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs @@ -36,11 +36,11 @@ public interface IAmAWorkflowStore /// /// The workflow void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData; - + /// /// Retrieves a workflow via its Id /// /// The id of the workflow /// if found, the workflow, otherwise null - Workflow? GetWorkflow(Guid id) ; + Workflow? GetWorkflow(string? id) ; } diff --git a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs index 94e5ca24dd..606c255e50 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs @@ -29,15 +29,16 @@ namespace Paramore.Brighter.MediatorWorkflow; public class InMemoryWorkflowStore : IAmAWorkflowStore { - private readonly Dictionary _flows = new(); + private readonly Dictionary _flows = new(); public void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData { _flows[workflow.Id] = workflow; } - public Workflow? GetWorkflow(Guid id) + public Workflow? GetWorkflow(string? id) { + if (id is null) return null; return _flows.TryGetValue(id, out var state) ? state : null; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index bfed58b774..6b6a57b048 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -56,10 +56,11 @@ public void RunWorkFlow(Step? firstStep) while (step is not null) { step.Flow.State = WorkflowState.Running; + step.Flow.CurrentStep = step; step.Action.Handle(step.Flow, _commandProcessor); if (step.Flow.State == WorkflowState.Waiting) { - + _workflowStore.SaveWorkflow(step.Flow); return; } step.OnCompletion(); @@ -72,8 +73,11 @@ public void RunWorkFlow(Step? firstStep) /// /// The event to process. /// Thrown when the workflow has not been initialized. - public void ReceiveWorklowEvent(Event @event) + public void ReceiveWorkflowEvent(Event @event) { + if (@event.CorrelationId is null) + throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); + var w = _workflowStore.GetWorkflow(@event.CorrelationId); if (w is not Workflow workflow) @@ -83,10 +87,14 @@ public void ReceiveWorklowEvent(Event @event) if (!workflow.PendingResponses.TryGetValue(eventType, out Action>? replyFactory)) return; + + if (workflow.CurrentStep is null) + throw new InvalidOperationException($"Current step of workflow #{workflow.Id} should not be null"); replyFactory(@event, workflow); + workflow.CurrentStep.OnCompletion(); workflow.State = WorkflowState.Running; workflow.PendingResponses.Remove(eventType); - RunWorkFlow(workflow.CurrentStep); + RunWorkFlow(workflow.CurrentStep.Next); } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 5208781a3a..59ce2ddd01 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -69,7 +69,7 @@ public class Workflow : Workflow where TData : IAmTheWorkflowData /// /// The id of the workflow, used to save-retrieve it from storage /// - public Guid Id { get; private set; } = Guid.NewGuid(); + public string Id { get; private set; } = Guid.NewGuid().ToString(); /// /// If we are awaiting a response, we store the type of the response and the action to take when it arrives diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 25b4563e4b..b149d095fe 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -18,9 +18,11 @@ public interface IWorkflowAction where TData : IAmTheWorkflowData public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest where TData : IAmTheWorkflowData { - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - commandProcessor.Send(requestFactory()); + var command = requestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); } } @@ -29,7 +31,9 @@ public class RequestAndReplyAction(Func reque { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - commandProcessor.Send(requestFactory()); + var command = requestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply)); } diff --git a/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs b/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs index 71282dbcac..8a58b62f45 100644 --- a/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs +++ b/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs @@ -2,13 +2,8 @@ namespace Paramore.Brighter.ServiceActivator.Control.Events; -public record NodeStatusEvent : IEvent +public class NodeStatusEvent() : Event(Guid.NewGuid().ToString()) { - /// - /// The event Id - /// - public string Id { get; set; } = null!; - /// /// The Diagnostics Span /// @@ -32,7 +27,7 @@ public record NodeStatusEvent : IEvent /// /// Is this node Healthy /// - public bool IsHealthy { get; init; } = false; + public bool IsHealthy { get; init; } /// /// The Number of Performers currently running on the Node diff --git a/src/Paramore.Brighter/Command.cs b/src/Paramore.Brighter/Command.cs index cc7949e516..4307b02dc9 100644 --- a/src/Paramore.Brighter/Command.cs +++ b/src/Paramore.Brighter/Command.cs @@ -31,6 +31,12 @@ namespace Paramore.Brighter /// public class Command : ICommand { + /// + /// If we are participating in a conversation, the correlation id allows us to correlate a request with other messages in the conversation + /// + public string? CorrelationId { get; set; } + + /// /// Gets or sets the identifier. /// /// The identifier. @@ -50,7 +56,7 @@ public Command(string id) /// Initializes a new instance of the class. /// /// The identifier - public Command(Guid id) + protected Command(Guid id) { Id = id.ToString(); } diff --git a/src/Paramore.Brighter/Event.cs b/src/Paramore.Brighter/Event.cs index 37e196ed87..7b3eec799f 100644 --- a/src/Paramore.Brighter/Event.cs +++ b/src/Paramore.Brighter/Event.cs @@ -35,9 +35,8 @@ public class Event : IEvent { /// /// An event may be the response to a command, in order to find the command that caused the event, we need to know the correlation id - /// In many cases correlation id is the command id /// - public Guid CorrelationId { get; set; } + public string? CorrelationId { get; set; } /// /// Gets or sets the identifier. @@ -54,14 +53,5 @@ public Event(string id) { Id = id; } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier. - public Event(Guid id) - { - Id = id.ToString(); - } } } diff --git a/src/Paramore.Brighter/IRequest.cs b/src/Paramore.Brighter/IRequest.cs index fd311b90df..6ef6393a2f 100644 --- a/src/Paramore.Brighter/IRequest.cs +++ b/src/Paramore.Brighter/IRequest.cs @@ -34,10 +34,16 @@ namespace Paramore.Brighter /// public interface IRequest { + /// + /// If we are participating in a conversation, the correlation id allows us to correlate a request with other messages in the conversation + /// + string? CorrelationId { get; set; } + /// /// Gets or sets the identifier. /// /// The identifier. string Id { get; set; } + } } diff --git a/src/Paramore.Brighter/IResponse.cs b/src/Paramore.Brighter/IResponse.cs index d499fade9f..7b24f1a697 100644 --- a/src/Paramore.Brighter/IResponse.cs +++ b/src/Paramore.Brighter/IResponse.cs @@ -8,9 +8,5 @@ namespace Paramore.Brighter /// public interface IResponse : IRequest { - /// - /// Allow us to correlate request and response - /// - Guid CorrelationId { get; } - } + } } diff --git a/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs b/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs index 7c860deb05..ca9ba9350a 100644 --- a/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs +++ b/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs @@ -66,7 +66,7 @@ public class MonitorEvent( DateTime eventTime, int timeElapsedMs, Exception? exception = null) - : Event(Guid.NewGuid()) + : Event(Guid.NewGuid().ToString()) { /// /// Any exception that was thrown when processing the handler pipeline diff --git a/src/Paramore.Brighter/Reply.cs b/src/Paramore.Brighter/Reply.cs index 2805ca551c..38c8085100 100644 --- a/src/Paramore.Brighter/Reply.cs +++ b/src/Paramore.Brighter/Reply.cs @@ -11,16 +11,12 @@ namespace Paramore.Brighter /// public class Reply : Command, IResponse { - /// - /// Use this correlation id so that sender knows what we are replying to - /// - public Guid CorrelationId { get; } /// /// The channel that we should reply to the sender on. /// public ReplyAddress SendersAddress { get; private set; } - public Reply(ReplyAddress sendersAddress) + protected Reply(ReplyAddress sendersAddress) : base(Guid.NewGuid()) { SendersAddress = sendersAddress; diff --git a/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs b/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs index 194ebdadf0..780e22e7f3 100644 --- a/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs +++ b/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs @@ -1,6 +1,6 @@ namespace Paramore.Brighter.Azure.Tests.TestDoubles; -public class SuperAwesomeEvent(string announcement) : Event(Guid.NewGuid()) +public class SuperAwesomeEvent(string announcement) : Event(Guid.NewGuid().ToString()) { public string Announcement { get; set; } = announcement; } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs index 9bf892f3a1..fa496b0a6e 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs @@ -4,7 +4,7 @@ namespace Paramore.Brighter.AzureServiceBus.Tests.TestDoubles { public class ASBTestEvent : Event { - public ASBTestEvent() : base(Guid.NewGuid()) + public ASBTestEvent() : base(Guid.NewGuid().ToString()) { } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs index 381961546f..183c9b9541 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs @@ -3,13 +3,5 @@ namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles { - internal class MyCommandToFail : ICommand - { - public string Id { get; set; } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } - } + internal class MyCommandToFail() : Command(Guid.NewGuid().ToString()); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs index cb74433155..15ae75934a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs @@ -26,22 +26,18 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles { - internal class MyEvent : Event, IEquatable + internal class MyEvent() : Event(Guid.NewGuid().ToString()), IEquatable { public int Data { get; set; } - public MyEvent() : base(Guid.NewGuid()) - { - } - - public bool Equals(MyEvent other) + public bool Equals(MyEvent? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Data == other.Data; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs index 91975134d0..43f47335bb 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs @@ -1,26 +1,12 @@ using System; -using System.Diagnostics; namespace Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -internal class MyFailingMapperEvent : IRequest +internal class MyFailingMapperEvent : Event { - /// - /// Gets or sets the identifier. - /// - /// The identifier. - public string Id { get; set; } - + /// /// Initializes a new instance of the class. /// - public MyFailingMapperEvent() - { - Id = Guid.NewGuid().ToString(); - } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } + public MyFailingMapperEvent() : base(Guid.NewGuid().ToString()) { } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs index 7f486e34aa..6892fc3ee2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs @@ -26,13 +26,14 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyCommandHandler : RequestHandler + internal class MyCommandHandler(IAmACommandProcessor? commandProcessor) : RequestHandler { public static List ReceivedCommands { get; } = []; public override MyCommand Handle(MyCommand command) { LogCommand(command); + commandProcessor?.Publish(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}); return base.Handle(command); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs index fe8c2e76be..03cbafc742 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs @@ -26,42 +26,8 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyEvent : Event, IEquatable + internal class MyEvent(string? value) : Event(Guid.NewGuid().ToString()) { - public int Data { get; set; } - - public MyEvent() : base(Guid.NewGuid()) - { - } - - public bool Equals(MyEvent other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Data == other.Data; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MyEvent)obj); - } - - public override int GetHashCode() - { - return Data; - } - - public static bool operator ==(MyEvent left, MyEvent right) - { - return Equals(left, right); - } - - public static bool operator !=(MyEvent left, MyEvent right) - { - return !Equals(left, right); - } + public string Value { get; set; } = value ?? string.Empty; } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs index fc3340e537..fa21274044 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs @@ -33,7 +33,7 @@ internal class MyEventHandler(Mediator? mediator) : RequestHan public override MyEvent Handle(MyEvent @event) { LogEvent(@event); - mediator?.ReceiveWorklowEvent(@event); + mediator?.ReceiveWorkflowEvent(@event); return base.Handle(@event); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index ba217629dd..12fe947aaa 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -17,10 +17,11 @@ public MediatorOneStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); - MyCommandHandler myCommandHandler = new(); - var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); - var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 982cd13679..a2bb18a613 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -17,10 +17,11 @@ public MediatorTwoStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); - MyCommandHandler myCommandHandler = new(); - var handlerFactory = new SimpleHandlerFactorySync(_ => myCommandHandler); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); - var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); @@ -37,7 +38,7 @@ public MediatorTwoStepFlowTests() var stepTwo = new Step("Test of Workflow Two", new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), () => { }, - flow, + flow, null ); @@ -52,6 +53,8 @@ public MediatorTwoStepFlowTests() [Fact] public void When_running_a_single_step_workflow() { + MyCommandHandler.ReceivedCommands.Clear(); + _mediator.RunWorkFlow(_stepOne); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 770dd9b3c5..e5b6c1dee0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; @@ -10,22 +12,24 @@ public class MediatorReplyStepFlowTests { private readonly Mediator _mediator; private bool _stepCompleted; + private readonly Step _step; public MediatorReplyStepFlowTests() { var registry = new SubscriberRegistry(); registry.Register(); registry.Register(); + + IAmACommandProcessor commandProcessor = null; var handlerFactory = new SimpleHandlerFactorySync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(), + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); - var commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), - new PolicyRegistry()); + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); @@ -33,10 +37,10 @@ public MediatorReplyStepFlowTests() var flow = new Workflow(workflowData) ; - var step = new Step("Test of Workflow", + _step = new Step("Test of Workflow", new RequestAndReplyAction( () => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }, - (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Data)), + (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompleted = true; }, flow, null); @@ -50,6 +54,14 @@ public MediatorReplyStepFlowTests() [Fact] public void When_running_a_workflow_with_reply() { + MyCommandHandler.ReceivedCommands.Clear(); + MyEventHandler.ReceivedEvents.Clear(); + + _mediator.RunWorkFlow(_step); + + _stepCompleted.Should().BeTrue(); + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs b/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs index 7cdbfece57..f62ba26995 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs @@ -3,10 +3,5 @@ namespace Tests { - public class TestEvent : Event - { - public TestEvent() : base(Guid.NewGuid()) - { - } - } -} \ No newline at end of file + public class TestEvent() : Event(Guid.NewGuid().ToString()); +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs index 465b4b2421..47cc124a0c 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs @@ -2,7 +2,7 @@ namespace Paramore.Brighter.InMemory.Tests.TestDoubles; -public class MyEvent() : Event(Guid.NewGuid()) +public class MyEvent() : Event(Guid.NewGuid().ToString()) { public string Value { get; set; } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs index 772b51ad2a..69a6bbe7f2 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs @@ -99,19 +99,5 @@ private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) } - public class ExampleCommand : ICommand - { - - public string Id { get; set; } - - public ExampleCommand() - { - Id = Guid.NewGuid().ToString(); - } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } - } + public class ExampleCommand() : Command(Guid.NewGuid()); } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs index 6bc4103a77..f2d1107e7c 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs @@ -26,23 +26,18 @@ THE SOFTWARE. */ namespace Paramore.Brighter.RMQ.Tests.TestDoubles { - internal class MyEvent : Event, IEquatable + internal class MyEvent() : Event(Guid.NewGuid().ToString()), IEquatable { - public int Data { get; private set; } + public int Data { get; private set; } = 7; - public MyEvent() : base(Guid.NewGuid()) - { - Data = 7; - } - - public bool Equals(MyEvent other) + public bool Equals(MyEvent? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Data == other.Data; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; From 5298d3867bfec9d227a92816473ee3b346d1c5e0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 6 Nov 2024 21:42:30 +0000 Subject: [PATCH 12/44] chore: check in to allow merging of master --- .../Mediator.cs | 31 +++++--- .../Workflows.cs | 71 +++++++++++++++-- src/Paramore.Brighter/CommandProcessor.cs | 24 ++++-- .../CommittableTransactionProvider.cs | 43 +++++++++++ src/Paramore.Brighter/IAmACommandProcessor.cs | 10 ++- src/Paramore.Brighter/InMemoryInbox.cs | 54 ++++++++----- .../Paramore.Brighter.csproj | 2 +- .../Paramore.Brighter.Core.Tests.csproj | 2 +- ...running_a_multistep_workflow_with_reply.cs | 76 +++++++++++++++++++ 9 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index 6b6a57b048..834b3aba45 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -27,18 +27,21 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; /// -/// The mediator orchestrates a workflow, executing each step in the sequence. +/// The `Mediator` class orchestrates a workflow by executing each step in a sequence. +/// It uses a command processor and a workflow store to manage the workflow's state and actions. /// +/// The type of the workflow data. public class Mediator where TData : IAmTheWorkflowData { private readonly IAmACommandProcessor _commandProcessor; private readonly IAmAWorkflowStore _workflowStore; + /// - /// The mediator orchestrates a workflow, executing each step in the sequence. + /// Initializes a new instance of the class. /// - /// - /// + /// The command processor used to handle commands. + /// The workflow store used to store and retrieve workflows. public Mediator(IAmACommandProcessor commandProcessor, IAmAWorkflowStore workflowStore) { _commandProcessor = commandProcessor; @@ -46,9 +49,9 @@ public Mediator(IAmACommandProcessor commandProcessor, IAmAWorkflowStore workflo } /// - /// /// Runs the workflow by executing each step in the sequence. /// + /// The first step of the workflow to execute. /// Thrown when the workflow has not been initialized. public void RunWorkFlow(Step? firstStep) { @@ -58,18 +61,13 @@ public void RunWorkFlow(Step? firstStep) step.Flow.State = WorkflowState.Running; step.Flow.CurrentStep = step; step.Action.Handle(step.Flow, _commandProcessor); - if (step.Flow.State == WorkflowState.Waiting) - { - _workflowStore.SaveWorkflow(step.Flow); - return; - } step.OnCompletion(); step = step.Next; } } /// - /// Receives an event and processes it if there is a pending response for the event type. + /// Call this method from a RequestHandler that listens for an expected event. This will process that event if there is a pending response for the event type. /// /// The event to process. /// Thrown when the workflow has not been initialized. @@ -95,6 +93,15 @@ public void ReceiveWorkflowEvent(Event @event) workflow.CurrentStep.OnCompletion(); workflow.State = WorkflowState.Running; workflow.PendingResponses.Remove(eventType); - RunWorkFlow(workflow.CurrentStep.Next); + } + + /// + /// Waits for a workflow event to be received. + /// - Stores the workflow state in the workflow store. + /// - Sets the workflow state to waiting. + /// - Sets the callback for the workflow to run on completion + /// + public void WaitOnWorkflowEvent() + { } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index b149d095fe..43a2eadd3e 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -1,23 +1,69 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; namespace Paramore.Brighter.MediatorWorkflow; /// -/// A step in the worfklow. Steps form a singly linked list. +/// Represents a step in the workflow. Steps form a singly linked list. /// -/// The name of the step -/// The type of action we take with the step -/// The workflow that we belong to -/// What is the next step in sequence +/// The type of the workflow data. +/// The name of the step. +/// The action to be taken with the step. +/// The action to be taken upon completion of the step. +/// The workflow that the step belongs to. +/// The next step in the sequence. public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Workflow Flow, Step? Next) where TData : IAmTheWorkflowData; +/// +/// Defines an interface for workflow actions. +/// +/// The type of the workflow data. public interface IWorkflowAction where TData : IAmTheWorkflowData { + /// + /// Handles the workflow action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. void Handle(Workflow state, IAmACommandProcessor commandProcessor); } +/// +/// Represents a fire-and-forget action in the workflow. +/// +/// The type of the request. +/// The type of the workflow data. +/// The factory method to create the request. public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest where TData : IAmTheWorkflowData { + /// + /// Handles the fire-and-forget action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { var command = requestFactory(); @@ -26,9 +72,22 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) } } +/// +/// Represents a request-and-reply action in the workflow. +/// +/// The type of the request. +/// The type of the reply. +/// The type of the workflow data. +/// The factory method to create the request. +/// The factory method to handle the reply. public class RequestAndReplyAction(Func requestFactory, Action replyFactory) : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest where TData : IAmTheWorkflowData { + /// + /// Handles the request-and-reply action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { var command = requestFactory(); diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index b0909e8dac..ddc39a337a 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -45,7 +45,10 @@ namespace Paramore.Brighter { /// /// Class CommandProcessor. - /// Implements both the Command Dispatcher + /// The `CommandProcessor` class implements both the Command Dispatcher and Command Processor design patterns. + /// It is responsible for handling commands and events, managing their execution, and ensuring reliable delivery. + /// The `CommandProcessor` class is the main entry point for the Brighter library. + /// It implements both the Command Dispatcher /// and Command Processor Design Patterns /// public class CommandProcessor : IAmACommandProcessor @@ -117,6 +120,7 @@ public class CommandProcessor : IAmACommandProcessor /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be + /// Thrown when no handler factory is set. public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -223,8 +227,8 @@ public CommandProcessor( /// /// The command. /// The context of the request; if null we will start one via a - /// - /// + /// Thrown when no handler factory is defined. + /// Thrown when no subscriber registry is configured or when the command has more than one handler. public void Send(T command, RequestContext? requestContext = null) where T : class, IRequest { if (_handlerFactorySync == null) @@ -265,9 +269,11 @@ public void Send(T command, RequestContext? requestContext = null) where T : /// The command. /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false - /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . - public async Task SendAsync( + /// Token to cancel the request pipeline. + /// An awaitable task. + /// Thrown when no async handler factory is defined. + /// Thrown when no subscriber registry is configured. + public async Task SendAsync( T command, RequestContext? requestContext = null, bool continueOnCapturedContext = true, @@ -317,6 +323,8 @@ await handlerChain.First().HandleAsync(command, cancellationToken) /// /// The event. /// The context of the request; if null we will start one via a + /// Thrown when no handler factory is defined. + /// Thrown when no subscriber registry is configured. public void Publish(T @event, RequestContext? requestContext = null) where T : class, IRequest { if (_handlerFactorySync == null) @@ -387,7 +395,9 @@ public void Publish(T @event, RequestContext? requestContext = null) where T /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . + /// An awaitable task. + /// Thrown when no async handler factory is defined. + /// Thrown when no subscriber registry is configured. public async Task PublishAsync( T @event, RequestContext? requestContext = null, diff --git a/src/Paramore.Brighter/CommittableTransactionProvider.cs b/src/Paramore.Brighter/CommittableTransactionProvider.cs index b3753edf6f..d461bb68ba 100644 --- a/src/Paramore.Brighter/CommittableTransactionProvider.cs +++ b/src/Paramore.Brighter/CommittableTransactionProvider.cs @@ -4,23 +4,42 @@ namespace Paramore.Brighter { + /// + /// Provides a committable transaction for use in a box transaction. + /// + /// + /// This class implements the IAmABoxTransactionProvider interface for CommittableTransaction. + /// It manages the creation, commitment, and rollback of transactions, as well as handling + /// the current transaction context. + /// public class CommittableTransactionProvider : IAmABoxTransactionProvider { private CommittableTransaction? _transaction; private Transaction? _existingTransaction; + /// + /// Closes the current transaction and restores the previous transaction context. + /// public void Close() { Transaction.Current = _existingTransaction; _transaction = null; } + /// + /// Commits the current transaction and closes it. + /// public void Commit() { _transaction?.Commit(); Close(); } + /// + /// Asynchronously commits the current transaction. + /// + /// A cancellation token that can be used to cancel the commit operation. + /// A task representing the asynchronous commit operation. public Task CommitAsync(CancellationToken cancellationToken = default) { if (_transaction is null) @@ -28,6 +47,10 @@ public Task CommitAsync(CancellationToken cancellationToken = default) return Task.Factory.FromAsync(_transaction.BeginCommit, _transaction.EndCommit, null, TaskCreationOptions.RunContinuationsAsynchronously); } + /// + /// Gets the current transaction, creating a new one if necessary. + /// + /// The current CommittableTransaction. public CommittableTransaction GetTransaction() { if (_transaction == null) @@ -39,6 +62,11 @@ public CommittableTransaction GetTransaction() return _transaction; } + /// + /// Asynchronously gets the current transaction, creating a new one if necessary. + /// + /// A cancellation token that can be used to cancel the operation. + /// A task representing the asynchronous operation, which resolves to the current CommittableTransaction. public Task GetTransactionAsync(CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -46,15 +74,30 @@ public Task GetTransactionAsync(CancellationToken cancel return tcs.Task; } + /// + /// Gets a value indicating whether there is an open transaction. + /// public bool HasOpenTransaction { get { return _transaction != null; } } + + /// + /// Gets a value indicating whether this provider uses a shared connection. + /// public bool IsSharedConnection => true; + /// + /// Rolls back the current transaction and closes it. + /// public void Rollback() { _transaction?.Rollback(); Close(); } + /// + /// Asynchronously rolls back the current transaction. + /// + /// A cancellation token that can be used to cancel the rollback operation. + /// A task representing the asynchronous rollback operation. public Task RollbackAsync(CancellationToken cancellationToken = default) { Rollback(); diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index bdedb4a081..7d978e1fdf 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -31,10 +31,12 @@ namespace Paramore.Brighter { /// /// Interface IAmACommandProcessor - /// Paramore.Brighter provides the default implementation of this interface and it is unlikely you need - /// to override this for anything other than testing purposes. The usual need is that in a you intend to publish an - /// to indicate the handler has completed to other components. In this case your tests should only verify that the correct - /// event was raised by listening to calls on this interface, using a mocking framework of your choice or bespoke + /// Provides the interface for the command processor, which dispatches commands and events to handlers, invoking any required middleware. + /// Brighter provides the default implementation of this interface and it is unlikely you need + /// to override this for anything other than testing purposes. + /// The usual testing need is that in a you intend to publish an to indicate the + /// handler has completed to other components. In this case your tests should only verify that the correct event was raised by + /// listening to calls on this interface, using a mocking framework of your choice or bespoke /// Test Double. /// public interface IAmACommandProcessor diff --git a/src/Paramore.Brighter/InMemoryInbox.cs b/src/Paramore.Brighter/InMemoryInbox.cs index 658f824d9e..19b234c5c4 100644 --- a/src/Paramore.Brighter/InMemoryInbox.cs +++ b/src/Paramore.Brighter/InMemoryInbox.cs @@ -71,10 +71,12 @@ public InboxItem(Type requestType, string requestBody, DateTimeOffset writeTime, public DateTimeOffset WriteTime { get; } /// - /// The Id and the key for the context i.e. message type, that we are looking for - /// Occurs because we may service the same message in different contexts and need to - /// know they are all handled or not + /// Gets the context key for the request. This is a combination of the request ID and the context identifier. /// + /// + /// The context key is used to uniquely identify a request within a specific processing context, + /// allowing the message to be handled differently in various contexts. + /// string Key { get;} /// @@ -82,8 +84,8 @@ public InboxItem(Type requestType, string requestBody, DateTimeOffset writeTime, /// /// The Guid for the request /// The handler this is for - /// - public static string CreateKey(string id, string contextKey) + /// A composite key combining the request ID and context key. + public static string CreateKey(string id, string contextKey) { return $"{id}:{contextKey}"; } @@ -92,14 +94,17 @@ public static string CreateKey(string id, string contextKey) /// /// Class InMemoryInbox. - /// A Inbox stores s for diagnostics or replay. + /// Provides an in-memory implementation of an inbox for storing and retrieving requests. An Inbox stores s for diagnostics or replay. + /// + /// /// This class is intended to be thread-safe, so you can use one InMemoryInbox across multiple performers. However, the state is not global i.e. static /// so you can use multiple instances safely as well. /// N.B. that the primary limitation of this in-memory inbox is that it will not work across processes. So if you use the competing consumers pattern /// the consumers will not be able to determine if another consumer has already processed this command. /// It is possible to use multiple performers within one process as competing consumers, and if you want to use an InMemoryInbox this is the most /// viable strategy - otherwise use an out-of-process inbox that provides shared state to all consumers - /// + /// + /// The time provider used for timestamp operations. public class InMemoryInbox(TimeProvider timeProvider) : InMemoryBox(timeProvider), IAmAnInboxSync, IAmAnInboxAsync { private readonly TimeProvider _timeProvider = timeProvider; @@ -118,9 +123,10 @@ public class InMemoryInbox(TimeProvider timeProvider) : InMemoryBox(t /// /// /// The command. - /// - /// The timeout in milliseconds. - public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest + /// The context-specific key for this request. + /// The timeout for the operation in milliseconds. Use -1 for no timeout. + /// Thrown when the request cannot be added to the inbox. + public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); @@ -141,11 +147,10 @@ public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) /// /// /// The command. - /// + /// The context-specific key for this request. /// The timeout in milliseconds. - /// - /// Allows the sender to cancel the call, optional - /// + /// A token to cancel the asynchronous operation. + /// Allows the sender to cancel the call, optional public Task AddAsync(T command, string contextKey, int timeoutInMilliseconds = -1, CancellationToken cancellationToken = default) where T : class, IRequest { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -165,13 +170,14 @@ public Task AddAsync(T command, string contextKey, int timeoutInMilliseconds /// /// Finds the command with the specified identifier. /// - /// - /// The identifier. - /// + /// The type of the request to retrieve, which must implement IRequest. + /// The unique identifier of the request. + /// The context-specific key for this request. /// The timeout in milliseconds. - /// ICommand. - /// - public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest + /// The requested item of type T. + /// Thrown when the requested item is not found in the inbox. + /// Thrown when the deserialized request body is null. + public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); @@ -186,6 +192,14 @@ public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) wh throw new RequestNotFoundException(id); } + /// + /// Checks if a request exists in the inbox. + /// + /// The type of the request, which must implement IRequest. + /// The unique identifier of the request. + /// The context-specific key for this request. + /// The timeout for the operation in milliseconds. Use -1 for no timeout. + /// True if the request exists in the inbox; otherwise, false. public bool Exists(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); diff --git a/src/Paramore.Brighter/Paramore.Brighter.csproj b/src/Paramore.Brighter/Paramore.Brighter.csproj index 12dba45b4f..37105ebb32 100644 --- a/src/Paramore.Brighter/Paramore.Brighter.csproj +++ b/src/Paramore.Brighter/Paramore.Brighter.csproj @@ -2,7 +2,7 @@ The Command Dispatcher pattern is an addition to the Command design pattern that decouples the dispatcher for a service from its execution. A Command Dispatcher component maps commands to handlers. A Command Processor pattern provides a framework for handling orthogonal concerns such as logging, timeouts, or circuit breakers Ian Cooper - netstandard2.0;net6.0;net8.0 + netstandard2.0;net8.0 Command;Event;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability latest enable diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index 340320e84a..0e07a0ead7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -1,7 +1,7 @@ false - net6.0 + net8.0 diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs new file mode 100644 index 0000000000..b44c1f1e7d --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorReplyMultiStepFlowTests +{ + private readonly Mediator _mediator; + private bool _stepCompletedOne; + private bool _stepCompletedTwo; + private readonly Step _stepOne; + private readonly Step _stepTwo; + + public MediatorReplyMultiStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + registry.Register(); + + IAmACommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), + _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + var flow = new Workflow(workflowData) ; + + _stepOne = new Step("Test of Workflow Step One", + new RequestAndReplyAction( + () => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }, + (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Value)), + () => { _stepCompletedOne = true; }, + flow, + null); + + _stepTwo = new Step("Test of Workflow Step Two", + new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + flow, + _stepOne); + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + [Fact] + public void When_running_a_workflow_with_reply() + { + MyCommandHandler.ReceivedCommands.Clear(); + MyEventHandler.ReceivedEvents.Clear(); + + _mediator.RunWorkFlow(_stepOne); + + _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeTrue(); + + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + } +} From 92e344dfd65746cd9d8e43748b7049904577e6b7 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 16:22:45 +0000 Subject: [PATCH 13/44] feat: move completed workflows to the done state --- .../GreetingsApp/Entities/Greeting.cs | 2 +- .../GreetingsApp/Entities/Person.cs | 2 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 6 ++- .../Responses/FindPersonsGreetings.cs | 4 +- .../Mediator.cs | 39 +++++++++---------- .../Workflow.cs | 9 ++++- .../Workflows.cs | 3 +- ...running_a_multistep_workflow_with_reply.cs | 33 +++++++--------- .../When_running_a_single_step_workflow.cs | 20 +++++----- .../When_running_a_two_step_workflow.cs | 33 ++++++++-------- .../When_running_a_workflow_with_reply.cs | 16 ++++---- 11 files changed, 86 insertions(+), 81 deletions(-) diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs index cf3d750338..d39584a91d 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs @@ -4,7 +4,7 @@ public class Greeting { public long Id { get; set; } - public string Message { get; set; } + public string? Message { get; set; } public long RecipientId { get; set; } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs index 0c7115cedb..45353b7e16 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs @@ -7,7 +7,7 @@ public class Person { public DateTime TimeStamp { get; set; } public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public IList Greetings { get; set; } = new List(); public Person() diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs index 9ddaa8677d..eb5cab8446 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; @@ -33,7 +34,10 @@ public override async Task ExecuteAsync(FindPersonByName query await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); IEnumerable people = await connection.QueryAsync("select * from Person where name = @name", new { name = query.Name }); - Person person = people.SingleOrDefault(); + Person? person = people.SingleOrDefault(); + + if (person == null) + throw new InvalidOperationException($"Could not find person named {query.Name}"); return new FindPersonResult(person); } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs index 30d7b5851c..5a5c5511a2 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs @@ -4,8 +4,8 @@ namespace GreetingsApp.Responses; public class FindPersonsGreetings { - public string Name { get; set; } - public IEnumerable Greetings { get; set; } + public string? Name { get; set; } + public IEnumerable? Greetings { get; set; } } public class Salutation diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index 834b3aba45..ab433014d9 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -51,19 +51,28 @@ public Mediator(IAmACommandProcessor commandProcessor, IAmAWorkflowStore workflo /// /// Runs the workflow by executing each step in the sequence. /// - /// The first step of the workflow to execute. + /// /// Thrown when the workflow has not been initialized. - public void RunWorkFlow(Step? firstStep) - { - var step = firstStep; - while (step is not null) + public void RunWorkFlow(Workflow workflow) + { + if (workflow.CurrentStep is null) { - step.Flow.State = WorkflowState.Running; - step.Flow.CurrentStep = step; - step.Action.Handle(step.Flow, _commandProcessor); - step.OnCompletion(); - step = step.Next; + workflow.State = WorkflowState.Done; + return; } + + workflow.State = WorkflowState.Running; + _workflowStore.SaveWorkflow(workflow); + + while (workflow.CurrentStep is not null) + { + workflow.CurrentStep.Action.Handle(workflow, _commandProcessor); + workflow.CurrentStep.OnCompletion(); + workflow.CurrentStep = workflow.CurrentStep.Next; + _workflowStore.SaveWorkflow(workflow); + } + + workflow.State = WorkflowState.Done; } /// @@ -94,14 +103,4 @@ public void ReceiveWorkflowEvent(Event @event) workflow.State = WorkflowState.Running; workflow.PendingResponses.Remove(eventType); } - - /// - /// Waits for a workflow event to be received. - /// - Stores the workflow state in the workflow store. - /// - Sets the workflow state to waiting. - /// - Sets the callback for the workflow to run on completion - /// - public void WaitOnWorkflowEvent() - { - } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 59ce2ddd01..263e5d6f05 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -83,7 +83,14 @@ public class Workflow : Workflow where TData : IAmTheWorkflowData /// /// Constructs a new Workflow + /// The first step of the workflow to execute. + /// State which is passed between steps of the workflow /// - public Workflow(TData data) { Data = data; } + public Workflow(Step firstStep, TData data) + { + CurrentStep = firstStep; + Data = data; + State = WorkflowState.Ready; + } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 43a2eadd3e..53234d3ea5 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -33,9 +33,8 @@ namespace Paramore.Brighter.MediatorWorkflow; /// The name of the step. /// The action to be taken with the step. /// The action to be taken upon completion of the step. -/// The workflow that the step belongs to. /// The next step in the sequence. -public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Workflow Flow, Step? Next) where TData : IAmTheWorkflowData; +public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next) where TData : IAmTheWorkflowData; /// /// Defines an interface for workflow actions. diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index b44c1f1e7d..89ccaa7faa 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -13,8 +13,7 @@ public class MediatorReplyMultiStepFlowTests private readonly Mediator _mediator; private bool _stepCompletedOne; private bool _stepCompletedTwo; - private readonly Step _stepOne; - private readonly Step _stepTwo; + private readonly Workflow _flow; public MediatorReplyMultiStepFlowTests() { @@ -22,7 +21,7 @@ public MediatorReplyMultiStepFlowTests() registry.Register(); registry.Register(); - IAmACommandProcessor commandProcessor = null; + IAmACommandProcessor? commandProcessor = null; var handlerFactory = new SimpleHandlerFactorySync((handlerType) => handlerType switch { @@ -36,22 +35,20 @@ public MediatorReplyMultiStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - - var flow = new Workflow(workflowData) ; - _stepOne = new Step("Test of Workflow Step One", + var stepTwo = new Step("Test of Workflow Step Two", + new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + Step stepOne = new("Test of Workflow Step One", new RequestAndReplyAction( - () => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }, - (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Value)), + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompletedOne = true; }, - flow, - null); - - _stepTwo = new Step("Test of Workflow Step Two", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), - () => { _stepCompletedTwo = true; }, - flow, - _stepOne); + stepTwo); + + _flow = new Workflow(stepOne, workflowData) ; _mediator = new Mediator( commandProcessor, @@ -65,12 +62,12 @@ public void When_running_a_workflow_with_reply() MyCommandHandler.ReceivedCommands.Clear(); MyEventHandler.ReceivedEvents.Clear(); - _mediator.RunWorkFlow(_stepOne); - + _mediator.RunWorkFlow(_flow); _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeTrue(); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(WorkflowState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 12fe947aaa..87427b8c22 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -11,7 +11,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { private readonly Mediator _mediator; - private readonly Step _step; + private readonly Workflow _flow; public MediatorOneStepFlowTests() { @@ -27,19 +27,18 @@ public MediatorOneStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var flow = new Workflow(workflowData) ; + var firstStep = new Step("Test of Workflow", + new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + () => { }, + null + ); + + _flow = new Workflow(firstStep, workflowData) ; _mediator = new Mediator( commandProcessor, new InMemoryWorkflowStore() ); - - _step = new Step("Test of Workflow", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), - () => { }, - flow, - null - ); } [Fact] @@ -47,8 +46,9 @@ public void When_running_a_single_step_workflow() { MyCommandHandler.ReceivedCommands.Clear(); - _mediator.RunWorkFlow(_step); + _mediator.RunWorkFlow(_flow); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(WorkflowState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index a2bb18a613..7a31f37b59 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -11,7 +11,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorTwoStepFlowTests { private readonly Mediator _mediator; - private readonly Step _stepOne; + private readonly Workflow _flow; public MediatorTwoStepFlowTests() { @@ -27,27 +27,25 @@ public MediatorTwoStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var flow = new Workflow(workflowData); - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() - ); - - - var stepTwo = new Step("Test of Workflow Two", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), + var secondStep = new Step("Test of Workflow Two", + new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { }, - flow, null ); - _stepOne = new Step("Test of Workflow One", - new FireAndForgetAction(() => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }), - () => { flow.Data.Bag["MyValue"] = "TestTwo"; }, - flow, - stepTwo + var firstStep = new Step("Test of Workflow One", + new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { workflowData.Bag["MyValue"] = "TestTwo"; }, + secondStep ); + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + + _flow = new Workflow(firstStep, workflowData); } [Fact] @@ -55,9 +53,10 @@ public void When_running_a_single_step_workflow() { MyCommandHandler.ReceivedCommands.Clear(); - _mediator.RunWorkFlow(_stepOne); + _mediator.RunWorkFlow(_flow); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _flow.State.Should().Be(WorkflowState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index e5b6c1dee0..346dc4ee2b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -12,7 +12,7 @@ public class MediatorReplyStepFlowTests { private readonly Mediator _mediator; private bool _stepCompleted; - private readonly Step _step; + private readonly Workflow _flow; public MediatorReplyStepFlowTests() { @@ -35,16 +35,15 @@ public MediatorReplyStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var flow = new Workflow(workflowData) ; - - _step = new Step("Test of Workflow", + Step firstStep = new("Test of Workflow", new RequestAndReplyAction( - () => new MyCommand { Value = (flow.Data.Bag["MyValue"] as string)! }, - (reply) => flow.Data.Bag.Add("MyReply", ((MyEvent)reply).Value)), + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompleted = true; }, - flow, null); + _flow = new Workflow(firstStep, workflowData) ; + _mediator = new Mediator( commandProcessor, new InMemoryWorkflowStore() @@ -57,11 +56,12 @@ public void When_running_a_workflow_with_reply() MyCommandHandler.ReceivedCommands.Clear(); MyEventHandler.ReceivedEvents.Clear(); - _mediator.RunWorkFlow(_step); + _mediator.RunWorkFlow(_flow); _stepCompleted.Should().BeTrue(); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(WorkflowState.Done); } } From 6840f04815c740d8a7bd04cb3e0c742ebf02eedd Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 16:46:16 +0000 Subject: [PATCH 14/44] feat: add an ADR for adding the specification pattern --- .../adr/0023-add-the_specification-pattern.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/adr/0023-add-the_specification-pattern.md diff --git a/docs/adr/0023-add-the_specification-pattern.md b/docs/adr/0023-add-the_specification-pattern.md new file mode 100644 index 0000000000..fac0ebb414 --- /dev/null +++ b/docs/adr/0023-add-the_specification-pattern.md @@ -0,0 +1,23 @@ +# 22. Add the Specification Pattern + +Date: 2024-11-09 + +## Status + +Proposed + +## Context + +The Specification Pattern is a software design pattern that is used to define business rules that can be combined to create complex rules. It is used to encapsulate business rules that can be used to determine if an object meets a certain criteria. The pattern was described by Eric Evans and Martin Fowler in [this article](https://martinfowler.com/apsupp/spec.pdf). + +Brighter needs the addition of the specification pattern, for two reasons: + +1. For use with its Mediator. The Mediator allows Brighter to execute a workflow that has a branching condition. The Specification Pattern can be used to define the branching conditions. See [ADR-0022](0022-use-the-mediator-pattern.md). +2. For use when implementing the [Agreement Dispatcher](https://martinfowler.com/eaaDev/AgreementDispatcher.html) pattern from Martin Fowler. The Agreement Dispatcher pattern is used to dispatch a message to a handler based on a set of criteria. The Specification Pattern can be used to define the criteria. + +## Decision +Add the Specification Pattern to Brighter. We could have taken a dependency on an off-the-shelf implementation. Many of the Brighter team worked at Huddle Engineering, and worked on [this](https://github.com/HuddleEng/Specification) implementation of the Specification Pattern. However, this forces Brighter to take a dependency on another project, and we would like to keep Brighter as self-contained as possible. So, whilst we may be inspired by Huddle's implementation, we will write our own. In this version, we don't need some of the complexity of Huddle's usage of the Visitor pattern, as we only need to control branching. + +## Consequences + +Brighter will provide an implementation of the Specification pattern. \ No newline at end of file From 9e6324474821ebaf17dd33c8181e3666cea2a453 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 17:25:08 +0000 Subject: [PATCH 15/44] feat: add the specification pattern --- .../adr/0023-add-the_specification-pattern.md | 4 +- .../Specification.cs | 78 +++++++++++++++++++ .../When_evaluating_a_specificaion.cs | 74 ++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/Paramore.Brighter.MediatorWorkflow/Specification.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs diff --git a/docs/adr/0023-add-the_specification-pattern.md b/docs/adr/0023-add-the_specification-pattern.md index fac0ebb414..1d14baaf94 100644 --- a/docs/adr/0023-add-the_specification-pattern.md +++ b/docs/adr/0023-add-the_specification-pattern.md @@ -16,7 +16,9 @@ Brighter needs the addition of the specification pattern, for two reasons: 2. For use when implementing the [Agreement Dispatcher](https://martinfowler.com/eaaDev/AgreementDispatcher.html) pattern from Martin Fowler. The Agreement Dispatcher pattern is used to dispatch a message to a handler based on a set of criteria. The Specification Pattern can be used to define the criteria. ## Decision -Add the Specification Pattern to Brighter. We could have taken a dependency on an off-the-shelf implementation. Many of the Brighter team worked at Huddle Engineering, and worked on [this](https://github.com/HuddleEng/Specification) implementation of the Specification Pattern. However, this forces Brighter to take a dependency on another project, and we would like to keep Brighter as self-contained as possible. So, whilst we may be inspired by Huddle's implementation, we will write our own. In this version, we don't need some of the complexity of Huddle's usage of the Visitor pattern, as we only need to control branching. +Add the Specification Pattern to Brighter. We could have taken a dependency on an off-the-shelf implementation. Many of the Brighter team worked at Huddle Engineering, and worked on [this](https://github.com/HuddleEng/Specification) implementation of the Specification Pattern. However, this forces Brighter to take a dependency on another project, and we would like to keep Brighter as self-contained as possible. So, whilst we may be inspired by Huddle's implementation, we will write our own. + +In this version, we don't need some of the complexity of Huddle's usage of the Visitor pattern, as we only need to control branching. In addition, Huddle's version was written before the wide usage of lambda expressions via delegates in C#, so we can simplify the implementation. ## Consequences diff --git a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs new file mode 100644 index 0000000000..b34699f3df --- /dev/null +++ b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs @@ -0,0 +1,78 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +namespace Paramore.Brighter.MediatorWorkflow; + +using System; + +public interface ISpecification +{ + bool IsSatisfiedBy(T entity); + + ISpecification And(ISpecification other); + ISpecification Or(ISpecification other); + ISpecification Not(); + ISpecification AndNot(ISpecification other); + ISpecification OrNot(ISpecification other); +} + +public class Specification : ISpecification +{ + private readonly Func _expression; + + public Specification(Func expression) + { + _expression = expression ?? throw new ArgumentNullException(nameof(expression)); + } + + public bool IsSatisfiedBy(T entity) + { + return _expression(entity); + } + + public ISpecification And(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) && other.IsSatisfiedBy(x)); + } + + public ISpecification Or(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) || other.IsSatisfiedBy(x)); + } + + public ISpecification Not() + { + return new Specification(x => !IsSatisfiedBy(x)); + } + + public ISpecification AndNot(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) && !other.IsSatisfiedBy(x)); + } + + public ISpecification OrNot(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) || !other.IsSatisfiedBy(x)); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs new file mode 100644 index 0000000000..707a62e9e3 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs @@ -0,0 +1,74 @@ +using Paramore.Brighter.MediatorWorkflow; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Specifications; + +public class SpecificationTests +{ + [Fact] + public void When_evaluating_a_specificaion() + { + var specification = new Specification(state => state == WorkflowState.Done); + Assert.True(specification.IsSatisfiedBy(WorkflowState.Done)); + Assert.False(specification.IsSatisfiedBy(WorkflowState.Ready)); + } + + [Fact] + public void When_combining_specifications_with_and() + { + var doneSpecification = new Specification(state => state == WorkflowState.Done); + var runningSpecification = new Specification(state => state == WorkflowState.Running); + var combinedSpecification = doneSpecification.And(runningSpecification); + + Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); + Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + } + + [Fact] + public void When_combining_specifications_with_or() + { + var doneSpecification = new Specification(state => state == WorkflowState.Done); + var runningSpecification = new Specification(state => state == WorkflowState.Running); + var combinedSpecification = doneSpecification.Or(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); + Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Ready)); + } + + [Fact] + public void When_negating_a_specification() + { + var doneSpecification = new Specification(state => state == WorkflowState.Done); + var notDoneSpecification = doneSpecification.Not(); + + Assert.False(notDoneSpecification.IsSatisfiedBy(WorkflowState.Done)); + Assert.True(notDoneSpecification.IsSatisfiedBy(WorkflowState.Ready)); + } + + [Fact] + public void When_combining_specifications_with_and_not() + { + var doneSpecification = new Specification(state => state == WorkflowState.Done); + var runningSpecification = new Specification(state => state == WorkflowState.Running); + var combinedSpecification = doneSpecification.AndNot(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); + Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + } + + [Fact] + public void When_combining_specifications_with_or_not() + { + var doneSpecification = new Specification(state => state == WorkflowState.Done); + var runningSpecification = new Specification(state => state == WorkflowState.Running); + var combinedSpecification = doneSpecification.OrNot(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); + Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Ready)); + Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + } +} + + + From f7cf152e7a64c21bc0efae50d3957eb29cb617fc Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 17:31:24 +0000 Subject: [PATCH 16/44] fix: typo in filename --- ...ating_a_specificaion.cs => When_evaluating_a_specification.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/Paramore.Brighter.Core.Tests/Specifications/{When_evaluating_a_specificaion.cs => When_evaluating_a_specification.cs} (100%) diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs similarity index 100% rename from tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specificaion.cs rename to tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs From b09c3a6039ee4f1759fea6fd2a314d846d8d3688 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 18:25:21 +0000 Subject: [PATCH 17/44] chore: safety dance --- .../Specification.cs | 16 ++--- .../Workflows.cs | 20 ++++++ .../TestDoubles/SpecificationTestState.cs | 18 +++++ .../When_evaluating_a_specification.cs | 51 +++++++------- .../When_running_a_choice_workflow_step.cs | 70 +++++++++++++++++++ 5 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs index b34699f3df..0c48e7ff4b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs @@ -26,18 +26,18 @@ namespace Paramore.Brighter.MediatorWorkflow; using System; -public interface ISpecification +public interface ISpecification where TData : IAmTheWorkflowData { - bool IsSatisfiedBy(T entity); + bool IsSatisfiedBy(TData entity); - ISpecification And(ISpecification other); - ISpecification Or(ISpecification other); - ISpecification Not(); - ISpecification AndNot(ISpecification other); - ISpecification OrNot(ISpecification other); + ISpecification And(ISpecification other); + ISpecification Or(ISpecification other); + ISpecification Not(); + ISpecification AndNot(ISpecification other); + ISpecification OrNot(ISpecification other); } -public class Specification : ISpecification +public class Specification : ISpecification where T : IAmTheWorkflowData { private readonly Func _expression; diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 53234d3ea5..3562ae42c3 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -96,3 +96,23 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply)); } } + +/// +/// Represents a workflow based on evaluating a specification to determine which one to send +/// +/// +/// +/// +/// +/// +/// +public class ChoiceAction(Func trueRequestFactory, Func falseRequestFactory, ISpecification predicate) + : IWorkflowAction where TTrueRequest : class, IRequest where TFalseRequest : class, IRequest where TData : IAmTheWorkflowData +{ + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + IRequest command = predicate.IsSatisfiedBy(state.Data) ? trueRequestFactory() : falseRequestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs new file mode 100644 index 0000000000..e4b0652131 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Paramore.Brighter.MediatorWorkflow; + +namespace Paramore.Brighter.Core.Tests.Specifications.TestDoubles; + +public enum TestState +{ + Done, + Ready, + Running, + Waiting +} + +public class SpecificationTestState : IAmTheWorkflowData +{ + public TestState State { get; set; } + public Dictionary Bag { get; set; } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs index 707a62e9e3..6bb647d6d3 100644 --- a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs @@ -1,4 +1,5 @@ -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Core.Tests.Specifications.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; using Xunit; namespace Paramore.Brighter.Core.Tests.Specifications; @@ -8,65 +9,65 @@ public class SpecificationTests [Fact] public void When_evaluating_a_specificaion() { - var specification = new Specification(state => state == WorkflowState.Done); - Assert.True(specification.IsSatisfiedBy(WorkflowState.Done)); - Assert.False(specification.IsSatisfiedBy(WorkflowState.Ready)); + var specification = new Specification(state => state.State == TestState.Done); + Assert.True(specification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(specification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); } [Fact] public void When_combining_specifications_with_and() { - var doneSpecification = new Specification(state => state == WorkflowState.Done); - var runningSpecification = new Specification(state => state == WorkflowState.Running); + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); var combinedSpecification = doneSpecification.And(runningSpecification); - Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); - Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); } [Fact] public void When_combining_specifications_with_or() { - var doneSpecification = new Specification(state => state == WorkflowState.Done); - var runningSpecification = new Specification(state => state == WorkflowState.Running); + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); var combinedSpecification = doneSpecification.Or(runningSpecification); - Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); - Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); - Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Ready)); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); } [Fact] public void When_negating_a_specification() { - var doneSpecification = new Specification(state => state == WorkflowState.Done); + var doneSpecification = new Specification(state => state.State == TestState.Done); var notDoneSpecification = doneSpecification.Not(); - Assert.False(notDoneSpecification.IsSatisfiedBy(WorkflowState.Done)); - Assert.True(notDoneSpecification.IsSatisfiedBy(WorkflowState.Ready)); + Assert.False(notDoneSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(notDoneSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); } [Fact] public void When_combining_specifications_with_and_not() { - var doneSpecification = new Specification(state => state == WorkflowState.Done); - var runningSpecification = new Specification(state => state == WorkflowState.Running); + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); var combinedSpecification = doneSpecification.AndNot(runningSpecification); - Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); - Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); } [Fact] public void When_combining_specifications_with_or_not() { - var doneSpecification = new Specification(state => state == WorkflowState.Done); - var runningSpecification = new Specification(state => state == WorkflowState.Running); + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); var combinedSpecification = doneSpecification.OrNot(runningSpecification); - Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Done)); - Assert.True(combinedSpecification.IsSatisfiedBy(WorkflowState.Ready)); - Assert.False(combinedSpecification.IsSatisfiedBy(WorkflowState.Running)); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs new file mode 100644 index 0000000000..5e7b435cf6 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs @@ -0,0 +1,70 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorChoiceFlowTests +{ + private readonly Mediator? _mediator; + private readonly Workflow _flow; + private bool _stepCompletedTwo; + private bool _stepCompletedOne; + + public MediatorChoiceFlowTests(bool stepCompletedTwo) + { + _stepCompletedTwo = stepCompletedTwo; + // arrange + var registry = new SubscriberRegistry(); + registry.Register(); + registry.Register(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), + _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + var stepTwo = new Step("Test of Workflow Step Two", + new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + Step stepOne = new("Test of Workflow Step One", + new RequestAndReplyAction( + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), + () => { _stepCompletedOne = true; }, + stepTwo); + + _flow = new Workflow(stepOne, workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + public void When_running_a_choice_workflow_step() + { + + + // act + _mediator.RunWorkFlow(_flow); + + // assert + _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeTrue(); + } +} From 531301825d9ee55257c484fd703fe2e3fabd733f Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 9 Nov 2024 18:27:19 +0000 Subject: [PATCH 18/44] chore: safety dance --- .../Workflows/When_running_a_choice_workflow_step.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs index 5e7b435cf6..74a63a1bcd 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs @@ -42,9 +42,10 @@ public MediatorChoiceFlowTests(bool stepCompletedTwo) null); Step stepOne = new("Test of Workflow Step One", - new RequestAndReplyAction( + new ChoiceAction()( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), + () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + new Specification(x => x.Bag["MyValue"] as string == "Test"), () => { _stepCompletedOne = true; }, stepTwo); From c41547a0c2ad840421d2c438ecd3d76bb1d1aabc Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 10 Nov 2024 14:34:24 +0000 Subject: [PATCH 19/44] feat: add a choice workflow action --- .../Workflows.cs | 17 +++++++-- .../Workflows/TestDoubles/MyCommand.cs | 11 ++---- .../Workflows/TestDoubles/MyOtherCommand.cs | 8 +++++ .../TestDoubles/MyOtherCommandHandler.cs | 21 +++++++++++ .../When_running_a_choice_workflow_step.cs | 36 ++++++++----------- 5 files changed, 60 insertions(+), 33 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 3562ae42c3..08d315ed44 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -111,8 +111,19 @@ public class ChoiceAction(Func { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - IRequest command = predicate.IsSatisfiedBy(state.Data) ? trueRequestFactory() : falseRequestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); + //NOTE: we chose the command handler by parameterized type from the argument to Send() so the type needs to be explicit here + // do not try to optimize this branch condition via a base type, it will not work + if (predicate.IsSatisfiedBy(state.Data)) + { + TTrueRequest command = trueRequestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + } + else + { + TFalseRequest command = falseRequestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + } } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs index 9d450f7b3f..ada1447797 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs @@ -26,15 +26,8 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - public class MyCommand : Command + public class MyCommand() : Command(Guid.NewGuid()) { - public MyCommand() - :base(Guid.NewGuid()) - - {} - - public string Value { get; set; } - public bool WasCancelled { get; set; } - public bool TaskCompleted { get; set; } + public string Value { get; set; } = string.Empty; } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs new file mode 100644 index 0000000000..7d4cc6fff5 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs @@ -0,0 +1,8 @@ +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class MyOtherCommand() : Command(Guid.NewGuid().ToString()) +{ + public string Value { get; set; } = string.Empty; +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs new file mode 100644 index 0000000000..8c74640e3d --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class MyOtherCommandHandler(IAmACommandProcessor commandProcessor) : RequestHandler +{ + public static List ReceivedCommands { get; set; } = []; + + public override MyOtherCommand Handle(MyOtherCommand command) + { + LogCommand(command); + commandProcessor?.Publish(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}); + return base.Handle(command); + } + + private void LogCommand(MyOtherCommand request) + { + ReceivedCommands.Add(request); + } + +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs index 74a63a1bcd..81636ae359 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; using Polly.Registry; +using Xunit; namespace Paramore.Brighter.Core.Tests.Workflows; @@ -10,23 +12,21 @@ public class MediatorChoiceFlowTests { private readonly Mediator? _mediator; private readonly Workflow _flow; - private bool _stepCompletedTwo; private bool _stepCompletedOne; - public MediatorChoiceFlowTests(bool stepCompletedTwo) + public MediatorChoiceFlowTests() { - _stepCompletedTwo = stepCompletedTwo; // arrange var registry = new SubscriberRegistry(); registry.Register(); - registry.Register(); + registry.Register(); IAmACommandProcessor? commandProcessor = null; var handlerFactory = new SimpleHandlerFactorySync((handlerType) => handlerType switch { _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ when handlerType == typeof(MyOtherCommandHandler) => new (commandProcessor), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -34,20 +34,15 @@ public MediatorChoiceFlowTests(bool stepCompletedTwo) PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag.Add("MyValue", "Pass"); - var stepTwo = new Step("Test of Workflow Step Two", - new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - () => { _stepCompletedTwo = true; }, - null); - - Step stepOne = new("Test of Workflow Step One", - new ChoiceAction()( + var stepOne = new Step("Test of Workflow Step One", + new ChoiceAction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - new Specification(x => x.Bag["MyValue"] as string == "Test"), + new Specification(x => x.Bag["MyValue"] as string == "Pass")), () => { _stepCompletedOne = true; }, - stepTwo); + null); _flow = new Workflow(stepOne, workflowData) ; @@ -57,15 +52,14 @@ public MediatorChoiceFlowTests(bool stepCompletedTwo) ); } + [Fact] public void When_running_a_choice_workflow_step() { - + _mediator?.RunWorkFlow(_flow); - // act - _mediator.RunWorkFlow(_flow); - - // assert _stepCompletedOne.Should().BeTrue(); - _stepCompletedTwo.Should().BeTrue(); + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Pass").Should().BeTrue(); + MyOtherCommandHandler.ReceivedCommands.Any().Should().BeFalse(); + _stepCompletedOne.Should().BeTrue(); } } From c42b6b22dc838557463ab16fbefdd41a4ffce5ac Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 10 Nov 2024 16:16:42 +0000 Subject: [PATCH 20/44] fix: shared fixture problems --- ..._running_a_failing_choice_workflow_step.cs | 68 +++++++++++++++++++ ...running_a_passing_choice_workflow_step.cs} | 7 +- .../When_running_a_single_step_workflow.cs | 3 +- 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs rename tests/Paramore.Brighter.Core.Tests/Workflows/{When_running_a_choice_workflow_step.cs => When_running_a_passing_choice_workflow_step.cs} (92%) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs new file mode 100644 index 0000000000..b4345a64f8 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorFailingChoiceFlowTests +{ + private readonly Mediator? _mediator; + private readonly Workflow _flow; + private bool _stepCompletedOne; + + public MediatorFailingChoiceFlowTests() + { + // arrange + var registry = new SubscriberRegistry(); + registry.Register(); + registry.Register(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), + _ when handlerType == typeof(MyOtherCommandHandler) => new MyOtherCommandHandler(commandProcessor), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Fail"); + + var stepOne = new Step("Test of Workflow Step One", + new ChoiceAction( + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + new Specification(x => x.Bag["MyValue"] as string == "Pass")), + () => { _stepCompletedOne = true; }, + null); + + _flow = new Workflow(stepOne, workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + [Fact] + public void When_running_a_choice_workflow_step() + { + MyCommandHandler.ReceivedCommands.Clear(); + MyOtherCommandHandler.ReceivedCommands.Clear(); + + _mediator?.RunWorkFlow(_flow); + + _stepCompletedOne.Should().BeTrue(); + MyOtherCommandHandler.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); + MyCommandHandler.ReceivedCommands.Any().Should().BeFalse(); + _stepCompletedOne.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs similarity index 92% rename from tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs rename to tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 81636ae359..ef747b9a49 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -8,13 +8,13 @@ namespace Paramore.Brighter.Core.Tests.Workflows; -public class MediatorChoiceFlowTests +public class MediatorPassingChoiceFlowTests { private readonly Mediator? _mediator; private readonly Workflow _flow; private bool _stepCompletedOne; - public MediatorChoiceFlowTests() + public MediatorPassingChoiceFlowTests() { // arrange var registry = new SubscriberRegistry(); @@ -55,6 +55,9 @@ public MediatorChoiceFlowTests() [Fact] public void When_running_a_choice_workflow_step() { + MyCommandHandler.ReceivedCommands.Clear(); + MyOtherCommandHandler.ReceivedCommands.Clear(); + _mediator?.RunWorkFlow(_flow); _stepCompletedOne.Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 87427b8c22..b32b2f3396 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.MediatorWorkflow; From 40bf2b4956ec4c1b604279edbf0bc65dad4da15a Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 10 Nov 2024 19:54:39 +0000 Subject: [PATCH 21/44] feat: add first version of robust flow --- .../Workflows.cs | 106 +++++++++++++----- ..._running_a_failing_choice_workflow_step.cs | 2 +- ...running_a_multistep_workflow_with_reply.cs | 4 +- ..._running_a_passing_choice_workflow_step.cs | 2 +- .../When_running_a_single_step_workflow.cs | 2 +- .../When_running_a_two_step_workflow.cs | 4 +- .../When_running_a_workflow_with_reply.cs | 4 +- 7 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 08d315ed44..b1b2904a0d 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -50,20 +50,66 @@ public interface IWorkflowAction where TData : IAmTheWorkflowData void Handle(Workflow state, IAmACommandProcessor commandProcessor); } +/// +/// Represents a workflow based on evaluating a specification to determine which one to send +/// +/// The type of the true branch +/// The type of the false branch +/// The rule that decides between the command issued by each branch +/// +/// +/// +public class Choice( + Func trueRequestFactory, + Func falseRequestFactory, + ISpecification predicate +) + : IWorkflowAction + where TTrueRequest : class, IRequest + where TFalseRequest : class, IRequest + where TData : IAmTheWorkflowData +{ + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + //NOTE: we chose the command handler by parameterized type from the argument to Send() so the type needs to be explicit here + // do not try to optimize this branch condition via a base type, it will not work + if (predicate.IsSatisfiedBy(state.Data)) + { + TTrueRequest command = trueRequestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + } + else + { + TFalseRequest command = falseRequestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + } + } +} + /// /// Represents a fire-and-forget action in the workflow. /// /// The type of the request. /// The type of the workflow data. /// The factory method to create the request. -public class FireAndForgetAction(Func requestFactory) : IWorkflowAction where TRequest : class, IRequest where TData : IAmTheWorkflowData +public class FireAndForget( + Func requestFactory + ) + : IWorkflowAction + where TRequest : class, IRequest + where TData : IAmTheWorkflowData { /// /// Handles the fire-and-forget action. /// /// The current state of the workflow. /// The command processor used to handle commands. - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + public void Handle( + Workflow state, + IAmACommandProcessor commandProcessor + ) { var command = requestFactory(); command.CorrelationId = state.Id; @@ -79,8 +125,14 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) /// The type of the workflow data. /// The factory method to create the request. /// The factory method to handle the reply. -public class RequestAndReplyAction(Func requestFactory, Action replyFactory) - : IWorkflowAction where TRequest : class, IRequest where TReply : class, IRequest where TData : IAmTheWorkflowData +public class RequestAndReaction( + Func requestFactory, + Action replyFactory + ) + : IWorkflowAction + where TRequest : class, IRequest + where TReply : Event + where TData : IAmTheWorkflowData { /// /// Handles the request-and-reply action. @@ -93,37 +145,37 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) command.CorrelationId = state.Id; commandProcessor.Send(command); - state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply)); + state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply as TReply)); } } /// -/// Represents a workflow based on evaluating a specification to determine which one to send +/// /// -/// -/// -/// -/// -/// +/// +/// +/// +/// /// -public class ChoiceAction(Func trueRequestFactory, Func falseRequestFactory, ISpecification predicate) - : IWorkflowAction where TTrueRequest : class, IRequest where TFalseRequest : class, IRequest where TData : IAmTheWorkflowData +/// +public class RobustRequestAndReaction( + Func requestFactory, + Action replyFactory, + Action faultFactory +) + : IWorkflowAction + where TRequest : class, IRequest + where TReply : Event + where TFault: Event + where TData : IAmTheWorkflowData { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - //NOTE: we chose the command handler by parameterized type from the argument to Send() so the type needs to be explicit here - // do not try to optimize this branch condition via a base type, it will not work - if (predicate.IsSatisfiedBy(state.Data)) - { - TTrueRequest command = trueRequestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - } - else - { - TFalseRequest command = falseRequestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - } + var command = requestFactory(); + command.CorrelationId = state.Id; + commandProcessor.Send(command); + + state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply as TReply)); + state.PendingResponses.Add(typeof(TFault), (reply, _) => faultFactory(reply as TFault)); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index b4345a64f8..6d69df2921 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -37,7 +37,7 @@ public MediatorFailingChoiceFlowTests() workflowData.Bag.Add("MyValue", "Fail"); var stepOne = new Step("Test of Workflow Step One", - new ChoiceAction( + new Choice( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, new Specification(x => x.Bag["MyValue"] as string == "Pass")), diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 89ccaa7faa..8cdd2a8218 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -37,12 +37,12 @@ public MediatorReplyMultiStepFlowTests() workflowData.Bag.Add("MyValue", "Test"); var stepTwo = new Step("Test of Workflow Step Two", - new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); Step stepOne = new("Test of Workflow Step One", - new RequestAndReplyAction( + new RequestAndReaction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompletedOne = true; }, diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index ef747b9a49..4af16bf8c4 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -37,7 +37,7 @@ public MediatorPassingChoiceFlowTests() workflowData.Bag.Add("MyValue", "Pass"); var stepOne = new Step("Test of Workflow Step One", - new ChoiceAction( + new Choice( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, new Specification(x => x.Bag["MyValue"] as string == "Pass")), diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index b32b2f3396..59cd2abad5 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -27,7 +27,7 @@ public MediatorOneStepFlowTests() workflowData.Bag.Add("MyValue", "Test"); var firstStep = new Step("Test of Workflow", - new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), () => { }, null ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 7a31f37b59..ca6bc4fcb0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -29,13 +29,13 @@ public MediatorTwoStepFlowTests() var secondStep = new Step("Test of Workflow Two", - new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { }, null ); var firstStep = new Step("Test of Workflow One", - new FireAndForgetAction(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 346dc4ee2b..6787f747a6 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -35,8 +35,8 @@ public MediatorReplyStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - Step firstStep = new("Test of Workflow", - new RequestAndReplyAction( + var firstStep = new Step("Test of Workflow", + new RequestAndReaction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompleted = true; }, From 56f577e1358b62606da9d1ccb6ad5c905670cb0b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 11 Nov 2024 08:34:22 +0000 Subject: [PATCH 22/44] fix: remove IAmTheWorkflowData as unnecessary abstraction. --- .../IAmAWorkflowStore.cs | 2 +- .../InMemoryWorkflowStore.cs | 2 +- .../Mediator.cs | 11 ++++-- .../Specification.cs | 4 +- .../Workflow.cs | 39 ++++++++++++------- .../Workflows.cs | 18 ++++----- .../TestDoubles/SpecificationTestState.cs | 2 +- .../Workflows/TestDoubles/WorkflowTestData.cs | 2 +- 8 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs index 4623734e9f..f2d04070e9 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs @@ -35,7 +35,7 @@ public interface IAmAWorkflowStore /// Saves the workflow /// /// The workflow - void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData; + void SaveWorkflow(Workflow workflow); /// /// Retrieves a workflow via its Id diff --git a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs index 606c255e50..c8c4fbcdd1 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs @@ -31,7 +31,7 @@ public class InMemoryWorkflowStore : IAmAWorkflowStore { private readonly Dictionary _flows = new(); - public void SaveWorkflow(Workflow workflow) where TData : IAmTheWorkflowData + public void SaveWorkflow(Workflow workflow) { _flows[workflow.Id] = workflow; } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index ab433014d9..18c9859c78 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -27,11 +27,11 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MediatorWorkflow; /// -/// The `Mediator` class orchestrates a workflow by executing each step in a sequence. +/// The class orchestrates a workflow by executing each step in a sequence. /// It uses a command processor and a workflow store to manage the workflow's state and actions. /// /// The type of the workflow data. -public class Mediator where TData : IAmTheWorkflowData +public class Mediator { private readonly IAmACommandProcessor _commandProcessor; private readonly IAmAWorkflowStore _workflowStore; @@ -92,13 +92,16 @@ public void ReceiveWorkflowEvent(Event @event) var eventType = @event.GetType(); - if (!workflow.PendingResponses.TryGetValue(eventType, out Action>? replyFactory)) + if (!workflow.PendingResponses.TryGetValue(eventType, out WorkflowResponse? workflowResponse)) return; + if (workflowResponse.Parser is null) + throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); + if (workflow.CurrentStep is null) throw new InvalidOperationException($"Current step of workflow #{workflow.Id} should not be null"); - replyFactory(@event, workflow); + workflowResponse.Parser(@event, workflow); workflow.CurrentStep.OnCompletion(); workflow.State = WorkflowState.Running; workflow.PendingResponses.Remove(eventType); diff --git a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs index 0c48e7ff4b..4952858e1d 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Specification.cs @@ -26,7 +26,7 @@ namespace Paramore.Brighter.MediatorWorkflow; using System; -public interface ISpecification where TData : IAmTheWorkflowData +public interface ISpecification { bool IsSatisfiedBy(TData entity); @@ -37,7 +37,7 @@ public interface ISpecification where TData : IAmTheWorkflowData ISpecification OrNot(ISpecification other); } -public class Specification : ISpecification where T : IAmTheWorkflowData +public class Specification : ISpecification { private readonly Func _expression; diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 263e5d6f05..820346b939 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -39,26 +39,39 @@ public enum WorkflowState } /// -/// empty class, used as maker for the workflow data +/// When we are awaiting a response for a workflow, we need to store information about how to continue the workflow +/// after receiving the event. /// -public abstract class Workflow { } +/// The parser to populate our worfkow from the event that forms the response +/// The type we expect a response to be - used to check the flow +/// The type we expect a fault to be - used to check the flow +/// The user-defined data, associated with a workflow +public class WorkflowResponse(Action> parser, Type responseType, Type? errorType) +{ + public Action>? Parser { get; set; } = parser; + public Type? ResponseType { get; set; } = responseType; + public Type? ErrorType { get; set; } = errorType; + + public bool HasError() => ErrorType is not null; +} /// -/// Interface for the data that is passed between steps in the workflow +/// empty class, used as maker for the workflow data /// -public interface IAmTheWorkflowData -{ - /// - /// Bucket for data that is passed between steps in the workflow - /// - public Dictionary Bag { get; set; } -} +public abstract class Workflow { } /// /// Workflow represents the current state of the workflow and tracks if it’s awaiting a response. /// -public class Workflow : Workflow where TData : IAmTheWorkflowData +/// The user defined data for the workflow +public class Workflow : Workflow { + + /// + /// A map of user defined values. Normally, use Data to pass data between steps + /// + public Dictionary Bag { get; } = new(); + /// /// What step are we currently at in the workflow /// @@ -74,12 +87,12 @@ public class Workflow : Workflow where TData : IAmTheWorkflowData /// /// If we are awaiting a response, we store the type of the response and the action to take when it arrives /// - public Dictionary>> PendingResponses { get; private set; } = new(); + public Dictionary> PendingResponses { get; private set; } = new(); /// /// Is the workflow currently awaiting an event response /// - public WorkflowState State { get; set; } = WorkflowState.Ready; + public WorkflowState State { get; set; } /// /// Constructs a new Workflow diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index b1b2904a0d..d78c278f14 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -34,13 +34,13 @@ namespace Paramore.Brighter.MediatorWorkflow; /// The action to be taken with the step. /// The action to be taken upon completion of the step. /// The next step in the sequence. -public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next) where TData : IAmTheWorkflowData; +public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next); /// /// Defines an interface for workflow actions. /// /// The type of the workflow data. -public interface IWorkflowAction where TData : IAmTheWorkflowData +public interface IWorkflowAction { /// /// Handles the workflow action. @@ -67,7 +67,6 @@ ISpecification predicate : IWorkflowAction where TTrueRequest : class, IRequest where TFalseRequest : class, IRequest - where TData : IAmTheWorkflowData { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { @@ -99,7 +98,6 @@ Func requestFactory ) : IWorkflowAction where TRequest : class, IRequest - where TData : IAmTheWorkflowData { /// /// Handles the fire-and-forget action. @@ -132,7 +130,6 @@ public class RequestAndReaction( : IWorkflowAction where TRequest : class, IRequest where TReply : Event - where TData : IAmTheWorkflowData { /// /// Handles the request-and-reply action. @@ -145,7 +142,8 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) command.CorrelationId = state.Id; commandProcessor.Send(command); - state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply as TReply)); + state.PendingResponses.Add(typeof(TReply), new WorkflowResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); + } } @@ -167,15 +165,13 @@ public class RobustRequestAndReaction( where TRequest : class, IRequest where TReply : Event where TFault: Event - where TData : IAmTheWorkflowData { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { var command = requestFactory(); command.CorrelationId = state.Id; commandProcessor.Send(command); - - state.PendingResponses.Add(typeof(TReply), (reply, _) => replyFactory(reply as TReply)); - state.PendingResponses.Add(typeof(TFault), (reply, _) => faultFactory(reply as TFault)); - } + + state.PendingResponses.Add(typeof(TReply), new WorkflowResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); + state.PendingResponses.Add(typeof(TFault), new WorkflowResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} } diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs index e4b0652131..af19bccdb3 100644 --- a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs @@ -11,7 +11,7 @@ public enum TestState Waiting } -public class SpecificationTestState : IAmTheWorkflowData +public class SpecificationTestState { public TestState State { get; set; } public Dictionary Bag { get; set; } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs index c39b95c442..f413973687 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs @@ -3,7 +3,7 @@ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -public class WorkflowTestData : IAmTheWorkflowData +public class WorkflowTestData { public Dictionary Bag { get; set; } = new(); } From 3cb3c4bf3e6eb06eea76db0e9dac802f4bda6889 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 11 Nov 2024 12:02:23 +0000 Subject: [PATCH 23/44] fix: make choice about choosing the next step from the workflow data --- .../Workflows.cs | 35 +++------ .../Workflows/TestDoubles/MyFault.cs | 33 +++++++++ ..._running_a_failing_choice_workflow_step.cs | 22 +++++- ..._running_a_passing_choice_workflow_step.cs | 18 ++++- ...ng_a_workflow_with_robust_reply_nofault.cs | 73 +++++++++++++++++++ 5 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index d78c278f14..2b4896b33e 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.MediatorWorkflow; /// The action to be taken with the step. /// The action to be taken upon completion of the step. /// The next step in the sequence. -public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next); +public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next, Action? OnFaulted = null, Step? FaultNext = null); /// /// Defines an interface for workflow actions. @@ -53,37 +53,24 @@ public interface IWorkflowAction /// /// Represents a workflow based on evaluating a specification to determine which one to send /// -/// The type of the true branch -/// The type of the false branch /// The rule that decides between the command issued by each branch -/// -/// -/// -public class Choice( - Func trueRequestFactory, - Func falseRequestFactory, +/// The workflow data, used to make the choice +public class Choice( + Func> OnTrue, + Func> OnFalse, ISpecification predicate ) : IWorkflowAction - where TTrueRequest : class, IRequest - where TFalseRequest : class, IRequest { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - //NOTE: we chose the command handler by parameterized type from the argument to Send() so the type needs to be explicit here - // do not try to optimize this branch condition via a base type, it will not work - if (predicate.IsSatisfiedBy(state.Data)) - { - TTrueRequest command = trueRequestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - } - else + if (state.CurrentStep is null) + throw new InvalidOperationException("The workflow has not been initialized."); + + state.CurrentStep = state.CurrentStep with { - TFalseRequest command = falseRequestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - } + Next = (predicate.IsSatisfiedBy(state.Data) ? OnTrue(state.Data) : OnFalse(state.Data)) + }; } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs new file mode 100644 index 0000000000..b46dfab693 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs @@ -0,0 +1,33 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyFault(string? value) : Event(Guid.NewGuid().ToString()) + { + public string Value { get; set; } = value ?? string.Empty; + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 6d69df2921..e7ecbca887 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -13,6 +13,8 @@ public class MediatorFailingChoiceFlowTests private readonly Mediator? _mediator; private readonly Workflow _flow; private bool _stepCompletedOne; + private bool _stepCompletedTwo; + private bool _stepCompletedThree; public MediatorFailingChoiceFlowTests() { @@ -36,10 +38,20 @@ public MediatorFailingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Fail"); - var stepOne = new Step("Test of Workflow Step One", - new Choice( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + var stepThree = new Step("Test of Workflow Step Three", + new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedThree = true; }, + null); + + var stepTwo = new Step("Test of Workflow Step Two", + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + var stepOne = new Step("Test of Workflow Step One", + new Choice( + (_) => stepTwo, + (_) => stepThree, new Specification(x => x.Bag["MyValue"] as string == "Pass")), () => { _stepCompletedOne = true; }, null); @@ -61,6 +73,8 @@ public void When_running_a_choice_workflow_step() _mediator?.RunWorkFlow(_flow); _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeFalse(); + _stepCompletedThree.Should().BeTrue(); MyOtherCommandHandler.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); MyCommandHandler.ReceivedCommands.Any().Should().BeFalse(); _stepCompletedOne.Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 4af16bf8c4..3a07565d09 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -13,6 +13,8 @@ public class MediatorPassingChoiceFlowTests private readonly Mediator? _mediator; private readonly Workflow _flow; private bool _stepCompletedOne; + private bool _stepCompletedTwo; + private bool _stepCompletedThree; public MediatorPassingChoiceFlowTests() { @@ -35,11 +37,21 @@ public MediatorPassingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Pass"); + + var stepThree = new Step("Test of Workflow Step Three", + new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedThree = true; }, + null); + + var stepTwo = new Step("Test of Workflow Step Two", + new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); var stepOne = new Step("Test of Workflow Step One", - new Choice( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - () => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + new Choice( + (_) => stepTwo, + (_) => stepThree, new Specification(x => x.Bag["MyValue"] as string == "Pass")), () => { _stepCompletedOne = true; }, null); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs new file mode 100644 index 0000000000..db396da885 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using Amazon.Runtime.Internal.Transform; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorRobustReplyNoFaultStepFlowTests +{ + private readonly Mediator _mediator; + private bool _stepCompleted; + private bool _stepFaulted; + private readonly Workflow _flow; + + public MediatorRobustReplyNoFaultStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + registry.Register(); + + IAmACommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), + _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + var firstStep = new Step("Test of Workflow", + new RobustRequestAndReaction( + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value), + (fault) => workflowData.Bag.Add("MyFault", ((MyFault)fault).Value)), + () => { _stepCompleted = true; }, + null, + () => { _stepFaulted = true; }, + null); + + _flow = new Workflow(firstStep, workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + [Fact] + public void When_running_a_workflow_with_reply() + { + MyCommandHandler.ReceivedCommands.Clear(); + MyEventHandler.ReceivedEvents.Clear(); + + _mediator.RunWorkFlow(_flow); + + _stepCompleted.Should().BeTrue(); + _stepFaulted.Should().BeFalse(); + + MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(WorkflowState.Done); + } +} From bd6d2e18e62a22c9970444b75275747b2d511b28 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 11 Nov 2024 15:11:02 +0000 Subject: [PATCH 24/44] fix: tests not checking all paths --- .../Workflows.cs | 43 ++++++++++++++++--- ..._running_a_passing_choice_workflow_step.cs | 2 + 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs index 2b4896b33e..4743ade68b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; namespace Paramore.Brighter.MediatorWorkflow; @@ -51,16 +52,44 @@ public interface IWorkflowAction } /// -/// Represents a workflow based on evaluating a specification to determine which one to send +/// Pauses a flow for a specified amount of time. Note that this is a blocking operation for the workflow thread. /// -/// The rule that decides between the command issued by each branch +/// The data for the workflow; not used +public class BlockingWait : IWorkflowAction +{ + private readonly TimeSpan _wait; + private readonly TimeProvider _timeProvider; + + /// + /// Pauses a flow for a specified amount of time. Note that this is a blocking operation for the workflow thread. + /// + /// The time to block the thread for + /// The time provider; defaults to TimeProvider.System; intended to be overriden with FakeTimeProvider in testing + /// The data for the workflow; not used + public BlockingWait(TimeSpan wait, TimeProvider? timeProvider = null) + { + _wait = wait; + _timeProvider = timeProvider ?? TimeProvider.System; + } + + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + _timeProvider.Delay(_wait).Wait(); + } +} + +/// +/// Represents a workflow based on evaluating a specification to determine which step to take next +/// +/// The step to take if the specification is satisfied +/// The step to take if the specification is not satisfied +/// The rule that decides between each branch /// The workflow data, used to make the choice public class Choice( - Func> OnTrue, - Func> OnFalse, + Func> onTrue, + Func> onFalse, ISpecification predicate -) - : IWorkflowAction +) : IWorkflowAction { public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { @@ -69,7 +98,7 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) state.CurrentStep = state.CurrentStep with { - Next = (predicate.IsSatisfiedBy(state.Data) ? OnTrue(state.Data) : OnFalse(state.Data)) + Next = (predicate.IsSatisfiedBy(state.Data) ? onTrue(state.Data) : onFalse(state.Data)) }; } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 3a07565d09..95c4619bc2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -73,6 +73,8 @@ public void When_running_a_choice_workflow_step() _mediator?.RunWorkFlow(_flow); _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeTrue(); + _stepCompletedThree.Should().BeFalse(); MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Pass").Should().BeTrue(); MyOtherCommandHandler.ReceivedCommands.Any().Should().BeFalse(); _stepCompletedOne.Should().BeTrue(); From 12a7ecda6177734a44be35bdc0add1b9e14bca01 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 12 Nov 2024 10:44:44 +0000 Subject: [PATCH 25/44] fix: add workflow patterns to ADR --- docs/adr/0022-add-a-mediator.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md index 79f1cc8227..b68c0e7694 100644 --- a/docs/adr/0022-add-a-mediator.md +++ b/docs/adr/0022-add-a-mediator.md @@ -17,6 +17,8 @@ In principle, nothing stops an end user from implementing a `Mediator` class tha Other dotnet messaging platforms erroneously conflate the Saga and Mediator patterns. A Saga is a long-running transaction that spans multiple services. A Mediator is an orchestrator that manages a workflow that involves multiple objects. One aspect of those implementations is typically the ability to store workflow state. +There is a pattern catalogue associated with workflows. [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) describes both basic and advanced patterns for workflows. We intend to use these patters as guidance for our offering, over traditional .NET workflow offerings in competing products such as Mass Transit and NServicBus, which have tended to be ersatz in design. + A particular reference for the requirements for this work is [AWS step functions](https://states-language.net/spec.html). AWS Step functions provide a state machine that mediates calls to AWS Lambda functions. When thinking about Brighter's `IHandleRequests` it is attractive to compare them to Lambda functions in the Step functions model : 1. The AWS Step funcions state machine does not hold the business logic, that is located in the functions called; the Step function handles calling the Lambda functions and state transitions (as well as error paths) @@ -24,8 +26,7 @@ A particular reference for the requirements for this work is [AWS step functions This approach is intended to enable flexible, event-driven workflows that can handle various business processes and requirements, including asynchronous event handling and conditional branching. -We are also influenced by the [Arazzo Specification](https://github.com/OAI/Arazzo-Specification/blob/main/versions/1.0.0.md) for defining workflows from AsyncAPI. - +Our experience has been that many teams adopt Step Functions to gain access to it as a workflow engine. But this forces them into Lambda Pinball architectures. We believe that Brighter could offer a compelling alternative. ## Decision @@ -43,7 +44,7 @@ We will add a `Mediator` class to Brighter that will: The Specification Pattern in ChoiceProcessState allows flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. -We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, such as process states, UIs for workflows will be a feature of later releases. +We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, such as process states, UIs for workflows will be a feature of later releases. Broady our goal within V10 would be to ensure that from [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) we can deliver the Basic Control Flow patterns. A stretch goal would be to offer some Iteration and Cnacellation patterns. ## Consequences From f24fac26f09092e6ca2f0ca8443042ac0ff66d9a Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 13 Nov 2024 13:28:06 +0200 Subject: [PATCH 26/44] feat: move to workflow patterns style, step and task; some behaviours shift --- docs/adr/0022-add-a-mediator.md | 26 ++-- .../Mediator.cs | 6 +- .../Steps.cs | 113 ++++++++++++++++++ .../{Workflows.cs => Tasks.cs} | 71 ++--------- .../Workflow.cs | 32 ++--- .../When_running_a_blocking_wait_workflow.cs | 58 +++++++++ .../When_running_a_change_workflow.cs | 64 ++++++++++ ..._running_a_failing_choice_workflow_step.cs | 17 +-- ...running_a_multistep_workflow_with_reply.cs | 6 +- ..._running_a_passing_choice_workflow_step.cs | 17 +-- .../When_running_a_single_step_workflow.cs | 3 +- .../When_running_a_two_step_workflow.cs | 6 +- .../When_running_a_workflow_with_reply.cs | 3 +- ...ng_a_workflow_with_robust_reply_nofault.cs | 3 +- 14 files changed, 310 insertions(+), 115 deletions(-) create mode 100644 src/Paramore.Brighter.MediatorWorkflow/Steps.cs rename src/Paramore.Brighter.MediatorWorkflow/{Workflows.cs => Tasks.cs} (64%) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md index b68c0e7694..11e4b53452 100644 --- a/docs/adr/0022-add-a-mediator.md +++ b/docs/adr/0022-add-a-mediator.md @@ -10,7 +10,7 @@ Proposed We have two approaches to a workflow: orchestration and choreography. In choreography the workflow emerges from the interaction of the participants. In orchestration, one participant executes the workflow, calling other participants as needed. Whilst choreography has low-coupling, it also has low-cohesion. At scale this can lead to the Pinball anti-pattern, where it is difficult to maintain the workflow. The [Mediator](https://www.oodesign.com/mediator-pattern) pattern provides an orchestrator that manages a workflow that involves multiple objects. In its simplest form, instead of talking to each other, objects talk to the mediator, which then calls other objects as required to execute the workflow. - + Brighter provides `IHandleRequests<>` to provide a handler for an individual request, either a command or an event. It is possible to have an emergent workflow, within Brighter, through the choreography of these handlers. However, Brighter provides no model for an orchestrator that manages a workflow that involves multiple handlers. In particular, Brighter does not support a class that can listen to multiple requests and then call other handlers as required to execute the workflow. In principle, nothing stops an end user from implementing a `Mediator` class that listens to multiple requests and then calls other handlers as required to execute the workflow. So orchestration has always been viable, but left as an exercise to the user. However, competing OSS projects provide popular workflow functionality, suggesting there is demand for an off-the-shelf solution. @@ -33,18 +33,18 @@ Our experience has been that many teams adopt Step Functions to gain access to i We will add a `Mediator` class to Brighter that will: 1. Manages and tracks a WorkflowState object representing the current step in the workflow. - 2. Supports multiple process states, including: - • StartState: Initiates the workflow. - • FireAndForgetProcessState: Dispatches a `Command` and immediately advances to the next state. - • RequestReactionProcessState: Dispatches a `Command` and waits for an event response before advancing. - • ChoiceProcessState: Evaluates conditions using the `Specification` Pattern and chooses the next `Command` to Dispatch based on the evaluation. - • WaitState: Suspends execution for a specified TimeSpan before advancing. - 3. Uses a CommandProcessor for routing commands and events to appropriate handlers. - 4. Can be passed events, and uses the correlation IDs to match events to specific workflow instances and advance the workflow accordingly. - -The Specification Pattern in ChoiceProcessState allows flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. - -We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, such as process states, UIs for workflows will be a feature of later releases. Broady our goal within V10 would be to ensure that from [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) we can deliver the Basic Control Flow patterns. A stretch goal would be to offer some Iteration and Cnacellation patterns. + 2. Support multiple steps: sequence, choice, parallel, wait. + 3. Supports multiple tasks, mapped to typical ws-messaging patterns including: + • FireAndForget: Dispatches a `Command` and immediately advances to the next state. + • RequestReaction: Dispatches a `Command` and waits for an event response before advancing. + • RobustRequestReaction: Reaction event can kick off an error flow. + 4. Uses a CommandProcessor for routing commands and events to appropriate handlers. + 5. Work is handled within Brighter handlers. They use glue code to call back to the workflow where necessary + 6. Can be passed events, and uses the correlation IDs to match events to specific workflow instances and advance the workflow accordingly. + +The Specification Pattern in a Choice steo will allow flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. + +We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, workflows, etc. will be a feature of later releases. Broady our goal within V10 would be to ensure that from [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) we can deliver the Basic Control Flow patterns. A stretch goal would be to offer some Iteration and Cnacellation patterns. ## Consequences diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs index 18c9859c78..f5bfbfaf9f 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs @@ -66,9 +66,7 @@ public void RunWorkFlow(Workflow workflow) while (workflow.CurrentStep is not null) { - workflow.CurrentStep.Action.Handle(workflow, _commandProcessor); - workflow.CurrentStep.OnCompletion(); - workflow.CurrentStep = workflow.CurrentStep.Next; + workflow.CurrentStep.Execute(workflow, _commandProcessor); _workflowStore.SaveWorkflow(workflow); } @@ -102,7 +100,7 @@ public void ReceiveWorkflowEvent(Event @event) throw new InvalidOperationException($"Current step of workflow #{workflow.Id} should not be null"); workflowResponse.Parser(@event, workflow); - workflow.CurrentStep.OnCompletion(); + workflow.CurrentStep.OnCompletion?.Invoke(); workflow.State = WorkflowState.Running; workflow.PendingResponses.Remove(eventType); } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Steps.cs b/src/Paramore.Brighter.MediatorWorkflow/Steps.cs new file mode 100644 index 0000000000..7213d3db25 --- /dev/null +++ b/src/Paramore.Brighter.MediatorWorkflow/Steps.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; + +namespace Paramore.Brighter.MediatorWorkflow; + +/// +/// The base type for a step in the workflow. +/// +/// The name of the step, used for tracing execution +/// The next step in the sequence, null if this is the last step. +/// The action to be taken with the step, null if no action +/// An optional callback to run, following completion of the step +/// The data that the step operates over +public abstract class Step(string name, Sequence? next, IStepTask? stepTask = null, Action? onCompletion = null) +{ + /// The name of the step, used for tracing execution + public string Name { get; init; } = name; + + /// The next step in the sequence, null if this is the last step + protected Sequence? Next { get; } = next; + + /// An optional callback to be run, following completion of the step. + public Action? OnCompletion { get; } = onCompletion; + + /// The action to be taken with the step. + protected IStepTask? StepTask { get; } = stepTask; + + public virtual void Execute(Workflow state, IAmACommandProcessor commandProcessor) + { + StepTask?.Handle(state, commandProcessor); + OnCompletion?.Invoke(); + } +} + +/// +/// Represents a step in the workflow. Steps form a singly linked list. +/// +/// The name of the step, used for tracing execution +/// The action to be taken with the step. +/// An optional callback to run, following completion of the step +/// The next step in the sequence, null if this is the last step. +/// An optional callback to run, following a faulted execution of the step +/// The next step in the sequence, following a faulted execution of the step +/// The data that the step operates over +public class Sequence( + string name, + IStepTask stepTask, + Action? onCompletion, + Sequence? next, + Action? onFaulted = null, + Sequence? faultNext = null + ) + : Step(name, next, stepTask, onCompletion) +{ + public override void Execute(Workflow state, IAmACommandProcessor commandProcessor) + { + try + { + StepTask?.Handle(state, commandProcessor); + OnCompletion?.Invoke(); + state.CurrentStep = Next; + } + catch (Exception) + { + onFaulted?.Invoke(); + state.CurrentStep = faultNext; + } + } +} + +/// +/// Allows the workflow to branch on a choice, taking either a right or left path. +/// +/// The name of the step, used for tracing execution +/// A composite specification that can be evaluated to determine the path to choose +/// An optional callback to run, following completion of the step +/// The next step in the sequence, if the predicate evaluates to true, null if this is the last step. +/// The next step in the sequence, if the predicate evaluates to false, null if this is the last step. +/// The data that the step operates over +public class ExclusiveChoice( + string name, + ISpecification predicate, + Action? onCompletion, + Sequence? nextTrue, + Sequence? nextFalse +) + : Step(name, null, null, onCompletion) +{ + public override void Execute(Workflow state, IAmACommandProcessor commandProcessor) + { + state.CurrentStep = predicate.IsSatisfiedBy(state.Data) ? nextTrue : nextFalse; + } +} + +/// +/// Allows the workflow to pause. This is a blocking operation that pauses the executing thread +/// +/// The name of the step, used for tracing execution +/// The period for which we pause +/// An optional callback to run, following completion of the step +/// The next step in the sequence, null if this is the last step. +/// The data that the step operates over +public class Wait(string name, TimeSpan duration, Action? onCompletion, Sequence? next) + : Step(name, next, null, onCompletion) +{ + public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + { + Task.Delay(duration).Wait(); + OnCompletion?.Invoke(); + } +} + + diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs b/src/Paramore.Brighter.MediatorWorkflow/Tasks.cs similarity index 64% rename from src/Paramore.Brighter.MediatorWorkflow/Workflows.cs rename to src/Paramore.Brighter.MediatorWorkflow/Tasks.cs index 4743ade68b..a82dafe750 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflows.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Tasks.cs @@ -23,25 +23,14 @@ THE SOFTWARE. */ #endregion using System; -using System.Threading.Tasks; namespace Paramore.Brighter.MediatorWorkflow; -/// -/// Represents a step in the workflow. Steps form a singly linked list. -/// -/// The type of the workflow data. -/// The name of the step. -/// The action to be taken with the step. -/// The action to be taken upon completion of the step. -/// The next step in the sequence. -public record Step(string Name, IWorkflowAction Action, Action OnCompletion, Step? Next, Action? OnFaulted = null, Step? FaultNext = null); - /// /// Defines an interface for workflow actions. /// /// The type of the workflow data. -public interface IWorkflowAction +public interface IStepTask { /// /// Handles the workflow action. @@ -52,54 +41,18 @@ public interface IWorkflowAction } /// -/// Pauses a flow for a specified amount of time. Note that this is a blocking operation for the workflow thread. +/// Essentially a pass through step, it alters Data property by running the transform +/// given by onChange over it /// -/// The data for the workflow; not used -public class BlockingWait : IWorkflowAction +/// Takes the Data property and transforms it +/// The workflow data, that we wish to transform +public class Change( + Func onChange +) : IStepTask { - private readonly TimeSpan _wait; - private readonly TimeProvider _timeProvider; - - /// - /// Pauses a flow for a specified amount of time. Note that this is a blocking operation for the workflow thread. - /// - /// The time to block the thread for - /// The time provider; defaults to TimeProvider.System; intended to be overriden with FakeTimeProvider in testing - /// The data for the workflow; not used - public BlockingWait(TimeSpan wait, TimeProvider? timeProvider = null) - { - _wait = wait; - _timeProvider = timeProvider ?? TimeProvider.System; - } - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) { - _timeProvider.Delay(_wait).Wait(); - } -} - -/// -/// Represents a workflow based on evaluating a specification to determine which step to take next -/// -/// The step to take if the specification is satisfied -/// The step to take if the specification is not satisfied -/// The rule that decides between each branch -/// The workflow data, used to make the choice -public class Choice( - Func> onTrue, - Func> onFalse, - ISpecification predicate -) : IWorkflowAction -{ - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) - { - if (state.CurrentStep is null) - throw new InvalidOperationException("The workflow has not been initialized."); - - state.CurrentStep = state.CurrentStep with - { - Next = (predicate.IsSatisfiedBy(state.Data) ? onTrue(state.Data) : onFalse(state.Data)) - }; + state.Data = onChange(state.Data); } } @@ -112,7 +65,7 @@ public void Handle(Workflow state, IAmACommandProcessor commandProcessor) public class FireAndForget( Func requestFactory ) - : IWorkflowAction + : IStepTask where TRequest : class, IRequest { /// @@ -143,7 +96,7 @@ public class RequestAndReaction( Func requestFactory, Action replyFactory ) - : IWorkflowAction + : IStepTask where TRequest : class, IRequest where TReply : Event { @@ -177,7 +130,7 @@ public class RobustRequestAndReaction( Action replyFactory, Action faultFactory ) - : IWorkflowAction + : IStepTask where TRequest : class, IRequest where TReply : Event where TFault: Event diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs index 820346b939..8b261b82cd 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs @@ -42,16 +42,25 @@ public enum WorkflowState /// When we are awaiting a response for a workflow, we need to store information about how to continue the workflow /// after receiving the event. /// -/// The parser to populate our worfkow from the event that forms the response +/// The parser to populate our workflow from the event that forms the response /// The type we expect a response to be - used to check the flow /// The type we expect a fault to be - used to check the flow /// The user-defined data, associated with a workflow public class WorkflowResponse(Action> parser, Type responseType, Type? errorType) { + /// Parses a response to a workflow sequence step public Action>? Parser { get; set; } = parser; + + /// The type we expect a response to be - used to check the flow public Type? ResponseType { get; set; } = responseType; + + /// The type we expect a fault to be - used to check the flow public Type? ErrorType { get; set; } = errorType; + /// + /// Do we have an error + /// + /// True if we have an error, false otherwise public bool HasError() => ErrorType is not null; } @@ -67,31 +76,22 @@ public abstract class Workflow { } public class Workflow : Workflow { - /// - /// A map of user defined values. Normally, use Data to pass data between steps - /// + /// A map of user defined values. Normally, use Data to pass data between steps public Dictionary Bag { get; } = new(); - /// - /// What step are we currently at in the workflow - /// + /// What step are we currently at in the workflow public Step? CurrentStep { get; set; } + /// The data that is passed between steps of the workflow public TData Data { get; set; } - /// - /// The id of the workflow, used to save-retrieve it from storage - /// + /// The id of the workflow, used to save-retrieve it from storage public string Id { get; private set; } = Guid.NewGuid().ToString(); - /// - /// If we are awaiting a response, we store the type of the response and the action to take when it arrives - /// + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives public Dictionary> PendingResponses { get; private set; } = new(); - /// - /// Is the workflow currently awaiting an event response - /// + /// Is the workflow currently awaiting an event response public WorkflowState State { get; set; } /// diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs new file mode 100644 index 0000000000..d37becb9b6 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorBlockingWaitStepFlowTests +{ + private readonly Mediator _mediator; + private readonly Workflow _flow; + private readonly FakeTimeProvider _timeProvider; + private bool _stepCompleted; + + public MediatorBlockingWaitStepFlowTests() + { + var registry = new SubscriberRegistry(); + registry.Register(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + _timeProvider = new FakeTimeProvider(); + + var firstStep = new Wait("Test of Workflow", + TimeSpan.FromMilliseconds(500), + () => { _stepCompleted = true; }, + null + ); + + _flow = new Workflow(firstStep, workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + [Fact] + public void When_running_a_single_step_workflow() + { + //We won't really see th block in action as the test will simply block for 500ms + _mediator.RunWorkFlow(_flow); + + _flow.State.Should().Be(WorkflowState.Done); + _stepCompleted.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs new file mode 100644 index 0000000000..5125cf03e2 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.MediatorWorkflow; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorChangeStepFlowTests +{ + private readonly Mediator _mediator; + private readonly Workflow _flow; + private readonly FakeTimeProvider _timeProvider; + private bool _stepCompleted; + + public MediatorChangeStepFlowTests () + { + var registry = new SubscriberRegistry(); + registry.Register(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag.Add("MyValue", "Test"); + + _timeProvider = new FakeTimeProvider(); + + var firstStep = new Sequence( + "Test of Workflow", + new Change( (flow) => + { + flow.Bag["MyValue"] = "Altered"; + return flow; + }), + () => { _stepCompleted = true; }, + null + ); + + _flow = new Workflow(firstStep, workflowData) ; + + _mediator = new Mediator( + commandProcessor, + new InMemoryWorkflowStore() + ); + } + + [Fact] + public void When_running_a_single_step_workflow() + { + //We won't really see th block in action as the test will simply block for 500ms + _mediator.RunWorkFlow(_flow); + + _flow.State.Should().Be(WorkflowState.Done); + _stepCompleted.Should().BeTrue(); + _flow.Bag["MyValue"].Should().Be("Altered"); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index e7ecbca887..cd8c0f6554 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -38,23 +38,24 @@ public MediatorFailingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Fail"); - var stepThree = new Step("Test of Workflow Step Three", + var stepThree = new Sequence( + "Test of Workflow SequenceStep Three", new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); - var stepTwo = new Step("Test of Workflow Step Two", + var stepTwo = new Sequence( + "Test of Workflow SequenceStep Two", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); - var stepOne = new Step("Test of Workflow Step One", - new Choice( - (_) => stepTwo, - (_) => stepThree, - new Specification(x => x.Bag["MyValue"] as string == "Pass")), + var stepOne = new ExclusiveChoice( + "Test of Workflow SequenceStep One", + new Specification(x => x.Bag["MyValue"] as string == "Pass"), () => { _stepCompletedOne = true; }, - null); + stepTwo, + stepThree); _flow = new Workflow(stepOne, workflowData) ; diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 8cdd2a8218..b5e2f946f9 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -36,12 +36,14 @@ public MediatorReplyMultiStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var stepTwo = new Step("Test of Workflow Step Two", + var stepTwo = new Sequence( + "Test of Workflow SequenceStep Two", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); - Step stepOne = new("Test of Workflow Step One", + Sequence stepOne = new( + "Test of Workflow SequenceStep One", new RequestAndReaction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 95c4619bc2..1ed3fc014b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -38,23 +38,24 @@ public MediatorPassingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Pass"); - var stepThree = new Step("Test of Workflow Step Three", + var stepThree = new Sequence( + "Test of Workflow SequenceStep Three", new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); - var stepTwo = new Step("Test of Workflow Step Two", + var stepTwo = new Sequence( + "Test of Workflow SequenceStep Two", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); - var stepOne = new Step("Test of Workflow Step One", - new Choice( - (_) => stepTwo, - (_) => stepThree, - new Specification(x => x.Bag["MyValue"] as string == "Pass")), + var stepOne = new ExclusiveChoice( + "Test of Workflow SequenceStep One", + new Specification(x => x.Bag["MyValue"] as string == "Pass"), () => { _stepCompletedOne = true; }, - null); + stepTwo, + stepThree); _flow = new Workflow(stepOne, workflowData) ; diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 59cd2abad5..0041254634 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -26,7 +26,8 @@ public MediatorOneStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Step("Test of Workflow", + var firstStep = new Sequence( + "Test of Workflow", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), () => { }, null diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index ca6bc4fcb0..87d568ad9e 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -28,13 +28,15 @@ public MediatorTwoStepFlowTests() workflowData.Bag.Add("MyValue", "Test"); - var secondStep = new Step("Test of Workflow Two", + var secondStep = new Sequence( + "Test of Workflow Two", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { }, null ); - var firstStep = new Step("Test of Workflow One", + var firstStep = new Sequence( + "Test of Workflow One", new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 6787f747a6..11cfdddd20 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -35,7 +35,8 @@ public MediatorReplyStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Step("Test of Workflow", + var firstStep = new Sequence( + "Test of Workflow", new RequestAndReaction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index db396da885..83fbb98261 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -37,7 +37,8 @@ public MediatorRobustReplyNoFaultStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Step("Test of Workflow", + var firstStep = new Sequence( + "Test of Workflow", new RobustRequestAndReaction( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value), From 991e4f2c4fd4bf6c8f28bbd6ad0624862772cbe4 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 17 Nov 2024 20:28:20 +0000 Subject: [PATCH 27/44] feat: first pass at Parallel; requires Scheduler-Runner split to Mediator --- Brighter.sln | 2 +- Directory.Packages.props | 1 + .../0024-add-parallel-split-to-mediator.md | 57 ++++++ src/Paramore.Brighter.Mediator/BinaryTree.cs | 54 +++++ .../IAmAJobChannel.cs | 63 ++++++ .../IAmAJobStoreAsync.cs} | 22 +- .../InMemoryJobChannel.cs | 97 +++++++++ .../InMemoryJobStoreAsync.cs} | 30 ++- .../Job.cs} | 47 +---- .../Paramore.Brighter.Mediator.csproj} | 4 + src/Paramore.Brighter.Mediator/Runner.cs | 93 +++++++++ .../Scheduler.cs} | 76 +++---- .../Specification.cs | 2 +- .../Steps.cs | 55 +++-- src/Paramore.Brighter.Mediator/Tasks.cs | 191 ++++++++++++++++++ .../Tasks.cs | 146 ------------- src/Paramore.Brighter/InternalBus.cs | 2 +- ...ling_A_Server_Via_The_Command_Processor.cs | 2 +- .../Paramore.Brighter.Core.Tests.csproj | 2 +- .../TestDoubles/SpecificationTestState.cs | 2 +- .../When_evaluating_a_specification.cs | 2 +- ...andHandler.cs => MyCommandHandlerAsync.cs} | 13 +- ...EventHandler.cs => MyEventHandlerAsync.cs} | 11 +- .../TestDoubles/MyOtherCommandHandler.cs | 12 +- .../Workflows/TestDoubles/WorkflowTestData.cs | 2 +- .../When_running_a_blocking_wait_workflow.cs | 39 ++-- .../When_running_a_change_workflow.cs | 51 ++--- ..._running_a_failing_choice_workflow_step.cs | 60 +++--- ...running_a_multistep_workflow_with_reply.cs | 60 +++--- ..._running_a_passing_choice_workflow_step.cs | 59 +++--- .../When_running_a_single_step_workflow.cs | 43 ++-- .../When_running_a_two_step_workflow.cs | 55 ++--- ...unning_a_workflow_with_a_parallel_split.cs | 81 ++++++++ .../When_running_a_workflow_with_reply.cs | 60 +++--- ...ng_a_workflow_with_robust_reply_nofault.cs | 55 ++--- 35 files changed, 1065 insertions(+), 486 deletions(-) create mode 100644 docs/adr/0024-add-parallel-split-to-mediator.md create mode 100644 src/Paramore.Brighter.Mediator/BinaryTree.cs create mode 100644 src/Paramore.Brighter.Mediator/IAmAJobChannel.cs rename src/{Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs => Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs} (71%) create mode 100644 src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs rename src/{Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs => Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs} (60%) rename src/{Paramore.Brighter.MediatorWorkflow/Workflow.cs => Paramore.Brighter.Mediator/Job.cs} (57%) rename src/{Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj => Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj} (91%) create mode 100644 src/Paramore.Brighter.Mediator/Runner.cs rename src/{Paramore.Brighter.MediatorWorkflow/Mediator.cs => Paramore.Brighter.Mediator/Scheduler.cs} (51%) rename src/{Paramore.Brighter.MediatorWorkflow => Paramore.Brighter.Mediator}/Specification.cs (98%) rename src/{Paramore.Brighter.MediatorWorkflow => Paramore.Brighter.Mediator}/Steps.cs (64%) create mode 100644 src/Paramore.Brighter.Mediator/Tasks.cs delete mode 100644 src/Paramore.Brighter.MediatorWorkflow/Tasks.cs rename tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/{MyCommandHandler.cs => MyCommandHandlerAsync.cs} (74%) rename tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/{MyEventHandler.cs => MyEventHandlerAsync.cs} (80%) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs diff --git a/Brighter.sln b/Brighter.sln index ebeb32aa21..4b195ad865 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,7 +315,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MediatorWorkflow", "src\Paramore.Brighter.MediatorWorkflow\Paramore.Brighter.MediatorWorkflow.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Mediator", "src\Paramore.Brighter.Mediator\Paramore.Brighter.Mediator.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Directory.Packages.props b/Directory.Packages.props index 7d4c3ddf80..44946c0089 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -89,6 +89,7 @@ + all diff --git a/docs/adr/0024-add-parallel-split-to-mediator.md b/docs/adr/0024-add-parallel-split-to-mediator.md new file mode 100644 index 0000000000..3c7ca1363e --- /dev/null +++ b/docs/adr/0024-add-parallel-split-to-mediator.md @@ -0,0 +1,57 @@ +# ADR: Implementing Parallel Split Step for Concurrent Workflow Execution + +## Context + +Our workflow currently supports sequential steps executed in a single thread of control. Each step in the workflow proceeds one after another, and the Mediator has been designed with this single-threaded assumption. + +To support more advanced control flow, we want to introduce a Parallel Split Step based on the Workflow Patterns Basic Control Flow Patterns. The Parallel Split Step is defined as “the divergence of a branch into two or more parallel branches, each of which execute concurrently.” This will enable the workflow to branch into parallel paths, executing multiple threads of control simultaneously. Each branch will operate independently of the others, continuing the workflow until either completion or a synchronization step (such as a Simple Merge) later in the process. + +We would expect a some point to implement the Simple Merge step to allow parallel branches to converge back into a single thread of control. However, this ADR will focus on the Parallel Split Step implementation, with the understanding that future steps will be added to support synchronization. + +### Key Requirements +1. Parallel Execution: + * Parallel Split Step must initiate two or more parallel branches within the workflow. + * Each branch should proceed as a separate thread of control, executing steps independently. +2. Concurrency Handling in the Mediator: + * The Mediator needs to manage multiple threads of execution rather than assuming a single-threaded flow. + * It must be able to initiate and track multiple branches for each Parallel Split Step within the workflow. +3. State Persistence for Parallel Branches: + * Workflow state management and persistence will need to be adapted to track the branches of the flow. + * In the case of a crash, each branch should be able to resume from its last saved state. +4. Integration with Future Synchronization Steps: + * The Parallel Split Step should integrate seamlessly with a future Simple Merge step, which will allow parallel branches to converge back into a single thread. + +## Decision +1. Parallel Split Step Implementation: + * Introduce a new class, ParallelSplitStep, derived from Step. + * Ths class will define multiple branches by specifying two or more independent workflow sequences to be executed in parallel. +2. Producer and Consumer Model for Parallel Execution + * The Mediator will now consist of two classes: a producer (Scheduler) and a consumer (Runner). + * Scheduling a workflow via the Scheduler causes it to send a job to a shared channel or blocking collection. + * The Runner class will act as a consumer, reading workflow jobs from the channel and executing them. + * The Runner is single-threaded, and runs a message pump to process jobs sequentially. + * The job queue is bounded to prevent excessive memory usage and ensure fair scheduling. + * The user can configure the job scheduler for backpressure (producer stalls) or load shedding (dropping jobs). + * The user configures the number of Runners; we don't just pull them from the thread pool. This allows users to control how many threads are used to process jobs. For example, a user could configure a single Runner for a single-threaded workflow, or multiple Runners for parallel execution. +3. In the In-Memory version the job channels will be implemented using a BlockingCollection with a bounded capacity. + * We won't separately store workflow data in a database; the job channel is the storage for work to be done, or in flight + * When we branch, we schedule onto the same channel; this means a Runner has a dependency on the Mediator +4. For resilience, we will need to use a persistent queue for the workflow channels. + * We assume that workflow will become unlocked when their owning Runner crashes, allowing another runner to pick them up + * We will use one derived from a database, not a message queue. + * This will be covered in a later ADR, and likely create some changes + +## Consequences + +### Positive Consequences +* Concurrency and Flexibility: The addition of Parallel Split allows workflows to handle concurrent tasks and enables more complex control flows. +* Scalability: Running parallel branches improves throughput, as tasks that are independent of each other can execute simultaneously. +* Adaptability for Future Steps: Implementing parallel branching prepares the workflow for synchronization steps (e.g., Simple Merge), allowing flexible convergence of parallel tasks. +* Resilience: + +### Negative Consequences +* Increased Complexity in State Management: Tracking multiple branches requires more complex state management to ensure each branch persists and resumes accurately. +* Concurrency Overhead in the Mediator: Managing multiple threads of control adds overhead. We now have both a Runner and a Scheduler. + +### Related ADRs +* Future ADR for implementing Simple Merge Step for synchronization of parallel branches. diff --git a/src/Paramore.Brighter.Mediator/BinaryTree.cs b/src/Paramore.Brighter.Mediator/BinaryTree.cs new file mode 100644 index 0000000000..787b3a8525 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/BinaryTree.cs @@ -0,0 +1,54 @@ +namespace Paramore.Brighter.Mediator; + +/// +/// Represents a node in a binary tree. +/// +/// The type of the value stored in the node. +public class BinaryTree +{ + /// + /// Gets or sets the value of the node. + /// + public T Value { get; set; } + + /// + /// Gets or sets the left child of the node. + /// + public BinaryTree? Left { get; set; } + + /// + /// Gets or sets the right child of the node. + /// + public BinaryTree? Right { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of the node. + public BinaryTree(T value) + { + Value = value; + } + + /// + /// Adds a left child to the node. + /// + /// The value of the left child. + /// The left child node. + public BinaryTree AddLeft(T value) + { + Left = new BinaryTree(value); + return Left; + } + + /// + /// Adds a right child to the node. + /// + /// The value of the right child. + /// The right child node. + public BinaryTree AddRight(T value) + { + Right = new BinaryTree(value); + return Right; + } +} diff --git a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs new file mode 100644 index 0000000000..e43fd1a4cc --- /dev/null +++ b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs @@ -0,0 +1,63 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// Represents a channel for job processing in a workflow. +/// +/// The type of the workflow data. +public interface IAmAJobChannel +{ + /// + /// Enqueues a job to the channel. + /// + /// The job to enqueue. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous enqueue operation. + Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default); + + /// + /// Dequeues a job from the channel. + /// + /// + /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. + Task> DequeueJobAsync(CancellationToken cancellationToken = default); + + /// + /// Streams jobs from the channel. + /// + /// An asynchronous enumerable of jobs. + IAsyncEnumerable> Stream(); + + /// + /// Determines whether the channel is closed. + /// + /// true if the channel is closed; otherwise, false. + bool IsClosed(); +} diff --git a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs b/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs similarity index 71% rename from src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs rename to src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs index f2d04070e9..0367dd5a0f 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs +++ b/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs @@ -22,25 +22,27 @@ THE SOFTWARE. */ #endregion -using System; +using System.Threading; +using System.Threading.Tasks; -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; /// /// Used to store the state of a workflow /// -public interface IAmAWorkflowStore +public interface IAmAJobStoreAsync { /// - /// Saves the workflow + /// Saves the job /// - /// The workflow - void SaveWorkflow(Workflow workflow); + /// The job + /// + Task SaveJobAsync(Job job, CancellationToken cancellationToken); /// - /// Retrieves a workflow via its Id + /// Retrieves a job via its Id /// - /// The id of the workflow - /// if found, the workflow, otherwise null - Workflow? GetWorkflow(string? id) ; + /// The id of the job + /// if found, the job, otherwise null + Task GetJobAsync(string? id) ; } diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs new file mode 100644 index 0000000000..22af50314f --- /dev/null +++ b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs @@ -0,0 +1,97 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +public enum FullChannelStrategy +{ + Wait, + Drop +} + + +public class InMemoryJobChannel : IAmAJobChannel +{ + private readonly Channel> _channel; + + public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullChannelStrategy = FullChannelStrategy.Wait) + { + if (boundedCapacity <= 0) + throw new System.ArgumentOutOfRangeException(nameof(boundedCapacity), "Bounded capacity must be greater than 0"); + + _channel = System.Threading.Channels.Channel.CreateBounded>(new BoundedChannelOptions(boundedCapacity) + { + SingleWriter = true, + SingleReader = false, + AllowSynchronousContinuations = true, + FullMode = fullChannelStrategy == FullChannelStrategy.Wait ? + BoundedChannelFullMode.Wait : + BoundedChannelFullMode.DropOldest + }); + } + + /// + /// Dequeues a job from the channel. + /// + /// + /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. + public async Task> DequeueJobAsync(CancellationToken cancellationToken) + { + return await _channel.Reader.ReadAsync(cancellationToken); + } + + /// + /// Enqueues a job to the channel. + /// + /// The job to enqueue. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous enqueue operation. + public async Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default) + { + await _channel.Writer.WriteAsync(job, cancellationToken); + } + + /// + /// Determines whether the channel is closed. + /// + /// true if the channel is closed; otherwise, false. + public bool IsClosed() + { + return _channel.Reader.Completion.IsCompleted; + } + + /// + /// Streams jobs from the channel. + /// + /// An asynchronous enumerable of jobs. + public IAsyncEnumerable> Stream() + { + return _channel.Reader.ReadAllAsync(); + } +} diff --git a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs b/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs similarity index 60% rename from src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs rename to src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs index c8c4fbcdd1..347e4855e5 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/InMemoryWorkflowStore.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs @@ -22,23 +22,35 @@ THE SOFTWARE. */ #endregion -using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; -public class InMemoryWorkflowStore : IAmAWorkflowStore +public class InMemoryJobStoreAsync : IAmAJobStoreAsync { - private readonly Dictionary _flows = new(); + private readonly Dictionary _flows = new(); - public void SaveWorkflow(Workflow workflow) + public Task SaveJobAsync(Job job, CancellationToken cancellationToken) { - _flows[workflow.Id] = workflow; + if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); + + _flows[job.Id] = job; + return Task.CompletedTask; } - public Workflow? GetWorkflow(string? id) + public Task GetJobAsync(string? id) { - if (id is null) return null; - return _flows.TryGetValue(id, out var state) ? state : null; + var tcs = new TaskCompletionSource(); + if (id is null) + { + tcs.SetResult(null); + return tcs.Task; + } + + var job = _flows.TryGetValue(id, out var state) ? state : null; + tcs.SetResult( job); + return tcs.Task; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs b/src/Paramore.Brighter.Mediator/Job.cs similarity index 57% rename from src/Paramore.Brighter.MediatorWorkflow/Workflow.cs rename to src/Paramore.Brighter.Mediator/Job.cs index 8b261b82cd..0325048e99 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Workflow.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -25,12 +25,12 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; /// /// What state is the workflow in /// -public enum WorkflowState +public enum JobState { Ready, Running, @@ -38,44 +38,19 @@ public enum WorkflowState Done, } -/// -/// When we are awaiting a response for a workflow, we need to store information about how to continue the workflow -/// after receiving the event. -/// -/// The parser to populate our workflow from the event that forms the response -/// The type we expect a response to be - used to check the flow -/// The type we expect a fault to be - used to check the flow -/// The user-defined data, associated with a workflow -public class WorkflowResponse(Action> parser, Type responseType, Type? errorType) -{ - /// Parses a response to a workflow sequence step - public Action>? Parser { get; set; } = parser; - - /// The type we expect a response to be - used to check the flow - public Type? ResponseType { get; set; } = responseType; - - /// The type we expect a fault to be - used to check the flow - public Type? ErrorType { get; set; } = errorType; - /// - /// Do we have an error - /// - /// True if we have an error, false otherwise - public bool HasError() => ErrorType is not null; -} /// -/// empty class, used as maker for the workflow data +/// empty class, used as marker for the branch data /// -public abstract class Workflow { } +public abstract class Job { } /// -/// Workflow represents the current state of the workflow and tracks if it’s awaiting a response. +/// Job represents the current state of the workflow and tracks if it’s awaiting a response. /// /// The user defined data for the workflow -public class Workflow : Workflow +public class Job : Job { - /// A map of user defined values. Normally, use Data to pass data between steps public Dictionary Bag { get; } = new(); @@ -89,21 +64,21 @@ public class Workflow : Workflow public string Id { get; private set; } = Guid.NewGuid().ToString(); /// If we are awaiting a response, we store the type of the response and the action to take when it arrives - public Dictionary> PendingResponses { get; private set; } = new(); + public Dictionary> PendingResponses { get; private set; } = new(); /// Is the workflow currently awaiting an event response - public WorkflowState State { get; set; } + public JobState State { get; set; } /// - /// Constructs a new Workflow + /// Constructs a new Job /// The first step of the workflow to execute. /// State which is passed between steps of the workflow /// - public Workflow(Step firstStep, TData data) + public Job(Step firstStep, TData data) { CurrentStep = firstStep; Data = data; - State = WorkflowState.Ready; + State = JobState.Ready; } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj similarity index 91% rename from src/Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj rename to src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj index 12a725bd9d..682ecc519c 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Paramore.Brighter.MediatorWorkflow.csproj +++ b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs new file mode 100644 index 0000000000..21d97577b5 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -0,0 +1,93 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// The class processes jobs from a job channel and executes them. +/// +/// The type of the workflow data. +public class Runner +{ + private readonly IAmAJobChannel _channel; + private readonly IAmAJobStoreAsync _jobStoreAsync; + private readonly IAmACommandProcessor _commandProcessor; + + /// + /// Initializes a new instance of the class. + /// + /// The job channel to process jobs from. + /// The job store to save job states. + /// The command processor to handle commands. + public Runner(IAmAJobChannel channel, IAmAJobStoreAsync jobStoreAsync, IAmACommandProcessor commandProcessor) + { + _channel = channel; + _jobStoreAsync = jobStoreAsync; + _commandProcessor = commandProcessor; + } + + /// + /// Runs the job processing loop. + /// + /// A token to monitor for cancellation requests. + public async Task RunAsync(CancellationToken cancellationToken = default) + { + await Task.Factory.StartNew(() => ProcessJobs(cancellationToken), cancellationToken); + } + + private async Task Execute(Job job, CancellationToken cancellationToken = default) + { + if (job.CurrentStep is null) + { + job.State = JobState.Done; + return; + } + + job.State = JobState.Running; + await _jobStoreAsync.SaveJobAsync(job, cancellationToken); + + while (job.CurrentStep is not null) + { + await job.CurrentStep.ExecuteAsync(job, _commandProcessor, cancellationToken); + await _jobStoreAsync.SaveJobAsync(job, cancellationToken); + } + + job.State = JobState.Done; + } + + private async Task ProcessJobs(CancellationToken cancellationToken) + { + while (!_channel.IsClosed()) + { + if (cancellationToken.IsCancellationRequested) + break; + + var job = await _channel.DequeueJobAsync(cancellationToken); + await Execute(job, cancellationToken); + } + } +} diff --git a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs similarity index 51% rename from src/Paramore.Brighter.MediatorWorkflow/Mediator.cs rename to src/Paramore.Brighter.Mediator/Scheduler.cs index f5bfbfaf9f..ed7960af42 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Mediator.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -23,54 +23,44 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; /// -/// The class orchestrates a workflow by executing each step in a sequence. +/// The class orchestrates a workflow by executing each step in a sequence. /// It uses a command processor and a workflow store to manage the workflow's state and actions. /// /// The type of the workflow data. -public class Mediator +public class Scheduler { private readonly IAmACommandProcessor _commandProcessor; - private readonly IAmAWorkflowStore _workflowStore; + private readonly IAmAJobChannel _channel; + private readonly IAmAJobStoreAsync _jobStoreAsync; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The command processor used to handle commands. - /// The workflow store used to store and retrieve workflows. - public Mediator(IAmACommandProcessor commandProcessor, IAmAWorkflowStore workflowStore) + /// The over which jobs flow. The is a producer + /// and the is the consumer from the channel + /// A store for pending jobs + public Scheduler(IAmACommandProcessor commandProcessor, IAmAJobChannel channel, IAmAJobStoreAsync jobStoreAsync) { _commandProcessor = commandProcessor; - _workflowStore = workflowStore; + _channel = channel; + _jobStoreAsync = jobStoreAsync; } /// - /// Runs the workflow by executing each step in the sequence. + /// Runs the job by executing each step in the sequence. /// - /// - /// Thrown when the workflow has not been initialized. - public void RunWorkFlow(Workflow workflow) + /// + /// Thrown when the job has not been initialized. + public async Task ScheduleAsync(Job job) { - if (workflow.CurrentStep is null) - { - workflow.State = WorkflowState.Done; - return; - } - - workflow.State = WorkflowState.Running; - _workflowStore.SaveWorkflow(workflow); - - while (workflow.CurrentStep is not null) - { - workflow.CurrentStep.Execute(workflow, _commandProcessor); - _workflowStore.SaveWorkflow(workflow); - } - - workflow.State = WorkflowState.Done; + await _channel.EnqueueJobAsync(job); } /// @@ -78,30 +68,30 @@ public void RunWorkFlow(Workflow workflow) /// /// The event to process. /// Thrown when the workflow has not been initialized. - public void ReceiveWorkflowEvent(Event @event) + public async Task ReceiveWorkflowEvent(Event @event) { if (@event.CorrelationId is null) throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); - var w = _workflowStore.GetWorkflow(@event.CorrelationId); + var w = await _jobStoreAsync.GetJobAsync(@event.CorrelationId); - if (w is not Workflow workflow) - throw new InvalidOperationException("Workflow has not been stored"); + if (w is not Job job) + throw new InvalidOperationException("Branch has not been stored"); - var eventType = @event.GetType(); + var eventType = @event.GetType(); - if (!workflow.PendingResponses.TryGetValue(eventType, out WorkflowResponse? workflowResponse)) - return; + if (!job.PendingResponses.TryGetValue(eventType, out TaskResponse? taskResponse)) + return; - if (workflowResponse.Parser is null) - throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); + if (taskResponse.Parser is null) + throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); - if (workflow.CurrentStep is null) - throw new InvalidOperationException($"Current step of workflow #{workflow.Id} should not be null"); + if (job.CurrentStep is null) + throw new InvalidOperationException($"Current step of workflow #{job.Id} should not be null"); - workflowResponse.Parser(@event, workflow); - workflow.CurrentStep.OnCompletion?.Invoke(); - workflow.State = WorkflowState.Running; - workflow.PendingResponses.Remove(eventType); + taskResponse.Parser(@event, job); + job.CurrentStep.OnCompletion?.Invoke(); + job.State = JobState.Running; + job.PendingResponses.Remove(eventType); } } diff --git a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs b/src/Paramore.Brighter.Mediator/Specification.cs similarity index 98% rename from src/Paramore.Brighter.MediatorWorkflow/Specification.cs rename to src/Paramore.Brighter.Mediator/Specification.cs index 4952858e1d..b17d32fb3b 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Specification.cs +++ b/src/Paramore.Brighter.Mediator/Specification.cs @@ -22,7 +22,7 @@ THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; using System; diff --git a/src/Paramore.Brighter.MediatorWorkflow/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs similarity index 64% rename from src/Paramore.Brighter.MediatorWorkflow/Steps.cs rename to src/Paramore.Brighter.Mediator/Steps.cs index 7213d3db25..0d28d73af0 100644 --- a/src/Paramore.Brighter.MediatorWorkflow/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -1,7 +1,8 @@ using System; +using System.Threading; using System.Threading.Tasks; -namespace Paramore.Brighter.MediatorWorkflow; +namespace Paramore.Brighter.Mediator; /// /// The base type for a step in the workflow. @@ -11,13 +12,13 @@ namespace Paramore.Brighter.MediatorWorkflow; /// The action to be taken with the step, null if no action /// An optional callback to run, following completion of the step /// The data that the step operates over -public abstract class Step(string name, Sequence? next, IStepTask? stepTask = null, Action? onCompletion = null) +public abstract class Step(string name, Sequential? next, IStepTask? stepTask = null, Action? onCompletion = null) { /// The name of the step, used for tracing execution public string Name { get; init; } = name; /// The next step in the sequence, null if this is the last step - protected Sequence? Next { get; } = next; + protected Sequential? Next { get; } = next; /// An optional callback to be run, following completion of the step. public Action? OnCompletion { get; } = onCompletion; @@ -25,15 +26,17 @@ public abstract class Step(string name, Sequence? next, IStepTask< /// The action to be taken with the step. protected IStepTask? StepTask { get; } = stepTask; - public virtual void Execute(Workflow state, IAmACommandProcessor commandProcessor) + public virtual Task ExecuteAsync(Job job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { - StepTask?.Handle(state, commandProcessor); + StepTask?.HandleAsync(job, commandProcessor, cancellationToken); OnCompletion?.Invoke(); + return Task.CompletedTask; } } - + /// -/// Represents a step in the workflow. Steps form a singly linked list. +/// Represents a sequential step in the workflow. Control flows to the next step in the list, or ends if next is null. +/// A set of sequential steps for a linked list. /// /// The name of the step, used for tracing execution /// The action to be taken with the step. @@ -42,21 +45,21 @@ public virtual void Execute(Workflow state, IAmACommandProcessor commandP /// An optional callback to run, following a faulted execution of the step /// The next step in the sequence, following a faulted execution of the step /// The data that the step operates over -public class Sequence( +public class Sequential( string name, IStepTask stepTask, Action? onCompletion, - Sequence? next, + Sequential? next, Action? onFaulted = null, - Sequence? faultNext = null + Sequential? faultNext = null ) : Step(name, next, stepTask, onCompletion) { - public override void Execute(Workflow state, IAmACommandProcessor commandProcessor) + public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { try { - StepTask?.Handle(state, commandProcessor); + StepTask?.HandleAsync(state, commandProcessor, cancellationToken); OnCompletion?.Invoke(); state.CurrentStep = Next; } @@ -65,6 +68,7 @@ public override void Execute(Workflow state, IAmACommandProcessor command onFaulted?.Invoke(); state.CurrentStep = faultNext; } + return Task.CompletedTask; } } @@ -81,14 +85,29 @@ public class ExclusiveChoice( string name, ISpecification predicate, Action? onCompletion, - Sequence? nextTrue, - Sequence? nextFalse + Sequential? nextTrue, + Sequential? nextFalse ) : Step(name, null, null, onCompletion) { - public override void Execute(Workflow state, IAmACommandProcessor commandProcessor) + public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { state.CurrentStep = predicate.IsSatisfiedBy(state.Data) ? nextTrue : nextFalse; + return Task.CompletedTask; + } +} + +public class ParallelSplit(string name, Action? onBranch, params Step[] branches) + : Step(name, null) +{ + public Step[] Branches { get; set; } = branches; + + public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + // Parallel split doesn't directly execute its jobs. + // Execution is handled by the Scheduler, which will handle running each branch concurrently. + onBranch?.Invoke(state.Data); + return Task.CompletedTask; } } @@ -100,12 +119,12 @@ public override void Execute(Workflow state, IAmACommandProcessor command /// An optional callback to run, following completion of the step /// The next step in the sequence, null if this is the last step. /// The data that the step operates over -public class Wait(string name, TimeSpan duration, Action? onCompletion, Sequence? next) +public class Wait(string name, TimeSpan duration, Action? onCompletion, Sequential? next) : Step(name, next, null, onCompletion) { - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) + public override async Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { - Task.Delay(duration).Wait(); + await Task.Delay(duration, cancellationToken); OnCompletion?.Invoke(); } } diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs new file mode 100644 index 0000000000..0df3d652cc --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -0,0 +1,191 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// Defines an interface for workflow actions. +/// +/// The type of the workflow data. +public interface IStepTask +{ + /// + /// Handles the workflow action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. + /// The cancellation token for this task + Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); +} + +/// +/// When we are awaiting a response for a workflow, we need to store information about how to continue the workflow +/// after receiving the event. +/// +/// The parser to populate our workflow from the event that forms the response +/// The type we expect a response to be - used to check the flow +/// The type we expect a fault to be - used to check the flow +/// The user-defined data, associated with a workflow +public class TaskResponse(Action> parser, Type responseType, Type? errorType) +{ + /// Parses a response to a workflow sequence step + public Action>? Parser { get; set; } = parser; + + /// The type we expect a response to be - used to check the flow + public Type? ResponseType { get; set; } = responseType; + + /// The type we expect a fault to be - used to check the flow + public Type? ErrorType { get; set; } = errorType; + + /// + /// Do we have an error + /// + /// True if we have an error, false otherwise + public bool HasError() => ErrorType is not null; +} + +/// +/// Essentially a pass through step, it alters Data property by running the transform +/// given by onChange over it +/// +/// Takes the Data property and transforms it +/// The workflow data, that we wish to transform +public class ChangeAsync( + Func> onChange +) : IStepTask +{ + + /// + /// Handles the workflow action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. + /// The cancellation token for this task + public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + state.Data = await onChange(state.Data); + } +} + +/// +/// Represents a fire-and-forget action in the workflow. +/// +/// The type of the request. +/// The type of the workflow data. +/// The factory method to create the request. +public class FireAndForgetAsync( + Func requestFactory + ) + : IStepTask + where TRequest : class, IRequest +{ + /// + /// Handles the fire-and-forget action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. + /// The cancellation token for this task + public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + var command = requestFactory(); + command.CorrelationId = state.Id; + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + } +} + +/// +/// Represents a request-and-reply action in the workflow. +/// +/// The type of the request. +/// The type of the reply. +/// The type of the workflow data. +/// The factory method to create the request. +/// The factory method to handle the reply. +public class RequestAndReactionAsync( + Func requestFactory, + Action replyFactory + ) + : IStepTask + where TRequest : class, IRequest + where TReply : Event +{ + /// + /// Handles the request-and-reply action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. + /// The cancellation token for this task + public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + var command = requestFactory(); + command.CorrelationId = state.Id; + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + + state.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); + + } +} + +/// +/// +/// +/// +/// +/// +/// +/// +/// +public class RobustRequestAndReactionAsync( + Func requestFactory, + Action replyFactory, + Action faultFactory +) + : IStepTask + where TRequest : class, IRequest + where TReply : Event + where TFault: Event +{ + /// + /// Handles the fire-and-forget action. + /// + /// The current state of the workflow. + /// The command processor used to handle commands. + /// The cancellation token for this task + public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + var command = requestFactory(); + command.CorrelationId = state.Id; + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + + state.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); + state.PendingResponses.Add(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} +} + + diff --git a/src/Paramore.Brighter.MediatorWorkflow/Tasks.cs b/src/Paramore.Brighter.MediatorWorkflow/Tasks.cs deleted file mode 100644 index a82dafe750..0000000000 --- a/src/Paramore.Brighter.MediatorWorkflow/Tasks.cs +++ /dev/null @@ -1,146 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2024 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using System; - -namespace Paramore.Brighter.MediatorWorkflow; - -/// -/// Defines an interface for workflow actions. -/// -/// The type of the workflow data. -public interface IStepTask -{ - /// - /// Handles the workflow action. - /// - /// The current state of the workflow. - /// The command processor used to handle commands. - void Handle(Workflow state, IAmACommandProcessor commandProcessor); -} - -/// -/// Essentially a pass through step, it alters Data property by running the transform -/// given by onChange over it -/// -/// Takes the Data property and transforms it -/// The workflow data, that we wish to transform -public class Change( - Func onChange -) : IStepTask -{ - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) - { - state.Data = onChange(state.Data); - } -} - -/// -/// Represents a fire-and-forget action in the workflow. -/// -/// The type of the request. -/// The type of the workflow data. -/// The factory method to create the request. -public class FireAndForget( - Func requestFactory - ) - : IStepTask - where TRequest : class, IRequest -{ - /// - /// Handles the fire-and-forget action. - /// - /// The current state of the workflow. - /// The command processor used to handle commands. - public void Handle( - Workflow state, - IAmACommandProcessor commandProcessor - ) - { - var command = requestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - } -} - -/// -/// Represents a request-and-reply action in the workflow. -/// -/// The type of the request. -/// The type of the reply. -/// The type of the workflow data. -/// The factory method to create the request. -/// The factory method to handle the reply. -public class RequestAndReaction( - Func requestFactory, - Action replyFactory - ) - : IStepTask - where TRequest : class, IRequest - where TReply : Event -{ - /// - /// Handles the request-and-reply action. - /// - /// The current state of the workflow. - /// The command processor used to handle commands. - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) - { - var command = requestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - - state.PendingResponses.Add(typeof(TReply), new WorkflowResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); - - } -} - -/// -/// -/// -/// -/// -/// -/// -/// -/// -public class RobustRequestAndReaction( - Func requestFactory, - Action replyFactory, - Action faultFactory -) - : IStepTask - where TRequest : class, IRequest - where TReply : Event - where TFault: Event -{ - public void Handle(Workflow state, IAmACommandProcessor commandProcessor) - { - var command = requestFactory(); - command.CorrelationId = state.Id; - commandProcessor.Send(command); - - state.PendingResponses.Add(typeof(TReply), new WorkflowResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); - state.PendingResponses.Add(typeof(TFault), new WorkflowResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} -} diff --git a/src/Paramore.Brighter/InternalBus.cs b/src/Paramore.Brighter/InternalBus.cs index d728734251..341daffc4b 100644 --- a/src/Paramore.Brighter/InternalBus.cs +++ b/src/Paramore.Brighter/InternalBus.cs @@ -38,7 +38,7 @@ namespace Paramore.Brighter; /// The maximum number of messages that can be enqueued; -1 is unbounded; default is -1 public class InternalBus(int boundedCapacity = -1) : IAmABus { - private ConcurrentDictionary> _messages = new(); + private readonly ConcurrentDictionary> _messages = new(); /// /// Enqueue a message to tbe bus diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs index 3eb18c74e9..d9b582efd4 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs @@ -116,7 +116,7 @@ public void When_Calling_A_Server_Via_The_Command_Processor() new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; - //Run the pump on a new thread + //RunAsync the pump on a new thread Task pump = Task.Factory.StartNew(() => messagePump.Run()); _commandProcessor.Call(_myRequest, timeOut: TimeSpan.FromMilliseconds(500)); diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index 0e07a0ead7..79585a3fd0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs index af19bccdb3..1716e2e96d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; namespace Paramore.Brighter.Core.Tests.Specifications.TestDoubles; diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs index 6bb647d6d3..eefd3c36f2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs @@ -1,5 +1,5 @@ using Paramore.Brighter.Core.Tests.Specifications.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Xunit; namespace Paramore.Brighter.Core.Tests.Specifications; diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs similarity index 74% rename from tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs rename to tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs index 6892fc3ee2..1604c5f25d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs @@ -23,18 +23,21 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyCommandHandler(IAmACommandProcessor? commandProcessor) : RequestHandler + internal class MyCommandHandlerAsync(IAmACommandProcessor? commandProcessor) : RequestHandlerAsync { public static List ReceivedCommands { get; } = []; - - public override MyCommand Handle(MyCommand command) + + + public override async Task HandleAsync(MyCommand command, CancellationToken cancellationToken = default) { LogCommand(command); - commandProcessor?.Publish(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}); - return base.Handle(command); + commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + return await base.HandleAsync(command, cancellationToken); } private void LogCommand(MyCommand request) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs similarity index 80% rename from tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs rename to tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs index fa21274044..1852848b3e 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs @@ -23,18 +23,21 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; -using Paramore.Brighter.MediatorWorkflow; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Mediator; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyEventHandler(Mediator? mediator) : RequestHandler + internal class MyEventHandlerAsync(Scheduler? mediator) : RequestHandlerAsync { public static List ReceivedEvents { get; } = []; - public override MyEvent Handle(MyEvent @event) + + public override async Task HandleAsync(MyEvent @event, CancellationToken cancellationToken = default) { LogEvent(@event); mediator?.ReceiveWorkflowEvent(@event); - return base.Handle(@event); + return await base.HandleAsync(@event, cancellationToken); } private void LogEvent(MyEvent request) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs index 8c74640e3d..650f0fb4e1 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs @@ -1,16 +1,18 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -public class MyOtherCommandHandler(IAmACommandProcessor commandProcessor) : RequestHandler +public class MyOtherCommandHandlerAsync(IAmACommandProcessor commandProcessor) : RequestHandlerAsync { public static List ReceivedCommands { get; set; } = []; - - public override MyOtherCommand Handle(MyOtherCommand command) + + public override async Task HandleAsync(MyOtherCommand command, CancellationToken cancellationToken = default) { LogCommand(command); - commandProcessor?.Publish(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}); - return base.Handle(command); + commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + return await base.HandleAsync(command, cancellationToken); } private void LogCommand(MyOtherCommand request) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs index f413973687..e0b1312341 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index d37becb9b6..e2b45d12c8 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -1,9 +1,8 @@ using System; -using System.Linq; +using System.Threading.Tasks; using FluentAssertions; -using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -11,18 +10,18 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorBlockingWaitStepFlowTests { - private readonly Mediator _mediator; - private readonly Workflow _flow; - private readonly FakeTimeProvider _timeProvider; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompleted; public MediatorBlockingWaitStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); + registry.RegisterAsync(); CommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); @@ -30,29 +29,35 @@ public MediatorBlockingWaitStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - _timeProvider = new FakeTimeProvider(); - - var firstStep = new Wait("Test of Workflow", + var firstStep = new Wait("Test of Job", TimeSpan.FromMilliseconds(500), () => { _stepCompleted = true; }, null ); - _flow = new Workflow(firstStep, workflowData) ; + _flow = new Job(firstStep, workflowData) ; + + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); - _mediator = new Mediator( + _scheduler = new Scheduler( commandProcessor, - new InMemoryWorkflowStore() + channel, + store ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_single_step_workflow() + public async Task When_running_a_single_step_workflow() { + await _scheduler.ScheduleAsync(_flow); + //We won't really see th block in action as the test will simply block for 500ms - _mediator.RunWorkFlow(_flow); + await _runner.RunAsync(); - _flow.State.Should().Be(WorkflowState.Done); + _flow.State.Should().Be(JobState.Done); _stepCompleted.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index 5125cf03e2..b983047ac7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -1,9 +1,7 @@ -using System; -using System.Linq; +using System.Threading.Tasks; using FluentAssertions; -using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -11,18 +9,18 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorChangeStepFlowTests { - private readonly Mediator _mediator; - private readonly Workflow _flow; - private readonly FakeTimeProvider _timeProvider; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompleted; public MediatorChangeStepFlowTests () { var registry = new SubscriberRegistry(); - registry.Register(); + registry.RegisterAsync(); - CommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + CommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); @@ -30,34 +28,39 @@ public MediatorChangeStepFlowTests () var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - _timeProvider = new FakeTimeProvider(); - - var firstStep = new Sequence( - "Test of Workflow", - new Change( (flow) => + var firstStep = new Sequential( + "Test of Job", + new ChangeAsync( (flow) => { flow.Bag["MyValue"] = "Altered"; - return flow; + return Task.FromResult(flow); }), () => { _stepCompleted = true; }, null ); - _flow = new Workflow(firstStep, workflowData) ; + _flow = new Job(firstStep, workflowData) ; + + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); - _mediator = new Mediator( + _scheduler = new Scheduler( commandProcessor, - new InMemoryWorkflowStore() - ); + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_single_step_workflow() + public async Task When_running_a_single_step_workflow() { - //We won't really see th block in action as the test will simply block for 500ms - _mediator.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + + await _runner.RunAsync(); - _flow.State.Should().Be(WorkflowState.Done); + _flow.State.Should().Be(JobState.Done); _stepCompleted.Should().BeTrue(); _flow.Bag["MyValue"].Should().Be("Altered"); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index cd8c0f6554..c1d02714e7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -1,8 +1,9 @@ using System; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -10,8 +11,9 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorFailingChoiceFlowTests { - private readonly Mediator? _mediator; - private readonly Workflow _flow; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompletedOne; private bool _stepCompletedTwo; private bool _stepCompletedThree; @@ -20,15 +22,15 @@ public MediatorFailingChoiceFlowTests() { // arrange var registry = new SubscriberRegistry(); - registry.Register(); - registry.Register(); + registry.RegisterAsync(); + registry.RegisterAsync(); IAmACommandProcessor? commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyOtherCommandHandler) => new MyOtherCommandHandler(commandProcessor), + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyOtherCommandHandlerAsync) => new MyOtherCommandHandlerAsync(commandProcessor), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -38,46 +40,54 @@ public MediatorFailingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Fail"); - var stepThree = new Sequence( - "Test of Workflow SequenceStep Three", - new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var stepThree = new Sequential( + "Test of Job SequenceStep Three", + new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); - var stepTwo = new Sequence( - "Test of Workflow SequenceStep Two", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( - "Test of Workflow SequenceStep One", + "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), () => { _stepCompletedOne = true; }, stepTwo, stepThree); - _flow = new Workflow(stepOne, workflowData) ; + _flow = new Job(stepOne, workflowData) ; - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_choice_workflow_step() + public async Task When_running_a_choice_workflow_step() { - MyCommandHandler.ReceivedCommands.Clear(); - MyOtherCommandHandler.ReceivedCommands.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync(_flow); - _mediator?.RunWorkFlow(_flow); + await _runner.RunAsync(); _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeFalse(); _stepCompletedThree.Should().BeTrue(); - MyOtherCommandHandler.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); - MyCommandHandler.ReceivedCommands.Any().Should().BeFalse(); + MyOtherCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any().Should().BeFalse(); _stepCompletedOne.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index b5e2f946f9..ff7dfb7ba6 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -1,8 +1,9 @@ using System; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -10,23 +11,24 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorReplyMultiStepFlowTests { - private readonly Mediator _mediator; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompletedOne; private bool _stepCompletedTwo; - private readonly Workflow _flow; public MediatorReplyMultiStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); - registry.Register(); + registry.RegisterAsync(); + registry.RegisterAsync(); IAmACommandProcessor? commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -36,40 +38,48 @@ public MediatorReplyMultiStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var stepTwo = new Sequence( - "Test of Workflow SequenceStep Two", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); - Sequence stepOne = new( - "Test of Workflow SequenceStep One", - new RequestAndReaction( + Sequential stepOne = new( + "Test of Job SequenceStep One", + new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompletedOne = true; }, stepTwo); - _flow = new Workflow(stepOne, workflowData) ; + _flow = new Job(stepOne, workflowData) ; - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_workflow_with_reply() + public async Task When_running_a_workflow_with_reply() { - MyCommandHandler.ReceivedCommands.Clear(); - MyEventHandler.ReceivedEvents.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); - _mediator.RunWorkFlow(_flow); _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeTrue(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(WorkflowState.Done); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 1ed3fc014b..e5541ee6c5 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -1,8 +1,9 @@ using System; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -10,8 +11,9 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorPassingChoiceFlowTests { - private readonly Mediator? _mediator; - private readonly Workflow _flow; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompletedOne; private bool _stepCompletedTwo; private bool _stepCompletedThree; @@ -20,15 +22,15 @@ public MediatorPassingChoiceFlowTests() { // arrange var registry = new SubscriberRegistry(); - registry.Register(); - registry.Register(); + registry.RegisterAsync(); + registry.RegisterAsync(); IAmACommandProcessor? commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyOtherCommandHandler) => new (commandProcessor), + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyOtherCommandHandlerAsync) => new (commandProcessor), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -38,46 +40,53 @@ public MediatorPassingChoiceFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Pass"); - var stepThree = new Sequence( - "Test of Workflow SequenceStep Three", - new FireAndForget(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var stepThree = new Sequential( + "Test of Job SequenceStep Three", + new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); - var stepTwo = new Sequence( - "Test of Workflow SequenceStep Two", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( - "Test of Workflow SequenceStep One", + "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), () => { _stepCompletedOne = true; }, stepTwo, stepThree); - _flow = new Workflow(stepOne, workflowData) ; + _flow = new Job(stepOne, workflowData) ; - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_choice_workflow_step() + public async Task When_running_a_choice_workflow_step() { - MyCommandHandler.ReceivedCommands.Clear(); - MyOtherCommandHandler.ReceivedCommands.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); - _mediator?.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeTrue(); _stepCompletedThree.Should().BeFalse(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Pass").Should().BeTrue(); - MyOtherCommandHandler.ReceivedCommands.Any().Should().BeFalse(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Pass").Should().BeTrue(); + MyOtherCommandHandlerAsync.ReceivedCommands.Any().Should().BeFalse(); _stepCompletedOne.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 0041254634..c56a236edb 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,7 +1,8 @@ using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -9,16 +10,17 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { - private readonly Mediator _mediator; - private readonly Workflow _flow; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; public MediatorOneStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); + registry.RegisterAsync(); CommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); @@ -26,29 +28,36 @@ public MediatorOneStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Sequence( - "Test of Workflow", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + var firstStep = new Sequential( + "Test of Job", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), () => { }, null ); - _flow = new Workflow(firstStep, workflowData) ; + _flow = new Job(firstStep, workflowData) ; - _mediator = new Mediator( + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( commandProcessor, - new InMemoryWorkflowStore() - ); + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_single_step_workflow() + public async Task When_running_a_single_step_workflow() { - MyCommandHandler.ReceivedCommands.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); - _mediator.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(WorkflowState.Done); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 87d568ad9e..a27928957b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -10,16 +11,17 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorTwoStepFlowTests { - private readonly Mediator _mediator; - private readonly Workflow _flow; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; public MediatorTwoStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); + registry.RegisterAsync(); CommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync(_ => new MyCommandHandler(commandProcessor)); + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); @@ -28,37 +30,44 @@ public MediatorTwoStepFlowTests() workflowData.Bag.Add("MyValue", "Test"); - var secondStep = new Sequence( - "Test of Workflow Two", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var secondStep = new Sequential( + "Test of Job Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { }, null ); - var firstStep = new Sequence( - "Test of Workflow One", - new FireAndForget(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + var firstStep = new Sequential( + "Test of Job One", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep ); - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() - ); - - _flow = new Workflow(firstStep, workflowData); + _flow = new Job(firstStep, workflowData) ; + + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_single_step_workflow() + public async Task When_running_a_single_step_workflow() { - MyCommandHandler.ReceivedCommands.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); - _mediator.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); - _flow.State.Should().Be(WorkflowState.Done); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs new file mode 100644 index 0000000000..9d69d94857 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorParallelSplitFlowTests +{ + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; + private bool _firstBranchFinished; + private bool _secondBranchFinished; + + public MediatorParallelSplitFlowTests() + { + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + + var secondBranch = new Sequential( + "Test of Job Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), + () => { _secondBranchFinished = true; }, + null + ); + + var firstBranch = new Sequential( + "Test of Job One", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _firstBranchFinished = true; }, + null + ); + + var parallelSplit = new ParallelSplit( + "Test of Job Parallel Split", + (data) => + { data.Bag.Add("MyValue", "TestOne"); + data.Bag["MyOtherValue"] = "TestTwo"; + }, + firstBranch, secondBranch + ); + + _flow = new Job(parallelSplit, workflowData) ; + + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); + } + + public async Task When_running_a_workflow_with_a_parallel_split() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + + _scheduler.ScheduleAsync(_flow); + _runner.RunAsync(); + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _firstBranchFinished.Should().BeTrue(); + _secondBranchFinished.Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 11cfdddd20..e9c007d9e7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,31 +1,38 @@ using System; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using MyCommand = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommand; +using MyCommandHandlerAsync = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommandHandlerAsync; +using MyEvent = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyEvent; +using MyEventHandlerAsync = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyEventHandlerAsync; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorReplyStepFlowTests { - private readonly Mediator _mediator; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompleted; - private readonly Workflow _flow; public MediatorReplyStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); - registry.Register(); + registry.RegisterAsync(); + registry.RegisterAsync(); IAmACommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -35,34 +42,41 @@ public MediatorReplyStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Sequence( - "Test of Workflow", - new RequestAndReaction( + var firstStep = new Sequential( + "Test of Job", + new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), () => { _stepCompleted = true; }, null); - _flow = new Workflow(firstStep, workflowData) ; + _flow = new Job(firstStep, workflowData) ; - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() - ); + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_workflow_with_reply() + public async Task When_running_a_workflow_with_reply() { - MyCommandHandler.ReceivedCommands.Clear(); - MyEventHandler.ReceivedEvents.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); - _mediator.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); _stepCompleted.Should().BeTrue(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(WorkflowState.Done); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index 83fbb98261..0fe0e3d23d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -1,9 +1,10 @@ using System; using System.Linq; +using System.Threading.Tasks; using Amazon.Runtime.Internal.Transform; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; -using Paramore.Brighter.MediatorWorkflow; +using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; @@ -11,23 +12,24 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorRobustReplyNoFaultStepFlowTests { - private readonly Mediator _mediator; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _flow; private bool _stepCompleted; private bool _stepFaulted; - private readonly Workflow _flow; public MediatorRobustReplyNoFaultStepFlowTests() { var registry = new SubscriberRegistry(); - registry.Register(); - registry.Register(); + registry.RegisterAsync(); + registry.RegisterAsync(); IAmACommandProcessor commandProcessor = null; - var handlerFactory = new SimpleHandlerFactorySync((handlerType) => + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { - _ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor), - _ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator), + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") }); @@ -37,9 +39,9 @@ public MediatorRobustReplyNoFaultStepFlowTests() var workflowData= new WorkflowTestData(); workflowData.Bag.Add("MyValue", "Test"); - var firstStep = new Sequence( - "Test of Workflow", - new RobustRequestAndReaction( + var firstStep = new Sequential( + "Test of Job", + new RobustRequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value), (fault) => workflowData.Bag.Add("MyFault", ((MyFault)fault).Value)), @@ -48,27 +50,34 @@ public MediatorRobustReplyNoFaultStepFlowTests() () => { _stepFaulted = true; }, null); - _flow = new Workflow(firstStep, workflowData) ; + _flow = new Job(firstStep, workflowData) ; - _mediator = new Mediator( - commandProcessor, - new InMemoryWorkflowStore() - ); + InMemoryJobStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); } [Fact] - public void When_running_a_workflow_with_reply() + public async Task When_running_a_workflow_with_reply() { - MyCommandHandler.ReceivedCommands.Clear(); - MyEventHandler.ReceivedEvents.Clear(); + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); - _mediator.RunWorkFlow(_flow); + await _scheduler.ScheduleAsync(_flow); + await _runner.RunAsync(); _stepCompleted.Should().BeTrue(); _stepFaulted.Should().BeFalse(); - MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(WorkflowState.Done); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _flow.State.Should().Be(JobState.Done); } } From 1373583bbb05210ec2ee68521f1dfa69f5755124 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 18 Nov 2024 20:11:31 +0000 Subject: [PATCH 28/44] chore: safety check-in during scheduler/runner split work --- .../IAmAJobChannel.cs | 2 +- .../IAmAJobStoreAsync.cs | 2 +- .../InMemoryJobChannel.cs | 56 ++++++-- .../InMemoryJobStoreAsync.cs | 23 +++- src/Paramore.Brighter.Mediator/Job.cs | 10 +- src/Paramore.Brighter.Mediator/Runner.cs | 22 +++- src/Paramore.Brighter.Mediator/Scheduler.cs | 4 +- src/Paramore.Brighter.Mediator/Steps.cs | 122 ++++++++++-------- src/Paramore.Brighter.Mediator/Tasks.cs | 49 ++++--- .../Workflows/TestDoubles/WorkflowTestData.cs | 5 +- .../When_running_a_blocking_wait_workflow.cs | 13 +- .../When_running_a_change_workflow.cs | 46 +++++-- ..._running_a_failing_choice_workflow_step.cs | 13 +- ...running_a_multistep_workflow_with_reply.cs | 18 ++- ..._running_a_passing_choice_workflow_step.cs | 15 ++- .../When_running_a_single_step_workflow.cs | 31 +++-- .../When_running_a_two_step_workflow.cs | 27 +++- ...unning_a_workflow_with_a_parallel_split.cs | 14 +- .../When_running_a_workflow_with_reply.cs | 15 ++- ...ng_a_workflow_with_robust_reply_nofault.cs | 17 +-- 20 files changed, 331 insertions(+), 173 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs index e43fd1a4cc..73edb0d2d4 100644 --- a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs +++ b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs @@ -47,7 +47,7 @@ public interface IAmAJobChannel /// /// /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. - Task> DequeueJobAsync(CancellationToken cancellationToken = default); + Task?> DequeueJobAsync(CancellationToken cancellationToken = default); /// /// Streams jobs from the channel. diff --git a/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs index 0367dd5a0f..ac4107430d 100644 --- a/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs @@ -37,7 +37,7 @@ public interface IAmAJobStoreAsync /// /// The job /// - Task SaveJobAsync(Job job, CancellationToken cancellationToken); + Task SaveJobAsync(Job? job, CancellationToken cancellationToken); /// /// Retrieves a job via its Id diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs index 22af50314f..1d8a0069b4 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs @@ -29,29 +29,48 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Mediator; +/// +/// Specifies the strategy to use when the channel is full. +/// public enum FullChannelStrategy { + /// + /// Wait for space to become available in the channel. + /// Wait, + + /// + /// Drop the oldest item in the channel to make space. + /// Drop -} - +} +/// +/// Represents an in-memory job channel for processing jobs. +/// +/// The type of the job data. public class InMemoryJobChannel : IAmAJobChannel { private readonly Channel> _channel; + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of jobs the channel can hold. + /// The strategy to use when the channel is full. + /// Thrown when the bounded capacity is less than or equal to 0. public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullChannelStrategy = FullChannelStrategy.Wait) { if (boundedCapacity <= 0) throw new System.ArgumentOutOfRangeException(nameof(boundedCapacity), "Bounded capacity must be greater than 0"); - + _channel = System.Threading.Channels.Channel.CreateBounded>(new BoundedChannelOptions(boundedCapacity) - { + { SingleWriter = true, SingleReader = false, AllowSynchronousContinuations = true, - FullMode = fullChannelStrategy == FullChannelStrategy.Wait ? - BoundedChannelFullMode.Wait : + FullMode = fullChannelStrategy == FullChannelStrategy.Wait ? + BoundedChannelFullMode.Wait : BoundedChannelFullMode.DropOldest }); } @@ -59,11 +78,16 @@ public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullCha /// /// Dequeues a job from the channel. /// - /// + /// A token to monitor for cancellation requests. /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. - public async Task> DequeueJobAsync(CancellationToken cancellationToken) - { - return await _channel.Reader.ReadAsync(cancellationToken); + public async Task?> DequeueJobAsync(CancellationToken cancellationToken) + { + Job? item = null; + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + while (_channel.Reader.TryRead(out item)) + return item; + + return item; } /// @@ -84,14 +108,22 @@ public async Task EnqueueJobAsync(Job job, CancellationToken cancellation public bool IsClosed() { return _channel.Reader.Completion.IsCompleted; - } + } + /// + /// This is mainly useful for help with testing, to stop the channel + /// + public void Stop() + { + _channel.Writer.Complete(); + } + /// /// Streams jobs from the channel. /// /// An asynchronous enumerable of jobs. public IAsyncEnumerable> Stream() - { + { return _channel.Reader.ReadAllAsync(); } } diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs b/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs index 347e4855e5..2a45064263 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs @@ -28,18 +28,35 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Mediator; +/// +/// Represents an in-memory store for jobs. +/// public class InMemoryJobStoreAsync : IAmAJobStoreAsync { - private readonly Dictionary _flows = new(); + private readonly Dictionary _flows = new(); - public Task SaveJobAsync(Job job, CancellationToken cancellationToken) + /// + /// Saves the job asynchronously. + /// + /// The type of the job data. + /// The job to save. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + public Task SaveJobAsync(Job? job, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); + if (job is null) return Task.CompletedTask; + _flows[job.Id] = job; return Task.CompletedTask; } + /// + /// Retrieves a job asynchronously by its Id. + /// + /// The Id of the job. + /// A task that represents the asynchronous retrieve operation. The task result contains the job if found; otherwise, null. public Task GetJobAsync(string? id) { var tcs = new TaskCompletionSource(); @@ -50,7 +67,7 @@ public Task SaveJobAsync(Job job, CancellationToken cancellationTo } var job = _flows.TryGetValue(id, out var state) ? state : null; - tcs.SetResult( job); + tcs.SetResult(job); return tcs.Task; } } diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index 0325048e99..d539ab2c8a 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -38,8 +38,6 @@ public enum JobState Done, } - - /// /// empty class, used as marker for the branch data /// @@ -55,10 +53,10 @@ public class Job : Job public Dictionary Bag { get; } = new(); /// What step are we currently at in the workflow - public Step? CurrentStep { get; set; } + public Step? Step { get; set; } /// The data that is passed between steps of the workflow - public TData Data { get; set; } + public TData Data { get; } /// The id of the workflow, used to save-retrieve it from storage public string Id { get; private set; } = Guid.NewGuid().ToString(); @@ -74,11 +72,11 @@ public class Job : Job /// The first step of the workflow to execute. /// State which is passed between steps of the workflow /// - public Job(Step firstStep, TData data) + public Job(TData data) { - CurrentStep = firstStep; Data = data; State = JobState.Ready; } } + diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 21d97577b5..104e8183d0 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -56,12 +56,24 @@ public Runner(IAmAJobChannel channel, IAmAJobStoreAsync jobStoreAsync, IA /// A token to monitor for cancellation requests. public async Task RunAsync(CancellationToken cancellationToken = default) { - await Task.Factory.StartNew(() => ProcessJobs(cancellationToken), cancellationToken); + await Task.Factory.StartNew(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + await ProcessJobs(cancellationToken); + + if (cancellationToken.IsCancellationRequested) + cancellationToken.ThrowIfCancellationRequested(); + + }, cancellationToken); } - private async Task Execute(Job job, CancellationToken cancellationToken = default) + private async Task Execute(Job? job, CancellationToken cancellationToken = default) { - if (job.CurrentStep is null) + if (job is null) + return; + + if (job.Step is null) { job.State = JobState.Done; return; @@ -70,9 +82,9 @@ private async Task Execute(Job job, CancellationToken cancellationToken = job.State = JobState.Running; await _jobStoreAsync.SaveJobAsync(job, cancellationToken); - while (job.CurrentStep is not null) + while (job.Step is not null) { - await job.CurrentStep.ExecuteAsync(job, _commandProcessor, cancellationToken); + await job.Step.ExecuteAsync(_commandProcessor, cancellationToken); await _jobStoreAsync.SaveJobAsync(job, cancellationToken); } diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index ed7960af42..20c014a3dc 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -86,11 +86,11 @@ public async Task ReceiveWorkflowEvent(Event @event) if (taskResponse.Parser is null) throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); - if (job.CurrentStep is null) + if (job.Step is null) throw new InvalidOperationException($"Current step of workflow #{job.Id} should not be null"); taskResponse.Parser(@event, job); - job.CurrentStep.OnCompletion?.Invoke(); + job.Step.OnCompletion?.Invoke(); job.State = JobState.Running; job.PendingResponses.Remove(eventType); } diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 0d28d73af0..b9cce17ffa 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -12,7 +12,12 @@ namespace Paramore.Brighter.Mediator; /// The action to be taken with the step, null if no action /// An optional callback to run, following completion of the step /// The data that the step operates over -public abstract class Step(string name, Sequential? next, IStepTask? stepTask = null, Action? onCompletion = null) +public abstract class Step( + string name, + Sequential? next, + Job job, + IStepTask? stepTask = null, + Action? onCompletion = null) { /// The name of the step, used for tracing execution public string Name { get; init; } = name; @@ -20,20 +25,63 @@ public abstract class Step(string name, Sequential? next, IStepTas /// The next step in the sequence, null if this is the last step protected Sequential? Next { get; } = next; + /// Which job is being executed by the step. + public Job Job { get; } = job; + /// An optional callback to be run, following completion of the step. public Action? OnCompletion { get; } = onCompletion; /// The action to be taken with the step. protected IStepTask? StepTask { get; } = stepTask; + + public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); +} + +/// +/// Allows the workflow to branch on a choice, taking either a right or left path. +/// +/// The name of the step, used for tracing execution +/// A composite specification that can be evaluated to determine the path to choose +/// An optional callback to run, following completion of the step +/// The next step in the sequence, if the predicate evaluates to true, null if this is the last step. +/// The next step in the sequence, if the predicate evaluates to false, null if this is the last step. +/// The data that the step operates over +public class ExclusiveChoice( + string name, + ISpecification predicate, + Job job, + Action? onCompletion, + Sequential? nextTrue, + Sequential? nextFalse +) + : Step(name, null, job, null, onCompletion) +{ + public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + { + Job.Step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; + return Task.CompletedTask; + } +} + +public class ParallelSplit( + string name, + Job job, + Action? onBranch, + params Step[] branches + ) + : Step(name, null, job) +{ + public Step[] Branches { get; set; } = branches; - public virtual Task ExecuteAsync(Job job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { - StepTask?.HandleAsync(job, commandProcessor, cancellationToken); - OnCompletion?.Invoke(); + // Parallel split doesn't directly execute its jobs. + // Execution is handled by the Scheduler, which will handle running each branch concurrently. + onBranch?.Invoke(Job.Data); return Task.CompletedTask; } } - + /// /// Represents a sequential step in the workflow. Control flows to the next step in the list, or ends if next is null. /// A set of sequential steps for a linked list. @@ -48,69 +96,31 @@ public virtual Task ExecuteAsync(Job job, IAmACommandProcessor commandPr public class Sequential( string name, IStepTask stepTask, + Job job, Action? onCompletion, Sequential? next, Action? onFaulted = null, Sequential? faultNext = null - ) - : Step(name, next, stepTask, onCompletion) +) + : Step(name, next, job, stepTask, onCompletion) { - public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { try { - StepTask?.HandleAsync(state, commandProcessor, cancellationToken); + StepTask?.HandleAsync(Job, commandProcessor, cancellationToken); OnCompletion?.Invoke(); - state.CurrentStep = Next; + Job.Step = Next; } catch (Exception) { onFaulted?.Invoke(); - state.CurrentStep = faultNext; + Job.Step = faultNext; } return Task.CompletedTask; } } -/// -/// Allows the workflow to branch on a choice, taking either a right or left path. -/// -/// The name of the step, used for tracing execution -/// A composite specification that can be evaluated to determine the path to choose -/// An optional callback to run, following completion of the step -/// The next step in the sequence, if the predicate evaluates to true, null if this is the last step. -/// The next step in the sequence, if the predicate evaluates to false, null if this is the last step. -/// The data that the step operates over -public class ExclusiveChoice( - string name, - ISpecification predicate, - Action? onCompletion, - Sequential? nextTrue, - Sequential? nextFalse -) - : Step(name, null, null, onCompletion) -{ - public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) - { - state.CurrentStep = predicate.IsSatisfiedBy(state.Data) ? nextTrue : nextFalse; - return Task.CompletedTask; - } -} - -public class ParallelSplit(string name, Action? onBranch, params Step[] branches) - : Step(name, null) -{ - public Step[] Branches { get; set; } = branches; - - public override Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) - { - // Parallel split doesn't directly execute its jobs. - // Execution is handled by the Scheduler, which will handle running each branch concurrently. - onBranch?.Invoke(state.Data); - return Task.CompletedTask; - } -} - /// /// Allows the workflow to pause. This is a blocking operation that pauses the executing thread /// @@ -119,10 +129,16 @@ public override Task ExecuteAsync(Job state, IAmACommandProcessor command /// An optional callback to run, following completion of the step /// The next step in the sequence, null if this is the last step. /// The data that the step operates over -public class Wait(string name, TimeSpan duration, Action? onCompletion, Sequential? next) - : Step(name, next, null, onCompletion) +public class Wait( + string name, + TimeSpan duration, + Job job, + Action? onCompletion, + Sequential? next + ) + : Step(name, next, job, null, onCompletion) { - public override async Task ExecuteAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { await Task.Delay(duration, cancellationToken); OnCompletion?.Invoke(); diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index 0df3d652cc..273890f43f 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -37,10 +37,10 @@ public interface IStepTask /// /// Handles the workflow action. /// - /// The current state of the workflow. + /// The current job of the workflow. /// The command processor used to handle commands. /// The cancellation token for this task - Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); + Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); } /// @@ -76,21 +76,25 @@ public class TaskResponse(Action> parser, Type response /// Takes the Data property and transforms it /// The workflow data, that we wish to transform public class ChangeAsync( - Func> onChange + Func onChange ) : IStepTask { /// /// Handles the workflow action. /// - /// The current state of the workflow. + /// The current job of the workflow. /// The command processor used to handle commands. /// The cancellation token for this task - public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (job is null) + return; + if (cancellationToken.IsCancellationRequested) return; - state.Data = await onChange(state.Data); + + await onChange(job.Data); } } @@ -109,13 +113,16 @@ Func requestFactory /// /// Handles the fire-and-forget action. /// - /// The current state of the workflow. + /// The current job of the workflow. /// The command processor used to handle commands. /// The cancellation token for this task - public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (job is null) + return; + var command = requestFactory(); - command.CorrelationId = state.Id; + command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); } } @@ -139,16 +146,19 @@ public class RequestAndReactionAsync( /// /// Handles the request-and-reply action. /// - /// The current state of the workflow. + /// The current job of the workflow. /// The command processor used to handle commands. /// The cancellation token for this task - public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (job is null) + return; + var command = requestFactory(); - command.CorrelationId = state.Id; + command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - state.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); + job.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); } } @@ -175,17 +185,20 @@ public class RobustRequestAndReactionAsync( /// /// Handles the fire-and-forget action. /// - /// The current state of the workflow. + /// The current job of the workflow. /// The command processor used to handle commands. /// The cancellation token for this task - public async Task HandleAsync(Job state, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (job is null) + return; + var command = requestFactory(); - command.CorrelationId = state.Id; + command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - state.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); - state.PendingResponses.Add(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} + job.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); + job.PendingResponses.Add(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs index e0b1312341..3626fe9d1c 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using Paramore.Brighter.Mediator; +using System.Collections.Concurrent; namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; public class WorkflowTestData { - public Dictionary Bag { get; set; } = new(); + public ConcurrentDictionary Bag { get; set; } = new(); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index e2b45d12c8..c7b2ebfa2d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -12,7 +12,7 @@ public class MediatorBlockingWaitStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompleted; public MediatorBlockingWaitStepFlowTests() @@ -27,15 +27,18 @@ public MediatorBlockingWaitStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; var firstStep = new Wait("Test of Job", TimeSpan.FromMilliseconds(500), + _job, () => { _stepCompleted = true; }, null ); - _flow = new Job(firstStep, workflowData) ; + _job.Step = firstStep; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -52,12 +55,12 @@ public MediatorBlockingWaitStepFlowTests() [Fact] public async Task When_running_a_single_step_workflow() { - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); //We won't really see th block in action as the test will simply block for 500ms await _runner.RunAsync(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); _stepCompleted.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index b983047ac7..55fda48083 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -1,21 +1,26 @@ -using System.Threading.Tasks; +using System; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorChangeStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompleted; - public MediatorChangeStepFlowTests () + public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -25,23 +30,27 @@ public MediatorChangeStepFlowTests () commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); PipelineBuilder.ClearPipelineCache(); - var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + var workflowData= new WorkflowTestData { Bag = { ["MyValue"] = "Test" } }; + + _job = new Job(workflowData) ; var firstStep = new Sequential( "Test of Job", new ChangeAsync( (flow) => { + var tcs = new TaskCompletionSource(); flow.Bag["MyValue"] = "Altered"; - return Task.FromResult(flow); + tcs.SetResult(); + return tcs.Task; }), + _job, () => { _stepCompleted = true; }, null ); - _flow = new Job(firstStep, workflowData) ; + _job.Step = firstStep; - InMemoryJobStoreAsync store = new(); + var store = new InMemoryJobStoreAsync (); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( @@ -56,12 +65,23 @@ public MediatorChangeStepFlowTests () [Fact] public async Task When_running_a_single_step_workflow() { - await _scheduler.ScheduleAsync(_flow); - - await _runner.RunAsync(); + await _scheduler.ScheduleAsync(_job); + + //let it run long enough to finish work, then terminate + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception ex) + { + // swallow the exception, we expect it to be cancelled + _testOutputHelper.WriteLine(ex.ToString()); + } - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); _stepCompleted.Should().BeTrue(); - _flow.Bag["MyValue"].Should().Be("Altered"); + _job.Bag["MyValue"].Should().Be("Altered"); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index c1d02714e7..470aeac189 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -13,7 +13,7 @@ public class MediatorFailingChoiceFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompletedOne; private bool _stepCompletedTwo; private bool _stepCompletedThree; @@ -38,28 +38,33 @@ public MediatorFailingChoiceFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Fail"); + workflowData.Bag["MyValue"] = "Fail"; + _job = new Job(workflowData) ; + var stepThree = new Sequential( "Test of Job SequenceStep Three", new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), + _job, () => { _stepCompletedOne = true; }, stepTwo, stepThree); - _flow = new Job(stepOne, workflowData) ; + _job.Step = stepOne; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -79,7 +84,7 @@ public async Task When_running_a_choice_workflow_step() MyCommandHandlerAsync.ReceivedCommands.Clear(); MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index ff7dfb7ba6..c9cddbf6e8 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -13,7 +13,7 @@ public class MediatorReplyMultiStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompletedOne; private bool _stepCompletedTwo; @@ -36,11 +36,14 @@ public MediatorReplyMultiStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + _job = new Job(workflowData) ; + var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _stepCompletedTwo = true; }, null); @@ -48,11 +51,12 @@ public MediatorReplyMultiStepFlowTests() "Test of Job SequenceStep One", new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), + (reply) => workflowData.Bag["MyReply"] = ((MyEvent)reply).Value), + _job, () => { _stepCompletedOne = true; }, stepTwo); - - _flow = new Job(stepOne, workflowData) ; + + _job.Step = stepOne; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -72,7 +76,7 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Clear(); MyEventHandlerAsync.ReceivedEvents.Clear(); - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(); _stepCompletedOne.Should().BeTrue(); @@ -80,6 +84,6 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index e5541ee6c5..8c5065032f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -13,7 +13,7 @@ public class MediatorPassingChoiceFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompletedOne; private bool _stepCompletedTwo; private bool _stepCompletedThree; @@ -38,28 +38,33 @@ public MediatorPassingChoiceFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Pass"); + workflowData.Bag["MyValue"] = "Pass"; + + _job = new Job(workflowData) ; var stepThree = new Sequential( "Test of Job SequenceStep Three", new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), + _job, () => { _stepCompletedOne = true; }, stepTwo, stepThree); - - _flow = new Job(stepOne, workflowData) ; + + _job.Step = stepOne; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -79,7 +84,7 @@ public async Task When_running_a_choice_workflow_step() MyCommandHandlerAsync.ReceivedCommands.Clear(); MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(); _stepCompletedOne.Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index c56a236edb..ad79259039 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; @@ -12,7 +14,7 @@ public class MediatorOneStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; public MediatorOneStepFlowTests() { @@ -26,16 +28,19 @@ public MediatorOneStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; var firstStep = new Sequential( "Test of Job", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + _job, () => { }, null ); - - _flow = new Job(firstStep, workflowData) ; + + _job.Step = firstStep; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -54,10 +59,20 @@ public async Task When_running_a_single_step_workflow() { MyCommandHandlerAsync.ReceivedCommands.Clear(); - await _scheduler.ScheduleAsync(_flow); - await _runner.RunAsync(); + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try + { + await _runner.RunAsync(ct.Token); + } + catch (TaskCanceledException) + { + // Expected + } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index a27928957b..859b28a9e3 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; @@ -13,7 +15,7 @@ public class MediatorTwoStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; public MediatorTwoStepFlowTests() { @@ -27,12 +29,14 @@ public MediatorTwoStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + _job = new Job(workflowData) ; var secondStep = new Sequential( "Test of Job Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { }, null ); @@ -40,11 +44,12 @@ public MediatorTwoStepFlowTests() var firstStep = new Sequential( "Test of Job One", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep ); - _flow = new Job(firstStep, workflowData) ; + _job.Step = firstStep; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -63,11 +68,19 @@ public async Task When_running_a_single_step_workflow() { MyCommandHandlerAsync.ReceivedCommands.Clear(); - await _scheduler.ScheduleAsync(_flow); - await _runner.RunAsync(); + var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + try + { + await _scheduler.ScheduleAsync(_job); + await _runner.RunAsync(ct.Token); + } + catch (TaskCanceledException) + { + // ignored + } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index 9d69d94857..35b0d2de73 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -11,7 +11,7 @@ public class MediatorParallelSplitFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _firstBranchFinished; private bool _secondBranchFinished; @@ -28,9 +28,12 @@ public MediatorParallelSplitFlowTests() var workflowData= new WorkflowTestData(); + _job = new Job(workflowData) ; + var secondBranch = new Sequential( "Test of Job Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), + _job, () => { _secondBranchFinished = true; }, null ); @@ -38,20 +41,21 @@ public MediatorParallelSplitFlowTests() var firstBranch = new Sequential( "Test of Job One", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + _job, () => { _firstBranchFinished = true; }, null ); var parallelSplit = new ParallelSplit( "Test of Job Parallel Split", + _job, (data) => - { data.Bag.Add("MyValue", "TestOne"); + { data.Bag["MyValue"] = "TestOne"; data.Bag["MyOtherValue"] = "TestTwo"; }, firstBranch, secondBranch ); - _flow = new Job(parallelSplit, workflowData) ; InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -69,13 +73,13 @@ public async Task When_running_a_workflow_with_a_parallel_split() { MyCommandHandlerAsync.ReceivedCommands.Clear(); - _scheduler.ScheduleAsync(_flow); + _scheduler.ScheduleAsync(_job); _runner.RunAsync(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); _firstBranchFinished.Should().BeTrue(); _secondBranchFinished.Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index e9c007d9e7..98f76500b8 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -18,7 +18,7 @@ public class MediatorReplyStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompleted; public MediatorReplyStepFlowTests() @@ -40,18 +40,19 @@ public MediatorReplyStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + _job = new Job(workflowData) ; + var firstStep = new Sequential( "Test of Job", new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value)), + (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }), + _job, () => { _stepCompleted = true; }, null); - _flow = new Job(firstStep, workflowData) ; - InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -70,13 +71,13 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Clear(); MyEventHandlerAsync.ReceivedEvents.Clear(); - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(); _stepCompleted.Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index 0fe0e3d23d..e373c48bf9 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -14,7 +14,7 @@ public class MediatorRobustReplyNoFaultStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; - private readonly Job _flow; + private readonly Job _job; private bool _stepCompleted; private bool _stepFaulted; @@ -37,21 +37,22 @@ public MediatorRobustReplyNoFaultStepFlowTests() PipelineBuilder.ClearPipelineCache(); var workflowData= new WorkflowTestData(); - workflowData.Bag.Add("MyValue", "Test"); + workflowData.Bag["MyValue"] = "Test"; + _job = new Job(workflowData) ; + var firstStep = new Sequential( "Test of Job", new RobustRequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value), - (fault) => workflowData.Bag.Add("MyFault", ((MyFault)fault).Value)), + (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }, + (fault) => { workflowData.Bag["MyFault"] = ((MyFault)fault).Value; }), + _job, () => { _stepCompleted = true; }, null, () => { _stepFaulted = true; }, null); - _flow = new Job(firstStep, workflowData) ; - InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -70,7 +71,7 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Clear(); MyEventHandlerAsync.ReceivedEvents.Clear(); - await _scheduler.ScheduleAsync(_flow); + await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(); _stepCompleted.Should().BeTrue(); @@ -78,6 +79,6 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); - _flow.State.Should().Be(JobState.Done); + _job.State.Should().Be(JobState.Done); } } From 6067bd43aa3dbe74df67e69fdf1db242fd7cefd4 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 19 Nov 2024 10:01:02 +0000 Subject: [PATCH 29/44] fix: refactor relationship between job and step to be more explicit. Make some fields private --- src/Paramore.Brighter.Mediator/Job.cs | 92 +++++++++++++++---- src/Paramore.Brighter.Mediator/Runner.cs | 6 +- src/Paramore.Brighter.Mediator/Scheduler.cs | 11 +-- src/Paramore.Brighter.Mediator/Steps.cs | 63 +++++++++---- src/Paramore.Brighter.Mediator/Tasks.cs | 6 +- .../When_running_a_blocking_wait_workflow.cs | 3 +- .../When_running_a_change_workflow.cs | 3 +- ..._running_a_failing_choice_workflow_step.cs | 5 +- ...running_a_multistep_workflow_with_reply.cs | 4 +- ..._running_a_passing_choice_workflow_step.cs | 5 +- .../When_running_a_single_step_workflow.cs | 3 +- .../When_running_a_two_step_workflow.cs | 4 +- ...unning_a_workflow_with_a_parallel_split.cs | 4 +- .../When_running_a_workflow_with_reply.cs | 1 - ...ng_a_workflow_with_robust_reply_nofault.cs | 1 - 15 files changed, 139 insertions(+), 72 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index d539ab2c8a..7537301e93 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -49,34 +49,94 @@ public abstract class Job { } /// The user defined data for the workflow public class Job : Job { + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives + private readonly Dictionary?> _pendingResponses = new(); + + /// The next step. Steps are a linked list. The final step in the list has null for it's next step. + private Step? _step; + /// A map of user defined values. Normally, use Data to pass data between steps public Dictionary Bag { get; } = new(); - - /// What step are we currently at in the workflow - public Step? Step { get; set; } - + /// The data that is passed between steps of the workflow - public TData Data { get; } - - /// The id of the workflow, used to save-retrieve it from storage - public string Id { get; private set; } = Guid.NewGuid().ToString(); + public TData Data { get; } - /// If we are awaiting a response, we store the type of the response and the action to take when it arrives - public Dictionary> PendingResponses { get; private set; } = new(); + /// The id of the workflow, used to save-retrieve it from storage + public string Id { get; private set; } = Guid.NewGuid().ToString(); - /// Is the workflow currently awaiting an event response + /// Is the job waiting to be run, running, waiting for a response or finished public JobState State { get; set; } /// - /// Constructs a new Job - /// The first step of the workflow to execute. + /// Constructs a new Job /// State which is passed between steps of the workflow /// - public Job(TData data) + public Job(TData data) { Data = data; - State = JobState.Ready; + State = JobState.Ready; + } + + /// + /// Initializes the steps of the workflow. + /// + /// The first step of the workflow to execute. + public void InitSteps(Step? firstStep) + { + _step = firstStep; + if (_step is not null) _step.AddToJob(this); + } + + /// + /// Gets the current step of the workflow. + /// + /// The current step of the workflow. + public Step? CurrentStep() + { + return _step; } -} + /// + /// Adds a pending response to the job. + /// + /// The expected type of the response + /// The task response to add. + public void AddPendingResponse(Type responseType, TaskResponse? taskResponse) + { + State = JobState.Waiting; + _pendingResponses.Add(responseType, taskResponse); + } + + /// + /// Finds a pending response by its type. + /// + /// The type of the event. + /// The task response if found. + public bool FindPendingResponse(Type eventType, out TaskResponse? taskResponse) + { + return _pendingResponses.TryGetValue(eventType, out taskResponse); + } + /// + /// Sets the next step of the workflow. + /// + /// The next step to set. + public void NextStep(Step? nextStep) + { + _step = nextStep; + if (_step is not null) _step.AddToJob(this); + } + + /// + /// Removes a pending response from the job, and resets the state to running. + /// + /// The type of event that we expect + /// + public bool RemovePendingResponse(Type eventType) + { + + var success = _pendingResponses.Remove(eventType); + if (success) State = JobState.Running; + return success; + } +} diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 104e8183d0..5447bbbdd8 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -73,7 +73,7 @@ private async Task Execute(Job? job, CancellationToken cancellationToken if (job is null) return; - if (job.Step is null) + if (job.CurrentStep() is null) { job.State = JobState.Done; return; @@ -82,9 +82,9 @@ private async Task Execute(Job? job, CancellationToken cancellationToken job.State = JobState.Running; await _jobStoreAsync.SaveJobAsync(job, cancellationToken); - while (job.Step is not null) + while (job.CurrentStep() is not null) { - await job.Step.ExecuteAsync(_commandProcessor, cancellationToken); + await job.CurrentStep()!.ExecuteAsync(_commandProcessor, cancellationToken); await _jobStoreAsync.SaveJobAsync(job, cancellationToken); } diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index 20c014a3dc..eff9a254b4 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -80,18 +80,17 @@ public async Task ReceiveWorkflowEvent(Event @event) var eventType = @event.GetType(); - if (!job.PendingResponses.TryGetValue(eventType, out TaskResponse? taskResponse)) + if (!job.FindPendingResponse(eventType, out TaskResponse? taskResponse)) return; - if (taskResponse.Parser is null) + if (taskResponse is null || taskResponse.Parser is null) throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); - if (job.Step is null) + if (job.CurrentStep() is null) throw new InvalidOperationException($"Current step of workflow #{job.Id} should not be null"); taskResponse.Parser(@event, job); - job.Step.OnCompletion?.Invoke(); - job.State = JobState.Running; - job.PendingResponses.Remove(eventType); + job.CurrentStep()!.OnCompletion?.Invoke(); + job.RemovePendingResponse(eventType); } } diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index b9cce17ffa..49131297cd 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -13,28 +13,47 @@ namespace Paramore.Brighter.Mediator; /// An optional callback to run, following completion of the step /// The data that the step operates over public abstract class Step( - string name, - Sequential? next, - Job job, - IStepTask? stepTask = null, + string name, + Sequential? next, + IStepTask? stepTask = null, Action? onCompletion = null) { + /// Which job is being executed by the step. + protected Job? Job ; + /// The name of the step, used for tracing execution public string Name { get; init; } = name; /// The next step in the sequence, null if this is the last step protected Sequential? Next { get; } = next; - /// Which job is being executed by the step. - public Job Job { get; } = job; - /// An optional callback to be run, following completion of the step. public Action? OnCompletion { get; } = onCompletion; /// The action to be taken with the step. - protected IStepTask? StepTask { get; } = stepTask; + protected IStepTask? StepTask = stepTask; public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); + + /// + /// Gets the job that is executing us + /// + /// The job that is executing us + public void GetJob(Job job) + { + Job = job; + } + + /// + /// Sets the job that is executing us + /// + /// The job that we are executing under + public void AddToJob(Job job) + { + Job = job; + } + + } /// @@ -49,32 +68,37 @@ public abstract class Step( public class ExclusiveChoice( string name, ISpecification predicate, - Job job, Action? onCompletion, Sequential? nextTrue, Sequential? nextFalse ) - : Step(name, null, job, null, onCompletion) + : Step(name, null, null, onCompletion) { public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { - Job.Step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; + if (Job is null) + throw new InvalidOperationException("Job is null"); + + var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; + Job.NextStep(step); return Task.CompletedTask; } } public class ParallelSplit( string name, - Job job, Action? onBranch, params Step[] branches ) - : Step(name, null, job) + : Step(name, null) { public Step[] Branches { get; set; } = branches; public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (Job is null) + throw new InvalidOperationException("Job is null"); + // Parallel split doesn't directly execute its jobs. // Execution is handled by the Scheduler, which will handle running each branch concurrently. onBranch?.Invoke(Job.Data); @@ -96,26 +120,28 @@ public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, Cancell public class Sequential( string name, IStepTask stepTask, - Job job, Action? onCompletion, Sequential? next, Action? onFaulted = null, Sequential? faultNext = null ) - : Step(name, next, job, stepTask, onCompletion) + : Step(name, next, stepTask, onCompletion) { public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (Job is null) + throw new InvalidOperationException("Job is null"); + try { StepTask?.HandleAsync(Job, commandProcessor, cancellationToken); OnCompletion?.Invoke(); - Job.Step = Next; + Job.NextStep(Next); } catch (Exception) { onFaulted?.Invoke(); - Job.Step = faultNext; + Job.NextStep(faultNext); } return Task.CompletedTask; } @@ -132,11 +158,10 @@ public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, Cancell public class Wait( string name, TimeSpan duration, - Job job, Action? onCompletion, Sequential? next ) - : Step(name, next, job, null, onCompletion) + : Step(name, next, null, onCompletion) { public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index 273890f43f..55ab245649 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -158,7 +158,7 @@ public async Task HandleAsync(Job? job, IAmACommandProcessor commandProce command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - job.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); + job.AddPendingResponse(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); } } @@ -197,8 +197,8 @@ public async Task HandleAsync(Job? job, IAmACommandProcessor commandProce command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - job.PendingResponses.Add(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); - job.PendingResponses.Add(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} + job.AddPendingResponse(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); + job.AddPendingResponse(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index c7b2ebfa2d..85e0477eff 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -33,12 +33,11 @@ public MediatorBlockingWaitStepFlowTests() var firstStep = new Wait("Test of Job", TimeSpan.FromMilliseconds(500), - _job, () => { _stepCompleted = true; }, null ); - _job.Step = firstStep; + _job.InitSteps(firstStep); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index 55fda48083..62e6be7cfc 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -43,12 +43,11 @@ public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) tcs.SetResult(); return tcs.Task; }), - _job, () => { _stepCompleted = true; }, null ); - _job.Step = firstStep; + _job.InitSteps(firstStep); var store = new InMemoryJobStoreAsync (); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 470aeac189..e8cdd80ff7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -45,26 +45,23 @@ public MediatorFailingChoiceFlowTests() var stepThree = new Sequential( "Test of Job SequenceStep Three", new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), - _job, () => { _stepCompletedOne = true; }, stepTwo, stepThree); - _job.Step = stepOne; + _job.InitSteps(stepOne); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index c9cddbf6e8..a54a6cc70a 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -43,7 +43,6 @@ public MediatorReplyMultiStepFlowTests() var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _stepCompletedTwo = true; }, null); @@ -52,11 +51,10 @@ public MediatorReplyMultiStepFlowTests() new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => workflowData.Bag["MyReply"] = ((MyEvent)reply).Value), - _job, () => { _stepCompletedOne = true; }, stepTwo); - _job.Step = stepOne; + _job.InitSteps(stepOne); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 8c5065032f..6779c26d78 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -45,26 +45,23 @@ public MediatorPassingChoiceFlowTests() var stepThree = new Sequential( "Test of Job SequenceStep Three", new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _stepCompletedTwo = true; }, null); var stepOne = new ExclusiveChoice( "Test of Job SequenceStep One", new Specification(x => x.Bag["MyValue"] as string == "Pass"), - _job, () => { _stepCompletedOne = true; }, stepTwo, stepThree); - _job.Step = stepOne; + _job.InitSteps(stepOne); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index ad79259039..406b34e1d4 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -35,12 +35,11 @@ public MediatorOneStepFlowTests() var firstStep = new Sequential( "Test of Job", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), - _job, () => { }, null ); - _job.Step = firstStep; + _job.InitSteps(firstStep); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 859b28a9e3..f4dfa5b9b6 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -36,7 +36,6 @@ public MediatorTwoStepFlowTests() var secondStep = new Sequential( "Test of Job Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { }, null ); @@ -44,12 +43,11 @@ public MediatorTwoStepFlowTests() var firstStep = new Sequential( "Test of Job One", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep ); - _job.Step = firstStep; + _job.InitSteps(firstStep); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index 35b0d2de73..fce281dcbc 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -33,7 +33,6 @@ public MediatorParallelSplitFlowTests() var secondBranch = new Sequential( "Test of Job Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), - _job, () => { _secondBranchFinished = true; }, null ); @@ -41,14 +40,12 @@ public MediatorParallelSplitFlowTests() var firstBranch = new Sequential( "Test of Job One", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - _job, () => { _firstBranchFinished = true; }, null ); var parallelSplit = new ParallelSplit( "Test of Job Parallel Split", - _job, (data) => { data.Bag["MyValue"] = "TestOne"; data.Bag["MyOtherValue"] = "TestTwo"; @@ -56,6 +53,7 @@ public MediatorParallelSplitFlowTests() firstBranch, secondBranch ); + _job.InitSteps(parallelSplit); InMemoryJobStoreAsync store = new(); InMemoryJobChannel channel = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 98f76500b8..e191a107b0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -49,7 +49,6 @@ public MediatorReplyStepFlowTests() new RequestAndReactionAsync( () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }), - _job, () => { _stepCompleted = true; }, null); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index e373c48bf9..b4fa9411ed 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -47,7 +47,6 @@ public MediatorRobustReplyNoFaultStepFlowTests() () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }, (fault) => { workflowData.Bag["MyFault"] = ((MyFault)fault).Value; }), - _job, () => { _stepCompleted = true; }, null, () => { _stepFaulted = true; }, From 8b4e5e40933df1c7932d0e2fa1c719080d491461 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 19 Nov 2024 10:47:18 +0000 Subject: [PATCH 30/44] fix: step advancement manages job state --- src/Paramore.Brighter.Mediator/Job.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index 7537301e93..582e410c57 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -83,8 +83,7 @@ public Job(TData data) /// The first step of the workflow to execute. public void InitSteps(Step? firstStep) { - _step = firstStep; - if (_step is not null) _step.AddToJob(this); + NextStep(firstStep); } /// @@ -124,7 +123,10 @@ public bool FindPendingResponse(Type eventType, out TaskResponse? taskRes public void NextStep(Step? nextStep) { _step = nextStep; - if (_step is not null) _step.AddToJob(this); + if (_step is not null) + _step.AddToJob(this); + else + if (State != JobState.Waiting) State = JobState.Done; } /// From e5a663931362bc5d79dfb1011d80c59825942433 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 19 Nov 2024 11:04:39 +0000 Subject: [PATCH 31/44] fix: add cancellation token interrupt of runner to all tests --- src/Paramore.Brighter.Mediator/Runner.cs | 8 ------ src/Paramore.Brighter.Mediator/Steps.cs | 4 +++ .../When_running_a_blocking_wait_workflow.cs | 25 ++++++++++++++----- ..._running_a_failing_choice_workflow_step.cs | 19 +++++++++++--- ...running_a_multistep_workflow_with_reply.cs | 19 ++++++++++++-- ..._running_a_passing_choice_workflow_step.cs | 18 +++++++++++-- .../When_running_a_single_step_workflow.cs | 14 ++++++++--- .../When_running_a_two_step_workflow.cs | 21 ++++++++++------ ...unning_a_workflow_with_a_parallel_split.cs | 24 +++++++++++++++--- .../When_running_a_workflow_with_reply.cs | 19 ++++++++++++-- ...ng_a_workflow_with_robust_reply_nofault.cs | 19 ++++++++++++-- 11 files changed, 151 insertions(+), 39 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 5447bbbdd8..f1796dc60d 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -73,12 +73,6 @@ private async Task Execute(Job? job, CancellationToken cancellationToken if (job is null) return; - if (job.CurrentStep() is null) - { - job.State = JobState.Done; - return; - } - job.State = JobState.Running; await _jobStoreAsync.SaveJobAsync(job, cancellationToken); @@ -87,8 +81,6 @@ private async Task Execute(Job? job, CancellationToken cancellationToken await job.CurrentStep()!.ExecuteAsync(_commandProcessor, cancellationToken); await _jobStoreAsync.SaveJobAsync(job, cancellationToken); } - - job.State = JobState.Done; } private async Task ProcessJobs(CancellationToken cancellationToken) diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 49131297cd..b83873dd4d 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -165,7 +165,11 @@ public class Wait( { public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) { + if (Job is null) + throw new InvalidOperationException("Job is null"); + await Task.Delay(duration, cancellationToken); + Job.NextStep(Next); OnCompletion?.Invoke(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index 85e0477eff..b4364f0811 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -1,10 +1,12 @@ using System; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; @@ -14,9 +16,11 @@ public class MediatorBlockingWaitStepFlowTests private readonly Runner _runner; private readonly Job _job; private bool _stepCompleted; + private readonly ITestOutputHelper _testOutputHelper; - public MediatorBlockingWaitStepFlowTests() + public MediatorBlockingWaitStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -52,14 +56,23 @@ public MediatorBlockingWaitStepFlowTests() } [Fact] - public async Task When_running_a_single_step_workflow() + public async Task When_running_a_wait_workflow() { await _scheduler.ScheduleAsync(_job); - //We won't really see th block in action as the test will simply block for 500ms - await _runner.RunAsync(); - - _job.State.Should().Be(JobState.Done); + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + _stepCompleted.Should().BeTrue(); + _job.State.Should().Be(JobState.Done); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index e8cdd80ff7..694b04c2d2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -1,16 +1,19 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorFailingChoiceFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; @@ -18,8 +21,9 @@ public class MediatorFailingChoiceFlowTests private bool _stepCompletedTwo; private bool _stepCompletedThree; - public MediatorFailingChoiceFlowTests() + public MediatorFailingChoiceFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; // arrange var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -83,13 +87,22 @@ public async Task When_running_a_choice_workflow_step() await _scheduler.ScheduleAsync(_job); - await _runner.RunAsync(); + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeFalse(); _stepCompletedThree.Should().BeTrue(); MyOtherCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any().Should().BeFalse(); - _stepCompletedOne.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index a54a6cc70a..591615e6a7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -1,24 +1,28 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorReplyMultiStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; private bool _stepCompletedOne; private bool _stepCompletedTwo; - public MediatorReplyMultiStepFlowTests() + public MediatorReplyMultiStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); registry.RegisterAsync(); @@ -75,7 +79,18 @@ public async Task When_running_a_workflow_with_reply() MyEventHandlerAsync.ReceivedEvents.Clear(); await _scheduler.ScheduleAsync(_job); - await _runner.RunAsync(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 6779c26d78..ec46f7cd62 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -1,16 +1,19 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorPassingChoiceFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; @@ -18,8 +21,9 @@ public class MediatorPassingChoiceFlowTests private bool _stepCompletedTwo; private bool _stepCompletedThree; - public MediatorPassingChoiceFlowTests() + public MediatorPassingChoiceFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; // arrange var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -82,8 +86,18 @@ public async Task When_running_a_choice_workflow_step() MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); await _scheduler.ScheduleAsync(_job); - await _runner.RunAsync(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } _stepCompletedOne.Should().BeTrue(); _stepCompletedTwo.Should().BeTrue(); _stepCompletedThree.Should().BeFalse(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 406b34e1d4..269fb818e3 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -7,17 +7,21 @@ using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorOneStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; + private bool _stepCompleted; - public MediatorOneStepFlowTests() + public MediatorOneStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -35,7 +39,7 @@ public MediatorOneStepFlowTests() var firstStep = new Sequential( "Test of Job", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), - () => { }, + () => { _stepCompleted = true; }, null ); @@ -62,16 +66,18 @@ public async Task When_running_a_single_step_workflow() var ct = new CancellationTokenSource(); ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try { await _runner.RunAsync(ct.Token); } - catch (TaskCanceledException) + catch (Exception e) { - // Expected + _testOutputHelper.WriteLine(e.ToString()); } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index f4dfa5b9b6..e6e5f9c7ce 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -8,17 +8,21 @@ using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorTwoStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; + private bool _stepsCompleted; - public MediatorTwoStepFlowTests() + public MediatorTwoStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -36,7 +40,7 @@ public MediatorTwoStepFlowTests() var secondStep = new Sequential( "Test of Job Two", new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - () => { }, + () => { _stepsCompleted = true; }, null ); @@ -62,23 +66,26 @@ public MediatorTwoStepFlowTests() } [Fact] - public async Task When_running_a_single_step_workflow() + public async Task When_running_a_two_step_workflow() { MyCommandHandlerAsync.ReceivedCommands.Clear(); + await _scheduler.ScheduleAsync(_job); - var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try { - await _scheduler.ScheduleAsync(_job); await _runner.RunAsync(ct.Token); } - catch (TaskCanceledException) + catch (Exception e) { - // ignored + _testOutputHelper.WriteLine(e.ToString()); } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); _job.State.Should().Be(JobState.Done); + _stepsCompleted.Should().BeTrue(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index fce281dcbc..c00595d04f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -1,22 +1,28 @@ -using System.Linq; +using System; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; +using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorParallelSplitFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; private bool _firstBranchFinished; private bool _secondBranchFinished; - public MediatorParallelSplitFlowTests() + public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); @@ -67,12 +73,24 @@ public MediatorParallelSplitFlowTests() _runner = new Runner(channel, store, commandProcessor); } + //[Fact] public async Task When_running_a_workflow_with_a_parallel_split() { MyCommandHandlerAsync.ReceivedCommands.Clear(); _scheduler.ScheduleAsync(_job); - _runner.RunAsync(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index e191a107b0..99720a3078 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; @@ -7,6 +8,7 @@ using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; using MyCommand = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommand; using MyCommandHandlerAsync = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommandHandlerAsync; using MyEvent = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyEvent; @@ -16,13 +18,15 @@ namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorReplyStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; private bool _stepCompleted; - public MediatorReplyStepFlowTests() + public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); registry.RegisterAsync(); @@ -71,7 +75,18 @@ public async Task When_running_a_workflow_with_reply() MyEventHandlerAsync.ReceivedEvents.Clear(); await _scheduler.ScheduleAsync(_job); - await _runner.RunAsync(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } _stepCompleted.Should().BeTrue(); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index b4fa9411ed..1c9682a161 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Amazon.Runtime.Internal.Transform; using FluentAssertions; @@ -7,19 +8,22 @@ using Paramore.Brighter.Mediator; using Polly.Registry; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.Core.Tests.Workflows; public class MediatorRobustReplyNoFaultStepFlowTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; private bool _stepCompleted; private bool _stepFaulted; - public MediatorRobustReplyNoFaultStepFlowTests() + public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); registry.RegisterAsync(); registry.RegisterAsync(); @@ -74,7 +78,18 @@ public async Task When_running_a_workflow_with_reply() await _runner.RunAsync(); _stepCompleted.Should().BeTrue(); - _stepFaulted.Should().BeFalse(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); From acef39d5f6c685e183948c16b30ad81ffc370199 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 19 Nov 2024 18:31:55 +0000 Subject: [PATCH 32/44] chore: safety check in; fixing failing tests --- ...obStoreAsync.cs => IAmAStateStoreAsync.cs} | 2 +- ...oreAsync.cs => InMemoryStateStoreAsync.cs} | 2 +- src/Paramore.Brighter.Mediator/Job.cs | 10 ++++----- src/Paramore.Brighter.Mediator/Runner.cs | 16 +++++++++----- src/Paramore.Brighter.Mediator/Scheduler.cs | 15 +++++++------ src/Paramore.Brighter.Mediator/Steps.cs | 22 +++++++++---------- .../When_running_a_blocking_wait_workflow.cs | 4 ++-- .../When_running_a_change_workflow.cs | 10 ++++----- ..._running_a_failing_choice_workflow_step.cs | 4 ++-- ...running_a_multistep_workflow_with_reply.cs | 2 +- ..._running_a_passing_choice_workflow_step.cs | 2 +- .../When_running_a_single_step_workflow.cs | 2 +- .../When_running_a_two_step_workflow.cs | 2 +- ...unning_a_workflow_with_a_parallel_split.cs | 2 +- .../When_running_a_workflow_with_reply.cs | 4 ++-- ...ng_a_workflow_with_robust_reply_nofault.cs | 2 +- 16 files changed, 52 insertions(+), 49 deletions(-) rename src/Paramore.Brighter.Mediator/{IAmAJobStoreAsync.cs => IAmAStateStoreAsync.cs} (97%) rename src/Paramore.Brighter.Mediator/{InMemoryJobStoreAsync.cs => InMemoryStateStoreAsync.cs} (97%) diff --git a/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs similarity index 97% rename from src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs rename to src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs index ac4107430d..55ffe4287f 100644 --- a/src/Paramore.Brighter.Mediator/IAmAJobStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs @@ -30,7 +30,7 @@ namespace Paramore.Brighter.Mediator; /// /// Used to store the state of a workflow /// -public interface IAmAJobStoreAsync +public interface IAmAStateStoreAsync { /// /// Saves the job diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs similarity index 97% rename from src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs rename to src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs index 2a45064263..259a611bfe 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryJobStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs @@ -31,7 +31,7 @@ namespace Paramore.Brighter.Mediator; /// /// Represents an in-memory store for jobs. /// -public class InMemoryJobStoreAsync : IAmAJobStoreAsync +public class InMemoryStateStoreAsync : IAmAStateStoreAsync { private readonly Dictionary _flows = new(); diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index 582e410c57..fa45187e3a 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -55,11 +55,8 @@ public class Job : Job /// The next step. Steps are a linked list. The final step in the list has null for it's next step. private Step? _step; - /// A map of user defined values. Normally, use Data to pass data between steps - public Dictionary Bag { get; } = new(); - /// The data that is passed between steps of the workflow - public TData Data { get; } + public TData Data { get; private set; } /// The id of the workflow, used to save-retrieve it from storage public string Id { get; private set; } = Guid.NewGuid().ToString(); @@ -134,10 +131,13 @@ public void NextStep(Step? nextStep) /// /// The type of event that we expect /// - public bool RemovePendingResponse(Type eventType) + public bool ResumeAfterEvent(Type eventType) { + if (_step is null) return false; var success = _pendingResponses.Remove(eventType); + _step.OnCompletion?.Invoke(); + _step = _step.Next; if (success) State = JobState.Running; return success; } diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index f1796dc60d..5a88250775 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -34,19 +34,19 @@ namespace Paramore.Brighter.Mediator; public class Runner { private readonly IAmAJobChannel _channel; - private readonly IAmAJobStoreAsync _jobStoreAsync; + private readonly IAmAStateStoreAsync _stateStoreAsync; private readonly IAmACommandProcessor _commandProcessor; /// /// Initializes a new instance of the class. /// /// The job channel to process jobs from. - /// The job store to save job states. + /// The job store to save job states. /// The command processor to handle commands. - public Runner(IAmAJobChannel channel, IAmAJobStoreAsync jobStoreAsync, IAmACommandProcessor commandProcessor) + public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStoreAsync, IAmACommandProcessor commandProcessor) { _channel = channel; - _jobStoreAsync = jobStoreAsync; + _stateStoreAsync = stateStoreAsync; _commandProcessor = commandProcessor; } @@ -74,12 +74,16 @@ private async Task Execute(Job? job, CancellationToken cancellationToken return; job.State = JobState.Running; - await _jobStoreAsync.SaveJobAsync(job, cancellationToken); + await _stateStoreAsync.SaveJobAsync(job, cancellationToken); while (job.CurrentStep() is not null) { await job.CurrentStep()!.ExecuteAsync(_commandProcessor, cancellationToken); - await _jobStoreAsync.SaveJobAsync(job, cancellationToken); + await _stateStoreAsync.SaveJobAsync(job, cancellationToken); + + //if the job has a pending step, finish execution of this job. + if (job.State == JobState.Waiting) + break; } } diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index eff9a254b4..a4851f1842 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -36,7 +36,7 @@ public class Scheduler { private readonly IAmACommandProcessor _commandProcessor; private readonly IAmAJobChannel _channel; - private readonly IAmAJobStoreAsync _jobStoreAsync; + private readonly IAmAStateStoreAsync _stateStoreAsync; /// @@ -45,12 +45,12 @@ public class Scheduler /// The command processor used to handle commands. /// The over which jobs flow. The is a producer /// and the is the consumer from the channel - /// A store for pending jobs - public Scheduler(IAmACommandProcessor commandProcessor, IAmAJobChannel channel, IAmAJobStoreAsync jobStoreAsync) + /// A store for pending jobs + public Scheduler(IAmACommandProcessor commandProcessor, IAmAJobChannel channel, IAmAStateStoreAsync stateStoreAsync) { _commandProcessor = commandProcessor; _channel = channel; - _jobStoreAsync = jobStoreAsync; + _stateStoreAsync = stateStoreAsync; } /// @@ -73,7 +73,7 @@ public async Task ReceiveWorkflowEvent(Event @event) if (@event.CorrelationId is null) throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); - var w = await _jobStoreAsync.GetJobAsync(@event.CorrelationId); + var w = await _stateStoreAsync.GetJobAsync(@event.CorrelationId); if (w is not Job job) throw new InvalidOperationException("Branch has not been stored"); @@ -90,7 +90,8 @@ public async Task ReceiveWorkflowEvent(Event @event) throw new InvalidOperationException($"Current step of workflow #{job.Id} should not be null"); taskResponse.Parser(@event, job); - job.CurrentStep()!.OnCompletion?.Invoke(); - job.RemovePendingResponse(eventType); + job.ResumeAfterEvent(eventType); + + await ScheduleAsync(job); } } diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index b83873dd4d..563f7632d1 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -25,24 +25,23 @@ public abstract class Step( public string Name { get; init; } = name; /// The next step in the sequence, null if this is the last step - protected Sequential? Next { get; } = next; + protected internal Step? Next { get; } = next; /// An optional callback to be run, following completion of the step. - public Action? OnCompletion { get; } = onCompletion; + protected internal Action? OnCompletion { get; } = onCompletion; /// The action to be taken with the step. protected IStepTask? StepTask = stepTask; - public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); - /// - /// Gets the job that is executing us + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The job that is executing us - public void GetJob(Job job) - { - Job = job; - } + /// The command processor, used to send requests to complete steps + /// The cancellation token, to end this workflow + /// + public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); /// /// Sets the job that is executing us @@ -52,8 +51,6 @@ public void AddToJob(Job job) { Job = job; } - - } /// @@ -81,6 +78,7 @@ public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, Cancell var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; Job.NextStep(step); + OnCompletion?.Invoke(); return Task.CompletedTask; } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index b4364f0811..fa2ea8f34b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -43,7 +43,7 @@ public MediatorBlockingWaitStepFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(firstStep); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( @@ -61,7 +61,7 @@ public async Task When_running_a_wait_workflow() await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(1) ); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); try { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index 62e6be7cfc..b039a99f28 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -36,10 +36,10 @@ public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) var firstStep = new Sequential( "Test of Job", - new ChangeAsync( (flow) => + new ChangeAsync( (data) => { var tcs = new TaskCompletionSource(); - flow.Bag["MyValue"] = "Altered"; + data.Bag["MyValue"] = "Altered"; tcs.SetResult(); return tcs.Task; }), @@ -49,7 +49,7 @@ public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) _job.InitSteps(firstStep); - var store = new InMemoryJobStoreAsync (); + var store = new InMemoryStateStoreAsync (); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( @@ -62,7 +62,7 @@ public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) } [Fact] - public async Task When_running_a_single_step_workflow() + public async Task When_running_a_change_workflow() { await _scheduler.ScheduleAsync(_job); @@ -81,6 +81,6 @@ public async Task When_running_a_single_step_workflow() _job.State.Should().Be(JobState.Done); _stepCompleted.Should().BeTrue(); - _job.Bag["MyValue"].Should().Be("Altered"); + _job.Data.Bag["MyValue"].Should().Be("Altered"); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 694b04c2d2..02aa88023c 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -60,14 +60,14 @@ public MediatorFailingChoiceFlowTests(ITestOutputHelper testOutputHelper) var stepOne = new ExclusiveChoice( "Test of Job SequenceStep One", - new Specification(x => x.Bag["MyValue"] as string == "Pass"), + new Specification(data => data.Bag["MyValue"] as string == "Pass"), () => { _stepCompletedOne = true; }, stepTwo, stepThree); _job.InitSteps(stepOne); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 591615e6a7..437e8ab2b8 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -60,7 +60,7 @@ public MediatorReplyMultiStepFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(stepOne); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index ec46f7cd62..e67370def1 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -67,7 +67,7 @@ public MediatorPassingChoiceFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(stepOne); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 269fb818e3..3095d102e4 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -45,7 +45,7 @@ public MediatorOneStepFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(firstStep); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index e6e5f9c7ce..0f984d17d7 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -53,7 +53,7 @@ public MediatorTwoStepFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(firstStep); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index c00595d04f..d8ab93864f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -61,7 +61,7 @@ public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) _job.InitSteps(parallelSplit); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 99720a3078..74d05b183f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -56,7 +56,7 @@ public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) () => { _stepCompleted = true; }, null); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( @@ -77,7 +77,7 @@ public async Task When_running_a_workflow_with_reply() await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(1) ); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); try { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index 1c9682a161..1fe9e80695 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -56,7 +56,7 @@ public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelpe () => { _stepFaulted = true; }, null); - InMemoryJobStoreAsync store = new(); + InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( From aa8ca95b4b306c21f12c13b76186f0fabba61a81 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 20 Nov 2024 09:18:12 +0000 Subject: [PATCH 33/44] fix: get the steps to save state, when they modify the job, not the runner. Timing for in-process handlers requires this. --- .../IAmAStateStoreAsync.cs | 2 +- src/Paramore.Brighter.Mediator/Runner.cs | 15 ++-- src/Paramore.Brighter.Mediator/Scheduler.cs | 2 +- src/Paramore.Brighter.Mediator/Steps.cs | 53 ++++++++++--- src/Paramore.Brighter.Mediator/Tasks.cs | 77 ++++++++++++++++--- .../TestDoubles/MyEventHandlerAsync.cs | 4 +- .../When_running_a_workflow_with_reply.cs | 4 +- 7 files changed, 124 insertions(+), 33 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs index 55ffe4287f..8219d24692 100644 --- a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs @@ -37,7 +37,7 @@ public interface IAmAStateStoreAsync /// /// The job /// - Task SaveJobAsync(Job? job, CancellationToken cancellationToken); + Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default); /// /// Retrieves a job via its Id diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 5a88250775..b3298dc4e6 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -34,19 +34,19 @@ namespace Paramore.Brighter.Mediator; public class Runner { private readonly IAmAJobChannel _channel; - private readonly IAmAStateStoreAsync _stateStoreAsync; + private readonly IAmAStateStoreAsync _stateStore; private readonly IAmACommandProcessor _commandProcessor; /// /// Initializes a new instance of the class. /// /// The job channel to process jobs from. - /// The job store to save job states. + /// The job store to save job states. /// The command processor to handle commands. - public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStoreAsync, IAmACommandProcessor commandProcessor) + public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAmACommandProcessor commandProcessor) { _channel = channel; - _stateStoreAsync = stateStoreAsync; + _stateStore = stateStore; _commandProcessor = commandProcessor; } @@ -74,17 +74,18 @@ private async Task Execute(Job? job, CancellationToken cancellationToken return; job.State = JobState.Running; - await _stateStoreAsync.SaveJobAsync(job, cancellationToken); + await _stateStore.SaveJobAsync(job, cancellationToken); while (job.CurrentStep() is not null) { - await job.CurrentStep()!.ExecuteAsync(_commandProcessor, cancellationToken); - await _stateStoreAsync.SaveJobAsync(job, cancellationToken); + await job.CurrentStep()!.ExecuteAsync(_commandProcessor, _stateStore, cancellationToken); //if the job has a pending step, finish execution of this job. if (job.State == JobState.Waiting) break; } + + if (job.State != JobState.Waiting) job.State = JobState.Done; } private async Task ProcessJobs(CancellationToken cancellationToken) diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index a4851f1842..1ed6363326 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -68,7 +68,7 @@ public async Task ScheduleAsync(Job job) /// /// The event to process. /// Thrown when the workflow has not been initialized. - public async Task ReceiveWorkflowEvent(Event @event) + public async Task ResumeAfterEvent(Event @event) { if (@event.CorrelationId is null) throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 563f7632d1..047b5d7726 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -39,9 +39,10 @@ public abstract class Step( /// The purpose of the step is to orchestrate the workflow, not to do the work. /// /// The command processor, used to send requests to complete steps + /// If the step updates the job, it needs to save its new state /// The cancellation token, to end this workflow /// - public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); + public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); /// /// Sets the job that is executing us @@ -71,7 +72,15 @@ public class ExclusiveChoice( ) : Step(name, null, null, onCompletion) { - public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// The command processor, used to send requests to complete steps + /// If the step updates the job, it needs to save its new state + /// The cancellation token, to end this workflow + public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) { if (Job is null) throw new InvalidOperationException("Job is null"); @@ -79,7 +88,7 @@ public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, Cancell var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; Job.NextStep(step); OnCompletion?.Invoke(); - return Task.CompletedTask; + await stateStore.SaveJobAsync(Job, cancellationToken); } } @@ -92,7 +101,15 @@ params Step[] branches { public Step[] Branches { get; set; } = branches; - public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// The command processor, used to send requests to complete steps + /// If the step updates the job, it needs to save its new state + /// The cancellation token, to end this workflow + public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) { if (Job is null) throw new InvalidOperationException("Job is null"); @@ -125,23 +142,32 @@ public class Sequential( ) : Step(name, next, stepTask, onCompletion) { - public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// The command processor, used to send requests to complete steps + /// If the step updates the job, it needs to save its new state + /// The cancellation token, to end this workflow + public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) { if (Job is null) throw new InvalidOperationException("Job is null"); try { - StepTask?.HandleAsync(Job, commandProcessor, cancellationToken); + StepTask?.HandleAsync(Job, commandProcessor, stateStore, cancellationToken); OnCompletion?.Invoke(); Job.NextStep(Next); + await stateStore.SaveJobAsync(Job, cancellationToken); } catch (Exception) { onFaulted?.Invoke(); Job.NextStep(faultNext); + await stateStore.SaveJobAsync(Job, cancellationToken); } - return Task.CompletedTask; } } @@ -161,14 +187,23 @@ public class Wait( ) : Step(name, next, null, onCompletion) { - public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// The command processor, used to send requests to complete steps + /// If the step updates the job, it needs to save its new state + /// The cancellation token, to end this workflow + public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) { if (Job is null) throw new InvalidOperationException("Job is null"); await Task.Delay(duration, cancellationToken); - Job.NextStep(Next); OnCompletion?.Invoke(); + Job.NextStep(Next); + await stateStore.SaveJobAsync(Job, cancellationToken); } } diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index 55ab245649..730ee81dbf 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -39,8 +39,9 @@ public interface IStepTask /// /// The current job of the workflow. /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler /// The cancellation token for this task - Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken); + Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); } /// @@ -85,8 +86,14 @@ Func onChange /// /// The current job of the workflow. /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler /// The cancellation token for this task - public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync( + Job? job, + IAmACommandProcessor commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken + ) { if (job is null) return; @@ -115,8 +122,14 @@ Func requestFactory /// /// The current job of the workflow. /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler /// The cancellation token for this task - public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync( + Job? job, + IAmACommandProcessor commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken + ) { if (job is null) return; @@ -146,20 +159,36 @@ public class RequestAndReactionAsync( /// /// Handles the request-and-reply action. /// + /// The logic here has to add the pending response, before the call to send the request. This is because the call to publish is not + /// over a bus, so it occurs sequentially within the Send before it exits. The event handler calls the 's + /// ResumeAfterEvent method to schedule handling the response. This will look up the pending response. So it needs to be stored prior + /// to this call completing /// The current job of the workflow. + /// The state store, required so that we can save the job state before sending the message /// The command processor used to handle commands. /// The cancellation token for this task - public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync( + Job? job, + IAmACommandProcessor commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken + ) { if (job is null) return; var command = requestFactory(); command.CorrelationId = job.Id; + + job.AddPendingResponse( + typeof(TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), + null + ) + ); + await stateStore.SaveJobAsync(job, cancellationToken); + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - - job.AddPendingResponse(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), null)); - } } @@ -187,18 +216,42 @@ public class RobustRequestAndReactionAsync( /// /// The current job of the workflow. /// The command processor used to handle commands. + /// The state store, required so that we can save the job state before sending the message /// The cancellation token for this task - public async Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, CancellationToken cancellationToken) + public async Task HandleAsync( + Job? job, + IAmACommandProcessor commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken + ) { if (job is null) return; - + var command = requestFactory(); + command.CorrelationId = job.Id; - await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - job.AddPendingResponse(typeof(TReply), new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), typeof(TFault))); - job.AddPendingResponse(typeof(TFault), new TaskResponse((reply, _) => faultFactory(reply as TFault), typeof(TReply), typeof(TFault)));} + job.AddPendingResponse( + typeof(TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply), + typeof(TReply), + typeof(TFault) + ) + ); + job.AddPendingResponse( + typeof(TFault), + new TaskResponse((reply, _) => faultFactory(reply as TFault), + typeof(TReply), + typeof(TFault) + ) + ); + await stateStore.SaveJobAsync(job, cancellationToken); + + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + + } + } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs index 1852848b3e..6da286c4f9 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs @@ -29,14 +29,14 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyEventHandlerAsync(Scheduler? mediator) : RequestHandlerAsync + internal class MyEventHandlerAsync(Scheduler? scheduler) : RequestHandlerAsync { public static List ReceivedEvents { get; } = []; public override async Task HandleAsync(MyEvent @event, CancellationToken cancellationToken = default) { LogEvent(@event); - mediator?.ReceiveWorkflowEvent(@event); + scheduler?.ResumeAfterEvent(@event); return await base.HandleAsync(@event, cancellationToken); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 74d05b183f..d2ce8f0a38 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -55,6 +55,8 @@ public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }), () => { _stepCompleted = true; }, null); + + _job.InitSteps(firstStep); InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -77,7 +79,7 @@ public async Task When_running_a_workflow_with_reply() await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(3) ); + ct.CancelAfter( TimeSpan.FromSeconds(180) ); try { From 1cea4eae31f4e286f1463b1ab612adf9ef08314d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 20 Nov 2024 18:44:22 +0000 Subject: [PATCH 34/44] fix: add fault version of robust request-reply --- .../TestDoubles/MyCommandHandlerAsync.cs | 9 +- .../TestDoubles/MyFaultHandlerAsync.cs | 48 ++++++++ .../When_running_a_workflow_with_reply.cs | 2 +- ...ng_a_workflow_with_robust_reply_nofault.cs | 10 +- ...a_workflow_with_robust_reply_with_fault.cs | 106 ++++++++++++++++++ 5 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs index 1604c5f25d..5fd95797f0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs @@ -28,7 +28,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles { - internal class MyCommandHandlerAsync(IAmACommandProcessor? commandProcessor) : RequestHandlerAsync + internal class MyCommandHandlerAsync(IAmACommandProcessor? commandProcessor, bool raiseFault = false) : RequestHandlerAsync { public static List ReceivedCommands { get; } = []; @@ -36,7 +36,12 @@ internal class MyCommandHandlerAsync(IAmACommandProcessor? commandProcessor) : R public override async Task HandleAsync(MyCommand command, CancellationToken cancellationToken = default) { LogCommand(command); - commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + if (!raiseFault) + await commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + else + await commandProcessor?.PublishAsync(new MyFault(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + + return await base.HandleAsync(command, cancellationToken); } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs new file mode 100644 index 0000000000..fb4161bab1 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs @@ -0,0 +1,48 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Mediator; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyFaultHandlerAsync(Scheduler? scheduler) : RequestHandlerAsync + { + public static List ReceivedFaults { get; } = []; + + public override async Task HandleAsync(MyFault @event, CancellationToken cancellationToken = default) + { + LogEvent(@event); + scheduler?.ResumeAfterEvent(@event); + return await base.HandleAsync(@event, cancellationToken); + } + + private void LogEvent(MyFault request) + { + ReceivedFaults.Add(request); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index d2ce8f0a38..fd47806184 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -79,7 +79,7 @@ public async Task When_running_a_workflow_with_reply() await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(180) ); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); try { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index 1fe9e80695..b5f9ceacbd 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -55,6 +55,8 @@ public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelpe null, () => { _stepFaulted = true; }, null); + + _job.InitSteps(firstStep); InMemoryStateStoreAsync store = new(); InMemoryJobChannel channel = new(); @@ -73,11 +75,10 @@ public async Task When_running_a_workflow_with_reply() { MyCommandHandlerAsync.ReceivedCommands.Clear(); MyEventHandlerAsync.ReceivedEvents.Clear(); + MyFaultHandlerAsync.ReceivedFaults.Clear(); await _scheduler.ScheduleAsync(_job); - await _runner.RunAsync(); - _stepCompleted.Should().BeTrue(); var ct = new CancellationTokenSource(); ct.CancelAfter( TimeSpan.FromSeconds(1) ); @@ -93,6 +94,11 @@ public async Task When_running_a_workflow_with_reply() MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + MyFaultHandlerAsync.ReceivedFaults.Should().BeEmpty(); + _job.Data.Bag["MyValue"].Should().Be("Test"); + _job.Data.Bag["MyReply"].Should().Be("Test"); _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + _stepFaulted.Should().BeFalse(); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs new file mode 100644 index 0000000000..9388cc3ea8 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime.Internal.Transform; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorRobustReplyFaultStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + private bool _stepFaulted; + + public MediatorRobustReplyFaultStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor, raiseFault: true), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), + _ when handlerType == typeof(MyFaultHandlerAsync) => new MyFaultHandlerAsync(_scheduler), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new RobustRequestAndReactionAsync( + () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, + (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }, + (fault) => { workflowData.Bag["MyFault"] = ((MyFault)fault).Value; }), + () => { _stepCompleted = true; }, + null, + () => { _stepFaulted = true; }, + null); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + commandProcessor, + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor); + } + + [Fact] + public async Task When_running_a_workflow_with_reply() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + MyFaultHandlerAsync.ReceivedFaults.Clear(); + + await _scheduler.ScheduleAsync(_job); + + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(180) ); + + try + { + await _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyFaultHandlerAsync.ReceivedFaults.Any(e => e.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Should().BeEmpty(); + _job.Data.Bag["MyValue"].Should().Be("Test"); + _job.Data.Bag["MyFault"].Should().Be("Test"); + _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + _stepFaulted.Should().BeFalse(); + } +} From 08b2e1815fca7a0b6068d12ffe44527efd660d15 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 20 Nov 2024 20:09:42 +0000 Subject: [PATCH 35/44] fix: add multi-threading support to Job --- src/Paramore.Brighter.Mediator/Job.cs | 40 +++++++++++++------ src/Paramore.Brighter.Mediator/Runner.cs | 4 +- .../When_running_a_blocking_wait_workflow.cs | 2 +- ...a_workflow_with_robust_reply_with_fault.cs | 2 +- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index fa45187e3a..b1e64c6e91 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Concurrent; using System.Collections.Generic; namespace Paramore.Brighter.Mediator; @@ -49,6 +50,9 @@ public abstract class Job { } /// The user defined data for the workflow public class Job : Job { + /// Used to manage access to state, as the job may be updated from multiple threads + private readonly object _lockObject = new(); + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives private readonly Dictionary?> _pendingResponses = new(); @@ -99,8 +103,11 @@ public void InitSteps(Step? firstStep) /// The task response to add. public void AddPendingResponse(Type responseType, TaskResponse? taskResponse) { - State = JobState.Waiting; - _pendingResponses.Add(responseType, taskResponse); + lock (_lockObject) + { + State = JobState.Waiting; + _pendingResponses.Add(responseType, taskResponse); + } } /// @@ -119,11 +126,15 @@ public bool FindPendingResponse(Type eventType, out TaskResponse? taskRes /// The next step to set. public void NextStep(Step? nextStep) { - _step = nextStep; - if (_step is not null) - _step.AddToJob(this); - else - if (State != JobState.Waiting) State = JobState.Done; + lock (_lockObject) + { + _step = nextStep; + if (_step is not null) + _step.AddToJob(this); + else + if (State != JobState.Waiting) + State = JobState.Done; + } } /// @@ -134,11 +145,14 @@ public void NextStep(Step? nextStep) public bool ResumeAfterEvent(Type eventType) { if (_step is null) return false; - - var success = _pendingResponses.Remove(eventType); - _step.OnCompletion?.Invoke(); - _step = _step.Next; - if (success) State = JobState.Running; - return success; + + lock (_lockObject) + { + var success = _pendingResponses.Remove(eventType); + _step.OnCompletion?.Invoke(); + _step = _step.Next; + if (success) State = JobState.Running; + return success; + } } } diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index b3298dc4e6..2f6c28c069 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion +using System; using System.Threading; using System.Threading.Tasks; @@ -85,7 +86,8 @@ private async Task Execute(Job? job, CancellationToken cancellationToken break; } - if (job.State != JobState.Waiting) job.State = JobState.Done; + if (job.State != JobState.Waiting) + job.State = JobState.Done; } private async Task ProcessJobs(CancellationToken cancellationToken) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index fa2ea8f34b..aec798e8ee 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -61,7 +61,7 @@ public async Task When_running_a_wait_workflow() await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(3) ); + //ct.CancelAfter( TimeSpan.FromSeconds(1) ); try { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs index 9388cc3ea8..0242c20d98 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -83,7 +83,7 @@ public async Task When_running_a_workflow_with_reply() var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(180) ); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); try { From 67f337663266222b932d4e001a1f75a17281d36c Mon Sep 17 00:00:00 2001 From: Paul Reardon Date: Thu, 21 Nov 2024 11:04:10 +0000 Subject: [PATCH 36/44] Update ASB Samples to use the Emulator (#3391) --- Brighter.sln | 6 + Directory.Packages.props | 40 +++--- samples/TaskQueue/ASBTaskQueue/.env | 13 ++ samples/TaskQueue/ASBTaskQueue/Config.Json | 135 ++++++++++++++++++ .../TaskQueue/ASBTaskQueue/Docker-Compose.yml | 34 +++++ .../Ports/Mappers/AddGreetingMessageMapper.cs | 12 +- .../GreetingEventAsyncMessageMapper.cs | 12 +- .../Mappers/GreetingEventMessageMapper.cs | 12 +- .../GreetingsReceiverConsole/Program.cs | 6 +- .../GreetingsScopedReceiverConsole/Program.cs | 6 +- .../GreetingsSender.Web/Program.cs | 10 +- .../ASBTaskQueue/GreetingsSender/Program.cs | 12 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 9 +- samples/TaskQueue/ASBTaskQueue/Readme.md | 5 + ...ore.Brighter.MessagingGateway.Redis.csproj | 2 + 15 files changed, 272 insertions(+), 42 deletions(-) create mode 100644 samples/TaskQueue/ASBTaskQueue/.env create mode 100644 samples/TaskQueue/ASBTaskQueue/Config.Json create mode 100644 samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml create mode 100644 samples/TaskQueue/ASBTaskQueue/Readme.md diff --git a/Brighter.sln b/Brighter.sln index 4b195ad865..c77b4465a6 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -104,6 +104,12 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.AzureServiceBus.Tests", "tests\Paramore.Brighter.AzureServiceBus.Tests\Paramore.Brighter.AzureServiceBus.Tests.csproj", "{48F584DF-0BA1-4485-A612-14FD4F6A4CF7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ASBTaskQueue", "ASBTaskQueue", "{48D0EECA-B928-4B80-BE46-2C08CF3A946B}" + ProjectSection(SolutionItems) = preProject + samples\TaskQueue\ASBTaskQueue\Config.Json = samples\TaskQueue\ASBTaskQueue\Config.Json + samples\TaskQueue\ASBTaskQueue\.env = samples\TaskQueue\ASBTaskQueue\.env + samples\TaskQueue\ASBTaskQueue\Docker-Compose.yml = samples\TaskQueue\ASBTaskQueue\Docker-Compose.yml + samples\TaskQueue\ASBTaskQueue\Readme.md = samples\TaskQueue\ASBTaskQueue\Readme.md + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.MsSql", "src\Paramore.Brighter.MsSql\Paramore.Brighter.MsSql.csproj", "{36CADB1E-3777-4A7E-86E3-BF650A951AC9}" EndProject diff --git a/Directory.Packages.props b/Directory.Packages.props index 44946c0089..5b2b747d23 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,9 +13,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,8 +35,8 @@ - - + + @@ -55,7 +55,7 @@ - + all @@ -89,6 +89,8 @@ + + @@ -107,21 +109,21 @@ - - - - - - - + + + + + + + - - - - - - + + + + + + diff --git a/samples/TaskQueue/ASBTaskQueue/.env b/samples/TaskQueue/ASBTaskQueue/.env new file mode 100644 index 0000000000..c12b64a688 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/.env @@ -0,0 +1,13 @@ +# Environment file for user defined variables in docker-compose.yml + +# 1. CONFIG_PATH: Path to Config.json file +# Ex: CONFIG_PATH="C:\\Config\\Config.json" +CONFIG_PATH="./Config.json" + +# 2. ACCEPT_EULA: Pass 'Y' to accept license terms for Azure SQL Edge and Azure Service Bus emulator. +# Service Bus emulator EULA : https://github.com/Azure/azure-service-bus-emulator-installer/blob/main/EMULATOR_EULA.txt +# SQL Edge EULA : https://go.microsoft.com/fwlink/?linkid=2139274 +ACCEPT_EULA="Y" + +# 3. MSSQL_SA_PASSWORD to be filled by user as per policy : https://learn.microsoft.com/en-us/sql/relational-databases/security/strong-passwords?view=sql-server-linux-ver16 +SQL_PASSWORD: "Password1!" \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Config.Json b/samples/TaskQueue/ASBTaskQueue/Config.Json new file mode 100644 index 0000000000..39cd9b0b88 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Config.Json @@ -0,0 +1,135 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "local", + "Queues": [], + "Topics": [ + { + "Name": "greeting.event", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + }, + { + "Name": "greeting.Asyncevent", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + }, + { + "Name": "greeting.addGreetingCommand", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml b/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml new file mode 100644 index 0000000000..42b2a12dda --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml @@ -0,0 +1,34 @@ +name: microsoft-azure-servicebus-emulator +services: + emulator: + container_name: "servicebus-emulator" + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + volumes: + - "${CONFIG_PATH}:/ServiceBus_Emulator/ConfigFiles/Config.json" + ports: + - "5672:5672" + environment: + SQL_SERVER: sqledge + MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # Password should be same as what is set for SQL Edge + ACCEPT_EULA: ${ACCEPT_EULA} + depends_on: + - sqledge + networks: + sb-emulator: + aliases: + - "sb-emulator" + sqledge: + container_name: "sqledge" + image: "mcr.microsoft.com/azure-sql-edge:latest" + ports: + - "11433:1433" + networks: + sb-emulator: + aliases: + - "sqledge" + environment: + ACCEPT_EULA: ${ACCEPT_EULA} + MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # To be filled by user as per policy : https://learn.microsoft.com/en-us/sql/relational-databases/security/strong-passwords?view=sql-server-linux-ver16 + +networks: + sb-emulator: \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs index 72a2d9e816..4d1d5ca9f2 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs @@ -1,13 +1,23 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Commands; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class AddGreetingMessageMapper : IAmAMessageMapper + public class AddGreetingMessageMapper : IAmAMessageMapper, IAmAMessageMapperAsync { public IRequestContext Context { get; set; } + public Task MapToMessageAsync(AddGreetingCommand request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); + public Message MapToMessage(AddGreetingCommand request, Publication publication) { var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_COMMAND); diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs index 4445aa5942..ed51fedfff 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs @@ -1,13 +1,23 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Events; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class GreetingEventAsyncMessageMapper : IAmAMessageMapper + public class GreetingEventAsyncMessageMapper : IAmAMessageMapperAsync, IAmAMessageMapper { public IRequestContext Context { get; set; } + public Task MapToMessageAsync(GreetingAsyncEvent request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); + public Message MapToMessage(GreetingAsyncEvent request, Publication publication) { var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs index 2ea5105881..7f2cd5ab1a 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs @@ -1,12 +1,22 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Events; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class GreetingEventMessageMapper : IAmAMessageMapper + public class GreetingEventMessageMapper : IAmAMessageMapper, IAmAMessageMapperAsync { public IRequestContext Context { get; set; } + + public Task MapToMessageAsync(GreetingEvent request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); public Message MapToMessage(GreetingEvent request, Publication publication) { diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs index ee2d846c4a..1c32724656 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs @@ -34,7 +34,7 @@ public async static Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: true), @@ -43,13 +43,13 @@ public async static Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: false) }; //TODO: add your ASB qualified name here - var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); + var clientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(clientProvider); services.AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs index 432a3ff6d2..37a4b21a6d 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs @@ -36,7 +36,7 @@ public static async Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: true), @@ -45,13 +45,13 @@ public static async Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: false) }; //TODO: add your ASB qualified name here - var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); + var asbClientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(asbClientProvider); services .AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs index d325ab613f..6143563f78 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -33,9 +33,9 @@ builder.Services.AddScoped(); //Brighter -string asbEndpoint = ".servicebus.windows.net"; +string asbEndpoint = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; -var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); +var asbConnection = new ServiceBusConnectionStringClientProvider(asbEndpoint); var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); @@ -43,9 +43,9 @@ asbConnection, new AzureServiceBusPublication[] { - new() { Topic = new RoutingKey("greeting.event") }, - new() { Topic = new RoutingKey("greeting.addGreetingCommand") }, - new() { Topic = new RoutingKey("greeting.Asyncevent") } + new() { Topic = new RoutingKey("greeting.event"), MakeChannels = OnMissingChannel.Assume}, + new() { Topic = new RoutingKey("greeting.addGreetingCommand"), MakeChannels = OnMissingChannel.Assume }, + new() { Topic = new RoutingKey("greeting.Asyncevent"), MakeChannels = OnMissingChannel.Assume } } ) .Create(); diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs index 3a2695d38c..f7b6ef5f44 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs @@ -21,7 +21,7 @@ static void Main(string[] args) serviceCollection.AddLogging(); //TODO: add your ASB qualified name here - var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider("fim-development-bus.servicebus.windows.net"); + var asbClientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var producerRegistry = new AzureServiceBusProducerRegistryFactory( asbClientProvider, @@ -30,17 +30,21 @@ static void Main(string[] args) new AzureServiceBusPublication { Topic = new RoutingKey("greeting.event"), - RequestType = typeof(GreetingEvent) + RequestType = typeof(GreetingEvent), + MakeChannels = OnMissingChannel.Assume }, new AzureServiceBusPublication { Topic = new RoutingKey("greeting.addGreetingCommand"), - RequestType = typeof(AddGreetingCommand) + RequestType = typeof(AddGreetingCommand), + MakeChannels = OnMissingChannel.Assume + }, new AzureServiceBusPublication { Topic = new RoutingKey("greeting.Asyncevent"), - RequestType = typeof(GreetingAsyncEvent) + RequestType = typeof(GreetingAsyncEvent), + MakeChannels = OnMissingChannel.Assume } } ).Create(); diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs index 0a9171624d..4404524aa2 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs @@ -34,7 +34,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: true, noOfPerformers: 2, unacceptableMessageLimit: 1), @@ -43,7 +43,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: false, noOfPerformers: 2), @@ -52,7 +52,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.addGreetingCommand"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, isAsync: true, noOfPerformers: 2) @@ -66,8 +66,7 @@ o.UseSqlServer(dbConnString); }); -//TODO: add your ASB qualified name here -var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); +var clientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(clientProvider); builder.Services.AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/Readme.md b/samples/TaskQueue/ASBTaskQueue/Readme.md new file mode 100644 index 0000000000..9ea97249c1 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Readme.md @@ -0,0 +1,5 @@ +#Docker Command + +```bash +podman compose -f .\Docker-Compose.yml up -d +``` \ No newline at end of file diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj b/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj index 7106fd75ed..435017738c 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj +++ b/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj @@ -12,6 +12,8 @@ + + From e118650d5ad46651c76ad944a7a625a30dcfbb50 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 23 Nov 2024 19:05:40 +0000 Subject: [PATCH 37/44] feat: adding a Wait step. --- .../IAmAStateStoreAsync.cs | 10 ++ .../InMemoryStateStoreAsync.cs | 71 +++++++-- src/Paramore.Brighter.Mediator/Job.cs | 34 ++-- src/Paramore.Brighter.Mediator/Runner.cs | 26 +++- src/Paramore.Brighter.Mediator/Scheduler.cs | 78 +++++++--- src/Paramore.Brighter.Mediator/Steps.cs | 145 +++++++++++++++--- .../TaskException.cs | 53 +++++++ src/Paramore.Brighter.Mediator/Tasks.cs | 21 ++- src/Paramore.Brighter.Mediator/Waker.cs | 82 ++++++++++ .../When_running_a_blocking_wait_workflow.cs | 42 +++-- .../When_running_a_change_workflow.cs | 3 +- ..._running_a_failing_choice_workflow_step.cs | 3 +- ...running_a_multistep_workflow_with_reply.cs | 3 +- ..._running_a_passing_choice_workflow_step.cs | 3 +- .../When_running_a_single_step_workflow.cs | 3 +- .../When_running_a_two_step_workflow.cs | 3 +- ...unning_a_workflow_with_a_parallel_split.cs | 3 +- .../When_running_a_workflow_with_reply.cs | 3 +- ...ng_a_workflow_with_robust_reply_nofault.cs | 3 +- ...a_workflow_with_robust_reply_with_fault.cs | 3 +- 20 files changed, 478 insertions(+), 114 deletions(-) create mode 100644 src/Paramore.Brighter.Mediator/TaskException.cs create mode 100644 src/Paramore.Brighter.Mediator/Waker.cs diff --git a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs index 8219d24692..a6b4c55c1b 100644 --- a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs @@ -22,6 +22,8 @@ THE SOFTWARE. */ #endregion +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -45,4 +47,12 @@ public interface IAmAStateStoreAsync /// The id of the job /// if found, the job, otherwise null Task GetJobAsync(string? id) ; + + /// + /// + /// + /// The time before now at which becomes scheduled + /// + /// + Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken); } diff --git a/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs index 259a611bfe..3f5643da7b 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs @@ -22,9 +22,14 @@ THE SOFTWARE. */ #endregion +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Mediator; @@ -33,23 +38,38 @@ namespace Paramore.Brighter.Mediator; /// public class InMemoryStateStoreAsync : IAmAStateStoreAsync { - private readonly Dictionary _flows = new(); + private readonly ConcurrentDictionary _jobs = new(); + private readonly TimeProvider _timeProvider; + private DateTimeOffset _sinceTime; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); /// - /// Saves the job asynchronously. + /// Represents an in-memory store for jobs. /// - /// The type of the job data. - /// The job to save. - /// A token to monitor for cancellation requests. - /// A task that represents the asynchronous save operation. - public Task SaveJobAsync(Job? job, CancellationToken cancellationToken) + public InMemoryStateStoreAsync(TimeProvider? timeProvider = null) { - if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); - - if (job is null) return Task.CompletedTask; + _timeProvider = timeProvider ?? TimeProvider.System; + } + + /// + /// + /// + /// A job is due now, less the jobAge span + /// A cancellation token to end the ongoing operation + /// + public Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken) + { + var dueJobs = _jobs.Values + .Where(job => + { + if (job is null || !job.IsScheduled) return false; + _sinceTime = _timeProvider.GetUtcNow().Subtract(jobAge); + return job.DueTime > _sinceTime; + }) + .ToList(); - _flows[job.Id] = job; - return Task.CompletedTask; + return Task.FromResult((IEnumerable)dueJobs); } /// @@ -66,8 +86,33 @@ public Task SaveJobAsync(Job? job, CancellationToken cancellationT return tcs.Task; } - var job = _flows.TryGetValue(id, out var state) ? state : null; + var job = _jobs.TryGetValue(id, out var state) ? state : null; tcs.SetResult(job); return tcs.Task; } + + /// + /// Saves the job asynchronously. + /// + /// The type of the job data. + /// The job to save. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + public Task SaveJobAsync(Job? job, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); + + if (job is null) return Task.CompletedTask; + + try + { + _jobs[job.Id] = job; + return Task.FromResult(true); + } + catch (Exception e) + { + s_logger.LogError($"Error saving job {job.Id} to in-memory store: {e.Message}"); + return Task.FromException(e); + } + } } diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index b1e64c6e91..b42866b278 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -37,12 +37,29 @@ public enum JobState Running, Waiting, Done, + Faulted } /// /// empty class, used as marker for the branch data /// -public abstract class Job { } +public abstract class Job +{ + /// Used to manage access to state, as the job may be updated from multiple threads + protected readonly object LockObject = new(); + + /// The time the job is due to run + public DateTimeOffset? DueTime { get; set; } + + /// The id of the workflow, used to save-retrieve it from storage + public string Id { get; private set; } = Guid.NewGuid().ToString(); + + /// Is the job scheduled to run? + public bool IsScheduled => DueTime.HasValue; + + /// Is the job waiting to be run, running, waiting for a response or finished + public JobState State { get; set; } +} /// /// Job represents the current state of the workflow and tracks if it’s awaiting a response. @@ -50,9 +67,7 @@ public abstract class Job { } /// The user defined data for the workflow public class Job : Job { - /// Used to manage access to state, as the job may be updated from multiple threads - private readonly object _lockObject = new(); - + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives private readonly Dictionary?> _pendingResponses = new(); @@ -61,12 +76,7 @@ public class Job : Job /// The data that is passed between steps of the workflow public TData Data { get; private set; } - - /// The id of the workflow, used to save-retrieve it from storage - public string Id { get; private set; } = Guid.NewGuid().ToString(); - /// Is the job waiting to be run, running, waiting for a response or finished - public JobState State { get; set; } /// /// Constructs a new Job @@ -103,7 +113,7 @@ public void InitSteps(Step? firstStep) /// The task response to add. public void AddPendingResponse(Type responseType, TaskResponse? taskResponse) { - lock (_lockObject) + lock (LockObject) { State = JobState.Waiting; _pendingResponses.Add(responseType, taskResponse); @@ -126,7 +136,7 @@ public bool FindPendingResponse(Type eventType, out TaskResponse? taskRes /// The next step to set. public void NextStep(Step? nextStep) { - lock (_lockObject) + lock (LockObject) { _step = nextStep; if (_step is not null) @@ -146,7 +156,7 @@ public bool ResumeAfterEvent(Type eventType) { if (_step is null) return false; - lock (_lockObject) + lock (LockObject) { var success = _pendingResponses.Remove(eventType); _step.OnCompletion?.Invoke(); diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 2f6c28c069..10fa679507 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -22,9 +22,10 @@ THE SOFTWARE. */ #endregion -using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Mediator; @@ -37,6 +38,9 @@ public class Runner private readonly IAmAJobChannel _channel; private readonly IAmAStateStoreAsync _stateStore; private readonly IAmACommandProcessor _commandProcessor; + private readonly Scheduler _scheduler; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); /// /// Initializes a new instance of the class. @@ -44,11 +48,13 @@ public class Runner /// The job channel to process jobs from. /// The job store to save job states. /// The command processor to handle commands. - public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAmACommandProcessor commandProcessor) + /// The scheduler which allows us to queue work that should be deferred + public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAmACommandProcessor commandProcessor, Scheduler scheduler) { _channel = channel; _stateStore = stateStore; _commandProcessor = commandProcessor; + _scheduler = scheduler; } /// @@ -77,17 +83,24 @@ private async Task Execute(Job? job, CancellationToken cancellationToken job.State = JobState.Running; await _stateStore.SaveJobAsync(job, cancellationToken); - while (job.CurrentStep() is not null) + var step = job.CurrentStep(); + while (step is not null) { - await job.CurrentStep()!.ExecuteAsync(_commandProcessor, _stateStore, cancellationToken); - + if (step.State == StepState.Queued) + { + await step.ExecuteAsync(_stateStore, _commandProcessor, _scheduler, cancellationToken); + } + //if the job has a pending step, finish execution of this job. if (job.State == JobState.Waiting) break; + + step = job.CurrentStep(); } if (job.State != JobState.Waiting) job.State = JobState.Done; + } private async Task ProcessJobs(CancellationToken cancellationToken) @@ -98,6 +111,9 @@ private async Task ProcessJobs(CancellationToken cancellationToken) break; var job = await _channel.DequeueJobAsync(cancellationToken); + if (job is null) + continue; + await Execute(job, cancellationToken); } } diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index 1ed6363326..91d3a11ff4 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter.Mediator; @@ -32,35 +33,25 @@ namespace Paramore.Brighter.Mediator; /// It uses a command processor and a workflow store to manage the workflow's state and actions. /// /// The type of the workflow data. -public class Scheduler +public class Scheduler { - private readonly IAmACommandProcessor _commandProcessor; private readonly IAmAJobChannel _channel; - private readonly IAmAStateStoreAsync _stateStoreAsync; + private readonly IAmAStateStoreAsync _stateStore; + private readonly TimeProvider _timeProvider; /// /// Initializes a new instance of the class. /// - /// The command processor used to handle commands. /// The over which jobs flow. The is a producer - /// and the is the consumer from the channel - /// A store for pending jobs - public Scheduler(IAmACommandProcessor commandProcessor, IAmAJobChannel channel, IAmAStateStoreAsync stateStoreAsync) + /// and the is the consumer from the channel + /// A store for pending jobs + /// Provides the time for scheduling, defaults to TimeProvider.System + public Scheduler(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, TimeProvider? timeProvider = null) { - _commandProcessor = commandProcessor; + _timeProvider = timeProvider ?? TimeProvider.System; _channel = channel; - _stateStoreAsync = stateStoreAsync; - } - - /// - /// Runs the job by executing each step in the sequence. - /// - /// - /// Thrown when the job has not been initialized. - public async Task ScheduleAsync(Job job) - { - await _channel.EnqueueJobAsync(job); + _stateStore = stateStore; } /// @@ -73,7 +64,7 @@ public async Task ResumeAfterEvent(Event @event) if (@event.CorrelationId is null) throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); - var w = await _stateStoreAsync.GetJobAsync(@event.CorrelationId); + var w = await _stateStore.GetJobAsync(@event.CorrelationId); if (w is not Job job) throw new InvalidOperationException("Branch has not been stored"); @@ -94,4 +85,51 @@ public async Task ResumeAfterEvent(Event @event) await ScheduleAsync(job); } + + /// + /// Runs the job by executing each step in the sequence. + /// + /// The job that we want a runner to execute + /// A cancellation token to end the ongoing operation + /// Thrown when the job has not been initialized. + public async Task ScheduleAsync(Job job, CancellationToken cancellationToken = default) + { + await _channel.EnqueueJobAsync(job, cancellationToken); + job.DueTime = null; // Clear any due time after queuing + await _stateStore.SaveJobAsync(job, cancellationToken); + } + + /// + /// + /// + /// The job that we want a runner to execute + /// The delay after which to schedule the job + /// A cancellation token to end the ongoing operation + /// Thrown when the job has not been initialized. + public async Task ScheduleAtAsync(Job job, TimeSpan delay, CancellationToken cancellationToken = default) + { + job.DueTime = _timeProvider.GetUtcNow().Add(delay); + await _stateStore.SaveJobAsync(job, cancellationToken); + } + + /// + /// Finds any jobs that are due to run and schedules them + /// + /// A job is due now, less the jobAge span + /// A cancellation token to end the ongoing operation + public async Task TriggerDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken) + { + var dueJobs = await _stateStore.GetDueJobsAsync(jobAge, cancellationToken); + + foreach (var j in dueJobs) + { + var job = j as Job; + + if (job is null) + continue; + + await ScheduleAsync(job, cancellationToken); + } + } + } diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 047b5d7726..4d76748ba5 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -1,9 +1,43 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Mediator; +public enum StepState +{ + Queued, + Running, + Done, + Faulted +} + /// /// The base type for a step in the workflow. /// @@ -20,6 +54,9 @@ public abstract class Step( { /// Which job is being executed by the step. protected Job? Job ; + + /// The logger for the step. + protected static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); /// The name of the step, used for tracing execution public string Name { get; init; } = name; @@ -31,18 +68,26 @@ public abstract class Step( protected internal Action? OnCompletion { get; } = onCompletion; /// The action to be taken with the step. - protected IStepTask? StepTask = stepTask; + protected readonly IStepTask? StepTask = stepTask; + + public StepState? State { get; set; } /// /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The command processor, used to send requests to complete steps /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow /// - public abstract Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); + public abstract Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default + ); /// /// Sets the job that is executing us @@ -51,6 +96,7 @@ public abstract class Step( public void AddToJob(Job job) { Job = job; + State = StepState.Queued; } } @@ -77,18 +123,29 @@ public class ExclusiveChoice( /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The command processor, used to send requests to complete steps /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow - public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default + ) { if (Job is null) throw new InvalidOperationException("Job is null"); + State = StepState.Running; + var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; Job.NextStep(step); OnCompletion?.Invoke(); + State = StepState.Done; await stateStore.SaveJobAsync(Job, cancellationToken); + } } @@ -106,17 +163,29 @@ params Step[] branches /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The command processor, used to send requests to complete steps /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow - public override Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) + /// + public override Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default + ) { if (Job is null) throw new InvalidOperationException("Job is null"); + State = StepState.Running; + // Parallel split doesn't directly execute its jobs. // Execution is handled by the Scheduler, which will handle running each branch concurrently. onBranch?.Invoke(Job.Data); + + State = StepState.Done; + return Task.CompletedTask; } } @@ -147,25 +216,45 @@ public class Sequential( /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The command processor, used to send requests to complete steps /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow - public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default + ) { if (Job is null) throw new InvalidOperationException("Job is null"); + if (StepTask is null) + { + s_logger.LogWarning("No task to execute for {Name}", Name); + State = StepState.Done; + await stateStore.SaveJobAsync(Job, cancellationToken); + return; + } + + State = StepState.Running; + try { - StepTask?.HandleAsync(Job, commandProcessor, stateStore, cancellationToken); + await StepTask.HandleAsync(Job, commandProcessor, stateStore, cancellationToken); OnCompletion?.Invoke(); Job.NextStep(Next); + State = StepState.Done; await stateStore.SaveJobAsync(Job, cancellationToken); } catch (Exception) { + Job.State = JobState.Faulted; onFaulted?.Invoke(); Job.NextStep(faultNext); + State = StepState.Faulted; await stateStore.SaveJobAsync(Job, cancellationToken); } } @@ -180,30 +269,40 @@ public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, I /// The next step in the sequence, null if this is the last step. /// The data that the step operates over public class Wait( - string name, - TimeSpan duration, - Action? onCompletion, - Sequential? next - ) - : Step(name, next, null, onCompletion) + string name, + TimeSpan duration, + Sequential? next) + : Step(name, next) { /// /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. /// The purpose of the step is to orchestrate the workflow, not to do the work. /// - /// The command processor, used to send requests to complete steps /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow - public override async Task ExecuteAsync(IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken) + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default + ) { if (Job is null) throw new InvalidOperationException("Job is null"); - await Task.Delay(duration, cancellationToken); - OnCompletion?.Invoke(); - Job.NextStep(Next); - await stateStore.SaveJobAsync(Job, cancellationToken); + if (scheduler is null) + throw new InvalidOperationException("Scheduler is null; a Wait Step must have a scheduler to schedule the next step"); + + State = StepState.Running; + + Job.DueTime = DateTime.UtcNow.Add(duration); + Job.State = JobState.Waiting; + State = StepState.Done; + await scheduler.ScheduleAtAsync(Job, duration, cancellationToken); } } diff --git a/src/Paramore.Brighter.Mediator/TaskException.cs b/src/Paramore.Brighter.Mediator/TaskException.cs new file mode 100644 index 0000000000..1bc37a2708 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/TaskException.cs @@ -0,0 +1,53 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Runtime.Serialization; + +namespace Paramore.Brighter.Mediator; + +/// +/// Represents errors that occur during task execution. +/// +[Serializable] +public class TaskException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public TaskException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public TaskException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public TaskException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index 730ee81dbf..fb2b4f4a19 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -41,7 +41,7 @@ public interface IStepTask /// The command processor used to handle commands. /// Used to store the state of a job, if it is altered in the handler /// The cancellation token for this task - Task HandleAsync(Job? job, IAmACommandProcessor commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); + Task HandleAsync(Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); } /// @@ -90,7 +90,7 @@ Func onChange /// The cancellation token for this task public async Task HandleAsync( Job? job, - IAmACommandProcessor commandProcessor, + IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken ) @@ -126,7 +126,7 @@ Func requestFactory /// The cancellation token for this task public async Task HandleAsync( Job? job, - IAmACommandProcessor commandProcessor, + IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken ) @@ -134,6 +134,9 @@ CancellationToken cancellationToken if (job is null) return; + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); + var command = requestFactory(); command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); @@ -169,7 +172,7 @@ public class RequestAndReactionAsync( /// The cancellation token for this task public async Task HandleAsync( Job? job, - IAmACommandProcessor commandProcessor, + IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken ) @@ -177,6 +180,9 @@ CancellationToken cancellationToken if (job is null) return; + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); + var command = requestFactory(); command.CorrelationId = job.Id; @@ -220,13 +226,16 @@ public class RobustRequestAndReactionAsync( /// The cancellation token for this task public async Task HandleAsync( Job? job, - IAmACommandProcessor commandProcessor, + IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken ) { if (job is null) return; + + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); var command = requestFactory(); @@ -249,9 +258,7 @@ CancellationToken cancellationToken await stateStore.SaveJobAsync(job, cancellationToken); await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); - } - } diff --git a/src/Paramore.Brighter.Mediator/Waker.cs b/src/Paramore.Brighter.Mediator/Waker.cs new file mode 100644 index 0000000000..10db24da81 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Waker.cs @@ -0,0 +1,82 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// The class is responsible for periodically waking up and triggering due jobs in the scheduler. +/// +/// The type of the job data. +public class Waker +{ + private readonly TimeSpan _jobAge; + private readonly Scheduler _scheduler; + + /// + /// Initializes a new instance of the class. + /// + /// The age of the job to determine if it is due. + /// The scheduler to trigger due jobs. + public Waker(TimeSpan jobAge, Scheduler scheduler) + { + _jobAge = jobAge; + _scheduler = scheduler; + } + + /// + /// Runs the > asynchronously. + /// This will periodically wake up and trigger due jobs in the scheduler. + /// + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous run operation. + public async Task RunAsync(CancellationToken cancellationToken = default) + { + await Task.Factory.StartNew(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + await Wake(cancellationToken); + + if (cancellationToken.IsCancellationRequested) + cancellationToken.ThrowIfCancellationRequested(); + + }, cancellationToken); + } + + private async Task Wake(CancellationToken cancellationToken) + { + while (true) + { + if (cancellationToken.IsCancellationRequested) + break; + + await _scheduler.TriggerDueJobsAsync(_jobAge, cancellationToken); + await Task.Delay(_jobAge, cancellationToken); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index aec798e8ee..776dc5566e 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; @@ -10,15 +11,17 @@ namespace Paramore.Brighter.Core.Tests.Workflows; -public class MediatorBlockingWaitStepFlowTests +public class MediatorWaitStepFlowTests { private readonly Scheduler _scheduler; private readonly Runner _runner; private readonly Job _job; private bool _stepCompleted; private readonly ITestOutputHelper _testOutputHelper; + private readonly FakeTimeProvider _timeProvider = new(); + private readonly Waker _waker; - public MediatorBlockingWaitStepFlowTests(ITestOutputHelper testOutputHelper) + public MediatorWaitStepFlowTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; var registry = new SubscriberRegistry(); @@ -33,39 +36,50 @@ public MediatorBlockingWaitStepFlowTests(ITestOutputHelper testOutputHelper) var workflowData= new WorkflowTestData(); workflowData.Bag["MyValue"] = "Test"; - _job = new Job(workflowData) ; + _job = new Job(workflowData); - var firstStep = new Wait("Test of Job", - TimeSpan.FromMilliseconds(500), + var secondStep = new Sequential( + "Test of Job", + new ChangeAsync( (_) => Task.CompletedTask), () => { _stepCompleted = true; }, null - ); + ); - _job.InitSteps(firstStep); + var firstStep = new Wait("Test of Job", + TimeSpan.FromMilliseconds(500), + secondStep + ); - InMemoryStateStoreAsync store = new(); + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(_timeProvider); InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); + _waker = new Waker(TimeSpan.FromMilliseconds(100), _scheduler); } [Fact] public async Task When_running_a_wait_workflow() { - await _scheduler.ScheduleAsync(_job); - var ct = new CancellationTokenSource(); - //ct.CancelAfter( TimeSpan.FromSeconds(1) ); + ct.CancelAfter( TimeSpan.FromSeconds(3)); try { - await _runner.RunAsync(ct.Token); + await _scheduler.ScheduleAsync(_job); + + _timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + + var jobExecutionTask = _runner.RunAsync(ct.Token); + var jobWakerTask = _waker.RunAsync(ct.Token); + + await Task.WhenAny( jobExecutionTask, jobWakerTask); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index b039a99f28..c7534ab511 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -53,12 +53,11 @@ public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 02aa88023c..478f0052f6 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -71,12 +71,11 @@ public MediatorFailingChoiceFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 437e8ab2b8..7b08bf7b49 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -64,12 +64,11 @@ public MediatorReplyMultiStepFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index e67370def1..107952232c 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -71,12 +71,11 @@ public MediatorPassingChoiceFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 3095d102e4..8dd8efb745 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -49,12 +49,11 @@ public MediatorOneStepFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 0f984d17d7..3b79014dc3 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -57,12 +57,11 @@ public MediatorTwoStepFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index d8ab93864f..e83048cdd8 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -65,12 +65,11 @@ public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } //[Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index fd47806184..5361d81db2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -62,12 +62,11 @@ public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index b5f9ceacbd..fb7115b0ac 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -62,12 +62,11 @@ public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelpe InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs index 0242c20d98..c52067ec4d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -64,12 +64,11 @@ public MediatorRobustReplyFaultStepFlowTests(ITestOutputHelper testOutputHelper) InMemoryJobChannel channel = new(); _scheduler = new Scheduler( - commandProcessor, channel, store ); - _runner = new Runner(channel, store, commandProcessor); + _runner = new Runner(channel, store, commandProcessor, _scheduler); } [Fact] From ca229f09d6a691e1acc8a3d009ac58a54653b865 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sat, 23 Nov 2024 21:04:29 +0000 Subject: [PATCH 38/44] fix: don't try to await a thread; ensure we leave time for scheduler to fire on a delay --- src/Paramore.Brighter.Mediator/Runner.cs | 29 ++++++++- src/Paramore.Brighter.Mediator/Steps.cs | 61 ++++++++++++++----- src/Paramore.Brighter.Mediator/Waker.cs | 15 ++++- .../When_running_a_blocking_wait_workflow.cs | 11 ++-- .../When_running_a_change_workflow.cs | 2 +- ..._running_a_failing_choice_workflow_step.cs | 2 +- ...running_a_multistep_workflow_with_reply.cs | 2 +- ..._running_a_passing_choice_workflow_step.cs | 2 +- .../When_running_a_single_step_workflow.cs | 2 +- .../When_running_a_two_step_workflow.cs | 2 +- ...unning_a_workflow_with_a_parallel_split.cs | 2 +- .../When_running_a_workflow_with_reply.cs | 2 +- ...ng_a_workflow_with_robust_reply_nofault.cs | 2 +- ...a_workflow_with_robust_reply_with_fault.cs | 2 +- 14 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index 10fa679507..f560f647f6 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -39,6 +40,7 @@ public class Runner private readonly IAmAStateStoreAsync _stateStore; private readonly IAmACommandProcessor _commandProcessor; private readonly Scheduler _scheduler; + private readonly string _runnerName = Guid.NewGuid().ToString("N"); private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); @@ -61,9 +63,11 @@ public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAm /// Runs the job processing loop. /// /// A token to monitor for cancellation requests. - public async Task RunAsync(CancellationToken cancellationToken = default) + public void RunAsync(CancellationToken cancellationToken = default) { - await Task.Factory.StartNew(async () => + s_logger.LogInformation("Starting runner {RunnerName}", _runnerName); + + var task = Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -73,6 +77,10 @@ await Task.Factory.StartNew(async () => cancellationToken.ThrowIfCancellationRequested(); }, cancellationToken); + + Task.WaitAll([task], cancellationToken); + + s_logger.LogInformation("Finished runner {RunnerName}", _runnerName); } private async Task Execute(Job? job, CancellationToken cancellationToken = default) @@ -80,12 +88,15 @@ private async Task Execute(Job? job, CancellationToken cancellationToken if (job is null) return; + s_logger.LogInformation("Executing job {JobId} on runner {RunnerName}", job.Id, _runnerName); + job.State = JobState.Running; await _stateStore.SaveJobAsync(job, cancellationToken); var step = job.CurrentStep(); while (step is not null) { + s_logger.LogInformation("Step is {StepName} with state {StepStste}", step.Name, step.State); if (step.State == StepState.Queued) { await step.ExecuteAsync(_stateStore, _commandProcessor, _scheduler, cancellationToken); @@ -95,26 +106,38 @@ private async Task Execute(Job? job, CancellationToken cancellationToken if (job.State == JobState.Waiting) break; + //assume execute determines next step step = job.CurrentStep(); + s_logger.LogInformation( + "Next step is {StepName} with state {StepState}", + step is not null ? step.Name : "flow ends", + step is not null ? step.State : StepState.Done); } if (job.State != JobState.Waiting) job.State = JobState.Done; + s_logger.LogInformation("Finished executing job {JobId} on {RunnerName}", job.Id, _runnerName); } private async Task ProcessJobs(CancellationToken cancellationToken) { - while (!_channel.IsClosed()) + while (true) { if (cancellationToken.IsCancellationRequested) break; + + if (_channel.IsClosed()) + break; + s_logger.LogInformation("Looking for jobs on {RunnerName}", _runnerName); var job = await _channel.DequeueJobAsync(cancellationToken); if (job is null) continue; + s_logger.LogInformation("Executing job {JobId} on {RunnerName}", job.Id, _runnerName); await Execute(job, cancellationToken); + s_logger.LogInformation("Finished job {JobId} on {RunnerName}", job.Id, _runnerName); } } } diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 4d76748ba5..6601938a08 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -141,9 +141,14 @@ public override async Task ExecuteAsync( State = StepState.Running; var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; + + State = StepState.Done; + + if (step != null) + step.State = StepState.Queued; + Job.NextStep(step); OnCompletion?.Invoke(); - State = StepState.Done; await stateStore.SaveJobAsync(Job, cancellationToken); } @@ -245,14 +250,22 @@ public override async Task ExecuteAsync( { await StepTask.HandleAsync(Job, commandProcessor, stateStore, cancellationToken); OnCompletion?.Invoke(); - Job.NextStep(Next); State = StepState.Done; + + if(Next != null) + Next.State = StepState.Queued; + + Job.NextStep(Next); await stateStore.SaveJobAsync(Job, cancellationToken); } catch (Exception) { Job.State = JobState.Faulted; onFaulted?.Invoke(); + + if (faultNext != null) + faultNext.State = StepState.Queued; + Job.NextStep(faultNext); State = StepState.Faulted; await stateStore.SaveJobAsync(Job, cancellationToken); @@ -263,17 +276,24 @@ public override async Task ExecuteAsync( /// /// Allows the workflow to pause. This is a blocking operation that pauses the executing thread /// -/// The name of the step, used for tracing execution -/// The period for which we pause -/// An optional callback to run, following completion of the step -/// The next step in the sequence, null if this is the last step. /// The data that the step operates over -public class Wait( - string name, - TimeSpan duration, - Sequential? next) - : Step(name, next) +public class Wait : Step { + private readonly TimeSpan _duration; + + /// + /// Allows the workflow to pause. This is a blocking operation that pauses the executing thread + /// + /// The name of the step, used for tracing execution + /// The period for which we pause + /// The next step in the sequence, null if this is the last step. + /// The data that the step operates over + public Wait(string name, TimeSpan duration, Sequential? next) + : base(name, next) + { + _duration = duration; + } + /// /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. @@ -296,13 +316,26 @@ public override async Task ExecuteAsync( if (scheduler is null) throw new InvalidOperationException("Scheduler is null; a Wait Step must have a scheduler to schedule the next step"); + + if (Next == null) + { + throw new InvalidOperationException("Next step is empty; wait schedule the next step, so it cannot be empty"); + } State = StepState.Running; - Job.DueTime = DateTime.UtcNow.Add(duration); - Job.State = JobState.Waiting; + Job.DueTime = DateTime.UtcNow.Add(_duration); + State = StepState.Done; - await scheduler.ScheduleAtAsync(Job, duration, cancellationToken); + + Next.State = StepState.Queued; + + Job.NextStep(Next); + + Job.State = JobState.Waiting; + + //this call will save the state of the Job, so no need to do it twice + await scheduler.ScheduleAtAsync(Job, _duration, cancellationToken); } } diff --git a/src/Paramore.Brighter.Mediator/Waker.cs b/src/Paramore.Brighter.Mediator/Waker.cs index 10db24da81..b00a411643 100644 --- a/src/Paramore.Brighter.Mediator/Waker.cs +++ b/src/Paramore.Brighter.Mediator/Waker.cs @@ -25,6 +25,8 @@ THE SOFTWARE. */ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Mediator; @@ -36,6 +38,9 @@ public class Waker { private readonly TimeSpan _jobAge; private readonly Scheduler _scheduler; + private readonly string _wakerName = Guid.NewGuid().ToString("N"); + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); /// /// Initializes a new instance of the class. @@ -54,9 +59,11 @@ public Waker(TimeSpan jobAge, Scheduler scheduler) /// /// A token to monitor for cancellation requests. /// A task that represents the asynchronous run operation. - public async Task RunAsync(CancellationToken cancellationToken = default) + public void RunAsync(CancellationToken cancellationToken = default) { - await Task.Factory.StartNew(async () => + s_logger.LogInformation("Starting waker {WakerName}", _wakerName); + + var task = Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -66,6 +73,10 @@ await Task.Factory.StartNew(async () => cancellationToken.ThrowIfCancellationRequested(); }, cancellationToken); + + Task.WaitAll([task], cancellationToken); + + s_logger.LogInformation("Finished waker {WakerName}", _wakerName); } private async Task Wake(CancellationToken cancellationToken) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs index 776dc5566e..5b5f4eac43 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -46,7 +46,7 @@ public MediatorWaitStepFlowTests(ITestOutputHelper testOutputHelper) ); var firstStep = new Wait("Test of Job", - TimeSpan.FromMilliseconds(500), + TimeSpan.FromMilliseconds(100), secondStep ); @@ -76,10 +76,11 @@ public async Task When_running_a_wait_workflow() _timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); - var jobExecutionTask = _runner.RunAsync(ct.Token); - var jobWakerTask = _waker.RunAsync(ct.Token); - - await Task.WhenAny( jobExecutionTask, jobWakerTask); + _runner.RunAsync(ct.Token); + _waker.RunAsync(ct.Token); + + await Task.Delay(5, ct.Token); + } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs index c7534ab511..07bd52c51b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -70,7 +70,7 @@ public async Task When_running_a_change_workflow() ct.CancelAfter( TimeSpan.FromSeconds(1) ); try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception ex) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 478f0052f6..69439f4f00 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -91,7 +91,7 @@ public async Task When_running_a_choice_workflow_step() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 7b08bf7b49..2c6a3cb3bc 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -84,7 +84,7 @@ public async Task When_running_a_workflow_with_reply() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index 107952232c..d27dc17845 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -91,7 +91,7 @@ public async Task When_running_a_choice_workflow_step() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 8dd8efb745..45eec68f9a 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -68,7 +68,7 @@ public async Task When_running_a_single_step_workflow() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 3b79014dc3..1e43206e73 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -75,7 +75,7 @@ public async Task When_running_a_two_step_workflow() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index e83048cdd8..0582f07c7e 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -84,7 +84,7 @@ public async Task When_running_a_workflow_with_a_parallel_split() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 5361d81db2..104afc0c4f 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -82,7 +82,7 @@ public async Task When_running_a_workflow_with_reply() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index fb7115b0ac..91e16d3b1e 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -84,7 +84,7 @@ public async Task When_running_a_workflow_with_reply() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs index c52067ec4d..cd86f6300b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -86,7 +86,7 @@ public async Task When_running_a_workflow_with_reply() try { - await _runner.RunAsync(ct.Token); + _runner.RunAsync(ct.Token); } catch (Exception e) { From 059fff03f05e9a0b6968983a9eb70bb43e17f0cc Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 25 Nov 2024 09:21:37 +0000 Subject: [PATCH 39/44] chore: merge from master --- docs/adr/0022-add-a-mediator.md | 6 +++--- docs/adr/0024-add-parallel-split-to-mediator.md | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md index 11e4b53452..317b2c8681 100644 --- a/docs/adr/0022-add-a-mediator.md +++ b/docs/adr/0022-add-a-mediator.md @@ -32,9 +32,9 @@ Our experience has been that many teams adopt Step Functions to gain access to i We will add a `Mediator` class to Brighter that will: - 1. Manages and tracks a WorkflowState object representing the current step in the workflow. + 1. Manages and tracks a WorkflowState object representing the current step in the workflow. 2. Support multiple steps: sequence, choice, parallel, wait. - 3. Supports multiple tasks, mapped to typical ws-messaging patterns including: + 3. Supports multiple tasks, mapped to typical ws-messaging patterns including: • FireAndForget: Dispatches a `Command` and immediately advances to the next state. • RequestReaction: Dispatches a `Command` and waits for an event response before advancing. • RobustRequestReaction: Reaction event can kick off an error flow. @@ -42,7 +42,7 @@ We will add a `Mediator` class to Brighter that will: 5. Work is handled within Brighter handlers. They use glue code to call back to the workflow where necessary 6. Can be passed events, and uses the correlation IDs to match events to specific workflow instances and advance the workflow accordingly. -The Specification Pattern in a Choice steo will allow flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. +The Specification Pattern in a Choice step will allow flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, workflows, etc. will be a feature of later releases. Broady our goal within V10 would be to ensure that from [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) we can deliver the Basic Control Flow patterns. A stretch goal would be to offer some Iteration and Cnacellation patterns. diff --git a/docs/adr/0024-add-parallel-split-to-mediator.md b/docs/adr/0024-add-parallel-split-to-mediator.md index 3c7ca1363e..f4e91fb2cb 100644 --- a/docs/adr/0024-add-parallel-split-to-mediator.md +++ b/docs/adr/0024-add-parallel-split-to-mediator.md @@ -53,5 +53,11 @@ We would expect a some point to implement the Simple Merge step to allow paralle * Increased Complexity in State Management: Tracking multiple branches requires more complex state management to ensure each branch persists and resumes accurately. * Concurrency Overhead in the Mediator: Managing multiple threads of control adds overhead. We now have both a Runner and a Scheduler. -### Related ADRs +### Use of Middleware or Db for The Job Channel +* We could use a middleware library to manage the job channel. Brighter itself manages a queue or stream of work with a single-threaded pump +* This would mean the scheduler uses the commandprocessor to deposit a job on a queue, and the runner would be our existing message pump, which would pass to a job handler that executed the workflow. +* The alternative here is to use the database as the job channel. This would mean that the scheduler would write a job to the database, and the runner would read from the database. +* For now, we defer this decision to a later ADR. First we want to understand the whole scope of the work, through an in-memory implementation, then we will determine what an out-of-process implementation would look like. + +### Merge of parallel branches * Future ADR for implementing Simple Merge Step for synchronization of parallel branches. From f35d999d2e0a8b1251d5d168393270a95a16deb1 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 2 Dec 2024 21:37:15 +0000 Subject: [PATCH 40/44] chore: safety check in; failing test on parallel split still --- Directory.Packages.props | 3 +- .../IAmAJobChannel.cs | 4 +- .../IAmAStateStoreAsync.cs | 4 +- .../InMemoryJobChannel.cs | 4 +- .../InMemoryStateStoreAsync.cs | 4 +- src/Paramore.Brighter.Mediator/Job.cs | 29 ++++++++--- src/Paramore.Brighter.Mediator/Runner.cs | 9 ++-- src/Paramore.Brighter.Mediator/Scheduler.cs | 6 +-- src/Paramore.Brighter.Mediator/Steps.cs | 47 ++++++++++------- src/Paramore.Brighter.Mediator/Tasks.cs | 10 ++-- src/Paramore.Brighter.Mediator/Waker.cs | 4 +- .../Paramore.Brighter.Core.Tests.csproj | 1 + ...unning_a_workflow_with_a_parallel_split.cs | 52 ++++++++++--------- 13 files changed, 105 insertions(+), 72 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 869559ddb0..7a08876089 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,10 +87,11 @@ + - + diff --git a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs index 73edb0d2d4..37d3798c68 100644 --- a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs +++ b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs @@ -40,14 +40,14 @@ public interface IAmAJobChannel /// The job to enqueue. /// A token to monitor for cancellation requests. /// A task that represents the asynchronous enqueue operation. - Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default); + Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default(CancellationToken)); /// /// Dequeues a job from the channel. /// /// /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. - Task?> DequeueJobAsync(CancellationToken cancellationToken = default); + Task?> DequeueJobAsync(CancellationToken cancellationToken = default(CancellationToken)); /// /// Streams jobs from the channel. diff --git a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs index a6b4c55c1b..67840cccb1 100644 --- a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs @@ -39,7 +39,7 @@ public interface IAmAStateStoreAsync /// /// The job /// - Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default); + Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default(CancellationToken)); /// /// Retrieves a job via its Id @@ -54,5 +54,5 @@ public interface IAmAStateStoreAsync /// The time before now at which becomes scheduled /// /// - Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken); + Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)); } diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs index 1d8a0069b4..d02f5c34a5 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs @@ -80,7 +80,7 @@ public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullCha /// /// A token to monitor for cancellation requests. /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. - public async Task?> DequeueJobAsync(CancellationToken cancellationToken) + public async Task?> DequeueJobAsync(CancellationToken cancellationToken = default(CancellationToken)) { Job? item = null; while (await _channel.Reader.WaitToReadAsync(cancellationToken)) @@ -96,7 +96,7 @@ public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullCha /// The job to enqueue. /// A token to monitor for cancellation requests. /// A task that represents the asynchronous enqueue operation. - public async Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default) + public async Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default(CancellationToken)) { await _channel.Writer.WriteAsync(job, cancellationToken); } diff --git a/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs index 3f5643da7b..91ea05ae1e 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs @@ -58,7 +58,7 @@ public InMemoryStateStoreAsync(TimeProvider? timeProvider = null) /// A job is due now, less the jobAge span /// A cancellation token to end the ongoing operation /// - public Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken) + public Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)) { var dueJobs = _jobs.Values .Where(job => @@ -98,7 +98,7 @@ public Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken /// The job to save. /// A token to monitor for cancellation requests. /// A task that represents the asynchronous save operation. - public Task SaveJobAsync(Job? job, CancellationToken cancellationToken) + public Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default(CancellationToken)) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs index b42866b278..2f5db8a3df 100644 --- a/src/Paramore.Brighter.Mediator/Job.cs +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -57,6 +57,9 @@ public abstract class Job /// Is the job scheduled to run? public bool IsScheduled => DueTime.HasValue; + /// The id of the parent job, if this is a child job + public string? ParentId { get; set; } + /// Is the job waiting to be run, running, waiting for a response or finished public JobState State { get; set; } } @@ -69,11 +72,13 @@ public class Job : Job { /// If we are awaiting a response, we store the type of the response and the action to take when it arrives - private readonly Dictionary?> _pendingResponses = new(); + private readonly ConcurrentDictionary?> _pendingResponses = new(); /// The next step. Steps are a linked list. The final step in the list has null for it's next step. private Step? _step; + private ConcurrentDictionary _children = new(); + /// The data that is passed between steps of the workflow public TData Data { get; private set; } @@ -116,7 +121,8 @@ public void AddPendingResponse(Type responseType, TaskResponse? taskRespo lock (LockObject) { State = JobState.Waiting; - _pendingResponses.Add(responseType, taskResponse); + if (!_pendingResponses.TryAdd(responseType, taskResponse)) + throw new InvalidOperationException($"A pending response for {responseType} already exists"); } } @@ -141,9 +147,8 @@ public void NextStep(Step? nextStep) _step = nextStep; if (_step is not null) _step.AddToJob(this); - else - if (State != JobState.Waiting) - State = JobState.Done; + else if (State != JobState.Waiting) + State = JobState.Done; } } @@ -158,11 +163,23 @@ public bool ResumeAfterEvent(Type eventType) lock (LockObject) { - var success = _pendingResponses.Remove(eventType); + var success = _pendingResponses.Remove(eventType, out _); _step.OnCompletion?.Invoke(); _step = _step.Next; if (success) State = JobState.Running; return success; } } + + /// + /// Sets an identifier on each child to indicate the parent id + /// Adds the child to a hashtable of children + /// + /// The job we want to add as a child + public void AddChildJob(Job child) + { + child.ParentId = Id; + _children.TryAdd(child.Id, child); + } + } diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index f560f647f6..da62c0b9a1 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -63,7 +63,7 @@ public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAm /// Runs the job processing loop. /// /// A token to monitor for cancellation requests. - public void RunAsync(CancellationToken cancellationToken = default) + public void RunAsync(CancellationToken cancellationToken = default(CancellationToken)) { s_logger.LogInformation("Starting runner {RunnerName}", _runnerName); @@ -73,8 +73,7 @@ public void RunAsync(CancellationToken cancellationToken = default) await ProcessJobs(cancellationToken); - if (cancellationToken.IsCancellationRequested) - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); }, cancellationToken); @@ -83,7 +82,7 @@ public void RunAsync(CancellationToken cancellationToken = default) s_logger.LogInformation("Finished runner {RunnerName}", _runnerName); } - private async Task Execute(Job? job, CancellationToken cancellationToken = default) + private async Task Execute(Job? job, CancellationToken cancellationToken = default(CancellationToken)) { if (job is null) return; @@ -120,7 +119,7 @@ private async Task Execute(Job? job, CancellationToken cancellationToken s_logger.LogInformation("Finished executing job {JobId} on {RunnerName}", job.Id, _runnerName); } - private async Task ProcessJobs(CancellationToken cancellationToken) + private async Task ProcessJobs(CancellationToken cancellationToken = default(CancellationToken)) { while (true) { diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index 91d3a11ff4..cfa4dbcbf7 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -92,7 +92,7 @@ public async Task ResumeAfterEvent(Event @event) /// The job that we want a runner to execute /// A cancellation token to end the ongoing operation /// Thrown when the job has not been initialized. - public async Task ScheduleAsync(Job job, CancellationToken cancellationToken = default) + public async Task ScheduleAsync(Job job,CancellationToken cancellationToken = default(CancellationToken)) { await _channel.EnqueueJobAsync(job, cancellationToken); job.DueTime = null; // Clear any due time after queuing @@ -106,7 +106,7 @@ public async Task ScheduleAsync(Job job, CancellationToken cancellationTo /// The delay after which to schedule the job /// A cancellation token to end the ongoing operation /// Thrown when the job has not been initialized. - public async Task ScheduleAtAsync(Job job, TimeSpan delay, CancellationToken cancellationToken = default) + public async Task ScheduleAtAsync(Job job, TimeSpan delay, CancellationToken cancellationToken = default(CancellationToken)) { job.DueTime = _timeProvider.GetUtcNow().Add(delay); await _stateStore.SaveJobAsync(job, cancellationToken); @@ -117,7 +117,7 @@ public async Task ScheduleAtAsync(Job job, TimeSpan delay, CancellationTo /// /// A job is due now, less the jobAge span /// A cancellation token to end the ongoing operation - public async Task TriggerDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken) + public async Task TriggerDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)) { var dueJobs = await _stateStore.GetDueJobsAsync(jobAge, cancellationToken); diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 6601938a08..49cef9001c 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -86,7 +87,7 @@ public abstract Task ExecuteAsync( IAmAStateStoreAsync stateStore, IAmACommandProcessor? commandProcessor = null, Scheduler? scheduler = null, - CancellationToken cancellationToken = default + CancellationToken cancellationToken = default(CancellationToken) ); /// @@ -132,7 +133,7 @@ public override async Task ExecuteAsync( IAmAStateStoreAsync stateStore, IAmACommandProcessor? commandProcessor = null, Scheduler? scheduler = null, - CancellationToken cancellationToken = default + CancellationToken cancellationToken = default(CancellationToken) ) { if (Job is null) @@ -155,14 +156,10 @@ public override async Task ExecuteAsync( } public class ParallelSplit( - string name, - Action? onBranch, - params Step[] branches - ) + string name, + Func>>? onMap) : Step(name, null) { - public Step[] Branches { get; set; } = branches; - /// /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. @@ -173,25 +170,41 @@ params Step[] branches /// The scheduler, used for queuing jobs that need to wait /// The cancellation token, to end this workflow /// - public override Task ExecuteAsync( + public override async Task ExecuteAsync( IAmAStateStoreAsync stateStore, IAmACommandProcessor? commandProcessor = null, Scheduler? scheduler = null, - CancellationToken cancellationToken = default + CancellationToken cancellationToken = default(CancellationToken) ) { if (Job is null) throw new InvalidOperationException("Job is null"); + if (onMap is null) + throw new InvalidOperationException("onMap is null; a ParallelSplit Step must have a mapping function to map to multiple branches"); + + if (scheduler is null) + throw new InvalidOperationException("Scheduler is null; a ParallelSplit Step must have a scheduler to schedule the next step"); + State = StepState.Running; - // Parallel split doesn't directly execute its jobs. - // Execution is handled by the Scheduler, which will handle running each branch concurrently. - onBranch?.Invoke(Job.Data); + //Map to multiple branches + var branches = onMap?.Invoke(Job.Data); + + if (branches is null) + return; + + foreach (Step branch in branches) + { + var childJob = new Job(Job.Data); + childJob.AddChildJob(Job); + childJob.InitSteps(branch); + await scheduler.ScheduleAsync(childJob, cancellationToken); + } State = StepState.Done; - return Task.CompletedTask; + return; } } @@ -230,7 +243,7 @@ public override async Task ExecuteAsync( IAmAStateStoreAsync stateStore, IAmACommandProcessor? commandProcessor = null, Scheduler? scheduler = null, - CancellationToken cancellationToken = default + CancellationToken cancellationToken = default(CancellationToken) ) { if (Job is null) @@ -308,7 +321,7 @@ public override async Task ExecuteAsync( IAmAStateStoreAsync stateStore, IAmACommandProcessor? commandProcessor = null, Scheduler? scheduler = null, - CancellationToken cancellationToken = default + CancellationToken cancellationToken = default(CancellationToken) ) { if (Job is null) @@ -318,9 +331,7 @@ public override async Task ExecuteAsync( throw new InvalidOperationException("Scheduler is null; a Wait Step must have a scheduler to schedule the next step"); if (Next == null) - { throw new InvalidOperationException("Next step is empty; wait schedule the next step, so it cannot be empty"); - } State = StepState.Running; diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index fb2b4f4a19..0a01ddadc5 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -41,7 +41,7 @@ public interface IStepTask /// The command processor used to handle commands. /// Used to store the state of a job, if it is altered in the handler /// The cancellation token for this task - Task HandleAsync(Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken); + Task HandleAsync(Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken = default(CancellationToken)); } /// @@ -92,7 +92,7 @@ public async Task HandleAsync( Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, - CancellationToken cancellationToken + CancellationToken cancellationToken = default(CancellationToken) ) { if (job is null) @@ -128,7 +128,7 @@ public async Task HandleAsync( Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, - CancellationToken cancellationToken + CancellationToken cancellationToken = default(CancellationToken) ) { if (job is null) @@ -174,7 +174,7 @@ public async Task HandleAsync( Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, - CancellationToken cancellationToken + CancellationToken cancellationToken = default(CancellationToken) ) { if (job is null) @@ -228,7 +228,7 @@ public async Task HandleAsync( Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, - CancellationToken cancellationToken + CancellationToken cancellationToken = default(CancellationToken) ) { if (job is null) diff --git a/src/Paramore.Brighter.Mediator/Waker.cs b/src/Paramore.Brighter.Mediator/Waker.cs index b00a411643..24ac2174d6 100644 --- a/src/Paramore.Brighter.Mediator/Waker.cs +++ b/src/Paramore.Brighter.Mediator/Waker.cs @@ -59,7 +59,7 @@ public Waker(TimeSpan jobAge, Scheduler scheduler) /// /// A token to monitor for cancellation requests. /// A task that represents the asynchronous run operation. - public void RunAsync(CancellationToken cancellationToken = default) + public void RunAsync(CancellationToken cancellationToken = default(CancellationToken)) { s_logger.LogInformation("Starting waker {WakerName}", _wakerName); @@ -79,7 +79,7 @@ public void RunAsync(CancellationToken cancellationToken = default) s_logger.LogInformation("Finished waker {WakerName}", _wakerName); } - private async Task Wake(CancellationToken cancellationToken) + private async Task Wake(CancellationToken cancellationToken = default(CancellationToken)) { while (true) { diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index 28b298cfd2..da36fffbd2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index 0582f07c7e..592bb54da4 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,6 +20,7 @@ public class MediatorParallelSplitFlowTests private readonly Job _job; private bool _firstBranchFinished; private bool _secondBranchFinished; + private readonly InMemoryJobChannel _channel; public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) { @@ -36,51 +38,53 @@ public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) _job = new Job(workflowData) ; - var secondBranch = new Sequential( - "Test of Job Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), - () => { _secondBranchFinished = true; }, - null - ); - - var firstBranch = new Sequential( - "Test of Job One", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), - () => { _firstBranchFinished = true; }, - null - ); - var parallelSplit = new ParallelSplit( "Test of Job Parallel Split", (data) => - { data.Bag["MyValue"] = "TestOne"; - data.Bag["MyOtherValue"] = "TestTwo"; - }, - firstBranch, secondBranch + { + data.Bag.TryAdd("MyValue", "Test"); + data.Bag.TryAdd("MyOtherValue", "TestTwo"); + + var secondBranch = new Sequential( + "Test of Job Two", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), + () => { _secondBranchFinished = true; }, + null + ); + + var firstBranch = new Sequential( + "Test of Job One", + new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + () => { _firstBranchFinished = true; }, + null + ); + + return [firstBranch, secondBranch]; + } ); _job.InitSteps(parallelSplit); InMemoryStateStoreAsync store = new(); - InMemoryJobChannel channel = new(); + _channel = new InMemoryJobChannel(); _scheduler = new Scheduler( - channel, + _channel, store ); - _runner = new Runner(channel, store, commandProcessor, _scheduler); + _runner = new Runner(_channel, store, commandProcessor, _scheduler); } - //[Fact] + [Fact] public async Task When_running_a_workflow_with_a_parallel_split() { MyCommandHandlerAsync.ReceivedCommands.Clear(); - _scheduler.ScheduleAsync(_job); + await _scheduler.ScheduleAsync(_job); var ct = new CancellationTokenSource(); - ct.CancelAfter( TimeSpan.FromSeconds(1) ); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); try { From 1ef0839dfeb70581761d9ca36ec1af9178ce56e2 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 4 Dec 2024 08:24:37 +0000 Subject: [PATCH 41/44] fix: parallel split was not terminating --- src/Paramore.Brighter.Mediator/Runner.cs | 3 ++- src/Paramore.Brighter.Mediator/Steps.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs index da62c0b9a1..1ea7ea2082 100644 --- a/src/Paramore.Brighter.Mediator/Runner.cs +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -105,7 +106,7 @@ public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAm if (job.State == JobState.Waiting) break; - //assume execute determines next step + //assume execute has advanced he step, if you your step loops endlessly it has not advanced the step!! step = job.CurrentStep(); s_logger.LogInformation( "Next step is {StepName} with state {StepState}", diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs index 49cef9001c..9c4495de05 100644 --- a/src/Paramore.Brighter.Mediator/Steps.cs +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -204,7 +204,9 @@ public override async Task ExecuteAsync( State = StepState.Done; - return; + //NOTE: parallel split is a final step - this might change when we bring in merge + Job.NextStep(null); + await stateStore.SaveJobAsync(Job, cancellationToken); } } From 392dd4dab53e311a8fae305b720d53df9725ac17 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 4 Dec 2024 09:35:43 +0000 Subject: [PATCH 42/44] fix: we should pass data to the callbacks; was capturing variable in enclosing scope in tests --- src/Paramore.Brighter.Mediator/Scheduler.cs | 14 +++++++++++ src/Paramore.Brighter.Mediator/Tasks.cs | 24 +++++++++---------- ..._running_a_failing_choice_workflow_step.cs | 6 +++-- ...running_a_multistep_workflow_with_reply.cs | 7 +++--- ..._running_a_passing_choice_workflow_step.cs | 6 +++-- .../When_running_a_single_step_workflow.cs | 3 ++- .../When_running_a_two_step_workflow.cs | 6 +++-- ...unning_a_workflow_with_a_parallel_split.cs | 6 +++-- .../When_running_a_workflow_with_reply.cs | 7 +++--- ...ng_a_workflow_with_robust_reply_nofault.cs | 6 ++--- ...a_workflow_with_robust_reply_with_fault.cs | 6 ++--- 11 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs index cfa4dbcbf7..0cddc7028a 100644 --- a/src/Paramore.Brighter.Mediator/Scheduler.cs +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -99,6 +100,19 @@ public async Task ResumeAfterEvent(Event @event) await _stateStore.SaveJobAsync(job, cancellationToken); } + /// + /// Schedules a list of jobs + /// + /// The jobs to schedule + /// A cancellation token to terminate the asynchronous operation + public async Task ScheduleAsync(IEnumerable> jobs, CancellationToken cancellationToken = default(CancellationToken)) + { + foreach (var job in jobs) + { + await ScheduleAsync(job, cancellationToken); + } + } + /// /// /// diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs index 0a01ddadc5..7e4854cbfc 100644 --- a/src/Paramore.Brighter.Mediator/Tasks.cs +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -112,7 +112,7 @@ public async Task HandleAsync( /// The type of the workflow data. /// The factory method to create the request. public class FireAndForgetAsync( - Func requestFactory + Func requestFactory ) : IStepTask where TRequest : class, IRequest @@ -137,7 +137,7 @@ public async Task HandleAsync( if (commandProcessor is null) throw new ArgumentNullException(nameof(commandProcessor)); - var command = requestFactory(); + var command = requestFactory(job.Data); command.CorrelationId = job.Id; await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); } @@ -152,8 +152,8 @@ public async Task HandleAsync( /// The factory method to create the request. /// The factory method to handle the reply. public class RequestAndReactionAsync( - Func requestFactory, - Action replyFactory + Func requestFactory, + Action replyFactory ) : IStepTask where TRequest : class, IRequest @@ -183,12 +183,12 @@ public async Task HandleAsync( if (commandProcessor is null) throw new ArgumentNullException(nameof(commandProcessor)); - var command = requestFactory(); + var command = requestFactory(job.Data); command.CorrelationId = job.Id; job.AddPendingResponse( typeof(TReply), - new TaskResponse((reply, _) => replyFactory(reply as TReply), typeof(TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply, job.Data), typeof(TReply), null ) ); @@ -208,9 +208,9 @@ public async Task HandleAsync( /// /// public class RobustRequestAndReactionAsync( - Func requestFactory, - Action replyFactory, - Action faultFactory + Func requestFactory, + Action replyFactory, + Action faultFactory ) : IStepTask where TRequest : class, IRequest @@ -237,20 +237,20 @@ public async Task HandleAsync( if (commandProcessor is null) throw new ArgumentNullException(nameof(commandProcessor)); - var command = requestFactory(); + var command = requestFactory(job.Data); command.CorrelationId = job.Id; job.AddPendingResponse( typeof(TReply), - new TaskResponse((reply, _) => replyFactory(reply as TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply, job.Data), typeof(TReply), typeof(TFault) ) ); job.AddPendingResponse( typeof(TFault), - new TaskResponse((reply, _) => faultFactory(reply as TFault), + new TaskResponse((reply, _) => faultFactory(reply as TFault, job.Data), typeof(TReply), typeof(TFault) ) diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs index 69439f4f00..a5266ee806 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -48,13 +48,15 @@ public MediatorFailingChoiceFlowTests(ITestOutputHelper testOutputHelper) var stepThree = new Sequential( "Test of Job SequenceStep Three", - new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyOtherCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs index 2c6a3cb3bc..936a38b572 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -46,15 +46,16 @@ public MediatorReplyMultiStepFlowTests(ITestOutputHelper testOutputHelper) var stepTwo = new Sequential( "Test of Job SequenceStep Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); Sequential stepOne = new( "Test of Job SequenceStep One", new RequestAndReactionAsync( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => workflowData.Bag["MyReply"] = ((MyEvent)reply).Value), + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => data.Bag["MyReply"] = ((MyEvent)reply).Value), () => { _stepCompletedOne = true; }, stepTwo); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs index d27dc17845..cdd100e1f0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -48,13 +48,15 @@ public MediatorPassingChoiceFlowTests(ITestOutputHelper testOutputHelper) var stepThree = new Sequential( "Test of Job SequenceStep Three", - new FireAndForgetAsync(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyOtherCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepCompletedThree = true; }, null); var stepTwo = new Sequential( "Test of Job SequenceStep Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepCompletedTwo = true; }, null); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs index 45eec68f9a..3bea73ee29 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -38,7 +38,8 @@ public MediatorOneStepFlowTests(ITestOutputHelper testOutputHelper) var firstStep = new Sequential( "Test of Job", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + new FireAndForgetAsync((data) => + new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), () => { _stepCompleted = true; }, null ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs index 1e43206e73..9f4be1cd13 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -39,14 +39,16 @@ public MediatorTwoStepFlowTests(ITestOutputHelper testOutputHelper) var secondStep = new Sequential( "Test of Job Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), () => { _stepsCompleted = true; }, null ); var firstStep = new Sequential( "Test of Job One", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), () => { workflowData.Bag["MyValue"] = "TestTwo"; }, secondStep ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs index 592bb54da4..4b3d6bf8e9 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -47,14 +47,16 @@ public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) var secondBranch = new Sequential( "Test of Job Two", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyOtherValue"] as string)! }), + new FireAndForgetAsync((d) => + new MyCommand { Value = (d.Bag["MyOtherValue"] as string)! }), () => { _secondBranchFinished = true; }, null ); var firstBranch = new Sequential( "Test of Job One", - new FireAndForgetAsync(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }), + new FireAndForgetAsync((d) => + new MyCommand { Value = (d.Bag["MyValue"] as string)! }), () => { _firstBranchFinished = true; }, null ); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs index 104afc0c4f..2c3f1f4c68 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; -using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; using Paramore.Brighter.Mediator; using Polly.Registry; @@ -31,7 +30,7 @@ public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) registry.RegisterAsync(); registry.RegisterAsync(); - IAmACommandProcessor commandProcessor = null; + IAmACommandProcessor? commandProcessor = null; var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => handlerType switch { @@ -51,8 +50,8 @@ public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) var firstStep = new Sequential( "Test of Job", new RequestAndReactionAsync( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }), + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply,data) => { data.Bag["MyReply"] = reply!.Value; }), () => { _stepCompleted = true; }, null); diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs index 91e16d3b1e..3d28de6222 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -48,9 +48,9 @@ public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelpe var firstStep = new Sequential( "Test of Job", new RobustRequestAndReactionAsync( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }, - (fault) => { workflowData.Bag["MyFault"] = ((MyFault)fault).Value; }), + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => { data.Bag["MyReply"] = ((MyEvent)reply).Value; }, + (fault, data) => { data.Bag["MyFault"] = ((MyFault)fault).Value; }), () => { _stepCompleted = true; }, null, () => { _stepFaulted = true; }, diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs index cd86f6300b..58edfbebbe 100644 --- a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -50,9 +50,9 @@ public MediatorRobustReplyFaultStepFlowTests(ITestOutputHelper testOutputHelper) var firstStep = new Sequential( "Test of Job", new RobustRequestAndReactionAsync( - () => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }, - (reply) => { workflowData.Bag["MyReply"] = ((MyEvent)reply).Value; }, - (fault) => { workflowData.Bag["MyFault"] = ((MyFault)fault).Value; }), + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => { data.Bag["MyReply"] = reply!.Value; }, + (fault, data) => { data.Bag["MyFault"] = fault!.Value; }), () => { _stepCompleted = true; }, null, () => { _stepFaulted = true; }, From 5dd7cf27a1333a84a328ecd3137d6282d8abc1e6 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 5 Dec 2024 09:58:18 +0000 Subject: [PATCH 43/44] fix: issue with dequeuejobasync hard crashing --- .../InMemoryJobChannel.cs | 7 +- .../When_running_multiple_workflows.cs | 100 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs index d02f5c34a5..4b3314ab2b 100644 --- a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs +++ b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs @@ -22,10 +22,13 @@ THE SOFTWARE. */ #endregion +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Mediator; @@ -52,6 +55,8 @@ public enum FullChannelStrategy public class InMemoryJobChannel : IAmAJobChannel { private readonly Channel> _channel; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); /// /// Initializes a new instance of the class. @@ -84,7 +89,7 @@ public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullCha { Job? item = null; while (await _channel.Reader.WaitToReadAsync(cancellationToken)) - while (_channel.Reader.TryRead(out item)) + while (_channel.Reader.TryRead(out item)) return item; return item; diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs new file mode 100644 index 0000000000..8f3ae2e63b --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorMultipleWorkflowFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _firstJob; + private readonly Job _secondJob; + private bool _jobOneCompleted; + private bool _jobTwoCompleted; + + public MediatorMultipleWorkflowFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var firstWorkflowData= new WorkflowTestData(); + firstWorkflowData.Bag["MyValue"] = "Test"; + + _firstJob = new Job(firstWorkflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)!}), + () => { _jobOneCompleted = true; }, + null + ); + + _firstJob.InitSteps(firstStep); + + var secondWorkflowData = new WorkflowTestData(); + secondWorkflowData.Bag["MyValue"] = "TestTwo"; + _secondJob = new Job(secondWorkflowData); + + var secondStep = new Sequential( + "Second Test of Job", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _jobTwoCompleted = true; }, + null + ); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_single_step_workflow() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync([_firstJob, _secondJob]); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(120) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _firstJob.State.Should().Be(JobState.Done); + _secondJob.State.Should().Be(JobState.Done); + _jobOneCompleted.Should().BeTrue(); + _jobTwoCompleted.Should().BeTrue(); + } +} From f13aa5978c64390b3557d71b49f489709d960b75 Mon Sep 17 00:00:00 2001 From: iancooper Date: Fri, 17 Jan 2025 12:30:18 +0000 Subject: [PATCH 44/44] feat: move the implementation to FBP --- .../0024-add-parallel-split-to-mediator.md | 4 ++ ...5-use-reactive-programming-for-mediator.md | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 docs/adr/0025-use-reactive-programming-for-mediator.md diff --git a/docs/adr/0024-add-parallel-split-to-mediator.md b/docs/adr/0024-add-parallel-split-to-mediator.md index f4e91fb2cb..d6ad4883f8 100644 --- a/docs/adr/0024-add-parallel-split-to-mediator.md +++ b/docs/adr/0024-add-parallel-split-to-mediator.md @@ -1,5 +1,9 @@ # ADR: Implementing Parallel Split Step for Concurrent Workflow Execution +## Status + +Proposed + ## Context Our workflow currently supports sequential steps executed in a single thread of control. Each step in the workflow proceeds one after another, and the Mediator has been designed with this single-threaded assumption. diff --git a/docs/adr/0025-use-reactive-programming-for-mediator.md b/docs/adr/0025-use-reactive-programming-for-mediator.md new file mode 100644 index 0000000000..59da97a5ce --- /dev/null +++ b/docs/adr/0025-use-reactive-programming-for-mediator.md @@ -0,0 +1,61 @@ +# 25. Use Reactive Progamming For Mediator + +Date: 2025-01-13 + +## Status + +Accepted + +## Context + +We have scenarios in any workflow where we need to split and then later merge. Our decision to handle the split in +[0024](./0024-add-parallel-split-to-mediator.md) led us on the path to seperating a scheduler and a runner - a +classic producer and consumer pattern. We can use `Channels` (or a `BlockingCollection`) in dotnet to support the +implementation of an internal producer-consumer (as opposed to one using messaging. + +Our approach to resolve split was simply to have one channel for the workflow to be scheduled on, so that we could +schedule the splits back to the channel. We don't have a solution for merging those splits. + +We also have an approach to waiting for an external event, that we halt the flow, save it's state, and then reschedule +once we are notified of the event we are waiting for. This works well for a single event, but external. It works +less well for multiple events, or internal events, that go best over a channel. + +## Decision + +We will move to a Flow Based Programming approach to implementing the work. Each `Step<>` in the workflow will +derive from a new type `Component`. + +As a FBP component it has an `In` port, an instance of `IAmAJobChannel`. When a component is activated it runs a +message pump to read work from the `In` port, until the port is marked as completed. Once there is no more work, the +`Component` deactivates. A component should save state before it deactivates, to indicate that it was completed. + +An `Out` port is actually a call to the next component. Putting work on the `Out`port activates the next component +and puts work on its `In` port. + +``` +--> [In][Component][Out] --> +``` + +On a split, there is an array of `Out` ports to write to, instead of a single port. Generically then we require an +overload of any Out method call on the base 'Component' that takes an array of `IAmAJobChannel` + +``` +--> [In][Component][Out...] --> +``` + +On a merge that is an array of 'In' ports to write to, instead of a single port. We may force you to wait for +everything to arrive before continuing, or allow you to proceed as soon as you arrive in the joined flow. + +We may choose to use the FBP brackets approach to any merge. The upstream sends an 'opening bracket' to 'In' +indicating a sequence follows. The 'bracket' indicates whether we are 'WaitAll' or 'WaitAny' and the channels to +listen on. The downstream component then listens to those channels, until they complete, and obeys the +'WaitAll' or 'WaitAny' as appropriate. + +For configuration of a downstream a component needs an `Opt` channel which can take generic configuration information +(most likely the payload here is a `Configuration` class with an `object` payload). + +## Consequences + +FBP is stongly aligned with workflows, so adopting concepts from FBP gives us a strong programming model to work with. +FBP has already solved many of the problems around running workflows, so it gives us a strong plan to work with. +