From 16703988e54b8777ff123d3ac72b3b801eeb3be8 Mon Sep 17 00:00:00 2001 From: seanbollin Date: Mon, 28 Sep 2026 16:59:32 -0700 Subject: [PATCH 1/7] Add Cloud Run worker sample using CloudRunId and OpenTelemetry plugins Co-Authored-By: Claude Opus 4.8 --- gcp/cloud-run/Dockerfile | 17 ++++ gcp/cloud-run/README.md | 69 +++++++++++++++ gcp/cloud-run/build.gradle | 25 ++++++ gcp/cloud-run/collector-config.yaml | 87 +++++++++++++++++++ .../samples/gcp/cloudrun/CloudRunWorker.java | 71 +++++++++++++++ .../gcp/cloudrun/GreetingActivities.java | 8 ++ .../gcp/cloudrun/GreetingActivitiesImpl.java | 8 ++ .../gcp/cloudrun/GreetingWorkflow.java | 10 +++ .../gcp/cloudrun/GreetingWorkflowImpl.java | 17 ++++ gcp/cloud-run/src/main/resources/logback.xml | 14 +++ .../gcp/cloudrun/GreetingWorkflowTest.java | 32 +++++++ gcp/cloud-run/worker-pool.yaml | 68 +++++++++++++++ settings.gradle | 7 ++ 13 files changed, 433 insertions(+) create mode 100644 gcp/cloud-run/Dockerfile create mode 100644 gcp/cloud-run/README.md create mode 100644 gcp/cloud-run/build.gradle create mode 100644 gcp/cloud-run/collector-config.yaml create mode 100644 gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/CloudRunWorker.java create mode 100644 gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivities.java create mode 100644 gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivitiesImpl.java create mode 100644 gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflow.java create mode 100644 gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowImpl.java create mode 100644 gcp/cloud-run/src/main/resources/logback.xml create mode 100644 gcp/cloud-run/src/test/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowTest.java create mode 100644 gcp/cloud-run/worker-pool.yaml diff --git a/gcp/cloud-run/Dockerfile b/gcp/cloud-run/Dockerfile new file mode 100644 index 00000000..74330767 --- /dev/null +++ b/gcp/cloud-run/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-temurin:17-jdk-jammy AS build + +WORKDIR /workspace +COPY . . + +# The temporal-gcp-cloud-run-* modules are unreleased; the composite build resolves them from a local SDK checkout (see README). +RUN ./gradlew --no-daemon :gcp:cloud-run:installDist + +FROM eclipse-temurin:17-jre-jammy + +RUN useradd --create-home --uid 10001 temporal +WORKDIR /app +COPY --from=build --chown=temporal:temporal \ + /workspace/gcp/cloud-run/build/install/cloud-run-worker/ /app/ + +USER 10001 +ENTRYPOINT ["/app/bin/cloud-run-worker"] diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md new file mode 100644 index 00000000..c5abfa9f --- /dev/null +++ b/gcp/cloud-run/README.md @@ -0,0 +1,69 @@ +# Temporal Cloud Run worker + +A Temporal Worker running in a Google Cloud Run **worker pool** that uses both GCP Cloud Run +plugins together. Worker pools keep CPU allocated, unlike request-driven Cloud Run services. Both +plugins are registered on the service stubs, so they propagate to the client and to workers created +from it. + +- `CloudRunIdPlugin` (`io.temporal:temporal-gcp-cloud-run-id`) sets the Temporal client identity + from Cloud Run instance metadata as `{instanceId}@{revision}`, unless an identity is already set. +- `CloudRunOpenTelemetryPlugin` (`io.temporal:temporal-gcp-cloud-run-opentelemetry`) configures the + SDK metrics scope, tracing interceptors, OTLP exporters (default `http://localhost:4317`), and + shutdown flushing, exporting to a + [Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run) + sidecar. It derives `service.name` from `CLOUD_RUN_WORKER_POOL` and reports metrics every 60 + seconds, matching the OpenTelemetry SDK default across the Temporal SDKs. + +> Google Cloud Run support is experimental and may change without notice. + +## Unreleased SDK dependency + +`temporal-gcp-cloud-run-id` and `temporal-gcp-cloud-run-opentelemetry` are not yet released. +`settings.gradle` resolves them (and the other `io.temporal:*` modules) from a local SDK checkout via +a Gradle composite build, defaulting to `../sdk-java` and overridable with `-PtemporalSdkPath`. CI +has no checkout, so its build stays red until the modules ship; then drop the composite block and +bump `javaSDKVersion`. + +```bash +./gradlew -PtemporalSdkPath=/path/to/sdk-java :gcp:cloud-run:build +``` + +## Files + +- `.../cloudrun/CloudRunWorker.java` — both plugins, client, Temporal Worker, bounded `SIGTERM` shutdown. +- `collector-config.yaml` — collector for cumulative Prometheus metrics and batched traces. +- `worker-pool.yaml` — worker and collector containers sharing localhost, config from Secret Manager. +- `Dockerfile` — packages the Gradle application as the worker container. + +## Deploy + +Run from the repository root, with a Temporal Cloud namespace and API key. + +1. Store the API key and collector config in Secret Manager: + + ```bash + printf '%s' "$TEMPORAL_API_KEY" | \ + gcloud secrets create temporal-api-key --data-file=- --project="$PROJECT_ID" + gcloud secrets create temporal-collector-config \ + --data-file=gcp/cloud-run/collector-config.yaml --project="$PROJECT_ID" + ``` + +2. Build and push the image (tag `REGION-docker.pkg.dev/PROJECT_ID/temporal-samples/cloud-run-worker:latest`) + with `docker build -f gcp/cloud-run/Dockerfile .`. + +3. Replace the placeholders in `worker-pool.yaml`, then deploy: + + ```bash + gcloud run worker-pools replace gcp/cloud-run/worker-pool.yaml --project="$PROJECT_ID" + ``` + +The service account needs `roles/monitoring.metricWriter`, `roles/telemetry.tracesWriter`, and +`roles/secretmanager.secretAccessor`. Start a workflow on task queue `cloud-run-worker`: + +```bash +temporal workflow start --type GreetingWorkflow --task-queue cloud-run-worker \ + --workflow-id cloud-run-greeting --input '"Google Cloud"' +``` + +The collector does **not** batch cumulative metrics: a shutdown flush batched with a recent periodic +export would collide on the same Prometheus series and be rejected as `Duplicate TimeSeries`. diff --git a/gcp/cloud-run/build.gradle b/gcp/cloud-run/build.gradle new file mode 100644 index 00000000..7ee55eb1 --- /dev/null +++ b/gcp/cloud-run/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'application' + +dependencies { + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + implementation "io.temporal:temporal-gcp-cloud-run-opentelemetry:$javaSDKVersion" + implementation "io.temporal:temporal-gcp-cloud-run-id:$javaSDKVersion" + runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' + + testImplementation "io.temporal:temporal-testing:$javaSDKVersion" + testImplementation "junit:junit:4.13.2" + testImplementation(platform("org.junit:junit-bom:5.10.3")) + testRuntimeOnly "org.junit.vintage:junit-vintage-engine" + + dependencies { + errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') + errorprone('com.google.errorprone:error_prone_core:2.28.0') + } +} + +application { + mainClass = 'io.temporal.samples.gcp.cloudrun.CloudRunWorker' + // Stable launcher name the Dockerfile relies on, independent of the Gradle project path. + applicationName = 'cloud-run-worker' +} diff --git a/gcp/cloud-run/collector-config.yaml b/gcp/cloud-run/collector-config.yaml new file mode 100644 index 00000000..483ea259 --- /dev/null +++ b/gcp/cloud-run/collector-config.yaml @@ -0,0 +1,87 @@ +# @@@SNIPSTART java-cloud-run-collector-config +receivers: + otlp: + protocols: + grpc: + endpoint: localhost:4317 + +processors: + # Batch traces for throughput. Do not add this processor to the cumulative metrics pipeline: + # a shutdown flush can otherwise be batched with a recent periodic export of the same series. + batch/traces: + send_batch_max_size: 200 + send_batch_size: 200 + timeout: 5s + memory_limiter: + # This is the collector's memory polling cadence, not the SDK metric export interval. + check_interval: 1s + limit_percentage: 65 + spike_limit_percentage: 20 + resourcedetection: + detectors: [gcp] + timeout: 10s + # Avoid collisions with labels that Google Managed Service for Prometheus adds. + 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: + # Google Cloud's supported OTLP path for traces is the Telemetry API. + otlp: + endpoint: telemetry.googleapis.com:443 + compression: none + balancer_name: pick_first + auth: + authenticator: googleclientauth + +extensions: + # Cloud Run container dependencies require a startup probe. This endpoint is also used for the + # collector liveness probe in worker-pool.yaml. + health_check: + endpoint: 0.0.0.0:13133 + googleclientauth: + +service: + extensions: + - health_check + - googleclientauth + pipelines: + metrics/otlp: + receivers: [otlp] + processors: [memory_limiter, resourcedetection, transform/collision] + exporters: [googlemanagedprometheus] + traces: + receivers: [otlp] + processors: [memory_limiter, resourcedetection, transform/set_project_id, batch/traces] + exporters: [otlp] + # Feed collector self-metrics back through the metrics pipeline. + telemetry: + metrics: + readers: + - periodic: + exporter: + otlp: + protocol: grpc + endpoint: http://localhost:4317 + insecure: true +# @@@SNIPEND diff --git a/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/CloudRunWorker.java b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/CloudRunWorker.java new file mode 100644 index 00000000..bd1845fb --- /dev/null +++ b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/CloudRunWorker.java @@ -0,0 +1,71 @@ +package io.temporal.samples.gcp.cloudrun; + +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.gcp.cloudrun.id.CloudRunIdPlugin; +import io.temporal.gcp.cloudrun.opentelemetry.CloudRunOpenTelemetryPlugin; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** A Temporal Worker for a Cloud Run worker pool. */ +public final class CloudRunWorker { + public static final String DEFAULT_TASK_QUEUE = "cloud-run-worker"; + + private CloudRunWorker() {} + + public static void main(String[] args) throws IOException { + // @@@SNIPSTART java-cloud-run-worker + ClientConfigProfile profile = ClientConfigProfile.load(); + CloudRunOpenTelemetryPlugin otelPlugin = CloudRunOpenTelemetryPlugin.newBuilder().build(); + CloudRunIdPlugin idPlugin = new CloudRunIdPlugin(); + + WorkflowServiceStubsOptions serviceOptions = + WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions()) + .setPlugins(otelPlugin, idPlugin) + .build(); + WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(serviceOptions); + // @@@SNIPEND + WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + WorkerFactory factory = WorkerFactory.newInstance(client); + + String taskQueue = taskQueue(); + Worker worker = factory.newWorker(taskQueue); + worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class); + worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); + + Runtime.getRuntime() + .addShutdownHook( + new Thread(() -> shutdown(factory, service, otelPlugin), "temporal-worker-shutdown")); + + factory.start(); + System.out.printf( + "Temporal worker started: taskQueue=%s, otelEndpoint=%s, serviceName=%s%n", + taskQueue, otelPlugin.getEndpoint(), otelPlugin.getServiceName()); + + // Keep the process alive until Cloud Run sends SIGTERM. + factory.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } + + private static String taskQueue() { + String configured = System.getenv("TEMPORAL_TASK_QUEUE"); + return configured == null || configured.trim().isEmpty() ? DEFAULT_TASK_QUEUE : configured; + } + + private static void shutdown( + WorkerFactory factory, WorkflowServiceStubs service, CloudRunOpenTelemetryPlugin otelPlugin) { + // Flush after worker shutdown to capture finishing tasks; Cloud Run allows 10s. + factory.shutdown(); + factory.awaitTermination(6, TimeUnit.SECONDS); + if (!factory.isTerminated()) { + factory.shutdownNow(); + factory.awaitTermination(1, TimeUnit.SECONDS); + } + otelPlugin.newFlushHook().run(Duration.ofSeconds(2)); + service.shutdown(); + } +} diff --git a/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivities.java b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivities.java new file mode 100644 index 00000000..64d9fa24 --- /dev/null +++ b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivities.java @@ -0,0 +1,8 @@ +package io.temporal.samples.gcp.cloudrun; + +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface GreetingActivities { + String composeGreeting(String name); +} diff --git a/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivitiesImpl.java b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivitiesImpl.java new file mode 100644 index 00000000..9d473a0d --- /dev/null +++ b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingActivitiesImpl.java @@ -0,0 +1,8 @@ +package io.temporal.samples.gcp.cloudrun; + +public final class GreetingActivitiesImpl implements GreetingActivities { + @Override + public String composeGreeting(String name) { + return "Hello " + name + "!"; + } +} diff --git a/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflow.java b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflow.java new file mode 100644 index 00000000..262e9977 --- /dev/null +++ b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflow.java @@ -0,0 +1,10 @@ +package io.temporal.samples.gcp.cloudrun; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface GreetingWorkflow { + @WorkflowMethod + String getGreeting(String name); +} diff --git a/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowImpl.java b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowImpl.java new file mode 100644 index 00000000..60c71cfa --- /dev/null +++ b/gcp/cloud-run/src/main/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowImpl.java @@ -0,0 +1,17 @@ +package io.temporal.samples.gcp.cloudrun; + +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.Workflow; +import java.time.Duration; + +public final class GreetingWorkflowImpl implements GreetingWorkflow { + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build()); + + @Override + public String getGreeting(String name) { + return activities.composeGreeting(name); + } +} diff --git a/gcp/cloud-run/src/main/resources/logback.xml b/gcp/cloud-run/src/main/resources/logback.xml new file mode 100644 index 00000000..28eb2cba --- /dev/null +++ b/gcp/cloud-run/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + diff --git a/gcp/cloud-run/src/test/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowTest.java b/gcp/cloud-run/src/test/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowTest.java new file mode 100644 index 00000000..3035e975 --- /dev/null +++ b/gcp/cloud-run/src/test/java/io/temporal/samples/gcp/cloudrun/GreetingWorkflowTest.java @@ -0,0 +1,32 @@ +package io.temporal.samples.gcp.cloudrun; + +import static org.junit.Assert.assertEquals; + +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.TestWorkflowRule; +import org.junit.Rule; +import org.junit.Test; + +public class GreetingWorkflowTest { + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(GreetingWorkflowImpl.class) + .setDoNotStart(true) + .build(); + + @Test + public void completesGreeting() { + testWorkflowRule.getWorker().registerActivitiesImplementations(new GreetingActivitiesImpl()); + testWorkflowRule.getTestEnvironment().start(); + + GreetingWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + GreetingWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello Google Cloud!", workflow.getGreeting("Google Cloud")); + } +} diff --git a/gcp/cloud-run/worker-pool.yaml b/gcp/cloud-run/worker-pool.yaml new file mode 100644 index 00000000..5c5e0042 --- /dev/null +++ b/gcp/cloud-run/worker-pool.yaml @@ -0,0 +1,68 @@ +apiVersion: run.googleapis.com/v1 +kind: WorkerPool +metadata: + name: temporal-cloud-run-worker + labels: + cloud.googleapis.com/location: REGION + annotations: + run.googleapis.com/scalingMode: manual + run.googleapis.com/manualInstanceCount: "1" +spec: + template: + metadata: + annotations: + # Cloud Run starts the worker only after the collector startup probe succeeds. + run.googleapis.com/container-dependencies: '{"worker":["collector"]}' + run.googleapis.com/execution-environment: gen2 + spec: + containerConcurrency: 0 + serviceAccountName: temporal-cloud-run-worker@PROJECT_ID.iam.gserviceaccount.com + containers: + - name: worker + image: REGION-docker.pkg.dev/PROJECT_ID/temporal-samples/cloud-run-worker:latest + env: + - name: TEMPORAL_ADDRESS + value: NAMESPACE_ID.tmprl.cloud:7233 + - name: TEMPORAL_NAMESPACE + value: NAMESPACE_ID.ACCOUNT_ID + - name: TEMPORAL_API_KEY + valueFrom: + secretKeyRef: + key: "1" + name: temporal-api-key + - name: TEMPORAL_TASK_QUEUE + value: cloud-run-worker + - 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: "1" + name: temporal-collector-config + startupProbe: + httpGet: + path: / + port: 13133 + timeoutSeconds: 5 + periodSeconds: 10 + failureThreshold: 12 + livenessProbe: + httpGet: + path: / + port: 13133 + timeoutSeconds: 5 + periodSeconds: 30 + failureThreshold: 3 + resources: + limits: + cpu: "1" + memory: 512Mi diff --git a/settings.gradle b/settings.gradle index b19f2fd7..af7651b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,3 +9,10 @@ include 'springboot' include 'springboot-basic' include 'lambda-worker:starter' include 'lambda-worker:worker' +include 'gcp:cloud-run' + +// The gcp:cloud-run sample uses unreleased temporal-gcp-cloud-run-* modules; resolve them from a local SDK checkout (default ../sdk-java, override with -PtemporalSdkPath) via composite build. +def temporalSdkPath = gradle.startParameter.projectProperties['temporalSdkPath'] ?: '../sdk-java' +if (file(temporalSdkPath).isDirectory()) { + includeBuild temporalSdkPath +} From 3c7f4c78cf79ae23ffdd0f7656bb42f56d80c215 Mon Sep 17 00:00:00 2001 From: seanbollin Date: Tue, 29 Sep 2026 15:47:55 -0700 Subject: [PATCH 2/7] Use released Cloud Run plugin artifacts --- gcp/cloud-run/README.md | 11 ++++------- gcp/cloud-run/build.gradle | 11 +++++++---- settings.gradle | 6 ------ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index c5abfa9f..152eadb6 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -16,16 +16,13 @@ from it. > Google Cloud Run support is experimental and may change without notice. -## Unreleased SDK dependency +## Build -`temporal-gcp-cloud-run-id` and `temporal-gcp-cloud-run-opentelemetry` are not yet released. -`settings.gradle` resolves them (and the other `io.temporal:*` modules) from a local SDK checkout via -a Gradle composite build, defaulting to `../sdk-java` and overridable with `-PtemporalSdkPath`. CI -has no checkout, so its build stays red until the modules ship; then drop the composite block and -bump `javaSDKVersion`. +`temporal-gcp-cloud-run-id` and `temporal-gcp-cloud-run-opentelemetry` are published on Maven +Central, so the sample resolves them like any other dependency. ```bash -./gradlew -PtemporalSdkPath=/path/to/sdk-java :gcp:cloud-run:build +./gradlew :gcp:cloud-run:build ``` ## Files diff --git a/gcp/cloud-run/build.gradle b/gcp/cloud-run/build.gradle index 7ee55eb1..b5cd037f 100644 --- a/gcp/cloud-run/build.gradle +++ b/gcp/cloud-run/build.gradle @@ -1,10 +1,13 @@ apply plugin: 'application' dependencies { - implementation "io.temporal:temporal-sdk:$javaSDKVersion" - implementation "io.temporal:temporal-envconfig:$javaSDKVersion" - implementation "io.temporal:temporal-gcp-cloud-run-opentelemetry:$javaSDKVersion" - implementation "io.temporal:temporal-gcp-cloud-run-id:$javaSDKVersion" + // The GCP Cloud Run modules first shipped in 1.40.0; pin the io.temporal deps to that + // released version here rather than bumping the repo-wide javaSDKVersion. The explicit + // temporal-sdk dep is required because temporal-gcp-cloud-run-id declares it as compileOnly. + implementation "io.temporal:temporal-sdk:1.40.0" + implementation "io.temporal:temporal-envconfig:1.40.0" + implementation "io.temporal:temporal-gcp-cloud-run-opentelemetry:1.40.0" + implementation "io.temporal:temporal-gcp-cloud-run-id:1.40.0" runtimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' testImplementation "io.temporal:temporal-testing:$javaSDKVersion" diff --git a/settings.gradle b/settings.gradle index af7651b6..e66f00a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,9 +10,3 @@ include 'springboot-basic' include 'lambda-worker:starter' include 'lambda-worker:worker' include 'gcp:cloud-run' - -// The gcp:cloud-run sample uses unreleased temporal-gcp-cloud-run-* modules; resolve them from a local SDK checkout (default ../sdk-java, override with -PtemporalSdkPath) via composite build. -def temporalSdkPath = gradle.startParameter.projectProperties['temporalSdkPath'] ?: '../sdk-java' -if (file(temporalSdkPath).isDirectory()) { - includeBuild temporalSdkPath -} From 682aa13588aef46fa7d90ae6a4572e901c3d87b4 Mon Sep 17 00:00:00 2001 From: seanbollin Date: Tue, 29 Sep 2026 16:21:14 -0700 Subject: [PATCH 3/7] Trim redundant dependency note from README --- gcp/cloud-run/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index 152eadb6..53079a94 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -18,8 +18,7 @@ from it. ## Build -`temporal-gcp-cloud-run-id` and `temporal-gcp-cloud-run-opentelemetry` are published on Maven -Central, so the sample resolves them like any other dependency. +`temporal-gcp-cloud-run-id` and `temporal-gcp-cloud-run-opentelemetry` are published on Maven Central. ```bash ./gradlew :gcp:cloud-run:build From 52a02763fd5bb7dc307b42be5616a7ba8638860c Mon Sep 17 00:00:00 2001 From: seanbollin Date: Wed, 30 Sep 2026 11:03:10 -0700 Subject: [PATCH 4/7] Add Artifact Registry step and workflow result to README --- gcp/cloud-run/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index 53079a94..b5a7b870 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -44,8 +44,14 @@ Run from the repository root, with a Temporal Cloud namespace and API key. --data-file=gcp/cloud-run/collector-config.yaml --project="$PROJECT_ID" ``` -2. Build and push the image (tag `REGION-docker.pkg.dev/PROJECT_ID/temporal-samples/cloud-run-worker:latest`) - with `docker build -f gcp/cloud-run/Dockerfile .`. +2. Create the Artifact Registry repo, then build and push the image: + + ```bash + gcloud artifacts repositories create temporal-samples --repository-format=docker \ + --location="$REGION" --project="$PROJECT_ID" + IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/temporal-samples/cloud-run-worker:latest" + docker build -f gcp/cloud-run/Dockerfile -t "$IMAGE" . && docker push "$IMAGE" + ``` 3. Replace the placeholders in `worker-pool.yaml`, then deploy: @@ -57,9 +63,11 @@ The service account needs `roles/monitoring.metricWriter`, `roles/telemetry.trac `roles/secretmanager.secretAccessor`. Start a workflow on task queue `cloud-run-worker`: ```bash -temporal workflow start --type GreetingWorkflow --task-queue cloud-run-worker \ +temporal workflow execute --type GreetingWorkflow --task-queue cloud-run-worker \ --workflow-id cloud-run-greeting --input '"Google Cloud"' ``` +It prints `Hello Google Cloud!`, confirming the deployed worker ran the task. + The collector does **not** batch cumulative metrics: a shutdown flush batched with a recent periodic export would collide on the same Prometheus series and be rejected as `Duplicate TimeSeries`. From d4508c90373165c0a64a5190fb87ef1f8171dad9 Mon Sep 17 00:00:00 2001 From: seanbollin Date: Mon, 5 Oct 2026 15:02:18 -0700 Subject: [PATCH 5/7] Fix stale Dockerfile comment and note one-time setup --- gcp/cloud-run/Dockerfile | 1 - gcp/cloud-run/README.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gcp/cloud-run/Dockerfile b/gcp/cloud-run/Dockerfile index 74330767..b91aa346 100644 --- a/gcp/cloud-run/Dockerfile +++ b/gcp/cloud-run/Dockerfile @@ -3,7 +3,6 @@ FROM eclipse-temurin:17-jdk-jammy AS build WORKDIR /workspace COPY . . -# The temporal-gcp-cloud-run-* modules are unreleased; the composite build resolves them from a local SDK checkout (see README). RUN ./gradlew --no-daemon :gcp:cloud-run:installDist FROM eclipse-temurin:17-jre-jammy diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index b5a7b870..74cdc9ca 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -35,7 +35,7 @@ from it. Run from the repository root, with a Temporal Cloud namespace and API key. -1. Store the API key and collector config in Secret Manager: +1. Store the API key and collector config in Secret Manager (one-time setup): ```bash printf '%s' "$TEMPORAL_API_KEY" | \ @@ -44,7 +44,7 @@ Run from the repository root, with a Temporal Cloud namespace and API key. --data-file=gcp/cloud-run/collector-config.yaml --project="$PROJECT_ID" ``` -2. Create the Artifact Registry repo, then build and push the image: +2. Create the Artifact Registry repo (one-time setup), then build and push the image: ```bash gcloud artifacts repositories create temporal-samples --repository-format=docker \ From 38d8f1e616b31ae1891aa37ce8186b6491111cdf Mon Sep 17 00:00:00 2001 From: seanbollin Date: Tue, 6 Oct 2026 15:39:33 -0700 Subject: [PATCH 6/7] Fix collector component names, namespace placeholders, and execute flags --- gcp/cloud-run/README.md | 8 ++++++-- gcp/cloud-run/collector-config.yaml | 10 +++++----- gcp/cloud-run/worker-pool.yaml | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index 74cdc9ca..ca3c3625 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -60,11 +60,15 @@ Run from the repository root, with a Temporal Cloud namespace and API key. ``` The service account needs `roles/monitoring.metricWriter`, `roles/telemetry.tracesWriter`, and -`roles/secretmanager.secretAccessor`. Start a workflow on task queue `cloud-run-worker`: +`roles/secretmanager.secretAccessor`. Set `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, and +`TEMPORAL_API_KEY` to your Temporal Cloud values, then start a workflow on task queue +`cloud-run-worker`: ```bash temporal workflow execute --type GreetingWorkflow --task-queue cloud-run-worker \ - --workflow-id cloud-run-greeting --input '"Google Cloud"' + --workflow-id cloud-run-greeting --input '"Google Cloud"' \ + --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" \ + --api-key "$TEMPORAL_API_KEY" --tls ``` It prints `Hello Google Cloud!`, confirming the deployed worker ran the task. diff --git a/gcp/cloud-run/collector-config.yaml b/gcp/cloud-run/collector-config.yaml index 483ea259..b4973f86 100644 --- a/gcp/cloud-run/collector-config.yaml +++ b/gcp/cloud-run/collector-config.yaml @@ -17,7 +17,7 @@ processors: check_interval: 1s limit_percentage: 65 spike_limit_percentage: 20 - resourcedetection: + resource_detection: detectors: [gcp] timeout: 10s # Avoid collisions with labels that Google Managed Service for Prometheus adds. @@ -47,7 +47,7 @@ processors: exporters: googlemanagedprometheus: # Google Cloud's supported OTLP path for traces is the Telemetry API. - otlp: + otlp_grpc: endpoint: telemetry.googleapis.com:443 compression: none balancer_name: pick_first @@ -68,12 +68,12 @@ service: pipelines: metrics/otlp: receivers: [otlp] - processors: [memory_limiter, resourcedetection, transform/collision] + processors: [memory_limiter, resource_detection, transform/collision] exporters: [googlemanagedprometheus] traces: receivers: [otlp] - processors: [memory_limiter, resourcedetection, transform/set_project_id, batch/traces] - exporters: [otlp] + processors: [memory_limiter, resource_detection, transform/set_project_id, batch/traces] + exporters: [otlp_grpc] # Feed collector self-metrics back through the metrics pipeline. telemetry: metrics: diff --git a/gcp/cloud-run/worker-pool.yaml b/gcp/cloud-run/worker-pool.yaml index 5c5e0042..0136d974 100644 --- a/gcp/cloud-run/worker-pool.yaml +++ b/gcp/cloud-run/worker-pool.yaml @@ -22,9 +22,9 @@ spec: image: REGION-docker.pkg.dev/PROJECT_ID/temporal-samples/cloud-run-worker:latest env: - name: TEMPORAL_ADDRESS - value: NAMESPACE_ID.tmprl.cloud:7233 + value: NAMESPACE.tmprl.cloud:7233 - name: TEMPORAL_NAMESPACE - value: NAMESPACE_ID.ACCOUNT_ID + value: NAMESPACE - name: TEMPORAL_API_KEY valueFrom: secretKeyRef: From ad7036925a914187d3a8f3823044ccd4604bbc0f Mon Sep 17 00:00:00 2001 From: seanbollin Date: Thu, 8 Oct 2026 13:41:56 -0700 Subject: [PATCH 7/7] Build the Cloud Run image for linux/amd64 --- gcp/cloud-run/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/cloud-run/README.md b/gcp/cloud-run/README.md index ca3c3625..f9aea074 100644 --- a/gcp/cloud-run/README.md +++ b/gcp/cloud-run/README.md @@ -50,7 +50,7 @@ Run from the repository root, with a Temporal Cloud namespace and API key. gcloud artifacts repositories create temporal-samples --repository-format=docker \ --location="$REGION" --project="$PROJECT_ID" IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/temporal-samples/cloud-run-worker:latest" - docker build -f gcp/cloud-run/Dockerfile -t "$IMAGE" . && docker push "$IMAGE" + docker build --platform linux/amd64 -f gcp/cloud-run/Dockerfile -t "$IMAGE" . && docker push "$IMAGE" ``` 3. Replace the placeholders in `worker-pool.yaml`, then deploy: