From ba78da3cdbc86cb08f5be59d88bcc3440b53b6d1 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 27 Apr 2026 17:25:02 -0400 Subject: [PATCH 1/3] Java hello/full samples --- README.md | 3 + build.gradle | 3 +- core/build.gradle | 3 + .../hello/HelloStandaloneActivity.java | 121 ++++++++++++++++++ .../java/io/temporal/samples/hello/README.md | 1 + .../standaloneactivities/CountActivities.java | 37 ++++++ .../standaloneactivities/ExecuteActivity.java | 51 ++++++++ .../GreetingActivities.java | 12 ++ .../GreetingActivitiesImpl.java | 16 +++ .../standaloneactivities/ListActivities.java | 37 ++++++ .../samples/standaloneactivities/README.md | 103 +++++++++++++++ .../StandaloneActivityWorker.java | 31 +++++ .../standaloneactivities/StartActivity.java | 55 ++++++++ .../hello/HelloStandaloneActivityTest.java | 101 +++++++++++++++ .../StandaloneActivitiesTest.java | 95 ++++++++++++++ 15 files changed, 668 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/io/temporal/samples/hello/HelloStandaloneActivity.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/CountActivities.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/ExecuteActivity.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivities.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivitiesImpl.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/ListActivities.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/README.md create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/StandaloneActivityWorker.java create mode 100644 core/src/main/java/io/temporal/samples/standaloneactivities/StartActivity.java create mode 100644 core/src/test/java/io/temporal/samples/hello/HelloStandaloneActivityTest.java create mode 100644 core/src/test/java/io/temporal/samples/standaloneactivities/StandaloneActivitiesTest.java diff --git a/README.md b/README.md index 4d00e320b..2b97cbfa9 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ See the README.md file in each main sample directory for cut/paste Gradle comman - [**HelloWorkflowTimer**](/core/src/main/java/io/temporal/samples/hello/HelloWorkflowTimer.java): Demonstrates how we can use workflow timer to restrict duration of workflow execution instead of workflow run/execution timeouts. - [**Auto-Heartbeating**](/core/src/main/java/io/temporal/samples/autoheartbeat/): Demonstrates use of Auto-heartbeating utility via activity interceptor. - [**HelloSignalWithStartAndWorkflowInit**](/core/src/main/java/io/temporal/samples/hello/HelloSignalWithStartAndWorkflowInit.java): Demonstrates how WorkflowInit can be useful with SignalWithStart to initialize workflow variables. + - [**HelloStandaloneActivity**](/core/src/main/java/io/temporal/samples/hello/HelloStandaloneActivity.java): Demonstrates how to execute a Standalone Activity directly from an ActivityClient, without a Workflow. #### Scenario-based samples @@ -147,6 +148,8 @@ Load client configuration from TOML files with programmatic overrides. - [**Exclude Workflow/ActivityTypes from Interceptors**](/core/src/main/java/io/temporal/samples/excludefrominterceptor): Demonstrates how to exclude certain workflow / activity types from interceptors. +- [**Standalone Activities**](/core/src/main/java/io/temporal/samples/standaloneactivities): Demonstrates how to start, execute, list, and count Standalone Activities — Activities that run independently without a Workflow, using ActivityClient. + #### SDK Metrics - [**Set up SDK metrics**](/core/src/main/java/io/temporal/samples/metrics): Demonstrates how to set up and scrape SDK metrics. diff --git a/build.gradle b/build.gradle index 0cafde690..07d1ace1d 100644 --- a/build.gradle +++ b/build.gradle @@ -26,12 +26,13 @@ subprojects { ext { otelVersion = '1.30.1' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.34.0' + javaSDKVersion = '1.35.0-SNAPSHOT' camelVersion = '3.22.1' jarVersion = '1.0.0' } repositories { + mavenLocal() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/core/build.gradle b/core/build.gradle index c5157db3d..3405d5054 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -61,4 +61,7 @@ dependencies { task execute(type: JavaExec) { mainClass = findProperty("mainClass") ?: "" classpath = sourceSets.main.runtimeClasspath + if (findProperty("args")) { + args findProperty("args").tokenize() + } } diff --git a/core/src/main/java/io/temporal/samples/hello/HelloStandaloneActivity.java b/core/src/main/java/io/temporal/samples/hello/HelloStandaloneActivity.java new file mode 100644 index 000000000..8a4bcc802 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/hello/HelloStandaloneActivity.java @@ -0,0 +1,121 @@ +package io.temporal.samples.hello; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; +import io.temporal.client.ActivityClient; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.StartActivityOptions; +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import java.io.IOException; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Sample Temporal application that executes a Standalone Activity — an Activity that runs + * independently, without being orchestrated by a Workflow. Requires a local instance of the + * Temporal service to be running. + * + *

