Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b0cb675
Add standalone Nexus client (start/describe/cancel/terminate/list/cou…
Evanthx May 19, 2026
6a81d4a
Renaming handle classes
Evanthx May 19, 2026
9354504
Removing the renamed classes
Evanthx May 19, 2026
360be4f
Using endpoints from the rule instead of creating them
Evanthx May 19, 2026
addccda
Moving common test code to SDKTestWorkflowRule
Evanthx May 19, 2026
e1e3fab
Ensuring we are using an external server, as the internal one doesn't…
Evanthx May 20, 2026
de4e785
PR changes for comments
Evanthx May 27, 2026
0ef9588
Changing deadline behavior
Evanthx May 27, 2026
7fc3e62
Addressing PR comments
Evanthx May 27, 2026
734374b
Responding to PR comments
Evanthx May 27, 2026
11cbcef
Removed a file
Evanthx May 27, 2026
50664ac
PR comments
Evanthx May 27, 2026
679a95d
PR responses
Evanthx May 28, 2026
f2c9c4a
Updated exception handling
Evanthx May 28, 2026
d2cdc2c
Test updates
Evanthx May 28, 2026
1dbf1cb
Removing some timeouts
Evanthx May 28, 2026
7439111
Refactor due to PR comment
Evanthx May 28, 2026
91c0414
More tests
Evanthx May 28, 2026
5e5e78c
Adding more tests
Evanthx May 28, 2026
7d7fac0
More checking in a test
Evanthx May 29, 2026
345fc85
Updated a test
Evanthx May 29, 2026
5f2508c
Adding async tests
Evanthx May 29, 2026
1289bd1
Some improvements
Evanthx May 29, 2026
d4d70d9
Adding tests
Evanthx May 29, 2026
351242e
Adding tests
Evanthx May 29, 2026
0f3fae5
Don't run the tests against the in-memory server as it doesn't suppor…
Evanthx May 29, 2026
b3a0854
Adding prereq check for SANO
Evanthx May 29, 2026
181428a
Checking for more SANO operations
Evanthx May 29, 2026
fb694fa
Test fix
Evanthx May 29, 2026
3aa1c54
Restoring test
Evanthx May 29, 2026
627a610
Now requiring UUID
Evanthx May 29, 2026
a6d529a
Some more checks on ID
Evanthx May 29, 2026
a372d27
Cleaned up some inputs
Evanthx May 29, 2026
c4ed084
Some cleanup from PRs
Evanthx Jun 2, 2026
1015ea6
PR responses
Evanthx Jun 2, 2026
428dd46
From doing a pass over the code
Evanthx Jun 3, 2026
5d51fc5
Changed poll to getResult
Evanthx Jun 3, 2026
ca7e289
Adding cancel tests
Evanthx Jun 3, 2026
912463d
Skipping some SANO nexus tests unless the server has them implemented
Evanthx Jun 4, 2026
6a22bed
Updated test server to one that supports SANO with Nexus
Evanthx Jun 4, 2026
694bf8c
Addressing some PR comments
Evanthx Jun 8, 2026
27ec3eb
Default namespace is now "default"
Evanthx Jun 8, 2026
041a221
Some cleanup for PR comments
Evanthx Jun 8, 2026
c09f80f
Addressing PR comments
Evanthx Jun 8, 2026
e443c5f
PR changes
Evanthx Jun 9, 2026
9c06c4f
PR comments
Evanthx Jun 9, 2026
80a5317
Address a PR comment
Evanthx Jun 9, 2026
0581e6a
Address PR comments
Evanthx Jun 9, 2026
573104d
Responding to PR comments
Evanthx Jun 9, 2026
05cae63
Code review and unit test coverage check
Evanthx Jun 9, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ jobs:
--dynamic-config-value 'component.callbacks.allowedAddresses=[{"Pattern":"localhost:7243","AllowInsecure":true}]' \
--dynamic-config-value frontend.activityAPIsEnabled=true \
--dynamic-config-value activity.enableStandalone=true \
--dynamic-config-value nexusoperation.enableStandalone=true \
--dynamic-config-value history.enableChasm=true \
--dynamic-config-value history.enableTransitionHistory=true &
sleep 10s
Expand Down
153 changes: 153 additions & 0 deletions temporal-sdk/src/main/java/io/temporal/client/NexusClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.temporal.client;

import io.temporal.common.Experimental;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.lang.reflect.Type;
import java.util.stream.Stream;
import javax.annotation.Nullable;

/**
* Client for managing standalone Nexus operation executions. Obtain an instance via {@link
* #newInstance(WorkflowServiceStubs)} or {@link #newInstance(WorkflowServiceStubs,
* NexusClientOptions)}. Do not create this object per request; share it for the lifetime of the
* process.
*
* <p>Standalone Nexus operations run independently of any workflow — they are scheduled, monitored,
* and managed directly through this client (and the service-bound clients it produces) rather than
* from within a workflow execution.
*
* <p>To start operations, build a service-bound client and call {@code start}/{@code execute}:
*
* <pre>{@code
* NexusClient client = NexusClient.newInstance(stubs, options);
*
* // Typed: bind to an @ServiceInterface and invoke a method reference.
* NexusServiceClient<MyService> svc =
* client.newNexusServiceClient(MyService.class, "my-endpoint");
* String result = svc.execute(MyService::greet, "world");
*
* // Untyped: dispatch by operation name string.
* UntypedNexusServiceClient untyped =
* client.newUntypedNexusServiceClient("my-endpoint", "MyService");
* UntypedNexusOperationHandle handle = untyped.start("greet", null, "world");
* }</pre>
*
* <p>To act on an existing operation (describe, cancel, terminate, get result), obtain a handle via
* {@link #getHandle}:
*
* <pre>{@code
* NexusOperationHandle<String> handle = client.getHandle(operationId, runId, String.class);
* String result = handle.getResult();
* handle.cancel("user requested");
* }</pre>
*
* <p>For visibility queries across all operations in the namespace, see {@link
* #listNexusOperationExecutions} and {@link #countNexusOperationExecutions}.
*
* @see NexusServiceClient
* @see UntypedNexusServiceClient
* @see NexusOperationHandle
*/
@Experimental
public interface NexusClient {
Comment thread
Evanthx marked this conversation as resolved.

/**
* Creates a client with default {@link NexusClientOptions}.
*
* @param service gRPC stubs connected to a Temporal Service endpoint
*/
static NexusClient newInstance(WorkflowServiceStubs service) {
return NexusClientImpl.newInstance(service, NexusClientOptions.getDefaultInstance());
}

/**
* Creates a client with the supplied options.
*
* @param service gRPC stubs connected to a Temporal Service endpoint
* @param options namespace, data converter, interceptors, and defaults applied to operations
* started through this client
*/
static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
return NexusClientImpl.newInstance(service, options);
}

/** Returns the underlying gRPC stubs this client routes RPCs through. */
WorkflowServiceStubs getWorkflowServiceStubs();

/**
* Returns an untyped handle to an existing operation execution, optionally pinned to a specific
* run.
*
* @param operationId the user-assigned operation ID
* @param runId the server-assigned run ID, or {@code null} to target the latest run
* @return an untyped handle
*/
UntypedNexusOperationHandle getHandle(String operationId, @Nullable String runId);

/**
* Returns a typed handle to an existing operation execution, bound to {@code resultClass}.
*
* @param operationId the user-assigned operation ID
* @param runId the server-assigned run ID, or {@code null} to target the latest run
* @param resultClass expected result type
* @param <R> result type
*/
<R> NexusOperationHandle<R> getHandle(
String operationId, @Nullable String runId, Class<R> resultClass);

/**
* Returns a typed handle to an existing operation execution, bound to {@code resultClass}/{@code
* resultType}. Use the {@code resultType} variant when the result is a generic type whose
* parameters cannot be captured by {@link Class} alone (e.g. {@code List<String>}).
*
* @param operationId the user-assigned operation ID
* @param runId the server-assigned run ID, or {@code null} to target the latest run
* @param resultClass expected result class
* @param resultType generic type for deserialization; may be {@code null}
* @param <R> result type
*/
<R> NexusOperationHandle<R> getHandle(
String operationId, @Nullable String runId, Class<R> resultClass, @Nullable Type resultType);

/**
* Builds a typed service-bound client targeting the given endpoint, dispatching operations by
* method reference on the {@code @ServiceInterface}-annotated {@code service}. Reuses this
* client's stubs, options, and interceptor chain.
*
* @param service the {@code @ServiceInterface}-annotated service type
* @param endpoint Nexus endpoint name registered on the Temporal Service
* @param <T> the service interface type
*/
<T> NexusServiceClient<T> newNexusServiceClient(Class<T> service, String endpoint);

/**
* Builds an untyped service-bound client targeting the given endpoint and service. Use this to
* dispatch operations by name string when no service interface is available.
*
* @param endpoint Nexus endpoint name registered on the Temporal Service
* @param serviceName Nexus service name on that endpoint
*/
UntypedNexusServiceClient newUntypedNexusServiceClient(String endpoint, String serviceName);
Comment thread
maciejdudko marked this conversation as resolved.

/**
* Returns a stream of standalone Nexus operation executions matching the given visibility query.
* The stream paginates lazily over server-side results — pages are fetched on demand as the
* stream is consumed.
*
* @param query Temporal visibility query string, or {@code null} to return all executions in the
* client namespace
* @return a lazy stream of matching executions
*/
Stream<NexusOperationExecutionMetadata> listNexusOperationExecutions(@Nullable String query);

/**
* Returns the count of standalone Nexus operation executions matching the given visibility query,
* optionally with aggregation groups.
*
* @param query Temporal visibility query string, or {@code null} to count all executions in the
* client namespace
* @return execution count, optionally with aggregation groups when the query uses {@code GROUP
* BY}
*/
NexusOperationExecutionCount countNexusOperationExecutions(@Nullable String query);
}
140 changes: 140 additions & 0 deletions temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.temporal.client;

import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread;

import com.uber.m3.tally.Scope;
import io.temporal.common.Experimental;
import io.temporal.common.interceptors.NexusClientCallsInterceptor;
import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput;
import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput;
import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput;
import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput;
import io.temporal.common.interceptors.NexusClientInterceptor;
import io.temporal.internal.WorkflowThreadMarker;
import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs;
import io.temporal.internal.client.NexusOperationHandleImpl;
import io.temporal.internal.client.RootNexusClientInvoker;
import io.temporal.internal.client.external.GenericWorkflowClient;
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.util.List;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Experimental
public class NexusClientImpl implements NexusClient {

private static final Logger log = LoggerFactory.getLogger(NexusClientImpl.class);

private final WorkflowServiceStubs workflowServiceStubs;
private final NexusClientOptions options;
private final GenericWorkflowClient genericClient;
private final Scope metricsScope;
private final NexusClientCallsInterceptor nexusClientCallsInvoker;
private final List<NexusClientInterceptor> interceptors;

public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
enforceNonWorkflowThread();
return WorkflowThreadMarker.protectFromWorkflowThread(
new NexusClientImpl(service, options), NexusClient.class);
}

NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOptions options) {
workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
this.workflowServiceStubs = workflowServiceStubs;
this.options = options;
this.metricsScope =
workflowServiceStubs
.getOptions()
.getMetricsScope()
.tagged(MetricsTag.defaultTags(options.getNamespace()));
this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope);
this.interceptors = options.getInterceptors();
this.nexusClientCallsInvoker = initializeClientInvoker();
if (log.isDebugEnabled()) {
log.debug(
"NexusClient initialized: namespace={}, interceptors={}",
options.getNamespace(),
interceptors.size());
}
}

private NexusClientCallsInterceptor initializeClientInvoker() {
NexusClientCallsInterceptor invoker = new RootNexusClientInvoker(genericClient, options);
for (NexusClientInterceptor clientInterceptor : interceptors) {
NexusClientCallsInterceptor wrapped = clientInterceptor.nexusClientCallsInterceptor(invoker);
if (wrapped == null) {
throw new IllegalStateException(
"NexusClientInterceptor "
+ clientInterceptor.getClass().getName()
+ " returned null from nexusClientCallsInterceptor; expected a non-null"
+ " NexusClientCallsInterceptor wrapping the supplied next link");
}
invoker = wrapped;
}
return invoker;
}

@Override
public WorkflowServiceStubs getWorkflowServiceStubs() {
return workflowServiceStubs;
}

@Override
public UntypedNexusOperationHandle getHandle(String operationId, @Nullable String runId) {
return new NexusOperationHandleImpl(operationId, runId, nexusClientCallsInvoker);
}

@Override
public <R> NexusOperationHandle<R> getHandle(
String operationId, @Nullable String runId, Class<R> resultClass) {
return getHandle(operationId, runId, resultClass, null);
}

@Override
public <R> NexusOperationHandle<R> getHandle(
String operationId,
@Nullable String runId,
Class<R> resultClass,
@Nullable java.lang.reflect.Type resultType) {
return NexusOperationHandle.fromUntyped(getHandle(operationId, runId), resultClass, resultType);
}

@Override
public <T> NexusServiceClient<T> newNexusServiceClient(Class<T> service, String endpoint) {
enforceNonWorkflowThread();
return WorkflowThreadMarker.protectFromWorkflowThread(
Comment thread
maciejdudko marked this conversation as resolved.
new NexusServiceClientImpl<>(nexusClientCallsInvoker, service, endpoint, options),
NexusServiceClient.class);
}

@Override
public UntypedNexusServiceClient newUntypedNexusServiceClient(
String endpoint, String serviceName) {
enforceNonWorkflowThread();
return WorkflowThreadMarker.protectFromWorkflowThread(
new UntypedNexusServiceClientImpl(nexusClientCallsInvoker, endpoint, serviceName, options),
UntypedNexusServiceClient.class);
}

@Override
public Stream<NexusOperationExecutionMetadata> listNexusOperationExecutions(
@Nullable String query) {
ListNexusOperationExecutionsOutput out =
nexusClientCallsInvoker.listNexusOperationExecutions(
new ListNexusOperationExecutionsInput(query));
return out.getOperations();
}

@Override
public NexusOperationExecutionCount countNexusOperationExecutions(@Nullable String query) {
CountNexusOperationExecutionsOutput out =
nexusClientCallsInvoker.countNexusOperationExecutions(
new CountNexusOperationExecutionsInput(query));
return out.getCount();
}
}
Loading
Loading