Skip to content

Standalone Activities for Java#2858

Open
GregoryTravis wants to merge 86 commits intomasterfrom
gmt/java-standalone-activities
Open

Standalone Activities for Java#2858
GregoryTravis wants to merge 86 commits intomasterfrom
gmt/java-standalone-activities

Conversation

@GregoryTravis
Copy link
Copy Markdown
Contributor

@GregoryTravis GregoryTravis commented Apr 22, 2026

Add standalone activity API (ActivityClient)

Introduces support for standalone activities — activities that execute independently of any workflow.

New public API surface:

  • ActivityClient — top-level client for starting, describing,
    listing, counting, cancelling, and terminating standalone activities
  • ActivityHandle<R> / UntypedActivityHandle — typed and untyped
    handles returned by ActivityClient.start(); provide getResult(),
    getResultAsync(), describe(), cancel(), and terminate()
  • StartActivityOptions — builder-based options for starting an
    activity (id, task queue, timeouts, retry, priority, etc.)
  • ActivityClientOptions — namespace / data converter / interceptor
    configuration for ActivityClient
  • ActivityExecutionDescription — rich descriptor returned by
    describe() and list*(); extends ActivityExecutionMetadata
  • ActivityExecutionMetadata — lightweight metadata used in list
    results
  • ActivityExecutionCount — result of countActivities()
  • ActivityListOptions / ActivityListPaginatedOptions — filtering
    and pagination options for list operations
  • ActivityListPage — page of results with continuation token
  • ActivityAlreadyStartedException — thrown when a duplicate activity
    id is rejected by the server
  • ActivityFailedException — thrown from getResult() when the
    activity fails

New interceptor API:

  • ActivityClientCallsInterceptor — per-call interceptor for all
    ActivityClient operations
  • ActivityClientCallsInterceptorBase — pass-through base
    implementation (delegates every method to the next interceptor)
  • ActivityClientInterceptor — factory interceptor that wraps the
    client-level invoker
  • ActivityClientInterceptorBase — no-op base implementation

ActivityInfo additions:

  • getActivityRunId() — run-scoped id assigned by the server to each
    activity execution
  • isWorkflowActivity() — distinguishes workflow-dispatched activities
    from standalone ones

ActivityCompletionClient additions:

  • New overloads taking (String activityId, Optional<String> runId, …)
    for completing, failing, sending heartbeats, and cancelling standalone
    activities without a workflow id

ActivitySerializationContext fix:

  • workflowId and workflowType are now @Nullable; removed
    requireNonNull guards that caused NPEs for standalone activities

Functions.java:

  • Added Func and VFunc (zero-arg typed/void functional interfaces)
    needed by ActivityClient.start() method-reference overloads

CI:

  • Enable standalone-activity server feature flags in the Temporal CLI
    dev server used by unit tests:
    frontend.activityAPIsEnabled, activity.enableStandalone,
    history.enableChasm, history.enableTransitionHistory

Tests:

  • StandaloneActivityTest — integration tests against a real server
    covering the full activity lifecycle (start, poll, complete, cancel,
    terminate, describe, list, heartbeat, async completion)
  • ActivityClientCallsInterceptorBaseTest — delegation tests for the
    base interceptor
  • ActivityClientCallsInterceptorChainTest — chain ordering tests
  • ActivityHandleImplTest — unit tests for handle dispatch
  • ActivityCompletionClientImplTest — unit tests for completion client
  • ActivityInfoStandaloneTest — unit tests for ActivityInfo in the
    standalone context
  • ActivitySerializationContextTest — confirms nullable workflow fields
  • StartActivityOptionsTest, ActivityClientOptionsTest,
    ActivityExecutionDescriptionTest, ActivityExecutionMetadataTest,
    ActivityAlreadyStartedExceptionTest — options and value-type tests

Remove isEmpty() check from isWorkflowActivity.
@@ -25,7 +25,7 @@ protected ActivityCompletionException(ActivityInfo info, Throwable cause) {
? "WorkflowId="
+ info.getWorkflowId()
+ ", RunId="
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should change the property name. Any down side to doing that?

Suggested change
+ ", RunId="
+ ", WorkflowRunId="

If we do that, there are likely other places where should do the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup PR.

return "ActivityInfo{"
+ "workflowId="
+ getWorkflowId()
+ ", runId="
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should no longer be using getRunId here. Either workflowRunId or activityRunId.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup PR.

* to reduce the total execution time.
*/
public class GetActivityResultAsyncOverServerLongPollWaitTest {
private static final int ACTIVITY_LONG_POLL_TIMEOUT_SECONDS = 20;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this "20 sec" come from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the server-side long poll time; see also GetResultsAsyncOverMaximumLongPollWaitTest.

These tests could be made more robust by checking that the retry count is at least two, but that would require more machinery in the test so I'm not sure it's a good idea.

Comment thread temporal-sdk/src/main/java/io/temporal/client/ActivityHandle.java
Copy link
Copy Markdown
Contributor

@maciejdudko maciejdudko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but there are a couple things that need to be fixed before merging, most notably caching inside ActivityHandleImpl and how AsyncCompletionClient creates ManualActivityCompletionClient for standalone activities. See comments in code.

Comment on lines +60 to +62
private ActivityExecutionInfo info() {
return info;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is unnecessary.

Comment thread temporal-sdk/src/main/java/io/temporal/activity/ActivityInfo.java
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite does not meaningfully test anything in real implementation. Instead, the tests should create ActivityClient with 2 interceptors and verify that they are being called in order when the client method is called.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: personal taste, but I would rename this suite to ActivityInfoImplTest.

}

@Test
public void testDescribeNoToken() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't support long poll describe, rename to testDescribe.

client.start(SimpleActivity.class, SimpleActivity::execute, simpleOpts(activityId), "test");
assertEquals("echo:test", handle.getResult());

UntypedActivityHandle handle2 = client.getHandle(activityId, handle.getActivityRunId());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a test for getHandle with null run ID.

Comment on lines +252 to +256
@Test
public void testExecuteActivityVoidResult() {
assumeTrue(SDKTestWorkflowRule.useExternalService);
newActivityClient().execute(VoidActivity.class, VoidActivity::execute, simpleOpts(uniqueId()));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a test that checks if the void execute overload properly discards result of non-void activity.

Implemented this by adding a Deadline to RootActivityClientInvoker.getActivityResult.
Also:
In tests, use .start(...).get() instead of .executeAsync, for clarity.
Added stopwatch check to testGetActivityResultAsyncTimeoutAbortsPolling to avoid possibility of false negative.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants