Skip to content

Commit 5501860

Browse files
committed
Code review results
1 parent 9203021 commit 5501860

9 files changed

Lines changed: 483 additions & 63 deletions

File tree

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,15 @@ public WorkflowSignalOutput signal(WorkflowSignalInput input) {
122122

123123
// If this signal is being issued from inside a Nexus operation handler, forward the inbound
124124
// Nexus task links so the SignalWorkflowExecution history event links back to the caller.
125-
if (CurrentNexusOperationContext.isNexusContext()) {
125+
boolean inNexusContext = CurrentNexusOperationContext.isNexusContext();
126+
if (inNexusContext) {
126127
request.addAllLinks(CurrentNexusOperationContext.get().getNexusOperationLinks());
128+
} else {
129+
// Most signal calls (from a regular client or workflow) won't be in a Nexus context — this
130+
// is normal. The log helps a debugger when a Nexus operation handler "mysteriously" lacks
131+
// link propagation because it spawned a thread to issue the signal (the thread-local
132+
// CurrentNexusOperationContext is invisible from that thread).
133+
log.debug("signal RPC issued outside a Nexus operation context; no link propagation");
127134
}
128135

129136
DataConverter dataConverterWitSignalContext =
@@ -138,7 +145,7 @@ public WorkflowSignalOutput signal(WorkflowSignalInput input) {
138145
SignalWorkflowExecutionResponse response = genericClient.signal(request.build());
139146
// Server >=1.31 with EnableCHASMSignalBacklinks returns a backlink pointing at the signal
140147
// event; older servers leave it unset. Propagate when present.
141-
if (CurrentNexusOperationContext.isNexusContext() && response.hasLink()) {
148+
if (inNexusContext && response.hasLink()) {
142149
CurrentNexusOperationContext.get().addBacklink(response.getLink());
143150
}
144151
return new WorkflowSignalOutput();
@@ -165,8 +172,12 @@ public WorkflowSignalWithStartOutput signalWithStart(WorkflowSignalWithStartInpu
165172
// If this signalWithStart is being issued from inside a Nexus operation handler, forward
166173
// the inbound Nexus task links so both the WorkflowExecutionStarted and
167174
// WorkflowExecutionSignaled events on the callee link back to the caller.
168-
if (CurrentNexusOperationContext.isNexusContext()) {
175+
boolean inNexusContext = CurrentNexusOperationContext.isNexusContext();
176+
if (inNexusContext) {
169177
requestBuilder.addAllLinks(CurrentNexusOperationContext.get().getNexusOperationLinks());
178+
} else {
179+
log.debug(
180+
"signalWithStart RPC issued outside a Nexus operation context; no link propagation");
170181
}
171182
SignalWithStartWorkflowExecutionRequest request = requestBuilder.build();
172183
SignalWithStartWorkflowExecutionResponse response = genericClient.signalWithStart(request);
@@ -177,7 +188,7 @@ public WorkflowSignalWithStartOutput signalWithStart(WorkflowSignalWithStartInpu
177188
.build();
178189
// Server >=1.31 with EnableCHASMSignalBacklinks returns a backlink pointing at the signal
179190
// event; older servers leave it unset. Propagate when present.
180-
if (CurrentNexusOperationContext.isNexusContext() && response.hasSignalLink()) {
191+
if (inNexusContext && response.hasSignalLink()) {
181192
CurrentNexusOperationContext.get().addBacklink(response.getSignalLink());
182193
}
183194
// TODO currently SignalWithStartWorkflowExecutionResponse doesn't have eagerWorkflowTask.

temporal-sdk/src/main/java/io/temporal/internal/common/LinkConverter.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.temporal.api.enums.v1.EventType;
77
import java.io.UnsupportedEncodingException;
88
import java.net.URI;
9+
import java.net.URISyntaxException;
910
import java.net.URLDecoder;
1011
import java.net.URLEncoder;
1112
import java.nio.charset.StandardCharsets;
@@ -33,8 +34,7 @@ public class LinkConverter {
3334
Link.WorkflowEvent.RequestIdReference.getDescriptor().getName();
3435

3536
// Fully-qualified proto descriptor names used as the `type` field on nexus.v1.Link. Match the
36-
// canonical server implementation at temporalio/temporal/common/nexus/link_converter.go so links
37-
// round-trip cleanly across SDKs.
37+
// server's Nexus link converter so links round-trip cleanly across SDKs.
3838
private static final String workflowEventType = Link.WorkflowEvent.getDescriptor().getFullName();
3939
private static final String nexusOperationType =
4040
Link.NexusOperation.getDescriptor().getFullName();
@@ -187,7 +187,7 @@ public static io.temporal.api.nexus.v1.Link nexusOperationToNexusLink(Link.Nexus
187187
.setUrl(url)
188188
.setType(nexusOperationType)
189189
.build();
190-
} catch (Exception e) {
190+
} catch (UnsupportedEncodingException e) {
191191
log.error("Failed to encode NexusOperation Nexus link URL", e);
192192
}
193193
return null;
@@ -200,6 +200,9 @@ public static io.temporal.api.nexus.v1.Link nexusOperationToNexusLink(Link.Nexus
200200
*/
201201
public static Link nexusLinkToNexusOperation(io.temporal.api.nexus.v1.Link nexusLink) {
202202
try {
203+
204+
//Lots of if statements, but this way we double-check the validity of the link
205+
//passed in.
203206
URI uri = new URI(nexusLink.getUrl());
204207
if (!"temporal".equals(uri.getScheme())) {
205208
log.error(
@@ -213,19 +216,40 @@ public static Link nexusLinkToNexusOperation(io.temporal.api.nexus.v1.Link nexus
213216
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
214217
return null;
215218
}
219+
if (!st.hasMoreTokens()) {
220+
log.error(
221+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
222+
return null;
223+
}
216224
String namespace = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
217225
if (!st.hasMoreTokens() || !"nexus-operations".equals(st.nextToken())) {
218226
log.error(
219227
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
220228
return null;
221229
}
230+
if (!st.hasMoreTokens()) {
231+
log.error(
232+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
233+
return null;
234+
}
222235
String operationId = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
236+
if (!st.hasMoreTokens()) {
237+
log.error(
238+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
239+
return null;
240+
}
223241
String runId = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
224242
if (!st.hasMoreTokens() || !"details".equals(st.nextToken())) {
225243
log.error(
226244
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
227245
return null;
228246
}
247+
if (st.hasMoreTokens()) {
248+
log.error(
249+
"Failed to parse NexusOperation Nexus link URL: extra tokens after 'details': {}",
250+
uri.getRawPath());
251+
return null;
252+
}
229253

230254
return Link.newBuilder()
231255
.setNexusOperation(
@@ -234,7 +258,7 @@ public static Link nexusLinkToNexusOperation(io.temporal.api.nexus.v1.Link nexus
234258
.setOperationId(operationId)
235259
.setRunId(runId))
236260
.build();
237-
} catch (Exception e) {
261+
} catch (URISyntaxException | UnsupportedEncodingException e) {
238262
log.error("Failed to parse NexusOperation Nexus link URL", e);
239263
return null;
240264
}

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ public class InternalNexusOperationContext {
2323
// SignalWorkflowExecutionRequest.links.
2424
private List<Link> nexusOperationLinks = Collections.emptyList();
2525
// Backlinks returned by outbound RPCs the operation handler issues (currently
26-
// SignalWorkflowExecutionResponse.link and SignalWithStartWorkflowExecutionResponse.signal_link;
27-
// future update/start variants attach the same way). One entry per outbound RPC that returned
28-
// a link. Drained by the task handler when building StartOperationResponse so each RPC the
29-
// handler issued gets a corresponding link on the caller workflow's history event.
26+
// SignalWorkflowExecutionResponse.link and SignalWithStartWorkflowExecutionResponse.signal_link).
27+
// One entry per outbound RPC that returned a link. Drained by the task handler when building
28+
// StartOperationResponse so each RPC the handler issued gets a corresponding link on the caller
29+
// workflow's history event.
3030
//
31-
// NOTE: this context is only safe for use from the single thread that runs the operation
32-
// handler (the Nexus task executor's thread). Handlers that spawn their own threads to issue
33-
// RPCs will not see the thread-local context, so the links from those RPCs will not propagate.
31+
// This context is only safe for use from the single thread that runs the operation handler (the
32+
// Nexus task executor's thread). The mutators below assert this contract; a stray cross-thread
33+
// call fails fast rather than silently corrupting the ArrayList.
3434
private final List<Link> responseBacklinks = new ArrayList<>();
35+
// Captured at construction (on the Nexus task executor's thread) and used to fail fast on any
36+
// cross-thread mutation. See note on responseBacklinks.
37+
private final Thread ownerThread;
3538

3639
public InternalNexusOperationContext(
3740
String namespace,
@@ -44,6 +47,18 @@ public InternalNexusOperationContext(
4447
this.endpoint = endpoint;
4548
this.metricScope = metricScope;
4649
this.client = client;
50+
this.ownerThread = Thread.currentThread();
51+
}
52+
53+
private void assertOwnerThread() {
54+
if (Thread.currentThread() != ownerThread) {
55+
throw new IllegalStateException(
56+
"InternalNexusOperationContext mutated from thread '"
57+
+ Thread.currentThread().getName()
58+
+ "' but is owned by '"
59+
+ ownerThread.getName()
60+
+ "'. Operation handlers must not spawn threads to issue link-propagating RPCs.");
61+
}
4762
}
4863

4964
public Scope getMetricsScope() {
@@ -90,6 +105,7 @@ public Link getStartWorkflowResponseLink() {
90105
* to RPCs issued by the operation handler.
91106
*/
92107
public void setNexusOperationLinks(List<Link> links) {
108+
assertOwnerThread();
93109
this.nexusOperationLinks = links == null ? Collections.emptyList() : links;
94110
}
95111

@@ -99,19 +115,23 @@ public List<Link> getNexusOperationLinks() {
99115
}
100116

101117
/**
102-
* Append a backlink returned by an outbound RPC the operation handler issued (e.g. signal,
103-
* signalWithStart, and future update/start variants). The task handler drains the list when
104-
* building the operation's StartOperationResponse.
118+
* Append a backlink returned by an outbound RPC the operation handler issued (signal or
119+
* signalWithStart). The task handler drains the list when building the operation's
120+
* StartOperationResponse.
105121
*/
106122
public void addBacklink(Link link) {
123+
assertOwnerThread();
107124
if (link != null) {
108125
this.responseBacklinks.add(link);
109126
}
110127
}
111128

112-
/** Backlinks from every outbound RPC the handler issued. Never null; may be empty. */
129+
/**
130+
* Backlinks from every outbound RPC the handler issued. Never null; may be empty. Returned as an
131+
* unmodifiable view; callers must not attempt to mutate.
132+
*/
113133
public List<Link> getBacklinks() {
114-
return responseBacklinks;
134+
return Collections.unmodifiableList(responseBacklinks);
115135
}
116136

117137
private class NexusOperationContextImpl implements NexusOperationContext {

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

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -317,49 +317,51 @@ private StartOperationResponse handleStartOperation(
317317
StartOperationResponse.Builder startResponseBuilder = StartOperationResponse.newBuilder();
318318
OperationContext context = ctx.build();
319319
try {
320-
try {
321-
OperationStartResult<HandlerResultContent> result =
322-
startOperation(context, operationStartDetails.build(), input.build());
323-
// If outbound RPCs the handler issued (signal, signalWithStart, future update/start
324-
// variants) returned backlinks, propagate them to the caller so the caller workflow's
325-
// history event links to each event on the callee. Same set of backlinks applies to both
326-
// sync and async response variants.
327-
List<io.temporal.api.nexus.v1.Link> backlinks = new ArrayList<>();
328-
for (io.temporal.api.common.v1.Link backlink :
329-
CurrentNexusOperationContext.get().getBacklinks()) {
330-
io.temporal.api.nexus.v1.Link converted = LinkConverter.commonLinkToNexusLink(backlink);
331-
if (converted != null) {
332-
backlinks.add(converted);
333-
}
334-
}
335-
336-
if (result.isSync()) {
337-
startResponseBuilder.setSyncSuccess(
338-
StartOperationResponse.Sync.newBuilder()
339-
.setPayload(Payload.parseFrom(result.getSyncResult().getDataBytes()))
340-
.addAllLinks(backlinks)
341-
.build());
320+
OperationStartResult<HandlerResultContent> result =
321+
startOperation(context, operationStartDetails.build(), input.build());
322+
// If signal or signalWithStart RPCs the handler issued returned backlinks, propagate them
323+
// to the caller so the caller workflow's history event links to each event on the callee.
324+
// Same set of backlinks applies to both sync and async response variants.
325+
List<io.temporal.api.nexus.v1.Link> backlinks = new ArrayList<>();
326+
for (io.temporal.api.common.v1.Link backlink :
327+
CurrentNexusOperationContext.get().getBacklinks()) {
328+
io.temporal.api.nexus.v1.Link converted = LinkConverter.commonLinkToNexusLink(backlink);
329+
if (converted != null) {
330+
backlinks.add(converted);
342331
} else {
343-
startResponseBuilder.setAsyncSuccess(
344-
StartOperationResponse.Async.newBuilder()
345-
.setOperationId(result.getAsyncOperationToken())
346-
.setOperationToken(result.getAsyncOperationToken())
347-
.addAllLinks(
348-
context.getLinks().stream()
349-
.map(
350-
link ->
351-
io.temporal.api.nexus.v1.Link.newBuilder()
352-
.setType(link.getType())
353-
.setUrl(link.getUri().toString())
354-
.build())
355-
.collect(Collectors.toList()))
356-
.addAllLinks(backlinks)
357-
.build());
332+
// The SDK stashed this backlink itself in RootWorkflowClientInvoker; failing to re-encode
333+
// it now means a LinkConverter regression or a malformed link from the server. Either is
334+
// an SDK invariant violation worth shouting about (warn is too quiet — the caller's
335+
// history event will be missing a link with no other diagnostic).
336+
log.error(
337+
"SDK-stashed backlink failed to re-encode as nexus.v1.Link; caller history will be"
338+
+ " missing a link. backlink={}",
339+
backlink);
358340
}
359-
} catch (OperationException e) {
360-
throw e;
361-
} catch (Throwable failure) {
362-
convertKnownFailures(failure);
341+
}
342+
343+
if (result.isSync()) {
344+
startResponseBuilder.setSyncSuccess(
345+
StartOperationResponse.Sync.newBuilder()
346+
.setPayload(Payload.parseFrom(result.getSyncResult().getDataBytes()))
347+
.addAllLinks(backlinks)
348+
.build());
349+
} else {
350+
startResponseBuilder.setAsyncSuccess(
351+
StartOperationResponse.Async.newBuilder()
352+
.setOperationId(result.getAsyncOperationToken())
353+
.setOperationToken(result.getAsyncOperationToken())
354+
.addAllLinks(
355+
context.getLinks().stream()
356+
.map(
357+
link ->
358+
io.temporal.api.nexus.v1.Link.newBuilder()
359+
.setType(link.getType())
360+
.setUrl(link.getUri().toString())
361+
.build())
362+
.collect(Collectors.toList()))
363+
.addAllLinks(backlinks)
364+
.build());
363365
}
364366
} catch (OperationException e) {
365367
TemporalFailure temporalFailure;
@@ -377,6 +379,8 @@ private StartOperationResponse handleStartOperation(
377379
new RuntimeException("Unknown operation state: " + e.getState()));
378380
}
379381
startResponseBuilder.setFailure(dataConverter.exceptionToFailure(temporalFailure));
382+
} catch (Throwable failure) {
383+
convertKnownFailures(failure);
380384
}
381385
return startResponseBuilder.build();
382386
}

0 commit comments

Comments
 (0)