Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda.OpenTelemetry" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.DiagnosticSource" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.Gcp.CloudRun.Id" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.Hosting" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.OpenTelemetry" Version="1.20.0" />
<PackageVersion Include="Temporalio.Extensions.WorkflowStreams" Version="1.20.0" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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](src/Gcp/CloudRun) - Run a Temporal Worker on a Google Cloud Run worker pool using the Cloud Run Id and OpenTelemetry plugins.
* [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.
Expand Down
3 changes: 3 additions & 0 deletions TemporalioSamples.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<Project Path="src/Encryption/Starter/TemporalioSamples.Encryption.Starter.csproj" />
<Project Path="src/Encryption/Worker/TemporalioSamples.Encryption.Worker.csproj" />
</Folder>
<Folder Name="/src/Gcp/">
<Project Path="src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj" />
</Folder>
<Folder Name="/src/LambdaWorker/">
<Project Path="src/LambdaWorker/Starter/TemporalioSamples.LambdaWorker.Starter.csproj" />
<Project Path="src/LambdaWorker/Worker/TemporalioSamples.LambdaWorker.Worker.csproj" />
Expand Down
14 changes: 14 additions & 0 deletions src/Gcp/CloudRun/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# syntax=docker/dockerfile:1
# Build from the repo root: docker build -f src/Gcp/CloudRun/Dockerfile -t <image> .
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY global.json Directory.Build.props Directory.Packages.props .editorconfig ./
COPY src/Gcp/CloudRun/ ./src/Gcp/CloudRun/
RUN dotnet publish src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj -c Release -o /app

FROM mcr.microsoft.com/dotnet/runtime:10.0
RUN useradd --create-home --uid 10001 worker
WORKDIR /app
COPY --from=build --chown=worker:worker /app ./
USER 10001
ENTRYPOINT ["dotnet", "TemporalioSamples.Gcp.CloudRun.dll"]
14 changes: 14 additions & 0 deletions src/Gcp/CloudRun/GreetingActivities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace TemporalioSamples.Gcp.CloudRun;

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

public static class GreetingActivities
{
[Activity]
public static string SayHello(string name)
{
ActivityExecutionContext.Current.Logger.LogInformation("SayHello activity: {Name}", name);
return $"Hello, {name}!";
}
}
19 changes: 19 additions & 0 deletions src/Gcp/CloudRun/GreetingWorkflow.workflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace TemporalioSamples.Gcp.CloudRun;

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

[Workflow]
public class GreetingWorkflow
Comment thread
jmaeagle99 marked this conversation as resolved.
{
[WorkflowRun]
public async Task<string> RunAsync(string name)
{
Workflow.Logger.LogInformation("GreetingWorkflow started: {Name}", name);
var result = await Workflow.ExecuteActivityAsync(
() => GreetingActivities.SayHello(name),
new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });
Workflow.Logger.LogInformation("GreetingWorkflow completed: {Result}", result);
return result;
}
}
73 changes: 73 additions & 0 deletions src/Gcp/CloudRun/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Temporalio.Client;
using Temporalio.Common.EnvConfig;
using Temporalio.Extensions.Gcp.CloudRun.Id;
using Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry;
using Temporalio.Worker;
using TemporalioSamples.Gcp.CloudRun;

// Connect from environment config (TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_API_KEY, ...).
var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
connectOptions.TargetHost ??= "localhost:7233";
connectOptions.LoggerFactory = LoggerFactory.Create(builder =>
builder.
AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
SetMinimumLevel(LogLevel.Information));

var taskQueue = Environment.GetEnvironmentVariable("TEMPORAL_TASK_QUEUE") ?? "cloud-run-worker";

// @@@SNIPSTART dotnet-cloud-run
// The Cloud Run Id plugin sets the client identity to "{instanceId}@{revision}" from Cloud Run
// metadata at connect time; every Worker created from the client inherits it.
connectOptions.Plugins = new ITemporalClientPlugin[] { new CloudRunIdPlugin() };

// ApplyGoogleCloudRunOpenTelemetryDefaults adds the tracing interceptor and a runtime exporting Core
// metrics + traces over OTLP to the collector sidecar; the returned handle owns the tracer provider.
// Both plugins configure the same connect options.
using var telemetry = connectOptions.ApplyGoogleCloudRunOpenTelemetryDefaults();

var client = await TemporalClient.ConnectAsync(connectOptions);
// @@@SNIPEND
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cts.Cancel();
};

// Cloud Run sends SIGTERM ~10s before SIGKILL; defer the default termination so the worker can drain.
using var sigterm = PosixSignalRegistration.Create(
PosixSignal.SIGTERM,
ctx =>
{
ctx.Cancel = true;
cts.Cancel();
});

using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions(taskQueue)
{
GracefulShutdownTimeout = TimeSpan.FromSeconds(5),
}.
AddWorkflow<GreetingWorkflow>().
AddActivity(GreetingActivities.SayHello));

