Skip to content

Commit 5f25aad

Browse files
authored
Standalone operations for Nexus (#2872)
1 parent 62a7f08 commit 5f25aad

41 files changed

Lines changed: 5148 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ jobs:
114114
--dynamic-config-value 'component.callbacks.allowedAddresses=[{"Pattern":"localhost:7243","AllowInsecure":true}]' \
115115
--dynamic-config-value frontend.activityAPIsEnabled=true \
116116
--dynamic-config-value activity.enableStandalone=true \
117+
--dynamic-config-value nexusoperation.enableStandalone=true \
117118
--dynamic-config-value history.enableChasm=true \
118119
--dynamic-config-value history.enableTransitionHistory=true &
119120
sleep 10s
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package io.temporal.client;
2+
3+
import io.temporal.common.Experimental;
4+
import io.temporal.serviceclient.WorkflowServiceStubs;
5+
import java.lang.reflect.Type;
6+
import java.util.stream.Stream;
7+
import javax.annotation.Nullable;
8+
9+
/**
10+
* Client for managing standalone Nexus operation executions. Obtain an instance via {@link
11+
* #newInstance(WorkflowServiceStubs)} or {@link #newInstance(WorkflowServiceStubs,
12+
* NexusClientOptions)}. Do not create this object per request; share it for the lifetime of the
13+
* process.
14+
*
15+
* <p>Standalone Nexus operations run independently of any workflow — they are scheduled, monitored,
16+
* and managed directly through this client (and the service-bound clients it produces) rather than
17+
* from within a workflow execution.
18+
*
19+
* <p>To start operations, build a service-bound client and call {@code start}/{@code execute}:
20+
*
21+
* <pre>{@code
22+
* NexusClient client = NexusClient.newInstance(stubs, options);
23+
*
24+
* // Typed: bind to an @ServiceInterface and invoke a method reference.
25+
* NexusServiceClient<MyService> svc =
26+
* client.newNexusServiceClient(MyService.class, "my-endpoint");
27+
* String result = svc.execute(MyService::greet, "world");
28+
*
29+
* // Untyped: dispatch by operation name string.
30+
* UntypedNexusServiceClient untyped =
31+
* client.newUntypedNexusServiceClient("my-endpoint", "MyService");
32+
* UntypedNexusOperationHandle handle = untyped.start("greet", null, "world");
33+
* }</pre>
34+
*
35+
* <p>To act on an existing operation (describe, cancel, terminate, get result), obtain a handle via
36+
* {@link #getHandle}:
37+
*
38+
* <pre>{@code
39+
* NexusOperationHandle<String> handle = client.getHandle(operationId, runId, String.class);
40+
* String result = handle.getResult();
41+
* handle.cancel("user requested");
42+
* }</pre>
43+
*
44+
* <p>For visibility queries across all operations in the namespace, see {@link
45+
* #listNexusOperationExecutions} and {@link #countNexusOperationExecutions}.
46+
*
47+
* @see NexusServiceClient
48+
* @see UntypedNexusServiceClient
49+
* @see NexusOperationHandle
50+
*/
51+
@Experimental
52+
public interface NexusClient {
53+
54+
/**
55+
* Creates a client with default {@link NexusClientOptions}.
56+
*
57+
* @param service gRPC stubs connected to a Temporal Service endpoint
58+
*/
59+
static NexusClient newInstance(WorkflowServiceStubs service) {
60+
return NexusClientImpl.newInstance(service, NexusClientOptions.getDefaultInstance());
61+
}
62+
63+
/**
64+
* Creates a client with the supplied options.
65+
*
66+
* @param service gRPC stubs connected to a Temporal Service endpoint
67+
* @param options namespace, data converter, interceptors, and defaults applied to operations
68+
* started through this client
69+
*/
70+
static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
71+
return NexusClientImpl.newInstance(service, options);
72+
}
73+
74+
/** Returns the underlying gRPC stubs this client routes RPCs through. */
75+
WorkflowServiceStubs getWorkflowServiceStubs();
76+
77+
/**
78+
* Returns an untyped handle to an existing operation execution, optionally pinned to a specific
79+
* run.
80+
*
81+
* @param operationId the user-assigned operation ID
82+
* @param runId the server-assigned run ID, or {@code null} to target the latest run
83+
* @return an untyped handle
84+
*/
85+
UntypedNexusOperationHandle getHandle(String operationId, @Nullable String runId);
86+
87+
/**
88+
* Returns a typed handle to an existing operation execution, bound to {@code resultClass}.
89+
*
90+
* @param operationId the user-assigned operation ID
91+
* @param runId the server-assigned run ID, or {@code null} to target the latest run
92+
* @param resultClass expected result type
93+
* @param <R> result type
94+
*/
95+
<R> NexusOperationHandle<R> getHandle(
96+
String operationId, @Nullable String runId, Class<R> resultClass);
97+
98+
/**
99+
* Returns a typed handle to an existing operation execution, bound to {@code resultClass}/{@code
100+
* resultType}. Use the {@code resultType} variant when the result is a generic type whose
101+
* parameters cannot be captured by {@link Class} alone (e.g. {@code List<String>}).
102+
*
103+
* @param operationId the user-assigned operation ID
104+
* @param runId the server-assigned run ID, or {@code null} to target the latest run
105+
* @param resultClass expected result class
106+
* @param resultType generic type for deserialization; may be {@code null}
107+
* @param <R> result type
108+
*/
109+
<R> NexusOperationHandle<R> getHandle(
110+
String operationId, @Nullable String runId, Class<R> resultClass, @Nullable Type resultType);
111+
112+
/**
113+
* Builds a typed service-bound client targeting the given endpoint, dispatching operations by
114+
* method reference on the {@code @ServiceInterface}-annotated {@code service}. Reuses this
115+
* client's stubs, options, and interceptor chain.
116+
*
117+
* @param service the {@code @ServiceInterface}-annotated service type
118+
* @param endpoint Nexus endpoint name registered on the Temporal Service
119+
* @param <T> the service interface type
120+
*/
121+
<T> NexusServiceClient<T> newNexusServiceClient(Class<T> service, String endpoint);
122+
123+
/**
124+
* Builds an untyped service-bound client targeting the given endpoint and service. Use this to
125+
* dispatch operations by name string when no service interface is available.
126+
*
127+
* @param endpoint Nexus endpoint name registered on the Temporal Service
128+
* @param serviceName Nexus service name on that endpoint
129+
*/
130+
UntypedNexusServiceClient newUntypedNexusServiceClient(String endpoint, String serviceName);
131+
132+
/**
133+
* Returns a stream of standalone Nexus operation executions matching the given visibility query.
134+
* The stream paginates lazily over server-side results — pages are fetched on demand as the
135+
* stream is consumed.
136+
*
137+
* @param query Temporal visibility query string, or {@code null} to return all executions in the
138+
* client namespace
139+
* @return a lazy stream of matching executions
140+
*/
141+
Stream<NexusOperationExecutionMetadata> listNexusOperationExecutions(@Nullable String query);
142+
143+
/**
144+
* Returns the count of standalone Nexus operation executions matching the given visibility query,
145+
* optionally with aggregation groups.
146+
*
147+
* @param query Temporal visibility query string, or {@code null} to count all executions in the
148+
* client namespace
149+
* @return execution count, optionally with aggregation groups when the query uses {@code GROUP
150+
* BY}
151+
*/
152+
NexusOperationExecutionCount countNexusOperationExecutions(@Nullable String query);
153+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package io.temporal.client;
2+
3+
import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread;
4+
5+
import com.uber.m3.tally.Scope;
6+
import io.temporal.common.Experimental;
7+
import io.temporal.common.interceptors.NexusClientCallsInterceptor;
8+
import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsInput;
9+
import io.temporal.common.interceptors.NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput;
10+
import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsInput;
11+
import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput;
12+
import io.temporal.common.interceptors.NexusClientInterceptor;
13+
import io.temporal.internal.WorkflowThreadMarker;
14+
import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs;
15+
import io.temporal.internal.client.NexusOperationHandleImpl;
16+
import io.temporal.internal.client.RootNexusClientInvoker;
17+
import io.temporal.internal.client.external.GenericWorkflowClient;
18+
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
19+
import io.temporal.serviceclient.MetricsTag;
20+
import io.temporal.serviceclient.WorkflowServiceStubs;
21+
import java.util.List;
22+
import java.util.stream.Stream;
23+
import javax.annotation.Nullable;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
@Experimental
28+
public class NexusClientImpl implements NexusClient {
29+
30+
private static final Logger log = LoggerFactory.getLogger(NexusClientImpl.class);
31+
32+
private final WorkflowServiceStubs workflowServiceStubs;
33+
private final NexusClientOptions options;
34+
private final GenericWorkflowClient genericClient;
35+
private final Scope metricsScope;
36+
private final NexusClientCallsInterceptor nexusClientCallsInvoker;
37+
private final List<NexusClientInterceptor> interceptors;
38+
39+
public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
40+
enforceNonWorkflowThread();
41+
return WorkflowThreadMarker.protectFromWorkflowThread(
42+
new NexusClientImpl(service, options), NexusClient.class);
43+
}
44+
45+
NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOptions options) {
46+
workflowServiceStubs =
47+
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
48+
this.workflowServiceStubs = workflowServiceStubs;
49+
this.options = options;
50+
this.metricsScope =
51+
workflowServiceStubs
52+
.getOptions()
53+
.getMetricsScope()
54+
.tagged(MetricsTag.defaultTags(options.getNamespace()));
55+
this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope);
56+
this.interceptors = options.getInterceptors();
57+
this.nexusClientCallsInvoker = initializeClientInvoker();
58+
if (log.isDebugEnabled()) {
59+
log.debug(
60+
"NexusClient initialized: namespace={}, interceptors={}",
61+
options.getNamespace(),
62+
interceptors.size());
63+
}
64+
}
65+
66+
private NexusClientCallsInterceptor initializeClientInvoker() {
67+
NexusClientCallsInterceptor invoker = new RootNexusClientInvoker(genericClient, options);
68+
for (NexusClientInterceptor clientInterceptor : interceptors) {
69+
NexusClientCallsInterceptor wrapped = clientInterceptor.nexusClientCallsInterceptor(invoker);
70+
if (wrapped == null) {
71+
throw new IllegalStateException(
72+
"NexusClientInterceptor "
73+
+ clientInterceptor.getClass().getName()
74+
+ " returned null from nexusClientCallsInterceptor; expected a non-null"
75+
+ " NexusClientCallsInterceptor wrapping the supplied next link");
76+
}
77+
invoker = wrapped;
78+
}
79+
return invoker;
80+
}
81+
82+
@Override
83+
public WorkflowServiceStubs getWorkflowServiceStubs() {
84+
return workflowServiceStubs;
85+
}
86+
87+
@Override
88+
public UntypedNexusOperationHandle getHandle(String operationId, @Nullable String runId) {
89+
return new NexusOperationHandleImpl(operationId, runId, nexusClientCallsInvoker);
90+
}
91+
92+
@Override
93+
public <R> NexusOperationHandle<R> getHandle(
94+
String operationId, @Nullable String runId, Class<R> resultClass) {
95+
return getHandle(operationId, runId, resultClass, null);
96+
}
97+
98+
@Override
99+
public <R> NexusOperationHandle<R> getHandle(
100+
String operationId,
101+
@Nullable String runId,
102+
Class<R> resultClass,
103+
@Nullable java.lang.reflect.Type resultType) {
104+
return NexusOperationHandle.fromUntyped(getHandle(operationId, runId), resultClass, resultType);
105+
}
106+
107+
@Override
108+
public <T> NexusServiceClient<T> newNexusServiceClient(Class<T> service, String endpoint) {
109+
enforceNonWorkflowThread();
110+
return WorkflowThreadMarker.protectFromWorkflowThread(
111+
new NexusServiceClientImpl<>(nexusClientCallsInvoker, service, endpoint, options),
112+
NexusServiceClient.class);
113+
}
114+
115+
@Override
116+
public UntypedNexusServiceClient newUntypedNexusServiceClient(
117+
String endpoint, String serviceName) {
118+
enforceNonWorkflowThread();
119+
return WorkflowThreadMarker.protectFromWorkflowThread(
120+
new UntypedNexusServiceClientImpl(nexusClientCallsInvoker, endpoint, serviceName, options),
121+
UntypedNexusServiceClient.class);
122+
}
123+
124+
@Override
125+
public Stream<NexusOperationExecutionMetadata> listNexusOperationExecutions(
126+
@Nullable String query) {
127+
ListNexusOperationExecutionsOutput out =
128+
nexusClientCallsInvoker.listNexusOperationExecutions(
129+
new ListNexusOperationExecutionsInput(query));
130+
return out.getOperations();
131+
}
132+
133+
@Override
134+
public NexusOperationExecutionCount countNexusOperationExecutions(@Nullable String query) {
135+
CountNexusOperationExecutionsOutput out =
136+
nexusClientCallsInvoker.countNexusOperationExecutions(
137+
new CountNexusOperationExecutionsInput(query));
138+
return out.getCount();
139+
}
140+
}

0 commit comments

Comments
 (0)