Skip to content
Draft
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 @@ -18,6 +18,7 @@ Prerequisites:
* [AspNet](src/AspNet) - Demonstration of a generic host worker and an ASP.NET workflow starter.
* [Bedrock](src/Bedrock) - Orchestrate a chatbot with Amazon Bedrock.
* [ClientMtls](src/ClientMtls) - How to use client certificate authentication, e.g. for Temporal Cloud.
* [Gcp/CloudRun/WorkerId](src/Gcp/CloudRun/WorkerId) - Run a long-lived Temporal Worker on a Google Cloud Run worker pool, deriving the worker identity from Cloud Run metadata.
* [ContextPropagation](src/ContextPropagation) - Context propagation via interceptors.
* [CounterInterceptor](src/CounterInterceptor/) - Simple Workflow and Client Interceptors example.
* [DependencyInjection](src/DependencyInjection) - How to inject dependencies in activities and use generic hosts for workers
Expand Down
14 changes: 14 additions & 0 deletions TemporalioSamples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SearchAttributes", "SearchA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.SearchAttributes", "src\SearchAttributes\TemporalioSamples.SearchAttributes.csproj", "{97376F57-BA10-464B-AFBD-583E187DE947}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Gcp.CloudRun.WorkerId", "src\Gcp\CloudRun\WorkerId\TemporalioSamples.Gcp.CloudRun.WorkerId.csproj", "{995322ED-0CF3-40F5-B521-380A9591DACC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -799,6 +801,18 @@ Global
{97376F57-BA10-464B-AFBD-583E187DE947}.Release|x64.Build.0 = Release|Any CPU
{97376F57-BA10-464B-AFBD-583E187DE947}.Release|x86.ActiveCfg = Release|Any CPU
{97376F57-BA10-464B-AFBD-583E187DE947}.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
Expand Down
16 changes: 16 additions & 0 deletions src/Gcp/CloudRun/WorkerId/Activities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace TemporalioSamples.Gcp.CloudRun.WorkerId;

using Microsoft.Extensions.Logging;
using Temporalio.Activities;

public static class Activities
{
[Activity]
public static string SayHello(string name)
{
ActivityExecutionContext.Current.Logger.LogInformation(
"SayHello activity invoked with name: {Name}",
name);
return $"Hello, {name}!";
}
}
81 changes: 81 additions & 0 deletions src/Gcp/CloudRun/WorkerId/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Temporalio.Client;
using Temporalio.Extensions.Gcp.CloudRun.WorkerId;
using Temporalio.Worker;
using TemporalioSamples.Gcp.CloudRun.WorkerId;

// Cloud Run injects these via `--set-env-vars`; fall back to a local dev server for convenience.
// @@@SNIPSTART dotnet-cloud-run-worker-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("CloudRunWorkerId");

// Register the Cloud Run plugin once on the client. At connect time it reads the Cloud Run instance
// id from the metadata server, and the worker pool / service name and revision from the environment,
// then sets the client Identity to the worker identity "{instanceId}@{revision}" (unless one was
// already configured). Every worker created from this client inherits that identity. The plugin only
// sets the worker identity; it does not configure anything else.
//
// NOTE: this requires the process to be running on a Cloud Run worker pool or service. Running it
// elsewhere throws at connect time because the metadata server is unreachable.
var clientOptions = new TemporalClientConnectOptions(address)
{
Namespace = temporalNamespace,
LoggerFactory = loggerFactory,
Plugins = new[] { new WorkerIdPlugin() },
};

var client = await TemporalClient.ConnectAsync(clientOptions);
// @@@SNIPEND

// The plugin already applied this identity to the client above; read the metadata directly to log
// the worker identity this process runs under.
var metadata = await GoogleCloudRunMetadata.FetchAsync();
logger.LogInformation("Cloud Run worker identity: {WorkerIdentity}", metadata.WorkerIdentity);

var workerOptions = new TemporalWorkerOptions(taskQueue).
AddWorkflow<SampleWorkflow>().
AddActivity(Activities.SayHello);

using var worker = new TemporalWorker(client, workerOptions);

// Cloud Run sends SIGTERM before stopping an instance; shut the worker down gracefully on that and
// on Ctrl+C so in-flight tasks can finish.
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;
146 changes: 146 additions & 0 deletions src/Gcp/CloudRun/WorkerId/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Cloud Run Worker Identity

This sample demonstrates how to run a long-lived Temporal Worker on
[Google Cloud Run](https://cloud.google.com/run) and derive its worker identity
from Cloud Run metadata using the `Temporalio.Extensions.Gcp.CloudRun.WorkerId`
package.

The sample registers a greeting Workflow and Activity and polls until the
container is stopped.

## How it works

Unlike the AWS Lambda extension, Cloud Run runs a long-lived container, so this
is a metadata-driven plugin rather than a worker wrapper. The sample registers a
single `WorkerIdPlugin` on `TemporalClientConnectOptions.Plugins`. At connect
time the plugin reads the instance id from the Cloud Run metadata server and the
name and revision from the environment, then sets the client `Identity` to
`{instanceId}@{revision}` (only when an identity is not already set). Every
Worker created from that client inherits the identity.

The name and revision come from the environment that Cloud Run injects:

| Value | Worker pool variable | Service variable |
| -------- | ----------------------- | ---------------- |
| Name | `CLOUD_RUN_WORKER_POOL` | `K_SERVICE` |
| Revision | `CLOUD_RUN_REVISION` | `K_REVISION` |

The worker-pool variables take precedence, so the same code works on a Cloud Run
[worker pool](https://cloud.google.com/run/docs/deploy-worker-pools) or a Cloud
Run service.

`Program.cs` also reads `GoogleCloudRunMetadata` directly and logs its
`WorkerIdentity`, so the identity the Worker runs under is visible in the logs.

### Why worker pools

A Cloud Run **worker pool** runs one or more long-lived container instances that
receive no inbound HTTP requests and are not scaled by request traffic, which is
exactly the shape of a Temporal Worker that polls a Task Queue. Each instance
gets a stable worker identity from its Cloud Run instance id and revision.

## Unreleased dependency

`Temporalio.Extensions.Gcp.CloudRun.WorkerId` is not published to NuGet yet, so this
sample cannot be built or deployed from a released package. To let it build
locally, `TemporalioSamples.Gcp.CloudRun.WorkerId.csproj` references the SDK from a
sibling checkout of [sdk-dotnet](https://github.com/temporalio/sdk-dotnet) laid
out next to this repository:

```text
temporalio/
samples-dotnet/ # this repo
sdk-dotnet-2/ # https://github.com/temporalio/sdk-dotnet on branch cloud-run-worker-id
```

Once the package is released, delete the temporary item groups in the `.csproj`
(the `Temporalio*` `PackageReference Remove` entries and the `ProjectReference`
entries) and replace them with:

```xml
<ItemGroup>
<PackageReference Include="Temporalio.Extensions.Gcp.CloudRun.WorkerId" />
</ItemGroup>
```

This is why the pull request that adds this sample is a draft.

## Prerequisites

- A [Temporal Cloud](https://temporal.io/cloud) namespace, or a self-hosted
Temporal cluster the worker pool can reach
- A Google Cloud project with billing enabled and the Cloud Run API enabled
- The [`gcloud` CLI](https://cloud.google.com/sdk/docs/install), authenticated
(`gcloud auth login`) with the project set (`gcloud config set project ...`)
- The [Temporal CLI](https://docs.temporal.io/cli)
- .NET 8 to build locally

## Configuration

The worker reads its connection settings from the environment; Cloud Run injects
these through `--set-env-vars`:

| Variable | Default | Description |
| --------------------- | -------------------------- | ----------------------------------- |
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal frontend `host:port`. |
| `TEMPORAL_NAMESPACE` | `default` | Temporal namespace. |
| `TEMPORAL_TASK_QUEUE` | `cloud-run-worker-sample` | Task Queue the worker polls. |

This sample uses a plaintext connection for brevity. For Temporal Cloud, add API
key / mTLS configuration to `Program.cs` before deploying.

## 1. Deploy to a Cloud Run worker pool

Deploy the sample as a worker pool from source (Cloud Build packages the
container with the .NET buildpack). Worker pools are currently a preview feature
and may require the `beta`/`alpha` track:

```bash
export REGION="us-central1"
export WORKER_POOL="temporal-dotnet-worker"

gcloud run worker-pools deploy "$WORKER_POOL" \
--source . \
--region "$REGION" \
--set-env-vars \
TEMPORAL_ADDRESS=<your-namespace>.<account>.tmprl.cloud:7233,TEMPORAL_NAMESPACE=<your-namespace>.<account>,TEMPORAL_TASK_QUEUE=cloud-run-worker-sample
```

Run this from `src/Gcp/CloudRun/WorkerId`. Cloud Run sets `CLOUD_RUN_WORKER_POOL`
to `$WORKER_POOL` and `CLOUD_RUN_REVISION` to the revision it creates, which the
plugin turns into the worker identity `{instanceId}@{revision}`.

## 2. Start a Workflow

Start the greeting Workflow on the same Task Queue and wait for the result:

```bash
temporal workflow execute \
--task-queue cloud-run-worker-sample \
--type SampleWorkflow \
--workflow-id cloud-run-worker-sample-1 \
--input '"Cloud Run"'
```

A successful run returns `"Hello, Cloud Run!"`.

## 3. Clean up

Delete the worker pool:

```bash
gcloud run worker-pools delete "$WORKER_POOL" --region "$REGION"
```

## Build locally

With the sibling `sdk-dotnet-2` checkout in place (see
[Unreleased dependency](#unreleased-dependency)):

```bash
dotnet build src/Gcp/CloudRun/WorkerId
```

Running the worker outside Cloud Run fails at startup because the Cloud Run
metadata server is unreachable; deploy it to a worker pool (or service) to run
it.
21 changes: 21 additions & 0 deletions src/Gcp/CloudRun/WorkerId/SampleWorkflow.workflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace TemporalioSamples.Gcp.CloudRun.WorkerId;

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

// A minimal greeting workflow that runs one activity. The WorkerIdPlugin only sets the worker
// identity, so this workflow runs exactly as it would on any other worker.
[Workflow]
public class SampleWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(string name)
{
Workflow.Logger.LogInformation("SampleWorkflow started with name: {Name}", name);
var result = await Workflow.ExecuteActivityAsync(
() => Activities.SayHello(name),
new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });
Workflow.Logger.LogInformation("SampleWorkflow completed with result: {Result}", result);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<!--
TEMPORARY WIRING FOR THE UNRELEASED HELPER.

This sample depends on Temporalio.Extensions.Gcp.CloudRun.WorkerId, which is not published to NuGet yet.
While it is unreleased we reference the SDK from a sibling checkout of
https://github.com/temporalio/sdk-dotnet (branch `cloud-run-worker-id`) laid out next to this
repo (../../../../../sdk-dotnet-2), and remove the Temporalio* package references that
Directory.Build.props adds by default so the local project references are used instead.

Once Temporalio.Extensions.Gcp.CloudRun.WorkerId is released, DELETE both item groups below and add:
<ItemGroup>
<PackageReference Include="Temporalio.Extensions.Gcp.CloudRun.WorkerId" />
</ItemGroup>
-->
<ItemGroup>
<PackageReference Remove="Temporalio" />
<PackageReference Remove="Temporalio.Extensions.DiagnosticSource" />
<PackageReference Remove="Temporalio.Extensions.Hosting" />
<PackageReference Remove="Temporalio.Extensions.OpenTelemetry" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\..\..\..\sdk-dotnet-2\src\Temporalio\Temporalio.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\..\..\..\sdk-dotnet-2\src\Temporalio.Extensions.Gcp.CloudRun.WorkerId\Temporalio.Extensions.Gcp.CloudRun.WorkerId.csproj" />
</ItemGroup>

</Project>
Loading