Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Mediator to Brighter #3370

Draft
wants to merge 45 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f52c18d
chore: save work in progress
iancooper Oct 28, 2024
36890f5
feat: add ADR for a mediator and assembly
iancooper Oct 30, 2024
d92cb84
feat: required workflow classes to write setup of rist mediator test
iancooper Oct 30, 2024
556bb7a
chore: safety check whilst releasing V9 and V10; does not build
iancooper Oct 31, 2024
3ed0e7e
chore: safety checkin
iancooper Nov 1, 2024
e0c9c85
feat: add fire and forget action
iancooper Nov 1, 2024
f8609b8
feat: add requestreply outline
iancooper Nov 1, 2024
255d086
fix: folder name causing issues on MacOS which believes it is an appl…
iancooper Nov 2, 2024
a321e30
feat: modifications to step and workflow responsibility
iancooper Nov 4, 2024
7a879e6
feat: add workflow data, over just using the bag
iancooper Nov 5, 2024
b8eded1
feat: need correlation id on event and command to support workflow
iancooper Nov 6, 2024
5298d38
chore: check in to allow merging of master
iancooper Nov 6, 2024
ab16834
chore: merge branch 'master' into mediator
iancooper Nov 6, 2024
92e344d
feat: move completed workflows to the done state
iancooper Nov 9, 2024
6840f04
feat: add an ADR for adding the specification pattern
iancooper Nov 9, 2024
9e63244
feat: add the specification pattern
iancooper Nov 9, 2024
f7cf152
fix: typo in filename
iancooper Nov 9, 2024
b09c3a6
chore: safety dance
iancooper Nov 9, 2024
5313018
chore: safety dance
iancooper Nov 9, 2024
c41547a
feat: add a choice workflow action
iancooper Nov 10, 2024
c42b6b2
fix: shared fixture problems
iancooper Nov 10, 2024
40bf2b4
feat: add first version of robust flow
iancooper Nov 10, 2024
2b11f6a
chore: ~Merge branch 'master' into mediator
iancooper Nov 10, 2024
56f577e
fix: remove IAmTheWorkflowData as unnecessary abstraction.
iancooper Nov 11, 2024
3cb3c4b
fix: make choice about choosing the next step from the workflow data
iancooper Nov 11, 2024
bd6d2e1
fix: tests not checking all paths
iancooper Nov 11, 2024
12a7ecd
fix: add workflow patterns to ADR
iancooper Nov 12, 2024
f24fac2
feat: move to workflow patterns style, step and task; some behaviours…
iancooper Nov 13, 2024
991e4f2
feat: first pass at Parallel; requires Scheduler-Runner split to Medi…
iancooper Nov 17, 2024
1373583
chore: safety check-in during scheduler/runner split work
iancooper Nov 18, 2024
6067bd4
fix: refactor relationship between job and step to be more explicit. …
iancooper Nov 19, 2024
8b4e5e4
fix: step advancement manages job state
iancooper Nov 19, 2024
e5a6639
fix: add cancellation token interrupt of runner to all tests
iancooper Nov 19, 2024
acef39d
chore: safety check in; fixing failing tests
iancooper Nov 19, 2024
aa8ca95
fix: get the steps to save state, when they modify the job, not the r…
iancooper Nov 20, 2024
1cea4ea
fix: add fault version of robust request-reply
iancooper Nov 20, 2024
08b2e18
fix: add multi-threading support to Job
iancooper Nov 20, 2024
67f3376
Update ASB Samples to use the Emulator (#3391)
preardon Nov 21, 2024
e118650
feat: adding a Wait step.
iancooper Nov 23, 2024
ca229f0
fix: don't try to await a thread; ensure we leave time for scheduler …
iancooper Nov 23, 2024
059fff0
chore: merge from master
iancooper Nov 25, 2024
a7019a2
chore: merge with master
iancooper Dec 2, 2024
f35d999
chore: safety check in; failing test on parallel split still
iancooper Dec 2, 2024
1ef0839
fix: parallel split was not terminating
iancooper Dec 4, 2024
392dd4d
fix: we should pass data to the callbacks; was capturing variable in …
iancooper Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Brighter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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.MediatorWorkflow", "src\Paramore.Brighter.MediatorWorkflow\Paramore.Brighter.MediatorWorkflow.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -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
{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
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0020-reduce-esb-complexity.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 20. Reduce External Service Bus Complexity

Date: 2019-08-01
Date: 2024-08-01

## Status

Expand Down
57 changes: 57 additions & 0 deletions docs/adr/0022-add-a-mediator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 22. Add a Mediator to Brighter

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 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.

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.

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

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

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.
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
46 changes: 46 additions & 0 deletions src/Paramore.Brighter.MediatorWorkflow/IAmAWorkflowStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#region Licence
/* The MIT License (MIT)
Copyright © 2024 Ian Cooper <ian_hammond_cooper@yahoo.co.uk>

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;

/// <summary>
/// Used to store the state of a workflow
/// </summary>
public interface IAmAWorkflowStore
{
/// <summary>
/// Saves the workflow
/// </summary>
/// <param name="workflow">The workflow</param>
void SaveWorkflow<TData>(Workflow<TData> workflow) where TData : IAmTheWorkflowData;

/// <summary>
/// Retrieves a workflow via its Id
/// </summary>
/// <param name="id">The id of the workflow</param>
/// <returns>if found, the workflow, otherwise null</returns>
Workflow? GetWorkflow(string? id) ;
}
Loading
Loading