Skip to content

Commit 480d289

Browse files
committed
Adding a Nexus link converter helper method
1 parent 921a910 commit 480d289

4 files changed

Lines changed: 301 additions & 33 deletions

File tree

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

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@
2222
import io.temporal.internal.nexus.OperationTokenUtil;
2323
import java.util.*;
2424
import java.util.stream.Collectors;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
2725

2826
/** Utility functions shared by the implementation code. */
2927
public final class InternalUtils {
3028
public static String TEMPORAL_RESERVED_PREFIX = "__temporal_";
3129

32-
private static final Logger log = LoggerFactory.getLogger(InternalUtils.class);
3330
private static String QUERY_TYPE_STACK_TRACE = "__stack_trace";
3431
private static String ENHANCED_QUERY_TYPE_STACK_TRACE = "__enhanced_stack_trace";
3532

@@ -94,19 +91,12 @@ public static NexusWorkflowStarter createNexusBoundStub(
9491
: request.getLinks().stream()
9592
.map(
9693
(link) -> {
97-
if (io.temporal.api.common.v1.Link.WorkflowEvent.getDescriptor()
98-
.getFullName()
99-
.equals(link.getType())) {
100-
io.temporal.api.nexus.v1.Link nexusLink =
101-
io.temporal.api.nexus.v1.Link.newBuilder()
102-
.setType(link.getType())
103-
.setUrl(link.getUri().toString())
104-
.build();
105-
return LinkConverter.nexusLinkToWorkflowEvent(nexusLink);
106-
} else {
107-
log.warn("ignoring unsupported link data type: {}", link.getType());
108-
return null;
109-
}
94+
io.temporal.api.nexus.v1.Link nexusLink =
95+
io.temporal.api.nexus.v1.Link.newBuilder()
96+
.setType(link.getType())
97+
.setUrl(link.getUri().toString())
98+
.build();
99+
return LinkConverter.nexusLinkToCommonLink(nexusLink);
110100
})
111101
.filter(Objects::nonNull)
112102
.collect(Collectors.toList());

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class LinkConverter {
2020
private static final Logger log = LoggerFactory.getLogger(LinkConverter.class);
2121

2222
private static final String linkPathFormat = "temporal:///namespaces/%s/workflows/%s/%s/history";
23+
private static final String linkPathNexusOperationFormat =
24+
"temporal:///namespaces/%s/nexus-operations/%s/%s/details";
2325
private static final String linkReferenceTypeKey = "referenceType";
2426
private static final String linkEventIDKey = "eventID";
2527
private static final String linkEventTypeKey = "eventType";
@@ -30,6 +32,13 @@ public class LinkConverter {
3032
private static final String requestIDReferenceType =
3133
Link.WorkflowEvent.RequestIdReference.getDescriptor().getName();
3234

35+
// 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.
38+
private static final String workflowEventType = Link.WorkflowEvent.getDescriptor().getFullName();
39+
private static final String nexusOperationType =
40+
Link.NexusOperation.getDescriptor().getFullName();
41+
3342
public static io.temporal.api.nexus.v1.Link workflowEventToNexusLink(Link.WorkflowEvent we) {
3443
try {
3544

@@ -160,6 +169,118 @@ public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusL
160169
return link.build();
161170
}
162171

172+
/**
173+
* Encode a {@link Link.NexusOperation} (a link to a standalone Nexus operation) into the (url,
174+
* type) form used on the Nexus wire. URL format matches the canonical server implementation:
175+
* {@code temporal:///namespaces/{ns}/nexus-operations/{op_id}/{run_id}/details}.
176+
*/
177+
public static io.temporal.api.nexus.v1.Link nexusOperationToNexusLink(Link.NexusOperation no) {
178+
try {
179+
String url =
180+
String.format(
181+
linkPathNexusOperationFormat,
182+
URLEncoder.encode(no.getNamespace(), StandardCharsets.UTF_8.toString()),
183+
URLEncoder.encode(no.getOperationId(), StandardCharsets.UTF_8.toString())
184+
.replace("+", "%20"),
185+
URLEncoder.encode(no.getRunId(), StandardCharsets.UTF_8.toString()));
186+
return io.temporal.api.nexus.v1.Link.newBuilder()
187+
.setUrl(url)
188+
.setType(nexusOperationType)
189+
.build();
190+
} catch (Exception e) {
191+
log.error("Failed to encode NexusOperation Nexus link URL", e);
192+
}
193+
return null;
194+
}
195+
196+
/**
197+
* Decode a {@code nexus.v1.Link} whose {@code type} is {@code Link.NexusOperation} into a {@code
198+
* common.v1.Link} carrying a {@link Link.NexusOperation} variant. The URL must match the format
199+
* produced by {@link #nexusOperationToNexusLink}.
200+
*/
201+
public static Link nexusLinkToNexusOperation(io.temporal.api.nexus.v1.Link nexusLink) {
202+
try {
203+
URI uri = new URI(nexusLink.getUrl());
204+
if (!"temporal".equals(uri.getScheme())) {
205+
log.error(
206+
"Failed to parse NexusOperation Nexus link URL: invalid scheme: {}", uri.getScheme());
207+
return null;
208+
}
209+
210+
StringTokenizer st = new StringTokenizer(uri.getRawPath(), "/");
211+
if (!st.hasMoreTokens() || !"namespaces".equals(st.nextToken())) {
212+
log.error(
213+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
214+
return null;
215+
}
216+
String namespace = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
217+
if (!st.hasMoreTokens() || !"nexus-operations".equals(st.nextToken())) {
218+
log.error(
219+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
220+
return null;
221+
}
222+
String operationId = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
223+
String runId = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
224+
if (!st.hasMoreTokens() || !"details".equals(st.nextToken())) {
225+
log.error(
226+
"Failed to parse NexusOperation Nexus link URL: invalid path: {}", uri.getRawPath());
227+
return null;
228+
}
229+
230+
return Link.newBuilder()
231+
.setNexusOperation(
232+
Link.NexusOperation.newBuilder()
233+
.setNamespace(namespace)
234+
.setOperationId(operationId)
235+
.setRunId(runId))
236+
.build();
237+
} catch (Exception e) {
238+
log.error("Failed to parse NexusOperation Nexus link URL", e);
239+
return null;
240+
}
241+
}
242+
243+
/**
244+
* Encode a {@code common.v1.Link} into the Nexus-wire {@code nexus.v1.Link} (url, type) form,
245+
* dispatching on the link's variant. Returns {@code null} (with a warn log) for variants the SDK
246+
* does not yet know how to encode ({@code Activity}, {@code BatchJob}, unset) — match the
247+
* server's link-converter behavior so we stay in lockstep.
248+
*/
249+
public static io.temporal.api.nexus.v1.Link commonLinkToNexusLink(Link commonLink) {
250+
switch (commonLink.getVariantCase()) {
251+
case WORKFLOW_EVENT:
252+
return workflowEventToNexusLink(commonLink.getWorkflowEvent());
253+
case NEXUS_OPERATION:
254+
return nexusOperationToNexusLink(commonLink.getNexusOperation());
255+
default:
256+
log.warn(
257+
"Cannot encode common.v1.Link variant {} as nexus.v1.Link: no encoder implemented",
258+
commonLink.getVariantCase());
259+
return null;
260+
}
261+
}
262+
263+
/**
264+
* Decode a Nexus-wire {@code nexus.v1.Link} (url, type) into a {@code common.v1.Link},
265+
* dispatching on the link's {@code type} field. Returns {@code null} (with a warn log) for types
266+
* the SDK does not yet know how to decode.
267+
*/
268+
public static Link nexusLinkToCommonLink(io.temporal.api.nexus.v1.Link nexusLink) {
269+
String type = nexusLink.getType();
270+
if (workflowEventType.equals(type)) {
271+
return nexusLinkToWorkflowEvent(nexusLink);
272+
}
273+
if (nexusOperationType.equals(type)) {
274+
return nexusLinkToNexusOperation(nexusLink);
275+
}
276+
log.warn(
277+
"Cannot decode nexus.v1.Link of type '{}' to common.v1.Link:"
278+
+ " no decoder implemented (url='{}')",
279+
type,
280+
nexusLink.getUrl());
281+
return null;
282+
}
283+
163284
private static Map<String, String> parseQueryParams(URI uri) throws UnsupportedEncodingException {
164285
final String query = uri.getQuery();
165286
if (query == null || query.isEmpty()) {

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

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -301,20 +301,12 @@ private StartOperationResponse handleStartOperation(
301301
"Invalid link URL: " + link.getUrl(),
302302
e);
303303
}
304-
// LinkConverter only returns a WorkflowEvent-shaped common.v1.Link; nexus links of
305-
// other shapes (e.g. non-temporal URLs) come back null and are intentionally not
306-
// forwarded onto SignalWorkflowExecutionRequest.links, which requires the
307-
// WorkflowEvent variant. Log so a debugging session can see what was dropped.
308-
io.temporal.api.common.v1.Link commonLink =
309-
LinkConverter.nexusLinkToWorkflowEvent(link);
304+
// Convert each inbound nexus.v1.Link to common.v1.Link, dispatching on the link's
305+
// type field (WorkflowEvent, NexusOperation, etc.). LinkConverter logs the warn for
306+
// any unknown type and returns null.
307+
io.temporal.api.common.v1.Link commonLink = LinkConverter.nexusLinkToCommonLink(link);
310308
if (commonLink != null) {
311309
inboundCommonLinks.add(commonLink);
312-
} else {
313-
log.warn(
314-
"Dropping inbound Nexus link from outbound signal propagation: type='{}',"
315-
+ " url='{}' (not a parseable temporal WorkflowEvent link)",
316-
link.getType(),
317-
link.getUrl());
318310
}
319311
});
320312
CurrentNexusOperationContext.get().setNexusOperationLinks(inboundCommonLinks);
@@ -335,11 +327,7 @@ private StartOperationResponse handleStartOperation(
335327
List<io.temporal.api.nexus.v1.Link> backlinks = new ArrayList<>();
336328
for (io.temporal.api.common.v1.Link backlink :
337329
CurrentNexusOperationContext.get().getBacklinks()) {
338-
if (!backlink.hasWorkflowEvent()) {
339-
continue;
340-
}
341-
io.temporal.api.nexus.v1.Link converted =
342-
LinkConverter.workflowEventToNexusLink(backlink.getWorkflowEvent());
330+
io.temporal.api.nexus.v1.Link converted = LinkConverter.commonLinkToNexusLink(backlink);
343331
if (converted != null) {
344332
backlinks.add(converted);
345333
}

0 commit comments

Comments
 (0)