Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) - 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.
Expand Down
17 changes: 16 additions & 1 deletion TemporalioSamples.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
23 changes: 23 additions & 0 deletions src/RefreshingClient/MyActivities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace TemporalioSamples.RefreshingClient;

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<string> SelectFromDatabaseAsync(string table) =>
dbClient.SelectValueAsync(table);

public class MyDatabaseClient
{
public Task<string> SelectValueAsync(string table) =>
Task.FromResult($"some-db-value from table {table}");
}
}
33 changes: 33 additions & 0 deletions src/RefreshingClient/MyWorkflow.workflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace TemporalioSamples.RefreshingClient;

using Microsoft.Extensions.Logging;
using Temporalio.Workflows;

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task<string> 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;
}
}
112 changes: 112 additions & 0 deletions src/RefreshingClient/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Microsoft.Extensions.Logging;
using Temporalio.Client;
using Temporalio.Client.EnvConfig;
using Temporalio.Worker;
using TemporalioSamples.RefreshingClient;

async Task<TemporalClient> 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<MyWorkflow>());

var replaceWorkerClient = (TemporalClient newClient) =>
{
worker.Client = newClient;
Console.WriteLine("Worker's client has been refreshed.");
return Task.FromResult(true);
};

try
{
await Task.WhenAll(ClientRefreshAsync(replaceWorkerClient, tokenSource.Token), worker.ExecuteAsync(tokenSource.Token));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is worth breaking out into 2 more separate methods vs just inlining the loop here, but not a big deal

}
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<TemporalClient, Task> asyncFunc, CancellationToken cancellationToken)
{
// 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<TemporalClient, Task> asyncFunc)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(interval, cancellationToken);
Console.WriteLine("Refreshing client...");
var client = await CreateClientAsync();
await asyncFunc(client);
}
catch (OperationCanceledException)
{
Console.WriteLine("Refreshing 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");
}
17 changes: 17 additions & 0 deletions src/RefreshingClient/README.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm, I wonder if a single client refresh after some time would be a better demonstration. I have not seen this sample in any other Core-based SDKs, so it is a bit strange to only be in .NET, but not a big deal. However, I do not want to encourage the practice of rotating a client every 10 seconds (many calls are up to a minute long anyways, so rotation may happen a few times before the client is even used). I fear the sample demonstrating this kind of frequent rotation may encourage it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that is a valid point. I will change to rotating it every 2 hours for the sample.
And leave a comment for making it configurable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, let me know if you still prefer the demonstration to be once instead of every 2 hours.
I do understand this is not a Workflow pattern, so happy to sample it elsewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for me, but realistically a user may never see the client refresh in the sample. Just want confirmation that's ok with you before merging.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Refreshing Client

This sample demonstrates how to periodically refresh the Temporal client in a Worker.
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.

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