Skip to content

Commit 16f16b3

Browse files
Add support activities to Nexus operations
1 parent 26ca1a4 commit 16f16b3

19 files changed

Lines changed: 2054 additions & 52 deletions

temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.temporal.common.interceptors.ActivityClientCallsInterceptor;
77
import io.temporal.common.interceptors.ActivityClientInterceptor;
88
import io.temporal.common.interceptors.Header;
9+
import io.temporal.internal.client.ActivityClientInternal;
910
import io.temporal.internal.client.ActivityHandleImpl;
1011
import io.temporal.internal.client.RootActivityClientInvoker;
1112
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
@@ -27,7 +28,7 @@
2728
* Implementation of {@link ActivityClient} that delegates calls through the activity interceptor
2829
* chain and ultimately to the Temporal service.
2930
*/
30-
class ActivityClientImpl implements ActivityClient {
31+
class ActivityClientImpl implements ActivityClient, ActivityClientInternal {
3132

3233
private final WorkflowServiceStubs stubs;
3334
private final ActivityClientOptions options;
@@ -56,6 +57,11 @@ private static ActivityClientCallsInterceptor initializeClientInvoker(
5657
return invoker;
5758
}
5859

60+
@Override
61+
public ActivityClientCallsInterceptor getInvoker() {
62+
return invoker;
63+
}
64+
5965
// ---- Interface-based start (Proc variants) ----
6066

6167
@Override

temporal-sdk/src/main/java/io/temporal/common/interceptors/ActivityClientCallsInterceptor.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import io.temporal.common.Experimental;
1010
import java.lang.reflect.Type;
1111
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Objects;
1214
import java.util.concurrent.CompletableFuture;
1315
import java.util.concurrent.TimeUnit;
1416
import java.util.concurrent.TimeoutException;
@@ -116,19 +118,56 @@ <R> GetActivityResultOutput<R> getActivityResult(GetActivityResultInput<R> input
116118
<R> CompletableFuture<GetActivityResultOutput<R>> getActivityResultAsync(
117119
GetActivityResultInput<R> input);
118120

121+
/**
122+
* Nexus completion-callback metadata propagated through {@link StartActivityInput} when an
123+
* activity is scheduled from a Nexus operation handler.
124+
*/
125+
@Experimental
126+
final class CompletionCallback {
127+
private final String url;
128+
private final Map<String, String> headers;
129+
130+
public CompletionCallback(String url, Map<String, String> headers) {
131+
this.url = Objects.requireNonNull(url);
132+
this.headers = Objects.requireNonNull(headers);
133+
}
134+
135+
public String getUrl() {
136+
return url;
137+
}
138+
139+
public Map<String, String> getHeaders() {
140+
return headers;
141+
}
142+
}
143+
119144
@Experimental
120145
final class StartActivityInput {
121146
private final String activityType;
122147
private final List<Object> args;
123148
private final StartActivityOptions options;
124149
private final Header header;
150+
private final @Nullable CompletionCallback completionCallback;
151+
private final @Nullable List<io.nexusrpc.Link> links;
125152

126153
public StartActivityInput(
127154
String activityType, List<Object> args, StartActivityOptions options, Header header) {
155+
this(activityType, args, options, header, null, null);
156+
}
157+
158+
public StartActivityInput(
159+
String activityType,
160+
List<Object> args,
161+
StartActivityOptions options,
162+
Header header,
163+
@Nullable CompletionCallback completionCallback,
164+
@Nullable List<io.nexusrpc.Link> links) {
128165
this.activityType = activityType;
129166
this.args = args;
130167
this.options = options;
131168
this.header = header;
169+
this.completionCallback = completionCallback;
170+
this.links = links;
132171
}
133172

134173
public String getActivityType() {
@@ -146,16 +185,38 @@ public StartActivityOptions getOptions() {
146185
public Header getHeader() {
147186
return header;
148187
}
188+
189+
@Nullable
190+
public CompletionCallback getCompletionCallback() {
191+
return completionCallback;
192+
}
193+
194+
@Nullable
195+
public List<io.nexusrpc.Link> getLinks() {
196+
return links;
197+
}
149198
}
150199

151200
@Experimental
152201
final class StartActivityOutput {
153202
private final String activityId;
154203
private final @Nullable String activityRunId;
155204

205+
/**
206+
* Set by the invoker when the start request included a Nexus completion callback; null
207+
* otherwise. Internal use; do not depend on this in user-facing code.
208+
*/
209+
private final @Nullable String nexusOperationToken;
210+
156211
public StartActivityOutput(String activityId, @Nullable String activityRunId) {
212+
this(activityId, activityRunId, null);
213+
}
214+
215+
public StartActivityOutput(
216+
String activityId, @Nullable String activityRunId, @Nullable String nexusOperationToken) {
157217
this.activityId = activityId;
158218
this.activityRunId = activityRunId;
219+
this.nexusOperationToken = nexusOperationToken;
159220
}
160221

161222
public String getActivityId() {
@@ -166,6 +227,11 @@ public String getActivityId() {
166227
public String getActivityRunId() {
167228
return activityRunId;
168229
}
230+
231+
@Nullable
232+
public String getNexusOperationToken() {
233+
return nexusOperationToken;
234+
}
169235
}
170236

171237
@Experimental
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.temporal.internal.client;
2+
3+
import io.temporal.common.interceptors.ActivityClientCallsInterceptor;
4+
5+
/**
6+
* Internal-only view of an {@code ActivityClient} that exposes the configured interceptor chain.
7+
*
8+
* <p>Lives in {@code io.temporal.internal.client} so that other internal SDK packages (e.g. {@code
9+
* io.temporal.nexus}) can route a fully-constructed {@link
10+
* ActivityClientCallsInterceptor.StartActivityInput} through the chain without bypassing
11+
* user-registered interceptors or the metrics-tagged scope, and without forcing the concrete impl
12+
* class to be public.
13+
*/
14+
public interface ActivityClientInternal {
15+
ActivityClientCallsInterceptor getInvoker();
16+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.temporal.internal.client;
2+
3+
import io.nexusrpc.Link;
4+
import io.temporal.client.StartActivityOptions;
5+
import io.temporal.common.Experimental;
6+
import io.temporal.common.interceptors.Header;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* Request used to start an activity from a Nexus operation handler. Mirrors {@link
12+
* NexusStartWorkflowRequest} but carries the activity-specific scheduling payload.
13+
*/
14+
@Experimental
15+
public final class NexusStartActivityRequest {
16+
private final String requestId;
17+
private final String callbackUrl;
18+
private final Map<String, String> callbackHeaders;
19+
private final String taskQueue;
20+
private final List<Link> links;
21+
private final String activityType;
22+
private final List<Object> args;
23+
private final StartActivityOptions options;
24+
private final Header header;
25+
26+
public NexusStartActivityRequest(
27+
String requestId,
28+
String callbackUrl,
29+
Map<String, String> callbackHeaders,
30+
String taskQueue,
31+
List<Link> links,
32+
String activityType,
33+
List<Object> args,
34+
StartActivityOptions options,
35+
Header header) {
36+
this.requestId = requestId;
37+
this.callbackUrl = callbackUrl;
38+
this.callbackHeaders = callbackHeaders;
39+
this.taskQueue = taskQueue;
40+
this.links = links;
41+
this.activityType = activityType;
42+
this.args = args;
43+
this.options = options;
44+
this.header = header;
45+
}
46+
47+
public String getRequestId() {
48+
return requestId;
49+
}
50+
51+
public String getCallbackUrl() {
52+
return callbackUrl;
53+
}
54+
55+
public Map<String, String> getCallbackHeaders() {
56+
return callbackHeaders;
57+
}
58+
59+
public String getTaskQueue() {
60+
return taskQueue;
61+
}
62+
63+
public List<Link> getLinks() {
64+
return links;
65+
}
66+
67+
public String getActivityType() {
68+
return activityType;
69+
}
70+
71+
public List<Object> getArgs() {
72+
return args;
73+
}
74+
75+
public StartActivityOptions getOptions() {
76+
return options;
77+
}
78+
79+
public Header getHeader() {
80+
return header;
81+
}
82+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.temporal.internal.client;
2+
3+
import io.temporal.common.Experimental;
4+
import javax.annotation.Nullable;
5+
6+
/**
7+
* Response returned from starting an activity via {@link NexusStartActivityRequest}. Mirrors {@link
8+
* NexusStartWorkflowResponse}.
9+
*/
10+
@Experimental
11+
public final class NexusStartActivityResponse {
12+
private final String activityId;
13+
private final @Nullable String runId;
14+
private final String operationToken;
15+
16+
public NexusStartActivityResponse(
17+
String activityId, @Nullable String runId, String operationToken) {
18+
this.activityId = activityId;
19+
this.runId = runId;
20+
this.operationToken = operationToken;
21+
}
22+
23+
public String getActivityId() {
24+
return activityId;
25+
}
26+
27+
@Nullable
28+
public String getRunId() {
29+
return runId;
30+
}
31+
32+
public String getOperationToken() {
33+
return operationToken;
34+
}
35+
}

temporal-sdk/src/main/java/io/temporal/internal/client/RootActivityClientInvoker.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import io.grpc.StatusRuntimeException;
1010
import io.temporal.api.activity.v1.ActivityExecutionOutcome;
1111
import io.temporal.api.common.v1.ActivityType;
12+
import io.temporal.api.common.v1.Callback;
13+
import io.temporal.api.common.v1.Link;
1214
import io.temporal.api.common.v1.Payloads;
1315
import io.temporal.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure;
1416
import io.temporal.api.sdk.v1.UserMetadata;
@@ -19,6 +21,8 @@
1921
import io.temporal.common.interceptors.ActivityClientCallsInterceptor;
2022
import io.temporal.internal.client.external.GenericWorkflowClient;
2123
import io.temporal.internal.common.HeaderUtils;
24+
import io.temporal.internal.common.InternalUtils;
25+
import io.temporal.internal.common.LinkConverter;
2226
import io.temporal.internal.common.ProtoConverters;
2327
import io.temporal.internal.common.ProtobufTimeUtils;
2428
import io.temporal.internal.common.SearchAttributesUtil;
@@ -28,14 +32,19 @@
2832
import java.util.concurrent.CompletableFuture;
2933
import java.util.concurrent.CompletionException;
3034
import java.util.concurrent.TimeoutException;
35+
import java.util.stream.Collectors;
3136
import java.util.stream.StreamSupport;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
3239

3340
/**
3441
* Terminus of the activity interceptor chain. Implements all activity RPCs against the Temporal
3542
* service.
3643
*/
3744
public class RootActivityClientInvoker implements ActivityClientCallsInterceptor {
3845

46+
private static final Logger log = LoggerFactory.getLogger(RootActivityClientInvoker.class);
47+
3948
private final GenericWorkflowClient genericClient;
4049
private final ActivityClientOptions clientOptions;
4150

@@ -104,6 +113,58 @@ public StartActivityOutput startActivity(StartActivityInput input) {
104113
io.temporal.api.common.v1.Header grpcHeader = HeaderUtils.toHeaderGrpc(input.getHeader(), null);
105114
request.setHeader(grpcHeader);
106115

116+
// Hoisted so it can be returned in StartActivityOutput when a Nexus callback is present.
117+
String nexusOperationToken = null;
118+
if (input.getCompletionCallback() != null) {
119+
List<Link> protoLinks = null;
120+
if (input.getLinks() != null) {
121+
protoLinks =
122+
input.getLinks().stream()
123+
.map(
124+
link -> {
125+
if (io.temporal.api.common.v1.Link.WorkflowEvent.getDescriptor()
126+
.getFullName()
127+
.equals(link.getType())) {
128+
io.temporal.api.nexus.v1.Link nexusLink =
129+
io.temporal.api.nexus.v1.Link.newBuilder()
130+
.setType(link.getType())
131+
.setUrl(link.getUri().toString())
132+
.build();
133+
return LinkConverter.nexusLinkToWorkflowEvent(nexusLink);
134+
} else {
135+
log.warn("ignoring unsupported link data type: {}", link.getType());
136+
return null;
137+
}
138+
})
139+
.filter(Objects::nonNull)
140+
.collect(Collectors.toList());
141+
if (!protoLinks.isEmpty()) {
142+
request.addAllLinks(protoLinks);
143+
} else {
144+
protoLinks = null;
145+
}
146+
}
147+
// Generate the operation token from the user-supplied activity ID and namespace so the
148+
// dual OPERATION_ID + OPERATION_TOKEN headers can be injected before the start RPC fires.
149+
try {
150+
nexusOperationToken =
151+
io.temporal.internal.nexus.OperationTokenUtil.generateActivityExecutionOperationToken(
152+
options.getId(), clientOptions.getNamespace());
153+
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
154+
throw new io.nexusrpc.handler.HandlerException(
155+
io.nexusrpc.handler.HandlerException.ErrorType.BAD_REQUEST,
156+
"failed to generate activity operation token",
157+
e);
158+
}
159+
Callback cb =
160+
InternalUtils.buildNexusCallback(
161+
input.getCompletionCallback().getUrl(),
162+
input.getCompletionCallback().getHeaders(),
163+
nexusOperationToken,
164+
protoLinks);
165+
request.addCompletionCallbacks(cb);
166+
}
167+
107168
StartActivityExecutionResponse response;
108169
try {
109170
response = genericClient.startActivity(request.build());
@@ -121,7 +182,7 @@ public StartActivityOutput startActivity(StartActivityInput input) {
121182
}
122183

123184
String runId = response.getRunId().isEmpty() ? null : response.getRunId();
124-
return new StartActivityOutput(options.getId(), runId);
185+
return new StartActivityOutput(options.getId(), runId, nexusOperationToken);
125186
}
126187

127188
@Override

0 commit comments

Comments
 (0)