Skip to content

Commit

Permalink
feat: add saga sample
Browse files Browse the repository at this point in the history
  • Loading branch information
mkassm committed Dec 11, 2023
1 parent 842af52 commit 904e76b
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 23 deletions.
53 changes: 30 additions & 23 deletions TemporalioSamples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,57 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1A647B41-53D0-4638-AE5A-6630BAAE45FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.ActivityWorker", "src\ActivityWorker\TemporalioSamples.ActivityWorker.csproj", "{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.ActivityWorker", "src\ActivityWorker\TemporalioSamples.ActivityWorker.csproj", "{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Tests", "tests\TemporalioSamples.Tests.csproj", "{3FA7E5DF-03B7-4586-A980-85C155B376C5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Tests", "tests\TemporalioSamples.Tests.csproj", "{3FA7E5DF-03B7-4586-A980-85C155B376C5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{E431D279-E02B-4670-B934-3DB9F15D8CCC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.AspNet.Web", "src\AspNet\Web\TemporalioSamples.AspNet.Web.csproj", "{31EC2647-6A5A-42D1-B7B5-02804B340726}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.AspNet.Web", "src\AspNet\Web\TemporalioSamples.AspNet.Web.csproj", "{31EC2647-6A5A-42D1-B7B5-02804B340726}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.AspNet.Worker", "src\AspNet\Worker\TemporalioSamples.AspNet.Worker.csproj", "{AFFA4143-DC28-4FBE-A33B-D6414F541EA4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.AspNet.Worker", "src\AspNet\Worker\TemporalioSamples.AspNet.Worker.csproj", "{AFFA4143-DC28-4FBE-A33B-D6414F541EA4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.ActivitySimple", "src\ActivitySimple\TemporalioSamples.ActivitySimple.csproj", "{7608AFB5-CFD1-427F-81FE-81C7EFE8AFBE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.ActivitySimple", "src\ActivitySimple\TemporalioSamples.ActivitySimple.csproj", "{7608AFB5-CFD1-427F-81FE-81C7EFE8AFBE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Encryption", "Encryption", "{2E1FF71C-0BE3-478E-9984-C6896A11DD3A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Encryption.Codec", "src\Encryption\Codec\TemporalioSamples.Encryption.Codec.csproj", "{F25A0BB4-6FF5-4187-932B-5189987C4B4A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Encryption.Codec", "src\Encryption\Codec\TemporalioSamples.Encryption.Codec.csproj", "{F25A0BB4-6FF5-4187-932B-5189987C4B4A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Encryption.Starter", "src\Encryption\Starter\TemporalioSamples.Encryption.Starter.csproj", "{DD8A2E0D-7644-4B95-91BE-A652CF85BACF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Encryption.Starter", "src\Encryption\Starter\TemporalioSamples.Encryption.Starter.csproj", "{DD8A2E0D-7644-4B95-91BE-A652CF85BACF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Encryption.Worker", "src\Encryption\Worker\TemporalioSamples.Encryption.Worker.csproj", "{6B50F9F9-9C17-475D-A34A-6F317558C446}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Encryption.Worker", "src\Encryption\Worker\TemporalioSamples.Encryption.Worker.csproj", "{6B50F9F9-9C17-475D-A34A-6F317558C446}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Encryption.CodecServer", "src\Encryption\CodecServer\TemporalioSamples.Encryption.CodecServer.csproj", "{8905D1CD-F136-41CA-810F-F15FB5204384}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Encryption.CodecServer", "src\Encryption\CodecServer\TemporalioSamples.Encryption.CodecServer.csproj", "{8905D1CD-F136-41CA-810F-F15FB5204384}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.ClientMtls", "src\ClientMtls\TemporalioSamples.ClientMtls.csproj", "{D2A3546F-2462-4B86-8B5E-999505483A2D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.ClientMtls", "src\ClientMtls\TemporalioSamples.ClientMtls.csproj", "{D2A3546F-2462-4B86-8B5E-999505483A2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.WorkerSpecificTaskQueues", "src\WorkerSpecificTaskQueues\TemporalioSamples.WorkerSpecificTaskQueues.csproj", "{974CCD5E-0254-4C85-9618-8CD014A2734F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.WorkerSpecificTaskQueues", "src\WorkerSpecificTaskQueues\TemporalioSamples.WorkerSpecificTaskQueues.csproj", "{974CCD5E-0254-4C85-9618-8CD014A2734F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Schedules", "src\Schedules\TemporalioSamples.Schedules.csproj", "{297A58BE-3959-4525-A329-222B3575139D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Schedules", "src\Schedules\TemporalioSamples.Schedules.csproj", "{297A58BE-3959-4525-A329-222B3575139D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.ActivityHeartbeatingCancellation", "src\ActivityHeartbeatingCancellation\TemporalioSamples.ActivityHeartbeatingCancellation.csproj", "{CD0D6B7E-2076-4771-AF5B-E1EFB82A44B3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.ActivityHeartbeatingCancellation", "src\ActivityHeartbeatingCancellation\TemporalioSamples.ActivityHeartbeatingCancellation.csproj", "{CD0D6B7E-2076-4771-AF5B-E1EFB82A44B3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Polling.Frequent", "src\Polling\Frequent\TemporalioSamples.Polling.Frequent.csproj", "{6935B8AC-160F-463D-BE03-AD6FF31585A3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Polling.Frequent", "src\Polling\Frequent\TemporalioSamples.Polling.Frequent.csproj", "{6935B8AC-160F-463D-BE03-AD6FF31585A3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Polling", "Polling", "{AE21E7F4-B114-4761-81B1-8FA63E9F6BB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Polling.Infrequent", "src\Polling\Infrequent\TemporalioSamples.Polling.Infrequent.csproj", "{DD2DE0CF-C127-461B-B4F1-D4E13BDD3B5D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Polling.Infrequent", "src\Polling\Infrequent\TemporalioSamples.Polling.Infrequent.csproj", "{DD2DE0CF-C127-461B-B4F1-D4E13BDD3B5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Polling.PeriodicSequence", "src\Polling\PeriodicSequence\TemporalioSamples.Polling.PeriodicSequence.csproj", "{11A5854B-EE6E-4752-9C46-F466503D853B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Polling.PeriodicSequence", "src\Polling\PeriodicSequence\TemporalioSamples.Polling.PeriodicSequence.csproj", "{11A5854B-EE6E-4752-9C46-F466503D853B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.DependencyInjection", "src\DependencyInjection\TemporalioSamples.DependencyInjection.csproj", "{10E6F7C9-7F6C-4A8E-94A1-99C10F46BBA4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.DependencyInjection", "src\DependencyInjection\TemporalioSamples.DependencyInjection.csproj", "{10E6F7C9-7F6C-4A8E-94A1-99C10F46BBA4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.WorkerVersioning", "src\WorkerVersioning\TemporalioSamples.WorkerVersioning.csproj", "{CA3FD1BC-C918-4B15-96F6-D6DDA125E63C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.WorkerVersioning", "src\WorkerVersioning\TemporalioSamples.WorkerVersioning.csproj", "{CA3FD1BC-C918-4B15-96F6-D6DDA125E63C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Mutex", "src\Mutex\TemporalioSamples.Mutex.csproj", "{3168FB2D-D821-433A-A761-309E0474DE48}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Mutex", "src\Mutex\TemporalioSamples.Mutex.csproj", "{3168FB2D-D821-433A-A761-309E0474DE48}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Saga", "src\Saga\TemporalioSamples.Saga.csproj", "{B79F07F7-3429-4C58-84C3-08587F748B2D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
Expand Down Expand Up @@ -134,6 +133,13 @@ Global
{3168FB2D-D821-433A-A761-309E0474DE48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3168FB2D-D821-433A-A761-309E0474DE48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3168FB2D-D821-433A-A761-309E0474DE48}.Release|Any CPU.Build.0 = Release|Any CPU
{B79F07F7-3429-4C58-84C3-08587F748B2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B79F07F7-3429-4C58-84C3-08587F748B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B79F07F7-3429-4C58-84C3-08587F748B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B79F07F7-3429-4C58-84C3-08587F748B2D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
Expand All @@ -150,12 +156,13 @@ Global
{974CCD5E-0254-4C85-9618-8CD014A2734F} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{297A58BE-3959-4525-A329-222B3575139D} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{CD0D6B7E-2076-4771-AF5B-E1EFB82A44B3} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{AE21E7F4-B114-4761-81B1-8FA63E9F6BB8} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{6935B8AC-160F-463D-BE03-AD6FF31585A3} = {AE21E7F4-B114-4761-81B1-8FA63E9F6BB8}
{AE21E7F4-B114-4761-81B1-8FA63E9F6BB8} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{DD2DE0CF-C127-461B-B4F1-D4E13BDD3B5D} = {AE21E7F4-B114-4761-81B1-8FA63E9F6BB8}
{11A5854B-EE6E-4752-9C46-F466503D853B} = {AE21E7F4-B114-4761-81B1-8FA63E9F6BB8}
{10E6F7C9-7F6C-4A8E-94A1-99C10F46BBA4} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{CA3FD1BC-C918-4B15-96F6-D6DDA125E63C} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{3168FB2D-D821-433A-A761-309E0474DE48} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{B79F07F7-3429-4C58-84C3-08587F748B2D} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
EndGlobalSection
EndGlobal
41 changes: 41 additions & 0 deletions src/Saga/Activities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.Logging;
using Temporalio.Activities;
using Temporalio.Exceptions;

namespace TemporalioSamples.Saga;

public record TransferDetails(decimal Amount, string FromAmount, string ToAmount, string ReferenceId);

public static class Activities
{
[Activity]
public static void Withdraw(TransferDetails d)
{
ActivityExecutionContext.Current.Logger.LogInformation("Withdrawing {Amount} from account {FromAmount}. ReferenceId: {ReferenceId}", d.Amount, d.FromAmount, d.ReferenceId);
}

[Activity]
public static void WithdrawCompensation(TransferDetails d)
{
ActivityExecutionContext.Current.Logger.LogInformation("Withdrawing Compensation {Amount} from account {FromAmount}. ReferenceId: {ReferenceId}", d.Amount, d.FromAmount, d.ReferenceId);
}

[Activity]
public static void Deposit(TransferDetails d)
{
ActivityExecutionContext.Current.Logger.LogInformation("Depositing {Amount} into account {ToAmount}. ReferenceId: {ReferenceId}", d.Amount, d.ToAmount, d.ReferenceId);
}

[Activity]
public static void DepositCompensation(TransferDetails d)
{
ActivityExecutionContext.Current.Logger.LogInformation("Depositing Compensation {Amount} int account {ToAmount}. ReferenceId: {ReferenceId}", d.Amount, d.ToAmount, d.ReferenceId);
}

[Activity]
public static void StepWithError(TransferDetails d)
{
ActivityExecutionContext.Current.Logger.LogInformation("Simulate failure to trigger compensation. ReferenceId: {ReferenceId}", d.ReferenceId);
throw new ApplicationFailureException("Simulated failure");
}
}
66 changes: 66 additions & 0 deletions src/Saga/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Temporalio.Client;
using Temporalio.Worker;
using TemporalioSamples.Saga;

var client = await TemporalClient.ConnectAsync(new("localhost:7233")
{
LoggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").SetMinimumLevel(LogLevel.Information)),
});

async Task RunWorkerAsync()
{
// Cancellation token cancelled on ctrl+c
using var tokenSource = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
tokenSource.Cancel();
eventArgs.Cancel = true;
};

// Run worker until cancelled
Console.WriteLine("Running worker");

using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions(taskQueue: "workflow-saga-sample")
.AddAllActivities(typeof(Activities), null)
.AddWorkflow<SagaWorkflow>());
try
{
await worker.ExecuteAsync(tokenSource.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Worker cancelled");
}
}
async Task ExecuteWorkflowAsync()
{
var workflowId = "test-" + Guid.NewGuid();
Console.WriteLine($"Starting test workflow with id '{workflowId}'.");

var sw = Stopwatch.StartNew();
var handle = await client.StartWorkflowAsync(
(SagaWorkflow wf) => wf.RunAsync(new TransferDetails(100, "acc1000", "acc2000", "1324")),
new(workflowId, "workflow-saga-sample"));

Console.WriteLine($"Test workflow '{workflowId}' started");

await handle.GetResultAsync();
Console.WriteLine($"Test workflow '{workflowId}' finished after {sw.ElapsedMilliseconds}ms");
}

switch (args.ElementAtOrDefault(0))
{
case "worker":
await RunWorkerAsync();
break;
case "workflow":
await ExecuteWorkflowAsync();
break;
default:
throw new ArgumentException("Must pass 'worker' or 'workflow' as the first argument");
}
3 changes: 3 additions & 0 deletions src/Saga/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Saga

This sample has a Saga
56 changes: 56 additions & 0 deletions src/Saga/Saga.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.Extensions.Logging;

namespace TemporalioSamples.Saga;

public class Saga
{
private ILogger log;
private Stack<Func<Task>> compensations;
private Func<ILogger, Task> onCompensationError = default!;
private Func<ILogger, Task> onCompensationComplete = default!;

public Saga(ILogger logger)
{
log = logger;
compensations = new Stack<Func<Task>>();
}

public void OnCompensationError(Func<ILogger, Task> onCompensationError)
{
this.onCompensationError = onCompensationError;
}

public void OnCompensationComplete(Func<ILogger, Task> onCompensationComplete)
{
this.onCompensationComplete = onCompensationComplete;
}

public void AddCompensation(Func<Task> compensation)
{
compensations.Push(compensation);
}

public async Task CompensateAsync()
{
int i = 0;
while (compensations.Count > 0)
{
i++;
var c = compensations.Pop();

try
{
log.LogInformation("Attempting compensation {I}...", i);
await c.Invoke();
log.LogInformation("Compensation {I} successfull!", i);
}
catch (Exception)
{
/* log details of all other compensations that have not yet been made if this is a show-stopper */
await onCompensationError(log);
throw;
}
}
await onCompensationComplete(log);
}
}
65 changes: 65 additions & 0 deletions src/Saga/SagaWorkflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Logging;
using Temporalio.Workflows;

namespace TemporalioSamples.Saga;

[Workflow]
public class SagaWorkflow
{
[WorkflowRun]
public async Task RunAsync(TransferDetails transfer)
{
var options = new ActivityOptions()
{
StartToCloseTimeout = TimeSpan.FromSeconds(90), // schedule a retry if the Activity function doesn't return within 90 seconds
RetryPolicy = new()
{
InitialInterval = TimeSpan.FromSeconds(15), // first try will occur after 15 seconds
BackoffCoefficient = 1, // double the delay after each retry
MaximumInterval = TimeSpan.FromMinutes(1), // up to a maximum delay of 1 minute
MaximumAttempts = 2, // fail the Activitiesivity after 2 attempts
},
};

var logger = Workflow.Logger;
var saga = new Saga(logger);

try
{
await Workflow.ExecuteActivityAsync(() => Activities.Withdraw(transfer), options);

saga.AddCompensation(async () => await Workflow.ExecuteActivityAsync(
() => Activities.WithdrawCompensation(transfer),
options));

await Workflow.ExecuteActivityAsync(() => Activities.Deposit(transfer), options);

saga.AddCompensation(async () => await Workflow.ExecuteActivityAsync(
() => Activities.DepositCompensation(transfer),
options));

// throw new Exception
await Workflow.ExecuteActivityAsync(() => Activities.StepWithError(transfer), options);
}
catch (Exception)
{
logger.LogInformation("Exception caught. Initiating compensation...");
saga.OnCompensationComplete((log) =>
{
/* Send "we're sorry, but.." email to customer... */
log.LogInformation("Done. Compensation complete!");
return Task.CompletedTask;
});

saga.OnCompensationError((log) =>
{
/* Send emails to internal supporting teams */
log.LogInformation("Done. Compensation unsuccessful... Manual intervention required!");
return Task.CompletedTask;
});

await saga.CompensateAsync();
throw;
}
}
}
7 changes: 7 additions & 0 deletions src/Saga/TemporalioSamples.Saga.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

</Project>

0 comments on commit 904e76b

Please sign in to comment.