From 61133fb9623728412d74071ef3fabd9a21c7b7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=E2=98=BA=EF=B8=8F?= <251288+lorensr@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:53:04 -0400 Subject: [PATCH] Add timer sample (#57) Co-authored-by: Chad Retz --- README.md | 1 + src/Timer/MyActivities.cs | 9 ++++ src/Timer/Program.cs | 63 ++++++++++++++++++++++++ src/Timer/README.md | 14 ++++++ src/Timer/Subscription.workflow.cs | 32 ++++++++++++ src/Timer/TemporalioSamples.Timer.csproj | 7 +++ 6 files changed, 126 insertions(+) create mode 100644 src/Timer/MyActivities.cs create mode 100644 src/Timer/Program.cs create mode 100644 src/Timer/README.md create mode 100644 src/Timer/Subscription.workflow.cs create mode 100644 src/Timer/TemporalioSamples.Timer.csproj diff --git a/README.md b/README.md index 08b98c9..2949982 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Prerequisites: * [Polling](src/Polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion. * [Saga](src/Saga) - Demonstrates how to implement a saga pattern. * [Schedules](src/Schedules) - How to schedule workflows to be run at specific times in the future. +* [Timer](src/Timer) - Use a timer to implement a monthly subscription; handle workflow cancellation. * [WorkerSpecificTaskQueues](src/WorkerSpecificTaskQueues) - Use a unique task queue per Worker to have certain Activities only run on that specific Worker. * [WorkerVersioning](src/WorkerVersioning) - How to use the Worker Versioning feature to more easily deploy changes to Workflow & other code. * [WorkflowUpdate](src/WorkflowUpdate) - How to use the Workflow Update feature while blocking in update method for concurrent updates. diff --git a/src/Timer/MyActivities.cs b/src/Timer/MyActivities.cs new file mode 100644 index 0000000..9b56571 --- /dev/null +++ b/src/Timer/MyActivities.cs @@ -0,0 +1,9 @@ +namespace TemporalioSamples.Timer; + +using Temporalio.Activities; + +public class MyActivities +{ + [Activity] + public static string Charge(string userId) => "charge successful"; +} \ No newline at end of file diff --git a/src/Timer/Program.cs b/src/Timer/Program.cs new file mode 100644 index 0000000..231efdd --- /dev/null +++ b/src/Timer/Program.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.Logging; +using Temporalio.Client; +using Temporalio.Worker; +using TemporalioSamples.Timer; + +// Create a client to localhost on default namespace +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; + }; + + // Create an activity instance with some state + var activities = new MyActivities(); + + // Run worker until cancelled + Console.WriteLine("Running worker"); + using var worker = new TemporalWorker( + client, + new TemporalWorkerOptions(taskQueue: "timer-sample"). + AddActivity(MyActivities.Charge). + AddWorkflow()); + try + { + await worker.ExecuteAsync(tokenSource.Token); + } + catch (OperationCanceledException) + { + Console.WriteLine("Worker cancelled"); + } +} + +async Task ExecuteWorkflowAsync() +{ + Console.WriteLine("Executing workflow"); + await client.ExecuteWorkflowAsync( + (Subscription wf) => wf.RunAsync("user-id-123"), + new(id: "timer-workflow-id", taskQueue: "timer-sample")); +} + +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 single argument"); +} \ No newline at end of file diff --git a/src/Timer/README.md b/src/Timer/README.md new file mode 100644 index 0000000..d2cb03f --- /dev/null +++ b/src/Timer/README.md @@ -0,0 +1,14 @@ +# Timer + +Use a timer (`Workflow.DelayAsync`) to implement a monthly subscription. Also, handle workflow cancellation. + +To run, first see [README.md](../../README.md) for prerequisites. Then, run the following from this directory +in a separate terminal to start the worker: + + dotnet run worker + +Then in another terminal, run the workflow from this directory: + + dotnet run workflow + +The worker terminal will show logs from running the workflow. diff --git a/src/Timer/Subscription.workflow.cs b/src/Timer/Subscription.workflow.cs new file mode 100644 index 0000000..2d8bc55 --- /dev/null +++ b/src/Timer/Subscription.workflow.cs @@ -0,0 +1,32 @@ +namespace TemporalioSamples.Timer; + +using Microsoft.Extensions.Logging; +using Temporalio.Workflows; + +[Workflow] +public class Subscription +{ + [WorkflowRun] + public async Task RunAsync(string userId) + { + try + { + while (true) + { + await Workflow.DelayAsync(TimeSpan.FromDays(30)); + + var result = await Workflow.ExecuteActivityAsync( + () => MyActivities.Charge(userId), + new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) }); + Workflow.Logger.LogInformation("Activity result: {Result}", result); + } + } + catch (Exception e) when (TemporalException.IsCanceledException(e)) + { + Workflow.Logger.LogInformation("Workflow cancelled, cleaning up..."); + // Handle any cleanup here + // Re-throw to close the workflow as Cancelled. Otherwise, it will be closed as Completed. + throw; + } + } +} \ No newline at end of file diff --git a/src/Timer/TemporalioSamples.Timer.csproj b/src/Timer/TemporalioSamples.Timer.csproj new file mode 100644 index 0000000..e3b6154 --- /dev/null +++ b/src/Timer/TemporalioSamples.Timer.csproj @@ -0,0 +1,7 @@ + + + + Exe + + + \ No newline at end of file