Unlike regular Activities, a Standalone Activity is started directly from a Temporal Client + * using {@link ActivityClient}, not from inside a Workflow Definition. Writing the Activity and + * registering it with the Worker is identical in both cases. + */ +public class HelloStandaloneActivity { + + static final String TASK_QUEUE = "HelloStandaloneActivityTaskQueue"; + static final String ACTIVITY_ID = "hello-standalone-activity-id"; + + /** + * Activity interface. Writing a Standalone Activity is identical to writing an Activity + * orchestrated by a Workflow — the same Activity can be used for both. + * + * @see io.temporal.activity.ActivityInterface + * @see io.temporal.activity.ActivityMethod + */ + @ActivityInterface + public interface GreetingActivities { + + // Define your activity method which can be called directly from a Temporal Client. + @ActivityMethod + String composeGreeting(String greeting, String name); + } + + /** Simple activity implementation that concatenates two strings. */ + public static class GreetingActivitiesImpl implements GreetingActivities { + + private static final Logger log = LoggerFactory.getLogger(GreetingActivitiesImpl.class); + + @Override + public String composeGreeting(String greeting, String name) { + log.info("Composing greeting..."); + return greeting + ", " + name + "!"; + } + } + + public static void main(String[] args) { + // Load configuration from environment and files. + ClientConfigProfile profile; + try { + profile = ClientConfigProfile.load(); + } catch (IOException e) { + throw new RuntimeException("Failed to load client configuration", e); + } + + // gRPC stubs wrapper that talks to the temporal service. + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + + // WorkflowClient is required to create a Worker. + WorkflowClient workflowClient = + WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + + // Worker factory that can be used to create workers for specific task queues. + WorkerFactory factory = WorkerFactory.newInstance(workflowClient); + + // Worker that listens on a task queue and hosts activity implementations. + Worker worker = factory.newWorker(TASK_QUEUE); + + // Activities are stateless and thread safe. So a shared instance is used. + worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); + + // Start listening to the activity task queue. + factory.start(); + + // ActivityClient executes standalone activities directly from application code, + // without a Workflow. + ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); + + // Options specifying the activity ID, task queue, and timeout. + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId(ACTIVITY_ID) + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build(); + + try { + // Execute the activity and wait for its result. The typed API uses an unbound method + // reference so the SDK can infer the activity type name and result type automatically. + String result = + client.execute( + GreetingActivities.class, + GreetingActivities::composeGreeting, + options, + "Hello", + "World"); + + System.out.println(result); + } finally { + // Shut down the worker before the service so polling threads stop cleanly. + factory.shutdown(); + service.shutdown(); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/hello/README.md b/core/src/main/java/io/temporal/samples/hello/README.md index c8ebbdb42..3c4b5ed3f 100644 --- a/core/src/main/java/io/temporal/samples/hello/README.md +++ b/core/src/main/java/io/temporal/samples/hello/README.md @@ -35,4 +35,5 @@ To run each hello world sample, use one of the following commands: ./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloUpdate ./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSignalWithTimer ./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloSignalWithStartAndWorkflowInit +./gradlew -q execute -PmainClass=io.temporal.samples.hello.HelloStandaloneActivity ``` diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/CountActivities.java b/core/src/main/java/io/temporal/samples/standaloneactivities/CountActivities.java new file mode 100644 index 000000000..118cd4ef5 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/CountActivities.java @@ -0,0 +1,37 @@ +package io.temporal.samples.standaloneactivities; + +import static io.temporal.samples.standaloneactivities.StandaloneActivityWorker.TASK_QUEUE; + +import io.temporal.client.ActivityClient; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.ActivityExecutionCount; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; + +/** Counts standalone activity executions on the task queue. */ +public class CountActivities { + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + + ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); + + try { + ActivityExecutionCount resp = client.countExecutions("TaskQueue = '" + TASK_QUEUE + "'"); + + System.out.println("Total activities: " + resp.getCount()); + resp.getGroups() + .forEach( + group -> + System.out.println("Group " + group.getGroupValues() + ": " + group.getCount())); + } finally { + service.shutdown(); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/ExecuteActivity.java b/core/src/main/java/io/temporal/samples/standaloneactivities/ExecuteActivity.java new file mode 100644 index 000000000..cb82a3b6d --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/ExecuteActivity.java @@ -0,0 +1,51 @@ +package io.temporal.samples.standaloneactivities; + +import static io.temporal.samples.standaloneactivities.StandaloneActivityWorker.TASK_QUEUE; + +import io.temporal.client.ActivityClient; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.StartActivityOptions; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; +import java.time.Duration; + +/** + * Executes a standalone activity and waits for the result. Requires a Worker running + * StandaloneActivityWorker. + */ +public class ExecuteActivity { + + static final String ACTIVITY_ID = "standalone-activity-id"; + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + + ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); + + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId(ACTIVITY_ID) + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build(); + + try { + String result = + client.execute( + GreetingActivities.class, + GreetingActivities::composeGreeting, + options, + "Hello", + "World"); + System.out.println("Activity result: " + result); + } finally { + service.shutdown(); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivities.java b/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivities.java new file mode 100644 index 000000000..87809ebb6 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivities.java @@ -0,0 +1,12 @@ +package io.temporal.samples.standaloneactivities; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +/** Activity interface shared by all programs in this sample. */ +@ActivityInterface +public interface GreetingActivities { + + @ActivityMethod + String composeGreeting(String greeting, String name); +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivitiesImpl.java b/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivitiesImpl.java new file mode 100644 index 000000000..0afa937b2 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/GreetingActivitiesImpl.java @@ -0,0 +1,16 @@ +package io.temporal.samples.standaloneactivities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Activity implementation. */ +public class GreetingActivitiesImpl implements GreetingActivities { + + private static final Logger log = LoggerFactory.getLogger(GreetingActivitiesImpl.class); + + @Override + public String composeGreeting(String greeting, String name) { + log.info("Composing greeting..."); + return greeting + ", " + name + "!"; + } +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/ListActivities.java b/core/src/main/java/io/temporal/samples/standaloneactivities/ListActivities.java new file mode 100644 index 000000000..fe6baaf3f --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/ListActivities.java @@ -0,0 +1,37 @@ +package io.temporal.samples.standaloneactivities; + +import static io.temporal.samples.standaloneactivities.StandaloneActivityWorker.TASK_QUEUE; + +import io.temporal.client.ActivityClient; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.ActivityExecutionMetadata; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; +import java.util.stream.Stream; + +/** Lists standalone activity executions on the task queue. */ +public class ListActivities { + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + + ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); + + try (Stream activities = + client.listExecutions("TaskQueue = '" + TASK_QUEUE + "'")) { + activities.forEach( + info -> + System.out.printf( + "ActivityID: %s, Type: %s, Status: %s%n", + info.getActivityId(), info.getActivityType(), info.getStatus())); + } finally { + service.shutdown(); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/README.md b/core/src/main/java/io/temporal/samples/standaloneactivities/README.md new file mode 100644 index 000000000..9023f6d37 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/README.md @@ -0,0 +1,103 @@ +# Standalone Activities + +This sample demonstrates [Standalone Activities](https://docs.temporal.io/develop/java/activities/standalone-activities), +which run independently without being orchestrated by a Workflow. You start and manage them directly +from a Temporal Client using `ActivityClient`. + +The sample has these separate programs: + +| Program | Purpose | +|---|---| +| `StandaloneActivityWorker` | Runs a Worker that processes activity tasks | +| `ExecuteActivity` | Starts an activity and waits for its result | +| `StartActivity` | Starts an activity without blocking, then waits for the result | +| `ListActivities` | Lists activity executions on the task queue | +| `CountActivities` | Counts activity executions on the task queue | + +## Prerequisites + +- Temporal dev server with Standalone Activity support. See the + [Java SDK Standalone Activities guide](https://docs.temporal.io/develop/java/activities/standalone-activities#get-started) + for download instructions. + +## Start the Temporal development server + +```bash +./temporal server start-dev +``` + +## Run the Worker + +In a terminal, start the Worker. Leave it running to process activities. + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.standaloneactivities.StandaloneActivityWorker +``` + +## Execute a Standalone Activity + +In another terminal, execute an activity and wait for its result: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.standaloneactivities.ExecuteActivity +``` + +Or use the Temporal CLI: + +```bash +./temporal activity execute \ + --type composeGreeting \ + --activity-id standalone-activity-id \ + --task-queue standalone-activity-task-queue \ + --start-to-close-timeout 10s \ + --input '"Hello"' \ + --input '"World"' +``` + +## Start a Standalone Activity without waiting + +Start an activity and retrieve its result separately: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.standaloneactivities.StartActivity +``` + +Or use the Temporal CLI: + +```bash +./temporal activity start \ + --type composeGreeting \ + --activity-id standalone-activity-id \ + --task-queue standalone-activity-task-queue \ + --start-to-close-timeout 10s \ + --input '"Hello"' \ + --input '"World"' +``` + +## List Standalone Activities + +List activity executions on the task queue: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.standaloneactivities.ListActivities +``` + +Or use the Temporal CLI: + +```bash +./temporal activity list +``` + +## Count Standalone Activities + +Count activity executions on the task queue: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.standaloneactivities.CountActivities +``` + +Or use the Temporal CLI: + +```bash +./temporal activity count +``` diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/StandaloneActivityWorker.java b/core/src/main/java/io/temporal/samples/standaloneactivities/StandaloneActivityWorker.java new file mode 100644 index 000000000..5b54d0279 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/StandaloneActivityWorker.java @@ -0,0 +1,31 @@ +package io.temporal.samples.standaloneactivities; + +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import java.io.IOException; + +/** + * Worker that processes standalone activity tasks. Run this before executing or starting standalone + * activities with ExecuteActivity or StartActivity. + */ +public class StandaloneActivityWorker { + + static final String TASK_QUEUE = "standalone-activity-task-queue"; + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + + WorkerFactory factory = WorkerFactory.newInstance(client); + Worker worker = factory.newWorker(TASK_QUEUE); + worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); + + factory.start(); + System.out.println("Worker running on task queue: " + TASK_QUEUE); + } +} diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/StartActivity.java b/core/src/main/java/io/temporal/samples/standaloneactivities/StartActivity.java new file mode 100644 index 000000000..72115ce6c --- /dev/null +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/StartActivity.java @@ -0,0 +1,55 @@ +package io.temporal.samples.standaloneactivities; + +import static io.temporal.samples.standaloneactivities.StandaloneActivityWorker.TASK_QUEUE; + +import io.temporal.client.ActivityClient; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.ActivityHandle; +import io.temporal.client.StartActivityOptions; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; +import java.time.Duration; + +/** + * Starts a standalone activity without blocking, then waits for the result using the returned + * handle. Requires a Worker running StandaloneActivityWorker. + */ +public class StartActivity { + + static final String ACTIVITY_ID = "standalone-activity-id"; + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + + ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); + + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId(ACTIVITY_ID) + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build(); + + try { + ActivityHandle handle = + client.start( + GreetingActivities.class, + GreetingActivities::composeGreeting, + options, + "Hello", + "World"); + System.out.println("Started activity ID: " + ACTIVITY_ID); + + String result = handle.getResult(); + System.out.println("Activity result: " + result); + } finally { + service.shutdown(); + } + } +} diff --git a/core/src/test/java/io/temporal/samples/hello/HelloStandaloneActivityTest.java b/core/src/test/java/io/temporal/samples/hello/HelloStandaloneActivityTest.java new file mode 100644 index 000000000..1f04a8789 --- /dev/null +++ b/core/src/test/java/io/temporal/samples/hello/HelloStandaloneActivityTest.java @@ -0,0 +1,101 @@ +package io.temporal.samples.hello; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import io.temporal.activity.ActivityOptions; +import io.temporal.client.WorkflowOptions; +import io.temporal.samples.hello.HelloStandaloneActivity.GreetingActivities; +import io.temporal.samples.hello.HelloStandaloneActivity.GreetingActivitiesImpl; +import io.temporal.testing.TestWorkflowRule; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.time.Duration; +import org.junit.Rule; +import org.junit.Test; + +/** + * Unit tests for {@link HelloStandaloneActivity}. Uses an embedded Temporal test server so no + * external service is required. + * + *

Standalone Activities do not use a Workflow at runtime, but the embedded test server only + * supports Activity execution through a Workflow. These tests therefore drive the Activity through + * a minimal wrapper Workflow so the Activity logic is exercised against the real SDK worker stack. + */ +public class HelloStandaloneActivityTest { + + /** + * Minimal wrapper Workflow used to invoke {@link GreetingActivities} through the embedded test + * worker. + */ + @WorkflowInterface + public interface TestWorkflow { + + @WorkflowMethod + String run(String greeting, String name); + } + + public static class TestWorkflowImpl implements TestWorkflow { + + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build()); + + @Override + public String run(String greeting, String name) { + return activities.composeGreeting(greeting, name); + } + } + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflowImpl.class) + .setDoNotStart(true) + .build(); + + @Test + public void testActivityImpl() { + testWorkflowRule.getWorker().registerActivitiesImplementations(new GreetingActivitiesImpl()); + testWorkflowRule.getTestEnvironment().start(); + + TestWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello, World!", workflow.run("Hello", "World")); + + testWorkflowRule.getTestEnvironment().shutdown(); + } + + @Test + public void testMockedActivity() { + // withoutAnnotations() prevents Mockito from copying @ActivityMethod from the interface onto + // the mock, which would cause worker registration to fail. + GreetingActivities activities = + mock(GreetingActivities.class, withSettings().withoutAnnotations()); + when(activities.composeGreeting("Hello", "World")).thenReturn("Hello, World!"); + testWorkflowRule.getWorker().registerActivitiesImplementations(activities); + testWorkflowRule.getTestEnvironment().start(); + + TestWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello, World!", workflow.run("Hello", "World")); + verify(activities).composeGreeting("Hello", "World"); + + testWorkflowRule.getTestEnvironment().shutdown(); + } +} diff --git a/core/src/test/java/io/temporal/samples/standaloneactivities/StandaloneActivitiesTest.java b/core/src/test/java/io/temporal/samples/standaloneactivities/StandaloneActivitiesTest.java new file mode 100644 index 000000000..e01185a78 --- /dev/null +++ b/core/src/test/java/io/temporal/samples/standaloneactivities/StandaloneActivitiesTest.java @@ -0,0 +1,95 @@ +package io.temporal.samples.standaloneactivities; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import io.temporal.activity.ActivityOptions; +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.TestWorkflowRule; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.time.Duration; +import org.junit.Rule; +import org.junit.Test; + +/** + * Unit tests for the standaloneactivities sample. Uses an embedded Temporal test server so no + * external service is required. + * + *

Standalone Activities do not use a Workflow at runtime, but the embedded test server only + * supports Activity execution through a Workflow. These tests therefore drive the Activity through + * a minimal wrapper Workflow so the Activity logic is exercised against the real SDK worker stack. + */ +public class StandaloneActivitiesTest { + + @WorkflowInterface + public interface TestWorkflow { + + @WorkflowMethod + String run(String greeting, String name); + } + + public static class TestWorkflowImpl implements TestWorkflow { + + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build()); + + @Override + public String run(String greeting, String name) { + return activities.composeGreeting(greeting, name); + } + } + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflowImpl.class) + .setDoNotStart(true) + .build(); + + @Test + public void testActivityImpl() { + testWorkflowRule.getWorker().registerActivitiesImplementations(new GreetingActivitiesImpl()); + testWorkflowRule.getTestEnvironment().start(); + + TestWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello, World!", workflow.run("Hello", "World")); + + testWorkflowRule.getTestEnvironment().shutdown(); + } + + @Test + public void testMockedActivity() { + // withoutAnnotations() prevents Mockito from copying @ActivityMethod from the interface onto + // the mock, which would cause worker registration to fail. + GreetingActivities activities = + mock(GreetingActivities.class, withSettings().withoutAnnotations()); + when(activities.composeGreeting("Hello", "World")).thenReturn("Hello, World!"); + testWorkflowRule.getWorker().registerActivitiesImplementations(activities); + testWorkflowRule.getTestEnvironment().start(); + + TestWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello, World!", workflow.run("Hello", "World")); + verify(activities).composeGreeting("Hello", "World"); + + testWorkflowRule.getTestEnvironment().shutdown(); + } +} From acd8b414ab07db183f365bb1649104497c5ead3d Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Tue, 28 Apr 2026 12:12:14 -0400 Subject: [PATCH 2/3] Capitalize type name --- .../java/io/temporal/samples/standaloneactivities/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/standaloneactivities/README.md b/core/src/main/java/io/temporal/samples/standaloneactivities/README.md index 9023f6d37..4d65619f9 100644 --- a/core/src/main/java/io/temporal/samples/standaloneactivities/README.md +++ b/core/src/main/java/io/temporal/samples/standaloneactivities/README.md @@ -46,7 +46,7 @@ Or use the Temporal CLI: ```bash ./temporal activity execute \ - --type composeGreeting \ + --type ComposeGreeting \ --activity-id standalone-activity-id \ --task-queue standalone-activity-task-queue \ --start-to-close-timeout 10s \ @@ -66,7 +66,7 @@ Or use the Temporal CLI: ```bash ./temporal activity start \ - --type composeGreeting \ + --type ComposeGreeting \ --activity-id standalone-activity-id \ --task-queue standalone-activity-task-queue \ --start-to-close-timeout 10s \ From 482ece5be8cb137ee726025450d30444da86179a Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Tue, 28 Apr 2026 12:40:47 -0400 Subject: [PATCH 3/3] javaSDKVersion = '1.35.0' --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 07d1ace1d..ab292b3ba 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ subprojects { ext { otelVersion = '1.30.1' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.35.0-SNAPSHOT' + javaSDKVersion = '1.35.0' camelVersion = '3.22.1' jarVersion = '1.0.0' }