Skip to content

Commit 86fdfff

Browse files
committed
First pass at adding links for signalling
1 parent 44bb603 commit 86fdfff

7 files changed

Lines changed: 337 additions & 15 deletions

File tree

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ public WorkflowSignalOutput signal(WorkflowSignalInput input) {
120120
.setRequestId(UUID.randomUUID().toString())
121121
.setHeader(HeaderUtils.toHeaderGrpc(input.getHeader(), null));
122122

123+
// If this signal is being issued from inside a Nexus operation handler, forward the inbound
124+
// Nexus task links so the SignalWorkflowExecution history event links back to the caller.
125+
if (CurrentNexusOperationContext.isNexusContext()) {
126+
request.addAllLinks(CurrentNexusOperationContext.get().getNexusOperationLinks());
127+
}
128+
123129
DataConverter dataConverterWitSignalContext =
124130
clientOptions
125131
.getDataConverter()
@@ -129,7 +135,12 @@ public WorkflowSignalOutput signal(WorkflowSignalInput input) {
129135

130136
Optional<Payloads> inputArgs = dataConverterWitSignalContext.toPayloads(input.getArguments());
131137
inputArgs.ifPresent(request::setInput);
132-
genericClient.signal(request.build());
138+
SignalWorkflowExecutionResponse response = genericClient.signal(request.build());
139+
// Server >=1.31 with EnableCHASMSignalBacklinks returns a backlink pointing at the signal
140+
// event; older servers leave it unset. Propagate when present.
141+
if (CurrentNexusOperationContext.isNexusContext() && response.hasLink()) {
142+
CurrentNexusOperationContext.get().setSignalWorkflowResponseLink(response.getLink());
143+
}
133144
return new WorkflowSignalOutput();
134145
}
135146

@@ -148,17 +159,27 @@ public WorkflowSignalWithStartOutput signalWithStart(WorkflowSignalWithStartInpu
148159

149160
Optional<Payloads> signalInput =
150161
dataConverterWithWorkflowContext.toPayloads(input.getSignalArguments());
151-
SignalWithStartWorkflowExecutionRequest request =
152-
requestsHelper
153-
.newSignalWithStartWorkflowExecutionRequest(
154-
startRequest, input.getSignalName(), signalInput.orElse(null))
155-
.build();
162+
SignalWithStartWorkflowExecutionRequest.Builder requestBuilder =
163+
requestsHelper.newSignalWithStartWorkflowExecutionRequest(
164+
startRequest, input.getSignalName(), signalInput.orElse(null));
165+
// If this signalWithStart is being issued from inside a Nexus operation handler, forward
166+
// the inbound Nexus task links so both the WorkflowExecutionStarted and
167+
// WorkflowExecutionSignaled events on the callee link back to the caller.
168+
if (CurrentNexusOperationContext.isNexusContext()) {
169+
requestBuilder.addAllLinks(CurrentNexusOperationContext.get().getNexusOperationLinks());
170+
}
171+
SignalWithStartWorkflowExecutionRequest request = requestBuilder.build();
156172
SignalWithStartWorkflowExecutionResponse response = genericClient.signalWithStart(request);
157173
WorkflowExecution execution =
158174
WorkflowExecution.newBuilder()
159175
.setRunId(response.getRunId())
160176
.setWorkflowId(request.getWorkflowId())
161177
.build();
178+
// Server >=1.31 with EnableCHASMSignalBacklinks returns a backlink pointing at the signal
179+
// event; older servers leave it unset. Propagate when present.
180+
if (CurrentNexusOperationContext.isNexusContext() && response.hasSignalLink()) {
181+
CurrentNexusOperationContext.get().setSignalWorkflowResponseLink(response.getSignalLink());
182+
}
162183
// TODO currently SignalWithStartWorkflowExecutionResponse doesn't have eagerWorkflowTask.
163184
// We should wire it when it's implemented server-side.
164185
return new WorkflowSignalWithStartOutput(new WorkflowStartOutput(execution));

temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public interface GenericWorkflowClient {
1010

1111
StartWorkflowExecutionResponse start(StartWorkflowExecutionRequest request);
1212

13-
void signal(SignalWorkflowExecutionRequest request);
13+
SignalWorkflowExecutionResponse signal(SignalWorkflowExecutionRequest request);
1414

1515
SignalWithStartWorkflowExecutionResponse signalWithStart(
1616
SignalWithStartWorkflowExecutionRequest request);

temporal-sdk/src/main/java/io/temporal/internal/client/external/GenericWorkflowClientImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ private static Map<String, String> tagsForStartWorkflow(StartWorkflowExecutionRe
6161
}
6262

6363
@Override
64-
public void signal(SignalWorkflowExecutionRequest request) {
64+
public SignalWorkflowExecutionResponse signal(SignalWorkflowExecutionRequest request) {
6565
Map<String, String> tags =
6666
new ImmutableMap.Builder<String, String>(1)
6767
.put(MetricsTag.SIGNAL_NAME, request.getSignalName())
6868
.build();
6969
Scope scope = metricsScope.tagged(tags);
70-
grpcRetryer.retry(
70+
return grpcRetryer.retryWithResult(
7171
() ->
7272
service
7373
.blockingStub()

temporal-sdk/src/main/java/io/temporal/internal/nexus/InternalNexusOperationContext.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import io.temporal.common.interceptors.NexusOperationOutboundCallsInterceptor;
77
import io.temporal.nexus.NexusOperationContext;
88
import io.temporal.nexus.NexusOperationInfo;
9+
import java.util.Collections;
10+
import java.util.List;
911

1012
public class InternalNexusOperationContext {
1113
private final String namespace;
@@ -15,6 +17,16 @@ public class InternalNexusOperationContext {
1517
private final WorkflowClient client;
1618
NexusOperationOutboundCallsInterceptor outboundCalls;
1719
Link startWorkflowResponseLink;
20+
// Links extracted from the inbound Nexus task. Stored once at the task-handler boundary so the
21+
// workflow client (signal, signalWithStart) can attach them to outgoing requests via
22+
// SignalWorkflowExecutionRequest.links, matching the Go SDK's NexusOperationLinksKey ctx value.
23+
private List<Link> nexusOperationLinks = Collections.emptyList();
24+
// Backlink returned by SignalWorkflowExecutionResponse.link /
25+
// SignalWithStartWorkflowExecutionResponse.signal_link.
26+
// Populated by the workflow client and consumed by the task handler when building
27+
// StartOperationResponse, so the caller workflow gets a link pointing at the signal event on
28+
// the callee.
29+
private Link signalWorkflowResponseLink;
1830

1931
public InternalNexusOperationContext(
2032
String namespace,
@@ -68,6 +80,27 @@ public Link getStartWorkflowResponseLink() {
6880
return startWorkflowResponseLink;
6981
}
7082

83+
/**
84+
* Set the {@code common.v1.Link}s extracted from the inbound Nexus task so they can be attached
85+
* to any signal RPCs issued by the operation handler.
86+
*/
87+
public void setNexusOperationLinks(List<Link> links) {
88+
this.nexusOperationLinks = links == null ? Collections.emptyList() : links;
89+
}
90+
91+
/** Links from the inbound Nexus task; empty if none. Never null. */
92+
public List<Link> getNexusOperationLinks() {
93+
return nexusOperationLinks;
94+
}
95+
96+
public void setSignalWorkflowResponseLink(Link link) {
97+
this.signalWorkflowResponseLink = link;
98+
}
99+
100+
public Link getSignalWorkflowResponseLink() {
101+
return signalWorkflowResponseLink;
102+
}
103+
71104
private class NexusOperationContextImpl implements NexusOperationContext {
72105
@Override
73106
public NexusOperationInfo getInfo() {

temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.temporal.failure.CanceledFailure;
2121
import io.temporal.failure.TemporalFailure;
2222
import io.temporal.internal.common.InternalUtils;
23+
import io.temporal.internal.common.LinkConverter;
2324
import io.temporal.internal.common.NexusUtil;
2425
import io.temporal.internal.worker.NexusTask;
2526
import io.temporal.internal.worker.NexusTaskHandler;
@@ -284,6 +285,10 @@ private StartOperationResponse handleStartOperation(
284285
.setCallbackUrl(task.getCallback())
285286
.setRequestId(task.getRequestId());
286287
task.getCallbackHeaderMap().forEach(operationStartDetails::putCallbackHeader);
288+
// Stash the inbound links in common.v1.Link form on the operation context so that signal
289+
// RPCs issued by the handler (e.g. SignalWithStartWorkflow on the callee) can attach them
290+
// to SignalWorkflowExecutionRequest.links.
291+
List<io.temporal.api.common.v1.Link> inboundCommonLinks = new ArrayList<>();
287292
task.getLinksList()
288293
.forEach(
289294
link -> {
@@ -296,7 +301,13 @@ private StartOperationResponse handleStartOperation(
296301
"Invalid link URL: " + link.getUrl(),
297302
e);
298303
}
304+
io.temporal.api.common.v1.Link commonLink =
305+
LinkConverter.nexusLinkToWorkflowEvent(link);
306+
if (commonLink != null) {
307+
inboundCommonLinks.add(commonLink);
308+
}
299309
});
310+
CurrentNexusOperationContext.get().setNexusOperationLinks(inboundCommonLinks);
300311

301312
HandlerInputContent.Builder input =
302313
HandlerInputContent.newBuilder().setDataStream(task.getPayload().toByteString().newInput());
@@ -307,13 +318,27 @@ private StartOperationResponse handleStartOperation(
307318
try {
308319
OperationStartResult<HandlerResultContent> result =
309320
startOperation(context, operationStartDetails.build(), input.build());
321+
// If a signal RPC issued by the handler returned a backlink, propagate it to the caller
322+
// so the caller workflow's history event links to the signal event on the callee. Same
323+
// backlink applies to both sync and async response variants.
324+
io.temporal.api.nexus.v1.Link signalBacklink = null;
325+
io.temporal.api.common.v1.Link signalResponseLink =
326+
CurrentNexusOperationContext.get().getSignalWorkflowResponseLink();
327+
if (signalResponseLink != null && signalResponseLink.hasWorkflowEvent()) {
328+
signalBacklink =
329+
LinkConverter.workflowEventToNexusLink(signalResponseLink.getWorkflowEvent());
330+
}
331+
310332
if (result.isSync()) {
311-
startResponseBuilder.setSyncSuccess(
333+
StartOperationResponse.Sync.Builder syncBuilder =
312334
StartOperationResponse.Sync.newBuilder()
313-
.setPayload(Payload.parseFrom(result.getSyncResult().getDataBytes()))
314-
.build());
335+
.setPayload(Payload.parseFrom(result.getSyncResult().getDataBytes()));
336+
if (signalBacklink != null) {
337+
syncBuilder.addLinks(signalBacklink);
338+
}
339+
startResponseBuilder.setSyncSuccess(syncBuilder.build());
315340
} else {
316-
startResponseBuilder.setAsyncSuccess(
341+
StartOperationResponse.Async.Builder asyncBuilder =
317342
StartOperationResponse.Async.newBuilder()
318343
.setOperationId(result.getAsyncOperationToken())
319344
.setOperationToken(result.getAsyncOperationToken())
@@ -325,8 +350,11 @@ private StartOperationResponse handleStartOperation(
325350
.setType(link.getType())
326351
.setUrl(link.getUri().toString())
327352
.build())
328-
.collect(Collectors.toList()))
329-
.build());
353+
.collect(Collectors.toList()));
354+
if (signalBacklink != null) {
355+
asyncBuilder.addLinks(signalBacklink);
356+
}
357+
startResponseBuilder.setAsyncSuccess(asyncBuilder.build());
330358
}
331359
} catch (OperationException e) {
332360
throw e;

0 commit comments

Comments
 (0)