From 16515055c46f55293016b674d74b2e01fe219ad3 Mon Sep 17 00:00:00 2001 From: seanbollin Date: Wed, 19 Aug 2026 15:01:05 -0700 Subject: [PATCH] Add Cloud Run worker OpenTelemetry sample Adds cloud-run-worker/: a long-running Temporal worker for Google Cloud Run worker pools that uses the temporal-gcp-cloud-run CloudRunOpenTelemetryPlugin to export Core metrics and traces over OTLP/gRPC to a local OpenTelemetry Collector sidecar (worker, greeting workflow/activities, collector-config.yaml, worker-pool.yaml, Dockerfile, README). Note: the SDK module temporal-gcp-cloud-run is not yet released, so the sample resolves it via the existing -PtemporalSdkPath composite build against a local sdk-java checkout. Bump javaSDKVersion to the release that ships the module before this is marked ready. Co-authored-by: Edward Amsden Co-Authored-By: Claude Opus 4.8 --- README.md | 1 + cloud-run-worker/Dockerfile | 19 ++ cloud-run-worker/README.md | 268 ++++++++++++++++++ cloud-run-worker/build.gradle | 22 ++ cloud-run-worker/collector-config.yaml | 85 ++++++ .../samples/cloudrun/CloudRunWorker.java | 71 +++++ .../samples/cloudrun/GreetingActivities.java | 8 + .../cloudrun/GreetingActivitiesImpl.java | 8 + .../samples/cloudrun/GreetingWorkflow.java | 10 + .../cloudrun/GreetingWorkflowImpl.java | 17 ++ .../cloudrun/CloudRunPluginDefaultsTest.java | 15 + .../cloudrun/GreetingWorkflowTest.java | 32 +++ cloud-run-worker/worker-pool.yaml | 68 +++++ settings.gradle | 9 + 14 files changed, 633 insertions(+) create mode 100644 cloud-run-worker/Dockerfile create mode 100644 cloud-run-worker/README.md create mode 100644 cloud-run-worker/build.gradle create mode 100644 cloud-run-worker/collector-config.yaml create mode 100644 cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/CloudRunWorker.java create mode 100644 cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivities.java create mode 100644 cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivitiesImpl.java create mode 100644 cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflow.java create mode 100644 cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflowImpl.java create mode 100644 cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/CloudRunPluginDefaultsTest.java create mode 100644 cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/GreetingWorkflowTest.java create mode 100644 cloud-run-worker/worker-pool.yaml diff --git a/README.md b/README.md index 898cc5b1..7d4836b7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It contains the following modules: * [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies. * [Spring AI](/springai): demonstrates the Temporal Spring AI integration — durable AI agents with chat models, tools, MCP servers, vector stores, and embeddings. * [Lambda Worker](/lambda-worker): demonstrates running a Temporal Java Worker inside AWS Lambda. +* [Cloud Run Worker](/cloud-run-worker): demonstrates running a Temporal Java Worker in a Google Cloud Run worker pool, exporting SDK metrics and traces through an OpenTelemetry collector sidecar. ## Learn more about Temporal and Java SDK diff --git a/cloud-run-worker/Dockerfile b/cloud-run-worker/Dockerfile new file mode 100644 index 00000000..aaa5a349 --- /dev/null +++ b/cloud-run-worker/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:17-jdk-jammy AS build + +WORKDIR /workspace +COPY . . + +# The worker depends on io.temporal:temporal-gcp-cloud-run, resolved through the samples' +# javaSDKVersion. Until a Temporal Java SDK release contains that module, build against a local +# SDK checkout with the composite build (see README), or publish the module to Maven Local first. +RUN ./gradlew --no-daemon :cloud-run-worker:installDist + +FROM eclipse-temurin:17-jre-jammy + +RUN useradd --create-home --uid 10001 temporal +WORKDIR /app +COPY --from=build --chown=temporal:temporal \ + /workspace/cloud-run-worker/build/install/cloud-run-worker/ /app/ + +USER 10001 +ENTRYPOINT ["/app/bin/cloud-run-worker"] diff --git a/cloud-run-worker/README.md b/cloud-run-worker/README.md new file mode 100644 index 00000000..d6e62884 --- /dev/null +++ b/cloud-run-worker/README.md @@ -0,0 +1,268 @@ +# Temporal Cloud Run OpenTelemetry worker + +This sample runs a continuously polling Temporal worker in a **Cloud Run worker pool** and +exports Temporal SDK metrics and traces through a +[Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run) +sidecar. + +It is intentionally not a Cloud Run function or request-driven Cloud Run service. Worker pools +keep CPU allocated while the Temporal worker performs continuous background polling. + +```text +Temporal worker + │ OTLP/gRPC, localhost:4317 + ▼ +Google-Built OpenTelemetry Collector sidecar + ├── metrics ──► Google Managed Service for Prometheus + └── traces ──► Telemetry (OTLP) API ──► Cloud Trace storage +``` + +The Java process uses `CloudRunOpenTelemetryPlugin` from `io.temporal:temporal-gcp-cloud-run`, +which configures the Temporal SDK metrics scope, tracing interceptors, OTLP exporters, and shutdown +flushing. The plugin defaults to `http://localhost:4317` and derives `service.name` from the Cloud +Run-provided `CLOUD_RUN_WORKER_POOL` environment variable. It reports and exports metrics every 60 +seconds by default, matching the upstream OpenTelemetry SDK default and the coordinated Temporal GCP +plugin default across Java, Go, .NET, and Python. This sample deliberately does not override that +interval. + +## Unreleased SDK dependency + +At the time this sample was added, `io.temporal:temporal-gcp-cloud-run` had not been released. The +Gradle build resolves it through the samples' `javaSDKVersion`, so a normal build from Maven Central +and the production Docker build remain blocked until a Temporal Java SDK version containing the +module is published. Do not replace the plugin with hand-written OpenTelemetry configuration; that +would stop this sample from exercising the supported SDK API. + +You can compile and test against an unmodified local `sdk-java` checkout with Gradle composite +substitution. The sample-level plugin-default test requires the coordinated 60-second SDK change, +so an older checkout fails instead of silently testing a different cadence: + +```bash +./gradlew \ + -PtemporalSdkPath=/path/to/sdk-java \ + :cloud-run-worker:test \ + :cloud-run-worker:installDist +``` + +Once `temporal-gcp-cloud-run` is released, bump `javaSDKVersion` in the samples root `build.gradle` +to that released version and the standard Maven Central build and Docker build work without the +composite substitution. + +## Files + +- `src/main/java/io/temporal/samples/cloudrun/CloudRunWorker.java` creates the plugin, client, + and long-lived worker and performs a bounded shutdown on `SIGTERM`. +- `collector-config.yaml` adapts Google's Cloud Run collector configuration for cumulative + Prometheus metrics and batched traces. +- `worker-pool.yaml` deploys the worker and collector as two containers sharing localhost and + injects the collector configuration from Secret Manager. +- `Dockerfile` packages the Gradle application as the worker container. + +## Metric cadence and collector batching + +`CloudRunOpenTelemetryPlugin` defaults to a 60-second metric reporting and export interval. +Applications can still override it with `Builder.setMetricsReportInterval(...)`; custom intervals +must remain above Google Cloud's five-second minimum. When an application supplies its own +`OpenTelemetry` instance, it must configure that instance's metric-reader cadence separately. + +Metric cadence and collector batching solve different problems. This collector does **not** put its +cumulative OTLP metrics through a batch processor: a forced shutdown flush can arrive immediately +after a periodic export, and a metric batch could combine both points for the same Prometheus time +series even when the periodic interval is much longer than the batch timeout. Managed Service for +Prometheus rejects that request as `Duplicate TimeSeries`. + +The five-second ingestion minimum and metric batching are separate constraints. Meeting the +minimum does not make batching cumulative metrics safe during shutdown. + +The dedicated `batch/traces` processor retains a five-second timeout because trace batching is +independently useful and does not have the cumulative-series collision behavior. Do not add +`batch/traces` to `metrics/otlp` or introduce another metric batch processor as a substitute for +choosing an application metric cadence. + +## Required Google Cloud APIs + +Enable these APIs in the project that hosts the worker pool: + +```bash +gcloud services enable \ + run.googleapis.com \ + artifactregistry.googleapis.com \ + secretmanager.googleapis.com \ + iam.googleapis.com \ + cloudresourcemanager.googleapis.com \ + monitoring.googleapis.com \ + telemetry.googleapis.com \ + cloudtrace.googleapis.com \ + --project="$PROJECT_ID" +``` + +`monitoring.googleapis.com` is required for Google Managed Service for Prometheus ingestion. +Traces are sent using authenticated OTLP to `telemetry.googleapis.com`; the Cloud Trace API must +also be enabled or Google Cloud discards trace data received by the Telemetry API. +The IAM API is used to create the runtime service account. The declarative worker-pool replacement +workflow can require the Cloud Resource Manager API to resolve the target project. + +The account enabling APIs needs `roles/serviceusage.serviceUsageAdmin` (or equivalent +permissions). + +## IAM + +Use a user-managed service account as the Cloud Run worker pool service identity. The collector +uses that identity through Application Default Credentials; do not set +`GOOGLE_APPLICATION_CREDENTIALS` in Cloud Run. + +Grant the worker-pool service account: + +- `roles/monitoring.metricWriter` on the telemetry project, for the + `googlemanagedprometheus` exporter. +- `roles/telemetry.tracesWriter` on the telemetry project, for OTLP traces sent to the Telemetry + API. `roles/cloudtrace.agent` also contains the write permission, but the narrower Telemetry role + is preferred here. +- `roles/serviceusage.serviceUsageConsumer` on the quota project (the same project in this + example). +- `roles/secretmanager.secretAccessor` on the collector-config and Temporal API-key secrets. + +For example: + +```bash +gcloud iam service-accounts create temporal-cloud-run-worker --project="$PROJECT_ID" + +SERVICE_ACCOUNT="temporal-cloud-run-worker@${PROJECT_ID}.iam.gserviceaccount.com" +for ROLE in \ + roles/monitoring.metricWriter \ + roles/telemetry.tracesWriter \ + roles/serviceusage.serviceUsageConsumer \ + roles/secretmanager.secretAccessor +do + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:${SERVICE_ACCOUNT}" \ + --role="$ROLE" +done +``` + +The deployer needs `roles/run.admin` (or the documented worker-pool deployment permissions) and +`roles/iam.serviceAccountUser` on this service account. Creating the service account, Artifact +Registry repository, secrets, and their IAM bindings also requires the corresponding administrative +permissions. The runtime service account does not need those administrative roles. + +The collector configuration does not export OTLP logs, so it does not require +`roles/logging.logWriter`. Cloud Run still captures the worker and collector containers' stdout and +stderr through its platform logging. + +## Build and deploy + +The commands below assume an existing Temporal Cloud namespace and API key. + +1. Create the secrets. Pin the API key to a numbered version in `worker-pool.yaml`; the collector + configuration is also pinned to a numbered version and injected as `OTELCOL_CONFIG`. + + ```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=cloud-run-worker/collector-config.yaml \ + --project="$PROJECT_ID" + ``` + + If either secret already exists, add a version with `gcloud secrets versions add` instead. + The manifest loads the collector YAML with `--config=env:OTELCOL_CONFIG`. This is intentional: + secret-backed file volumes have been rejected by some Cloud Run worker-pool rollouts even where + current documentation advertises support. + +2. After `temporal-gcp-cloud-run` is released, create an Artifact Registry repository and build and + push the worker image from the repository root: + + ```bash + REGION=us-central1 + IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/temporal-samples/cloud-run-worker:latest" + + gcloud artifacts repositories create temporal-samples \ + --repository-format=docker \ + --location="$REGION" \ + --project="$PROJECT_ID" + gcloud auth configure-docker "${REGION}-docker.pkg.dev" + + docker build \ + -f cloud-run-worker/Dockerfile \ + -t "$IMAGE" \ + . + docker push "$IMAGE" + ``` + +3. Edit the placeholders in `worker-pool.yaml`: + + - `PROJECT_ID` and `REGION`. + - `NAMESPACE_ID.ACCOUNT_ID` and the matching Temporal Cloud address. + - The Temporal API-key and collector-config secret versions if either is not version `1`. + - Image tag, task queue, instance count, and container resources as appropriate. + +4. Deploy the worker pool: + + ```bash + gcloud run worker-pools replace cloud-run-worker/worker-pool.yaml \ + --dry-run \ + --project="$PROJECT_ID" + + gcloud run worker-pools replace cloud-run-worker/worker-pool.yaml \ + --project="$PROJECT_ID" + ``` + +Worker pools use manual instance counts. This manifest starts one continuously allocated instance; +setting the count to zero disables the worker pool. + +## Collector startup and health requirements + +The collector is a required dependency, not an optional observability add-on: + +- `run.googleapis.com/container-dependencies` declares that `worker` depends on `collector`. +- The collector enables `health_check` on `0.0.0.0:13133` and has a startup probe on `/`. + Cloud Run worker pools do not supply a default startup probe. Without this probe Cloud Run can + start the worker even when the collector failed to load its configuration. +- The collector configuration is a Secret Manager-backed environment variable loaded through the + collector's `env` configuration provider. Keep the YAML below the Cloud Run secret-environment + size limit; this sample configuration is intentionally small. +- The worker starts only after the collector startup probe succeeds. A liveness probe restarts the + collector if it later becomes unhealthy. +- The OTLP receiver listens on `localhost:4317`, which is reachable by both containers because + containers in a worker-pool instance share a network namespace. +- A successful health probe confirms that the collector is running and accepted its configuration; + it does not prove that Google Cloud ingestion and IAM are working. Check collector logs for + exporter errors and verify both signals after deployment. + +If the collector is unavailable after startup, the Temporal worker continues processing work but +telemetry delivery can be delayed or lost. Treat collector liveness and exporter failures as +operational alerts. + +## Generate and view telemetry + +Start `GreetingWorkflow` on task queue `cloud-run-worker` with a single string argument. For +example, with an already configured Temporal CLI: + +```bash +temporal workflow start \ + --workflow-id cloud-run-greeting \ + --type GreetingWorkflow \ + --task-queue cloud-run-worker \ + --input '"Google Cloud"' +``` + +Temporal SDK metrics appear as Prometheus metrics in Cloud Monitoring. Traces appear in Trace +Explorer after passing through the Telemetry API. The OpenTelemetry service name defaults to the +value of `CLOUD_RUN_WORKER_POOL`; set `OTEL_SERVICE_NAME` on the worker container only if you need +an explicit override. + +For end-to-end metric verification, observe at least one normal 60-second periodic export, then +terminate or replace a worker-pool revision to exercise the forced shutdown flush. Confirm that +both exports reach Managed Service for Prometheus and that the collector logs contain no +`Duplicate TimeSeries` rejection. A short smoke test that exercises only one of these paths is not +sufficient evidence for the cumulative-metric pipeline. + +## Shutdown + +Cloud Run sends `SIGTERM` and allows 10 seconds before `SIGKILL`. The shutdown hook reserves six +seconds for graceful worker shutdown, one second for forced shutdown if necessary, and two seconds +for the plugin's Temporal-metrics and OpenTelemetry flush before closing the service stubs. The +flush runs after worker termination so it includes telemetry emitted by finishing tasks. +Long-running Activities must still use heartbeats and cancellation handling so they can stop within +the platform shutdown window. diff --git a/cloud-run-worker/build.gradle b/cloud-run-worker/build.gradle new file mode 100644 index 00000000..f588c7fb --- /dev/null +++ b/cloud-run-worker/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'application' + +dependencies { + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + implementation "io.temporal:temporal-gcp-cloud-run:$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.cloudrun.CloudRunWorker' +} diff --git a/cloud-run-worker/collector-config.yaml b/cloud-run-worker/collector-config.yaml new file mode 100644 index 00000000..801471de --- /dev/null +++ b/cloud-run-worker/collector-config.yaml @@ -0,0 +1,85 @@ +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 diff --git a/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/CloudRunWorker.java b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/CloudRunWorker.java new file mode 100644 index 00000000..6e9e5f9c --- /dev/null +++ b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/CloudRunWorker.java @@ -0,0 +1,71 @@ +package io.temporal.samples.cloudrun; + +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.gcp.cloudrun.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 continuously polling 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 { + ClientConfigProfile profile = ClientConfigProfile.load(); + CloudRunOpenTelemetryPlugin telemetryPlugin = CloudRunOpenTelemetryPlugin.newBuilder().build(); + + WorkflowServiceStubsOptions serviceOptions = + WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions()) + .setPlugins(telemetryPlugin) + .build(); + WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(serviceOptions); + 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, telemetryPlugin), "temporal-worker-shutdown")); + + factory.start(); + System.out.printf( + "Temporal worker started: taskQueue=%s, otelEndpoint=%s, serviceName=%s%n", + taskQueue, telemetryPlugin.getEndpoint(), telemetryPlugin.getServiceName()); + + // Cloud Run worker pools are continuous workloads. Keep the process alive until 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 telemetryPlugin) { + // Cloud Run allows 10 seconds between SIGTERM and SIGKILL. Flush only after the asynchronous + // worker shutdown so telemetry produced by finishing tasks is included. + factory.shutdown(); + factory.awaitTermination(6, TimeUnit.SECONDS); + if (!factory.isTerminated()) { + factory.shutdownNow(); + factory.awaitTermination(1, TimeUnit.SECONDS); + } + telemetryPlugin.newFlushHook().run(Duration.ofSeconds(2)); + service.shutdown(); + } +} diff --git a/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivities.java b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivities.java new file mode 100644 index 00000000..caccc96f --- /dev/null +++ b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivities.java @@ -0,0 +1,8 @@ +package io.temporal.samples.cloudrun; + +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface GreetingActivities { + String composeGreeting(String name); +} diff --git a/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivitiesImpl.java b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivitiesImpl.java new file mode 100644 index 00000000..14aeb9d3 --- /dev/null +++ b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingActivitiesImpl.java @@ -0,0 +1,8 @@ +package io.temporal.samples.cloudrun; + +public final class GreetingActivitiesImpl implements GreetingActivities { + @Override + public String composeGreeting(String name) { + return "Hello " + name + "!"; + } +} diff --git a/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflow.java b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflow.java new file mode 100644 index 00000000..3b57b355 --- /dev/null +++ b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflow.java @@ -0,0 +1,10 @@ +package io.temporal.samples.cloudrun; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface GreetingWorkflow { + @WorkflowMethod + String getGreeting(String name); +} diff --git a/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflowImpl.java b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflowImpl.java new file mode 100644 index 00000000..21b0607e --- /dev/null +++ b/cloud-run-worker/src/main/java/io/temporal/samples/cloudrun/GreetingWorkflowImpl.java @@ -0,0 +1,17 @@ +package io.temporal.samples.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/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/CloudRunPluginDefaultsTest.java b/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/CloudRunPluginDefaultsTest.java new file mode 100644 index 00000000..5152b4e8 --- /dev/null +++ b/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/CloudRunPluginDefaultsTest.java @@ -0,0 +1,15 @@ +package io.temporal.samples.cloudrun; + +import static org.junit.Assert.assertEquals; + +import io.temporal.gcp.cloudrun.CloudRunOpenTelemetryPlugin; +import java.time.Duration; +import org.junit.Test; + +public class CloudRunPluginDefaultsTest { + @Test + public void usesCoordinatedMetricExportInterval() { + assertEquals( + Duration.ofSeconds(60), CloudRunOpenTelemetryPlugin.DEFAULT_METRICS_REPORT_INTERVAL); + } +} diff --git a/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/GreetingWorkflowTest.java b/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/GreetingWorkflowTest.java new file mode 100644 index 00000000..f5385c46 --- /dev/null +++ b/cloud-run-worker/src/test/java/io/temporal/samples/cloudrun/GreetingWorkflowTest.java @@ -0,0 +1,32 @@ +package io.temporal.samples.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/cloud-run-worker/worker-pool.yaml b/cloud-run-worker/worker-pool.yaml new file mode 100644 index 00000000..5c5e0042 --- /dev/null +++ b/cloud-run-worker/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..2775d22e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,3 +9,12 @@ include 'springboot' include 'springboot-basic' include 'lambda-worker:starter' include 'lambda-worker:worker' +include 'cloud-run-worker' + +// The cloud-run-worker sample depends on io.temporal:temporal-gcp-cloud-run, which is not yet +// released. Pass -PtemporalSdkPath=/path/to/sdk-java to resolve it (and the other temporal-* +// modules) against a local SDK checkout through Gradle composite-build dependency substitution. +def temporalSdkPath = gradle.startParameter.projectProperties['temporalSdkPath'] +if (temporalSdkPath) { + includeBuild temporalSdkPath +}