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
17 changes: 17 additions & 0 deletions gcp/cloud-run/id/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM eclipse-temurin:17-jdk-jammy AS build

WORKDIR /workspace
COPY . .

# temporal-gcp-cloud-run-id is unreleased; the composite build resolves it from a local SDK checkout (see README).
RUN ./gradlew --no-daemon :gcp:cloud-run:id: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/id/build/install/cloud-run-id/ /app/

USER 10001
ENTRYPOINT ["/app/bin/cloud-run-id"]
60 changes: 60 additions & 0 deletions gcp/cloud-run/id/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Temporal Cloud Run Id sample

A Temporal Java Worker for a Google Cloud Run **worker pool** that registers
`CloudRunIdPlugin` from `io.temporal:temporal-gcp-cloud-run-id` on the client, so the Temporal
client identity is set from Cloud Run instance metadata as `{instanceId}@{revision}`. The plugin
sets identity only. A small greeting workflow and activity run until Cloud Run stops the instance.

> Google Cloud Run support is experimental and may change without notice.

## Unreleased SDK dependency

`temporal-gcp-cloud-run-id` is not yet released. `settings.gradle` resolves it (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 module ships; then drop the composite block and bump `javaSDKVersion`.

## How it works

Cloud Run worker pools set `CLOUD_RUN_WORKER_POOL` and `CLOUD_RUN_REVISION` (services set `K_SERVICE`
and `K_REVISION`). `CloudRunIdPlugin` reads those plus the instance id from the Cloud Run metadata
server and sets the client identity to `{instanceId}@{revision}` unless one is already set; workers
created from the client inherit it. `GoogleCloudRunMetadata.fetch().identity()` exposes the same
value, which the worker logs at startup.

The worker reads `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`), `TEMPORAL_NAMESPACE` (default
`default`), and `TEMPORAL_TASK_QUEUE` (default `cloud-run-id`). A plaintext connection is used;
configure TLS or an API key in `CloudRunWorker.java` for a secured Service such as Temporal Cloud.

## Build and test

```bash
./gradlew :gcp:cloud-run:id:test
./gradlew -PtemporalSdkPath=/path/to/sdk-java :gcp:cloud-run:id:installDist
```

## Deploy

Worker pools keep CPU allocated between requests. Until `temporal-gcp-cloud-run-id` is released a
remote `--source` build cannot resolve it, so build the image locally against your SDK checkout and
deploy it by tag:

```bash
export REGION=us-central1
gcloud run worker-pools deploy cloud-run-id \
--image "$REGION-docker.pkg.dev/$PROJECT_ID/<repo>/cloud-run-id:latest" \
--region "$REGION" \
--set-env-vars "TEMPORAL_ADDRESS=<addr>,TEMPORAL_NAMESPACE=<ns>,TEMPORAL_TASK_QUEUE=cloud-run-id"
```

Each revision starts a fresh instance whose worker reports a distinct identity.

## Start a workflow

```bash
temporal workflow start --type GreetingWorkflow --task-queue cloud-run-id \
--workflow-id cloud-run-greeting --input '"Cloud Run"'
```

The identity appears on the task-queue pollers (`temporal task-queue describe`) and recorded events.
Delete the pool with `gcloud run worker-pools delete cloud-run-id --region "$REGION"`.
23 changes: 23 additions & 0 deletions gcp/cloud-run/id/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apply plugin: 'application'