Console.WriteLine(
"Worker running: taskQueue={0} address={1} namespace={2}",
taskQueue,
connectOptions.TargetHost,
connectOptions.Namespace ?? "default");
try
{
await worker.ExecuteAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Worker shutting down");
}

// Flush traces within the shutdown grace window (Core metrics export periodically, no explicit flush).
await telemetry.FlushAsync(TimeSpan.FromSeconds(2));
Console.WriteLine("Worker stopped");
65 changes: 65 additions & 0 deletions src/Gcp/CloudRun/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Cloud Run Worker

Run a Temporal Worker on a
[Google Cloud Run worker pool](https://cloud.google.com/run/docs/deploy-worker-pools) using both GCP
Cloud Run extensions together:

- `Temporalio.Extensions.Gcp.CloudRun.Id` derives the client identity `{instanceId}@{revision}` from
Cloud Run instance metadata, so each instance is identifiable in Temporal.
- `Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry` exports Core SDK metrics and traces to a
[Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run)
sidecar (metrics to Managed Service for Prometheus, traces to Cloud Trace).

`Program.cs` registers a `CloudRunIdPlugin` on `TemporalClientConnectOptions.Plugins` and calls
`ApplyGoogleCloudRunOpenTelemetryDefaults()`, which adds the tracing interceptor and an OTLP exporter
aimed at the sidecar, then runs a greeting workflow and activity until SIGTERM.

## Prerequisites

- A Temporal server the worker pool can reach (`TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE`)
- A Google Cloud project with 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 10

## Deploy

Set the placeholders used by `worker-pool.yaml`, then build the image, store the collector config as
a secret, and deploy. Run from the repo root:

```bash
export REGION=us-central1 WORKER_POOL=temporal-cloud-run-worker INSTANCE_COUNT=1
export SERVICE_ACCOUNT_EMAIL=<sa>@<project>.iam.gserviceaccount.com
export WORKER_IMAGE=$REGION-docker.pkg.dev/$(gcloud config get-value project)/samples/cloud-run
export TEMPORAL_ADDRESS=<host:7233> TEMPORAL_NAMESPACE=<namespace> TEMPORAL_TASK_QUEUE=cloud-run-worker
export COLLECTOR_CONFIG_SECRET=otel-collector-config COLLECTOR_CONFIG_SECRET_VERSION=latest
# Temporal Cloud: the API key the worker authenticates with, stored in Secret Manager.
export TEMPORAL_API_KEY_SECRET=temporal-api-key TEMPORAL_API_KEY_SECRET_VERSION=latest

# One-time setup: Artifact Registry repo, collector-config secret, and Temporal Cloud API key secret.
gcloud artifacts repositories create samples --repository-format=docker --location "$REGION"
gcloud secrets create "$COLLECTOR_CONFIG_SECRET" --data-file=src/Gcp/CloudRun/collector-config.yaml
Comment thread
seanbollin marked this conversation as resolved.
# Temporal Cloud: store the API key (from `tcld apikey create ...`) so the worker can read it.
printf '%s' "<temporal-cloud-api-key>" | gcloud secrets create "$TEMPORAL_API_KEY_SECRET" --data-file=-

# Build, push, and deploy (re-run to update). Cloud Run requires linux/amd64 images, so build for
# that platform explicitly (on arm64 Macs a native build would produce an unrunnable arm64 image).
docker build --platform linux/amd64 -f src/Gcp/CloudRun/Dockerfile -t "$WORKER_IMAGE" . && docker push "$WORKER_IMAGE"
envsubst < src/Gcp/CloudRun/worker-pool.yaml > /tmp/worker-pool.yaml
gcloud run worker-pools replace /tmp/worker-pool.yaml
```

The service account needs the `monitoring.metricWriter`, `cloudtrace.agent`, and
`secretmanager.secretAccessor` roles. The worker reads `TEMPORAL_API_KEY` from the Secret Manager
secret above (see `worker-pool.yaml`) to authenticate to Temporal Cloud.

## Run a workflow

For Temporal Cloud, pass your connection values (`--address`, `--namespace`, `--api-key`):

```bash
temporal workflow execute --task-queue cloud-run-worker --type GreetingWorkflow --input '"Temporal"' \
--address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --api-key "$TEMPORAL_API_KEY" --tls
```

Metrics appear in Metrics Explorer and traces in Trace Explorer, tagged with the Cloud Run identity.
Delete the pool with `gcloud run worker-pools delete "$WORKER_POOL" --region "$REGION"`.
12 changes: 12 additions & 0 deletions src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="Temporalio.Extensions.Gcp.CloudRun.Id" />
<PackageReference Include="Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry" />
</ItemGroup>

</Project>
84 changes: 84 additions & 0 deletions src/Gcp/CloudRun/collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Google-Built OpenTelemetry Collector config for the sidecar: metrics -> Managed Service for Prometheus, traces -> Cloud Trace. Auth via the worker-pool service account (ADC).
receivers:
# otelcol-google 0.156.0 has no `otlp_grpc` receiver (that name exists only as an exporter); `otlp`
# is the canonical OTLP receiver here.
otlp:
protocols:
grpc:
endpoint: localhost:4317

processors:
# Batch traces for throughput. Do NOT add a batch processor to the cumulative-metrics pipeline: a
# shutdown flush could be batched with a recent periodic export of the same series and rejected as
# a duplicate time series.
batch/traces:
send_batch_max_size: 200
send_batch_size: 200
timeout: 5s
memory_limiter:
check_interval: 1s
limit_percentage: 65
spike_limit_percentage: 20
resource_detection:
detectors: [gcp]
timeout: 10s
# Rename Temporal datapoint labels that collide with the target labels Managed Service for Prometheus injects (e.g. `namespace`).
transform/collision:
metric_statements:
- context: datapoint
statements:
- set(attributes["exported_location"], attributes["location"])
- delete_key(attributes, "location")
- set(attributes["exported_cluster"], attributes["cluster"])
- delete_key(attributes, "cluster")
- set(attributes["exported_namespace"], attributes["namespace"])
- delete_key(attributes, "namespace")
- set(attributes["exported_job"], attributes["job"])
- delete_key(attributes, "job")
- set(attributes["exported_instance"], attributes["instance"])
- delete_key(attributes, "instance")
- set(attributes["exported_project_id"], attributes["project_id"])
- delete_key(attributes, "project_id")
# The Telemetry API expects the Google Cloud project in gcp.project_id.
transform/set_project_id:
error_mode: ignore
trace_statements:
- set(resource.attributes["gcp.project_id"], resource.attributes["gcp.project.id"]) where resource.attributes["gcp.project.id"] != nil
- set(resource.attributes["gcp.project_id"], resource.attributes["cloud.account.id"]) where resource.attributes["gcp.project_id"] == nil and resource.attributes["cloud.account.id"] != nil

exporters:
googlemanagedprometheus:
otlp_grpc:
endpoint: telemetry.googleapis.com:443
compression: none
balancer_name: pick_first
auth:
authenticator: googleclientauth

extensions:
health_check:
endpoint: 0.0.0.0:13133
googleclientauth:

service:
extensions:
- health_check
- googleclientauth
pipelines:
metrics:
receivers: [otlp]
processors: [memory_limiter, resource_detection, transform/collision]
exporters: [googlemanagedprometheus]
traces:
receivers: [otlp]
processors: [memory_limiter, resource_detection, transform/set_project_id, batch/traces]
exporters: [otlp_grpc]
telemetry:
metrics:
readers:
- periodic:
exporter:
otlp:
protocol: grpc
endpoint: http://localhost:4317
insecure: true
62 changes: 62 additions & 0 deletions src/Gcp/CloudRun/worker-pool.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Cloud Run WorkerPool: a Temporal Worker + a Collector sidecar. Render placeholders with `envsubst` (see README) then `gcloud run worker-pools replace`. For Temporal Cloud, add TEMPORAL_API_KEY from a Secret Manager secretKeyRef.
apiVersion: run.googleapis.com/v1
kind: WorkerPool
metadata:
name: "${WORKER_POOL}"
labels:
cloud.googleapis.com/location: "${REGION}"
annotations:
run.googleapis.com/scalingMode: manual
run.googleapis.com/manualInstanceCount: "${INSTANCE_COUNT}"
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: '{"worker":["collector"]}'
run.googleapis.com/execution-environment: gen2
spec:
containerConcurrency: 0
serviceAccountName: "${SERVICE_ACCOUNT_EMAIL}"
containers:
- name: worker
image: "${WORKER_IMAGE}"
env:
- name: TEMPORAL_ADDRESS
value: "${TEMPORAL_ADDRESS}"
- name: TEMPORAL_NAMESPACE
value: "${TEMPORAL_NAMESPACE}"
- name: TEMPORAL_TASK_QUEUE
value: "${TEMPORAL_TASK_QUEUE}"
# Temporal Cloud API key, read from Secret Manager (same pattern as the collector config).
- name: TEMPORAL_API_KEY
valueFrom:
secretKeyRef:
key: "${TEMPORAL_API_KEY_SECRET_VERSION}"
name: "${TEMPORAL_API_KEY_SECRET}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://localhost:4317
resources:
limits:
cpu: "1"
memory: 512Mi
- name: collector
image: us-docker.pkg.dev/cloud-ops-agents-artifacts/google-cloud-opentelemetry-collector/otelcol-google:0.156.0
args:
- --config=env:OTELCOL_CONFIG
env:
- name: OTELCOL_CONFIG
valueFrom:
secretKeyRef:
key: "${COLLECTOR_CONFIG_SECRET_VERSION}"
name: "${COLLECTOR_CONFIG_SECRET}"
startupProbe:
httpGet:
path: /
port: 13133
timeoutSeconds: 1
periodSeconds: 2
failureThreshold: 30
resources:
limits:
cpu: "1"
memory: 512Mi
Loading
Loading