From 8afca44655018dcfa06451948600dc9ddcdfd94a Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Thu, 27 Nov 2025 18:55:57 +0530 Subject: [PATCH 1/6] add sample for refreshing TemporalClient at Worker --- README.md | 1 + TemporalioSamples.sln | 17 ++- src/RefreshingClient/MyActivities.cs | 23 ++++ src/RefreshingClient/MyWorkflow.workflow.cs | 33 ++++++ src/RefreshingClient/Program.cs | 111 ++++++++++++++++++ src/RefreshingClient/README.md | 15 +++ .../TemporalioSamples.RefreshingClient.csproj | 10 ++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/RefreshingClient/MyActivities.cs create mode 100644 src/RefreshingClient/MyWorkflow.workflow.cs create mode 100644 src/RefreshingClient/Program.cs create mode 100644 src/RefreshingClient/README.md create mode 100644 src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj diff --git a/README.md b/README.md index c325158..d823904 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Prerequisites: * [OpenTelemetry](src/OpenTelemetry) - Demonstrates how to set up OpenTelemetry tracing and metrics for both the client and worker, using both the .NET metrics API and internal forwarding from the Core SDK. * [Patching](src/Patching) - Alter workflows safely with Patch and DeprecatePatch. * [Polling](src/Polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion. +* [RefreshingClient](src/RefreshingClient) - Recommended implementation of a Worker, resfreshing the client periodically every 10 seconds. * [SafeMessageHandlers](src/SafeMessageHandlers) - Use `Semaphore` to ensure operations are atomically processed in a workflow. * [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. diff --git a/TemporalioSamples.sln b/TemporalioSamples.sln index d181ea5..7afff65 100644 --- a/TemporalioSamples.sln +++ b/TemporalioSamples.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -98,6 +98,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.EnvConfig EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Timer", "src\Timer\TemporalioSamples.Timer.csproj", "{B37B3E98-4B04-48B8-9017-F0EDEDC7BD98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.RefreshingClient", "src\RefreshingClient\TemporalioSamples.RefreshingClient.csproj", "{9654050C-AA1E-4376-BA4E-8190D1842818}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -588,6 +590,18 @@ Global {B37B3E98-4B04-48B8-9017-F0EDEDC7BD98}.Release|x64.Build.0 = Release|Any CPU {B37B3E98-4B04-48B8-9017-F0EDEDC7BD98}.Release|x86.ActiveCfg = Release|Any CPU {B37B3E98-4B04-48B8-9017-F0EDEDC7BD98}.Release|x86.Build.0 = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|x64.ActiveCfg = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|x64.Build.0 = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|x86.ActiveCfg = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Debug|x86.Build.0 = Debug|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|Any CPU.Build.0 = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|x64.ActiveCfg = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|x64.Build.0 = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|x86.ActiveCfg = Release|Any CPU + {9654050C-AA1E-4376-BA4E-8190D1842818}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -637,5 +651,6 @@ Global {8BE23F78-7178-4924-AB45-4AF74454CC97} = {18E26AEE-5DA3-7BF8-A1AD-13A28A6C7BA3} {52CE80AF-09C3-4209-8A21-6CFFAA3B2B01} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC} {B37B3E98-4B04-48B8-9017-F0EDEDC7BD98} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC} + {9654050C-AA1E-4376-BA4E-8190D1842818} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC} EndGlobalSection EndGlobal diff --git a/src/RefreshingClient/MyActivities.cs b/src/RefreshingClient/MyActivities.cs new file mode 100644 index 0000000..c7ec236 --- /dev/null +++ b/src/RefreshingClient/MyActivities.cs @@ -0,0 +1,23 @@ +namespace TemporalioSamples.ActivitySimple; + +using Temporalio.Activities; + +public class MyActivities +{ + private readonly MyDatabaseClient dbClient = new(); + + // Activities can be static and/or sync + [Activity] + public static string DoStaticThing() => "some-static-value"; + + // Activities can be methods that can access state + [Activity] + public Task SelectFromDatabaseAsync(string table) => + dbClient.SelectValueAsync(table); + + public class MyDatabaseClient + { + public Task SelectValueAsync(string table) => + Task.FromResult($"some-db-value from table {table}"); + } +} \ No newline at end of file diff --git a/src/RefreshingClient/MyWorkflow.workflow.cs b/src/RefreshingClient/MyWorkflow.workflow.cs new file mode 100644 index 0000000..11347df --- /dev/null +++ b/src/RefreshingClient/MyWorkflow.workflow.cs @@ -0,0 +1,33 @@ +namespace TemporalioSamples.ActivitySimple; + +using Microsoft.Extensions.Logging; +using Temporalio.Workflows; + +[Workflow] +public class MyWorkflow +{ + [WorkflowRun] + public async Task RunAsync() + { + // Run an async instance method activity. + var result1 = await Workflow.ExecuteActivityAsync( + (MyActivities act) => act.SelectFromDatabaseAsync("some-db-table"), + new() + { + StartToCloseTimeout = TimeSpan.FromMinutes(5), + }); + Workflow.Logger.LogInformation("Activity instance method result: {Result}", result1); + + // Run a sync static method activity. + var result2 = await Workflow.ExecuteActivityAsync( + () => MyActivities.DoStaticThing(), + new() + { + StartToCloseTimeout = TimeSpan.FromMinutes(5), + }); + Workflow.Logger.LogInformation("Activity static method result: {Result}", result2); + + // We'll go ahead and return this result + return result2; + } +} \ No newline at end of file diff --git a/src/RefreshingClient/Program.cs b/src/RefreshingClient/Program.cs new file mode 100644 index 0000000..57d1508 --- /dev/null +++ b/src/RefreshingClient/Program.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging; +using Temporalio.Client; +using Temporalio.Client.EnvConfig; +using Temporalio.Worker; +using TemporalioSamples.ActivitySimple; + +async Task CreateClientAsync() +{ + var connectOptions = ClientEnvConfig.LoadClientConnectOptions(); + if (string.IsNullOrEmpty(connectOptions.TargetHost)) + { + connectOptions.TargetHost = "localhost:7233"; + } + connectOptions.LoggerFactory = LoggerFactory.Create(builder => + builder. + AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] "). + SetMinimumLevel(LogLevel.Information)); + return await TemporalClient.ConnectAsync(connectOptions); +} + +async Task RunWorkerAsync(TemporalClient client) +{ + // 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: "activity-simple-sample"). + AddActivity(activities.SelectFromDatabaseAsync). + AddActivity(MyActivities.DoStaticThing). + AddWorkflow()); + + var replaceWorkerClient = (TemporalClient newClient) => + { + worker.Client = newClient; + return Task.FromResult(true); + }; + + try + { + await Task.WhenAll(ClientRefreshAsync(replaceWorkerClient, tokenSource.Token), worker.ExecuteAsync(tokenSource.Token)); + } + catch (OperationCanceledException) + { + Console.WriteLine("Worker cancelled"); + } +} + +async Task ExecuteWorkflowAsync(TemporalClient client) +{ + Console.WriteLine("Executing workflow"); + await client.ExecuteWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync(), + new(id: "activity-simple-workflow-id", taskQueue: "activity-simple-sample")); +} + +async Task ClientRefreshAsync(Func asyncFunc, CancellationToken cancellationToken) +{ + Console.WriteLine("This program will refresh its Temporal client every 10 seconds."); + await RunRecurringTaskAsync(TimeSpan.FromSeconds(10), cancellationToken, asyncFunc); +} + +async Task RunRecurringTaskAsync(TimeSpan interval, CancellationToken cancellationToken, Func asyncFunc) +{ + await Task.Delay(interval, cancellationToken); + while (!cancellationToken.IsCancellationRequested) + { + Console.WriteLine("Refreshing client..."); + try + { + var client = await CreateClientAsync(); + await asyncFunc(client); + await Task.Delay(interval, cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Task cancelled."); + break; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + // Continue running even if one iteration fails + } +#pragma warning restore CA1031 // Do not catch general exception types + } +} + +var client = await CreateClientAsync(); +switch (args.ElementAtOrDefault(0)) +{ + case "worker": + await RunWorkerAsync(client); + break; + case "workflow": + await ExecuteWorkflowAsync(client); + break; + default: + throw new ArgumentException("Must pass 'worker' or 'workflow' as the single argument"); +} \ No newline at end of file diff --git a/src/RefreshingClient/README.md b/src/RefreshingClient/README.md new file mode 100644 index 0000000..7dfd5d0 --- /dev/null +++ b/src/RefreshingClient/README.md @@ -0,0 +1,15 @@ +# Activity Simple + +This sample shows a workflow executing a synchronous static activity method and an asynchronous instance activity method. +The Worker program will refresh the Temporal client periodically every 10 seconds. + +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 a workflow every second from this directory: + + watch -n1 dotnet run workflow + +This will show logs in the worker window of the workflow running. \ No newline at end of file diff --git a/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj b/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj new file mode 100644 index 0000000..206b89a --- /dev/null +++ b/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + From a88e34558706bf6aabca2968ad43e48aaf1acb57 Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Thu, 27 Nov 2025 19:02:48 +0530 Subject: [PATCH 2/6] fix namespace, readme and reduce refresh to 10 seconds --- README.md | 2 +- src/RefreshingClient/MyActivities.cs | 2 +- src/RefreshingClient/MyWorkflow.workflow.cs | 2 +- src/RefreshingClient/Program.cs | 2 +- src/RefreshingClient/README.md | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d823904..22e5ad0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Prerequisites: * [OpenTelemetry](src/OpenTelemetry) - Demonstrates how to set up OpenTelemetry tracing and metrics for both the client and worker, using both the .NET metrics API and internal forwarding from the Core SDK. * [Patching](src/Patching) - Alter workflows safely with Patch and DeprecatePatch. * [Polling](src/Polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion. -* [RefreshingClient](src/RefreshingClient) - Recommended implementation of a Worker, resfreshing the client periodically every 10 seconds. +* [RefreshingClient](src/RefreshingClient) - Demonstrates how to periodically refresh the Temporal client in a Worker every 10 seconds. * [SafeMessageHandlers](src/SafeMessageHandlers) - Use `Semaphore` to ensure operations are atomically processed in a workflow. * [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. diff --git a/src/RefreshingClient/MyActivities.cs b/src/RefreshingClient/MyActivities.cs index c7ec236..7533255 100644 --- a/src/RefreshingClient/MyActivities.cs +++ b/src/RefreshingClient/MyActivities.cs @@ -1,4 +1,4 @@ -namespace TemporalioSamples.ActivitySimple; +namespace TemporalioSamples.RefreshingClient; using Temporalio.Activities; diff --git a/src/RefreshingClient/MyWorkflow.workflow.cs b/src/RefreshingClient/MyWorkflow.workflow.cs index 11347df..b9be245 100644 --- a/src/RefreshingClient/MyWorkflow.workflow.cs +++ b/src/RefreshingClient/MyWorkflow.workflow.cs @@ -1,4 +1,4 @@ -namespace TemporalioSamples.ActivitySimple; +namespace TemporalioSamples.RefreshingClient; using Microsoft.Extensions.Logging; using Temporalio.Workflows; diff --git a/src/RefreshingClient/Program.cs b/src/RefreshingClient/Program.cs index 57d1508..2329b70 100644 --- a/src/RefreshingClient/Program.cs +++ b/src/RefreshingClient/Program.cs @@ -2,7 +2,7 @@ using Temporalio.Client; using Temporalio.Client.EnvConfig; using Temporalio.Worker; -using TemporalioSamples.ActivitySimple; +using TemporalioSamples.RefreshingClient; async Task CreateClientAsync() { diff --git a/src/RefreshingClient/README.md b/src/RefreshingClient/README.md index 7dfd5d0..5422892 100644 --- a/src/RefreshingClient/README.md +++ b/src/RefreshingClient/README.md @@ -1,7 +1,7 @@ -# Activity Simple +# Refreshing Client -This sample shows a workflow executing a synchronous static activity method and an asynchronous instance activity method. -The Worker program will refresh the Temporal client periodically every 10 seconds. +This sample demonstrates how to periodically refresh the Temporal client in a Worker. +The Worker program refreshes the Temporal client every 10 seconds, which is useful for scenarios requiring credential mTLS or api key rotation. 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: From b972d4a9a78bcb559418b103c847b3bf24a3be7a Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Fri, 28 Nov 2025 07:55:01 +0530 Subject: [PATCH 3/6] minor: sequence of delay and refresh --- src/RefreshingClient/Program.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/RefreshingClient/Program.cs b/src/RefreshingClient/Program.cs index 2329b70..f0c6368 100644 --- a/src/RefreshingClient/Program.cs +++ b/src/RefreshingClient/Program.cs @@ -72,15 +72,14 @@ async Task ClientRefreshAsync(Func asyncFunc, Cancellation async Task RunRecurringTaskAsync(TimeSpan interval, CancellationToken cancellationToken, Func asyncFunc) { - await Task.Delay(interval, cancellationToken); while (!cancellationToken.IsCancellationRequested) { - Console.WriteLine("Refreshing client..."); try { + await Task.Delay(interval, cancellationToken); + Console.WriteLine("Refreshing client..."); var client = await CreateClientAsync(); await asyncFunc(client); - await Task.Delay(interval, cancellationToken); } catch (OperationCanceledException) { From 1b1b9da9c6a113e8e67d256cfaff65bceefe47d3 Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Fri, 28 Nov 2025 07:58:31 +0530 Subject: [PATCH 4/6] add readme details --- src/RefreshingClient/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/RefreshingClient/README.md b/src/RefreshingClient/README.md index 5422892..de22401 100644 --- a/src/RefreshingClient/README.md +++ b/src/RefreshingClient/README.md @@ -3,6 +3,8 @@ This sample demonstrates how to periodically refresh the Temporal client in a Worker. The Worker program refreshes the Temporal client every 10 seconds, which is useful for scenarios requiring credential mTLS or api key rotation. +`ClientRefreshAsync` accepts a Func to deliver a new client, to replace the callers Worker client. + 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: From 456b1ce90c47ce30933c96e7aabd3e096557ff6d Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Fri, 28 Nov 2025 08:29:06 +0530 Subject: [PATCH 5/6] print new handle to console --- src/RefreshingClient/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RefreshingClient/Program.cs b/src/RefreshingClient/Program.cs index f0c6368..1694256 100644 --- a/src/RefreshingClient/Program.cs +++ b/src/RefreshingClient/Program.cs @@ -43,6 +43,7 @@ async Task RunWorkerAsync(TemporalClient client) var replaceWorkerClient = (TemporalClient newClient) => { worker.Client = newClient; + Console.WriteLine("Client's new handle: {0}", worker.Client.BridgeClientProvider?.BridgeClient?.DangerousGetHandle()); return Task.FromResult(true); }; From ea5e468669047c61e04afc6d844cc94fcffd2ed2 Mon Sep 17 00:00:00 2001 From: Gaurav Thadani Date: Tue, 2 Dec 2025 11:58:48 +0530 Subject: [PATCH 6/6] change rotation to 2 hours --- README.md | 2 +- src/RefreshingClient/Program.cs | 9 +++++---- src/RefreshingClient/README.md | 2 +- .../TemporalioSamples.RefreshingClient.csproj | 3 --- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 22e5ad0..6239bd5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Prerequisites: * [OpenTelemetry](src/OpenTelemetry) - Demonstrates how to set up OpenTelemetry tracing and metrics for both the client and worker, using both the .NET metrics API and internal forwarding from the Core SDK. * [Patching](src/Patching) - Alter workflows safely with Patch and DeprecatePatch. * [Polling](src/Polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion. -* [RefreshingClient](src/RefreshingClient) - Demonstrates how to periodically refresh the Temporal client in a Worker every 10 seconds. +* [RefreshingClient](src/RefreshingClient) - Demonstrates how to periodically refresh the Temporal client in a Worker every 2 hours. * [SafeMessageHandlers](src/SafeMessageHandlers) - Use `Semaphore` to ensure operations are atomically processed in a workflow. * [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. diff --git a/src/RefreshingClient/Program.cs b/src/RefreshingClient/Program.cs index 1694256..8f2cb11 100644 --- a/src/RefreshingClient/Program.cs +++ b/src/RefreshingClient/Program.cs @@ -43,7 +43,7 @@ async Task RunWorkerAsync(TemporalClient client) var replaceWorkerClient = (TemporalClient newClient) => { worker.Client = newClient; - Console.WriteLine("Client's new handle: {0}", worker.Client.BridgeClientProvider?.BridgeClient?.DangerousGetHandle()); + Console.WriteLine("Worker's client has been refreshed."); return Task.FromResult(true); }; @@ -67,8 +67,9 @@ await client.ExecuteWorkflowAsync( async Task ClientRefreshAsync(Func asyncFunc, CancellationToken cancellationToken) { - Console.WriteLine("This program will refresh its Temporal client every 10 seconds."); - await RunRecurringTaskAsync(TimeSpan.FromSeconds(10), cancellationToken, asyncFunc); + // Change the frequency of rotation as per your requirements + Console.WriteLine("This program will refresh its Temporal client every 2 hours."); + await RunRecurringTaskAsync(TimeSpan.FromHours(2), cancellationToken, asyncFunc); } async Task RunRecurringTaskAsync(TimeSpan interval, CancellationToken cancellationToken, Func asyncFunc) @@ -84,7 +85,7 @@ async Task RunRecurringTaskAsync(TimeSpan interval, CancellationToken cancellati } catch (OperationCanceledException) { - Console.WriteLine("Task cancelled."); + Console.WriteLine("Refreshing task cancelled."); break; } #pragma warning disable CA1031 // Do not catch general exception types diff --git a/src/RefreshingClient/README.md b/src/RefreshingClient/README.md index de22401..c585f67 100644 --- a/src/RefreshingClient/README.md +++ b/src/RefreshingClient/README.md @@ -1,7 +1,7 @@ # Refreshing Client This sample demonstrates how to periodically refresh the Temporal client in a Worker. -The Worker program refreshes the Temporal client every 10 seconds, which is useful for scenarios requiring credential mTLS or api key rotation. +The Worker program refreshes the Temporal client every 2 hours, which is useful for scenarios requiring credential mTLS or api key rotation. `ClientRefreshAsync` accepts a Func to deliver a new client, to replace the callers Worker client. diff --git a/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj b/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj index 206b89a..967ff4c 100644 --- a/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj +++ b/src/RefreshingClient/TemporalioSamples.RefreshingClient.csproj @@ -2,9 +2,6 @@ Exe - net8.0 - enable - enable