dependencies {
implementation "io.temporal:temporal-sdk:$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.id.CloudRunWorker'
// Stable launcher name the Dockerfile relies on, independent of the Gradle project path.
applicationName = 'cloud-run-id'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.temporal.samples.gcp.cloudrun.id;

import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.gcp.cloudrun.id.CloudRunIdPlugin;
import io.temporal.gcp.cloudrun.id.GoogleCloudRunMetadata;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** A Temporal Worker for a Google Cloud Run worker pool. */
public final class CloudRunWorker {
private static final Logger logger = LoggerFactory.getLogger(CloudRunWorker.class);

static final String ADDRESS_ENV = "TEMPORAL_ADDRESS";
static final String NAMESPACE_ENV = "TEMPORAL_NAMESPACE";
static final String TASK_QUEUE_ENV = "TEMPORAL_TASK_QUEUE";

static final String DEFAULT_ADDRESS = "127.0.0.1:7233";
static final String DEFAULT_NAMESPACE = "default";
static final String DEFAULT_TASK_QUEUE = "cloud-run-id";

private CloudRunWorker() {}

public static void main(String[] args) {
String address = envOrDefault(ADDRESS_ENV, DEFAULT_ADDRESS);
String namespace = envOrDefault(NAMESPACE_ENV, DEFAULT_NAMESPACE);
String taskQueue = envOrDefault(TASK_QUEUE_ENV, DEFAULT_TASK_QUEUE);

// Plaintext connection; add TLS or an API key here for Temporal Cloud.
WorkflowServiceStubs service =
WorkflowServiceStubs.newServiceStubs(
WorkflowServiceStubsOptions.newBuilder().setTarget(address).build());

// CloudRunIdPlugin sets the client identity to {instanceId}@{revision} from metadata.
WorkflowClient client =
WorkflowClient.newInstance(
service,
WorkflowClientOptions.newBuilder()
.setNamespace(namespace)
.setPlugins(new CloudRunIdPlugin())
.build());

WorkerFactory factory = WorkerFactory.newInstance(client);

Worker worker = factory.newWorker(taskQueue);
worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
worker.registerActivitiesImplementations(new GreetingActivitiesImpl());

Runtime.getRuntime()
.addShutdownHook(new Thread(() -> shutdown(factory, service), "temporal-worker-shutdown"));

factory.start();
logger.info(
"Temporal worker started (identity={}, taskQueue={})",
GoogleCloudRunMetadata.fetch().identity(),
taskQueue);

// Keep the process alive until Cloud Run sends SIGTERM.
factory.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
}

private static void shutdown(WorkerFactory factory, WorkflowServiceStubs service) {
// Cloud Run sends SIGTERM before SIGKILL; stop polling, drain in-flight tasks, then close.
factory.shutdown();
factory.awaitTermination(6, TimeUnit.SECONDS);
if (!factory.isTerminated()) {
factory.shutdownNow();
factory.awaitTermination(1, TimeUnit.SECONDS);
}
service.shutdown();
}

private static String envOrDefault(String name, String defaultValue) {
String value = System.getenv(name);
return value == null || value.trim().isEmpty() ? defaultValue : value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.temporal.samples.gcp.cloudrun.id;

import io.temporal.activity.ActivityInterface;

@ActivityInterface
public interface GreetingActivities {
String composeGreeting(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.temporal.samples.gcp.cloudrun.id;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class GreetingActivitiesImpl implements GreetingActivities {

private static final Logger logger = LoggerFactory.getLogger(GreetingActivitiesImpl.class);

@Override
public String composeGreeting(String name) {
logger.info("Composing greeting for {}", name);
return "Hello, " + name + "!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.temporal.samples.gcp.cloudrun.id;

import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

@WorkflowInterface
public interface GreetingWorkflow {
@WorkflowMethod
String getGreeting(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.temporal.samples.gcp.cloudrun.id;

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);
}
}
14 changes: 14 additions & 0 deletions gcp/cloud-run/id/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<logger name="io.grpc" level="WARN"/>
<logger name="io.netty" level="WARN"/>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.temporal.samples.gcp.cloudrun.id;

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)
.setActivityImplementations(new GreetingActivitiesImpl())
.build();

@Test
public void returnsGreeting() {
GreetingWorkflow workflow =
testWorkflowRule
.getWorkflowClient()
.newWorkflowStub(
GreetingWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());

assertEquals("Hello, Cloud Run!", workflow.getGreeting("Cloud Run"));
}
}
7 changes: 7 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ include 'springboot'
include 'springboot-basic'
include 'lambda-worker:starter'
include 'lambda-worker:worker'
include 'gcp:cloud-run:id'

// The gcp:cloud-run samples use 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
}
Loading