diff --git a/README.md b/README.md index 4d00e320..2b97cbfa 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 0cafde69..ab292b3b 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' 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 c5157db3..3405d505 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 00000000..8a4bcc80 --- /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 c8ebbdb4..3c4b5ed3 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 00000000..118cd4ef
--- /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 00000000..cb82a3b6
--- /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 00000000..87809ebb
--- /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 00000000..0afa937b
--- /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 00000000..fe6baaf3
--- /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 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 00000000..e01185a7
--- /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();
+ }
+}