diff --git a/Directory.Packages.props b/Directory.Packages.props index 38177b2..e852a05 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/README.md b/README.md index a09c161..fb9d886 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Prerequisites: * [EagerWorkflowStart](src/EagerWorkflowStart) - Demonstrates usage of Eager Workflow Start to reduce latency for workflows that start with a local activity. * [Encryption](src/Encryption) - End-to-end encryption with Temporal payload codecs. * [EnvConfig](src/EnvConfig) - Load client configuration from TOML files with programmatic overrides +* [Gcp/CloudRun/Id](src/Gcp/CloudRun/Id) - Run a Temporal Worker on a Google Cloud Run worker pool, deriving the client identity from Cloud Run metadata. * [LambdaWorker](src/LambdaWorker) - Run a Temporal Worker inside an AWS Lambda function. * [Mutex](src/Mutex) - How to implement a mutex as a workflow. Demonstrates how to avoid race conditions or parallel mutually exclusive operations on the same resource. * [NexusCancellation](src/NexusCancellation) - Demonstrates how to cancel a running Nexus operation from a caller workflow. diff --git a/TemporalioSamples.sln b/TemporalioSamples.sln index c945182..90ca1d2 100644 --- a/TemporalioSamples.sln +++ b/TemporalioSamples.sln @@ -145,6 +145,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NexusStandaloneActivity", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.NexusStandaloneActivity", "src\NexusStandaloneActivity\TemporalioSamples.NexusStandaloneActivity.csproj", "{4D8C9F9B-F8E3-4160-9286-32C3966A0125}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Gcp.CloudRun.Id", "src\Gcp\CloudRun\Id\TemporalioSamples.Gcp.CloudRun.Id.csproj", "{995322ED-0CF3-40F5-B521-380A9591DACC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -815,6 +817,18 @@ Global {4D8C9F9B-F8E3-4160-9286-32C3966A0125}.Release|x64.Build.0 = Release|Any CPU {4D8C9F9B-F8E3-4160-9286-32C3966A0125}.Release|x86.ActiveCfg = Release|Any CPU {4D8C9F9B-F8E3-4160-9286-32C3966A0125}.Release|x86.Build.0 = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|x64.ActiveCfg = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|x64.Build.0 = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|x86.ActiveCfg = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Debug|x86.Build.0 = Debug|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|Any CPU.Build.0 = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|x64.ActiveCfg = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|x64.Build.0 = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|x86.ActiveCfg = Release|Any CPU + {995322ED-0CF3-40F5-B521-380A9591DACC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..f5652aa --- /dev/null +++ b/nuget.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Gcp/CloudRun/Id/Activities.cs b/src/Gcp/CloudRun/Id/Activities.cs new file mode 100644 index 0000000..9004a8b --- /dev/null +++ b/src/Gcp/CloudRun/Id/Activities.cs @@ -0,0 +1,14 @@ +namespace TemporalioSamples.Gcp.CloudRun.Id; + +using Microsoft.Extensions.Logging; +using Temporalio.Activities; + +public static class Activities +{ + [Activity] + public static string SayHello(string name) + { + ActivityExecutionContext.Current.Logger.LogInformation("SayHello activity: {Name}", name); + return $"Hello, {name}!"; + } +} diff --git a/src/Gcp/CloudRun/Id/Dockerfile b/src/Gcp/CloudRun/Id/Dockerfile new file mode 100644 index 0000000..923fe5a --- /dev/null +++ b/src/Gcp/CloudRun/Id/Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:1 +# Build from the repo root: docker build -f src/Gcp/CloudRun/Id/Dockerfile -t . +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY global.json Directory.Build.props Directory.Packages.props .editorconfig nuget.config ./ +COPY src/Gcp/CloudRun/Id/ ./src/Gcp/CloudRun/Id/ +RUN dotnet publish src/Gcp/CloudRun/Id/TemporalioSamples.Gcp.CloudRun.Id.csproj -c Release -o /app + +FROM mcr.microsoft.com/dotnet/runtime:8.0 +RUN useradd --create-home --uid 10001 worker +WORKDIR /app +COPY --from=build --chown=worker:worker /app ./ +USER 10001 +ENTRYPOINT ["dotnet", "TemporalioSamples.Gcp.CloudRun.Id.dll"] diff --git a/src/Gcp/CloudRun/Id/Program.cs b/src/Gcp/CloudRun/Id/Program.cs new file mode 100644 index 0000000..1b992d9 --- /dev/null +++ b/src/Gcp/CloudRun/Id/Program.cs @@ -0,0 +1,69 @@ +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Temporalio.Client; +using Temporalio.Extensions.Gcp.CloudRun.Id; +using Temporalio.Worker; +using TemporalioSamples.Gcp.CloudRun.Id; + +// @@@SNIPSTART dotnet-cloud-run-id +var address = GetEnvironmentVariable("TEMPORAL_ADDRESS") ?? "localhost:7233"; +var temporalNamespace = GetEnvironmentVariable("TEMPORAL_NAMESPACE") ?? "default"; +var taskQueue = GetEnvironmentVariable("TEMPORAL_TASK_QUEUE") ?? "cloud-run-worker-sample"; + +using var loggerFactory = LoggerFactory.Create(builder => builder. + AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] "). + SetMinimumLevel(LogLevel.Information)); +var logger = loggerFactory.CreateLogger("CloudRunId"); + +// Derives the client identity "{instanceId}@{revision}" from Cloud Run metadata at connect time. +var clientOptions = new TemporalClientConnectOptions(address) +{ + Namespace = temporalNamespace, + LoggerFactory = loggerFactory, + Plugins = new[] { new CloudRunIdPlugin() }, +}; + +var client = await TemporalClient.ConnectAsync(clientOptions); +// @@@SNIPEND +var metadata = await GoogleCloudRunMetadata.FetchAsync(); +logger.LogInformation("Cloud Run identity: {Identity}", metadata.Identity); + +var workerOptions = new TemporalWorkerOptions(taskQueue). + AddWorkflow(). + AddActivity(Activities.SayHello); + +using var worker = new TemporalWorker(client, workerOptions); + +// Cloud Run sends SIGTERM before stopping an instance; shut down gracefully on that and Ctrl+C. +using var cancellationSource = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + eventArgs.Cancel = true; + cancellationSource.Cancel(); +}; +using var sigterm = PosixSignalRegistration.Create( + PosixSignal.SIGTERM, + context => + { + context.Cancel = true; + cancellationSource.Cancel(); + }); + +logger.LogInformation( + "Worker started on task queue '{TaskQueue}' against {Address} (namespace '{Namespace}').", + taskQueue, + address, + temporalNamespace); +try +{ + await worker.ExecuteAsync(cancellationSource.Token); +} +catch (OperationCanceledException) +{ + logger.LogInformation("Shutdown signal received; worker stopped."); +} + +static string? GetEnvironmentVariable(string name) => + Environment.GetEnvironmentVariable(name) is { } value && !string.IsNullOrWhiteSpace(value) + ? value + : null; diff --git a/src/Gcp/CloudRun/Id/README.md b/src/Gcp/CloudRun/Id/README.md new file mode 100644 index 0000000..90f832a --- /dev/null +++ b/src/Gcp/CloudRun/Id/README.md @@ -0,0 +1,49 @@ +# Cloud Run Id + +Run a Temporal Worker on a +[Google Cloud Run worker pool](https://cloud.google.com/run/docs/deploy-worker-pools) and derive its +identity from Cloud Run instance metadata using the `Temporalio.Extensions.Gcp.CloudRun.Id` package. + +`Program.cs` registers a `CloudRunIdPlugin` on `TemporalClientConnectOptions.Plugins`. At connect +time the plugin sets the client `Identity` to `{instanceId}@{revision}` from the Cloud Run metadata +server and environment, and every Worker created from the client inherits it. A greeting workflow and +activity poll the task queue until the container stops. Worker pools run instances that take no HTTP +traffic, which suits a Temporal Worker; the same code also works on a Cloud Run service. + +> The extension is not on nuget.org yet, so the sample restores it from the committed +> `local-packages/` feed (see `nuget.config`) until it ships. + +## Prerequisites + +- A Temporal server the worker pool can reach +- A Google Cloud project with billing and the Cloud Run and Artifact Registry APIs enabled +- [`gcloud`](https://cloud.google.com/sdk/docs/install), authenticated with the project set +- The [Temporal CLI](https://docs.temporal.io/cli) and .NET 8 + +## Deploy + +Build the image from the repo root (the Dockerfile pulls in the shared props and the local feed), +push it, then deploy a worker pool. Worker pools may require the `beta` track. + +```bash +export REGION=us-central1 WORKER_POOL=temporal-dotnet-worker +export WORKER_IMAGE=$REGION-docker.pkg.dev/$(gcloud config get-value project)/samples/cloud-run-id + +docker build -f src/Gcp/CloudRun/Id/Dockerfile -t "$WORKER_IMAGE" . && docker push "$WORKER_IMAGE" + +gcloud run worker-pools deploy "$WORKER_POOL" --image "$WORKER_IMAGE" --region "$REGION" \ + --set-env-vars TEMPORAL_ADDRESS=,TEMPORAL_NAMESPACE=,TEMPORAL_TASK_QUEUE=cloud-run-worker-sample +``` + +Cloud Run sets `CLOUD_RUN_WORKER_POOL` and `CLOUD_RUN_REVISION`, which the plugin turns into the +client identity. The sample connects in plaintext; for Temporal Cloud add API key / mTLS to +`Program.cs`. + +## Run a workflow + +```bash +temporal workflow execute --task-queue cloud-run-worker-sample --type SampleWorkflow --input '"Cloud Run"' +``` + +A successful run returns `"Hello, Cloud Run!"`. Delete the pool with +`gcloud run worker-pools delete "$WORKER_POOL" --region "$REGION"`. diff --git a/src/Gcp/CloudRun/Id/SampleWorkflow.workflow.cs b/src/Gcp/CloudRun/Id/SampleWorkflow.workflow.cs new file mode 100644 index 0000000..f1db85d --- /dev/null +++ b/src/Gcp/CloudRun/Id/SampleWorkflow.workflow.cs @@ -0,0 +1,19 @@ +namespace TemporalioSamples.Gcp.CloudRun.Id; + +using Microsoft.Extensions.Logging; +using Temporalio.Workflows; + +[Workflow] +public class SampleWorkflow +{ + [WorkflowRun] + public async Task RunAsync(string name) + { + Workflow.Logger.LogInformation("SampleWorkflow started: {Name}", name); + var result = await Workflow.ExecuteActivityAsync( + () => Activities.SayHello(name), + new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) }); + Workflow.Logger.LogInformation("SampleWorkflow completed: {Result}", result); + return result; + } +} diff --git a/src/Gcp/CloudRun/Id/TemporalioSamples.Gcp.CloudRun.Id.csproj b/src/Gcp/CloudRun/Id/TemporalioSamples.Gcp.CloudRun.Id.csproj new file mode 100644 index 0000000..8cc6af6 --- /dev/null +++ b/src/Gcp/CloudRun/Id/TemporalioSamples.Gcp.CloudRun.Id.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/src/Gcp/CloudRun/Id/local-packages/Temporalio.Extensions.Gcp.CloudRun.Id.1.19.0.nupkg b/src/Gcp/CloudRun/Id/local-packages/Temporalio.Extensions.Gcp.CloudRun.Id.1.19.0.nupkg new file mode 100644 index 0000000..36e8cce Binary files /dev/null and b/src/Gcp/CloudRun/Id/local-packages/Temporalio.Extensions.Gcp.CloudRun.Id.1.19.0.nupkg differ