Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static io.temporal.internal.common.HeaderUtils.intoPayloadMap;
import static io.temporal.internal.common.WorkflowExecutionUtils.makeUserMetaData;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.Iterators;
import io.grpc.Deadline;
import io.grpc.Status;
Expand All @@ -23,7 +24,11 @@
import io.temporal.common.interceptors.WorkflowClientCallsInterceptor;
import io.temporal.internal.client.external.GenericWorkflowClient;
import io.temporal.internal.common.HeaderUtils;
import io.temporal.internal.common.InternalUtils;
import io.temporal.internal.nexus.CurrentNexusOperationContext;
import io.temporal.internal.nexus.InternalNexusOperationContext;
import io.temporal.internal.nexus.NexusOperationMetadata;
import io.temporal.internal.nexus.OperationTokenUtil;
import io.temporal.internal.worker.WorkerVersioningProtoUtils;
import io.temporal.payload.context.WorkflowSerializationContext;
import io.temporal.serviceclient.StatusUtils;
Expand Down Expand Up @@ -473,6 +478,19 @@ public <R> WorkflowUpdateHandle<R> startUpdate(StartUpdateInput<R> input) {
}
} while (updateNotYetDurable(input, result));

// If triggered by a Nexus Operation, set necessary fields- link, result
if (CurrentNexusOperationContext.isNexusContext()) {
NexusOperationMetadata nexusOperationMetadata =
CurrentNexusOperationContext.get().getNexusOperationMetadata();
if (nexusOperationMetadata != null) {
if (result.hasLink()) {
// add forward links for caller->handler
CurrentNexusOperationContext.get().addResponseLink(result.getLink());
}
nexusOperationMetadata.operationCompleted = result.hasOutcome();
}
}

return toUpdateHandle(input, result, dataConverterWithWorkflowContext);
}

Expand All @@ -495,14 +513,49 @@ private <R> UpdateWorkflowExecutionRequest toUpdateWorkflowExecutionRequest(
.setName(input.getUpdateName());
inputArgs.ifPresent(updateInput::setArgs);

Request request =
Request.Builder requestBuilder =
Request.newBuilder()
.setMeta(
Meta.newBuilder()
.setUpdateId(input.getUpdateId())
.setIdentity(clientOptions.getIdentity()))
.setInput(updateInput)
.build();
.setInput(updateInput);

// If this update is being issued from inside a Nexus operation handler via
// TemporalNexusClientImpl.startWorkflowUpdate, set the fields the server needs
// to deliver the Nexus completion callback.
if (CurrentNexusOperationContext.isNexusContext()) {
InternalNexusOperationContext nexusContext = CurrentNexusOperationContext.get();
// already in a Nexus operation context, dont need to check nexusContext again
NexusOperationMetadata nexusOperationMetadata = nexusContext.getNexusOperationMetadata();
if (nexusOperationMetadata != null) {
try {
nexusOperationMetadata.operationToken =
OperationTokenUtil.generateWorkflowUpdateOperationToken(
clientOptions.getNamespace(),
input.getWorkflowExecution().getWorkflowId(),
input.getWorkflowExecution().getRunId(),
input.getUpdateId());
} catch (JsonProcessingException e) {
throw new IllegalStateException("failed to generate update operation token", e);
}
List<Link> requestLinks = nexusContext.getRequestLinks();
requestBuilder
.setRequestId(nexusOperationMetadata.requestId)
.addCompletionCallbacks(
InternalUtils.buildNexusCallback(
nexusOperationMetadata.callbackHeaders,
nexusOperationMetadata.callbackUrl,
nexusOperationMetadata.operationToken,
requestLinks))
.addAllLinks(requestLinks);
} else {
log.error("unexpected error fetching nexusOperationMetadata");
// maybe throw an exception but should never really happen as its all sdk
}
}

Request request = requestBuilder.build();

return UpdateWorkflowExecutionRequest.newBuilder()
.setNamespace(clientOptions.getNamespace())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ public static NexusWorkflowStarter createNexusBoundStub(
return new NexusWorkflowStarter(stub.newInstance(nexusWorkflowOptions.build()), operationToken);
}

/** Helper to build a Nexus Callback from the provided input. */
public static Callback buildNexusCallback(
Map<String, String> callbackHeaders,
String callbackUrl,
String operationToken,
List<Link> links) {
Map<String, String> headers =
callbackHeaders.entrySet().stream()
.collect(
Collectors.toMap(
(k) -> k.getKey().toLowerCase(),
Map.Entry::getValue,
(a, b) -> a,
TreeMap::new));
headers.put(Header.OPERATION_TOKEN.toLowerCase(), operationToken);
Callback.Builder cbBuilder =
Callback.newBuilder()
.setNexus(
Callback.Nexus.newBuilder().setUrl(callbackUrl).putAllHeader(headers).build());
if (links != null) {
cbBuilder.addAllLinks(links);
}
return cbBuilder.build();
}

/** Check the method name for reserved prefixes or names. */
public static void checkMethodName(POJOWorkflowMethodMetadata methodMetadata) {
if (methodMetadata.getName().startsWith(TEMPORAL_RESERVED_PREFIX)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class LinkConverter {

private static final Logger log = LoggerFactory.getLogger(LinkConverter.class);

private static final String temporalUrlScheme = "temporal";
private static final String linkPathFormat = "temporal:///namespaces/%s/workflows/%s/%s/history";
private static final String nexusOperationLinkPathFormat =
"temporal:///namespaces/%s/nexus-operations/%s/%s/details";
Expand All @@ -35,6 +36,7 @@ public class LinkConverter {
Link.WorkflowEvent.getDescriptor().getFullName();
private static final String nexusOperationLinkType =
Link.NexusOperation.getDescriptor().getFullName();
private static final String workflowLinkType = Link.Workflow.getDescriptor().getFullName();

public static io.temporal.api.nexus.v1.Link workflowEventToNexusLink(Link.WorkflowEvent we) {
try {
Expand Down Expand Up @@ -93,6 +95,24 @@ public static io.temporal.api.nexus.v1.Link workflowEventToNexusLink(Link.Workfl
return null;
}

public static io.temporal.api.nexus.v1.Link workflowLinkToNexusLink(Link.Workflow w) {
try {
String namespace = URLEncoder.encode(w.getNamespace(), StandardCharsets.UTF_8.toString());
String workflowId =
URLEncoder.encode(w.getWorkflowId(), StandardCharsets.UTF_8.toString())
.replace("+", "%20"); // handle workflowIds supporting spaces
String runId = URLEncoder.encode(w.getRunId(), StandardCharsets.UTF_8.toString());
String url = String.format(linkPathFormat, namespace, workflowId, runId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.format can throw IllegalFormatException which is uncaught - should you just catch Exception instead of limiting it?

return io.temporal.api.nexus.v1.Link.newBuilder()
.setUrl(url)
.setType(workflowLinkType)
.build();
} catch (Exception e) {
log.error("Failed to encode Nexus link URL", e);
return null;
}
}

public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusLink) {
Link.Builder link = Link.newBuilder();
try {
Expand Down Expand Up @@ -166,6 +186,44 @@ public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusL
return link.build();
}

public static Link nexusLinkToWorkflowLink(io.temporal.api.nexus.v1.Link nexusLink) {
Link.Builder link = Link.newBuilder();
try {
URI uri = new URI(nexusLink.getUrl());
log.debug("Parsing nexus link URL: {}", uri.getRawPath());
if (!uri.getScheme().equals(temporalUrlScheme)) {
log.error("Failed to parse Nexus link URL: invalid scheme: {}", uri.getScheme());
return null;
}
StringTokenizer st = new StringTokenizer(uri.getRawPath(), "/");
// maybe add constants for "namespaces", "workflows" too
if (!st.nextToken().equals("namespaces")) {
log.error("Failed to parse Nexus link URL: invalid path: {}", uri.getRawPath());
return null;
}
String namespace = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
if (!st.nextToken().equals("workflows")) {
log.error("Failed to parse Nexus link URL: invalid path: {}", uri.getRawPath());
return null;
}
String workflowID = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
if (!st.hasMoreTokens()) {
log.error("Failed to parse Nexus link URL: invalid path: {}", uri.getRawPath());
return null;
}
String runID = URLDecoder.decode(st.nextToken(), StandardCharsets.UTF_8.toString());
link.setWorkflow(
Link.Workflow.newBuilder()
.setNamespace(namespace)
.setWorkflowId(workflowID)
.setRunId(runID));
} catch (Exception e) {
log.error("Failed to parse Nexus link URL", e);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might still want to log the bad URL, like you did above

return null;
}
return link.build();
}

/**
* Dispatches on the oneof variant of {@code commonLink} and converts to the matching {@link
* io.temporal.api.nexus.v1.Link}. Returns {@code null} if no variant is set or encoding fails.
Expand All @@ -177,6 +235,9 @@ public static io.temporal.api.nexus.v1.Link linkToNexusLink(Link commonLink) {
if (commonLink.hasNexusOperation()) {
return nexusOperationToNexusLink(commonLink.getNexusOperation());
}
if (commonLink.hasWorkflow()) {
return workflowLinkToNexusLink(commonLink.getWorkflow());
}
return null;
}

Expand All @@ -192,6 +253,9 @@ public static Link nexusLinkToLink(io.temporal.api.nexus.v1.Link nexusLink) {
if (nexusOperationLinkType.equals(type)) {
return nexusLinkToNexusOperation(nexusLink);
}
if (workflowLinkType.equals(type)) {
return nexusLinkToWorkflowLink(nexusLink);
}
log.warn("ignoring unsupported nexus link type: {}", type);
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ public class InternalNexusOperationContext {
private final Object responseLinksLock = new Object();
private final List<Link> responseLinks = new ArrayList<>();

private NexusOperationMetadata nexusOperationMetadata;

/**
* Set the Nexus operation metadata
*
* @param metadata {@link NexusOperationMetadata} to be set
*/
public void setNexusOperationMetadata(NexusOperationMetadata metadata) {
this.nexusOperationMetadata = metadata;
}

public NexusOperationMetadata getNexusOperationMetadata() {
return nexusOperationMetadata;
}

public InternalNexusOperationContext(
String namespace,
String taskQueue,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.temporal.internal.nexus;

import io.temporal.common.Experimental;
import java.util.Map;

/** Container for an in-flight Nexus operation metadata. */
@Experimental
public final class NexusOperationMetadata {
public final String requestId;
public final String callbackUrl;
public final Map<String, String> callbackHeaders;

public String operationToken;
public boolean operationCompleted;

public NexusOperationMetadata(
String requestId, String callbackUrl, Map<String, String> callbackHeaders) {
this.requestId = requestId;
this.callbackUrl = callbackUrl;
this.callbackHeaders = callbackHeaders;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,49 @@ public class OperationToken {
@JsonProperty("wid")
private final String workflowId;

@JsonProperty("rid")
@JsonInclude(JsonInclude.Include.NON_NULL)
// only set for updates and activities
private final String runId;

@JsonProperty("uid")
@JsonInclude(JsonInclude.Include.NON_NULL)
// only set for updates
private final String updateId;

public OperationToken(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could consider splitting the OperationToken into different Java types for the different t. Should be possible using jackson. Although I would be fine defering this until we have activities here as well and then doing the refactor then

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about it with a base class that does common encode/decodes but left it as overloads give hints and the remaining is mostly similar
But agree on maybe changing after activities- if changing here, will track in other SDKs too for consistency

@JsonProperty("t") Integer type,
@JsonProperty("ns") String namespace,
@JsonProperty("wid") String workflowId,
@JsonProperty("rid") String runId,
@JsonProperty("uid") String updateId,
@JsonProperty("v") Integer version) {
this.type = OperationTokenType.fromValue(type);
this.namespace = namespace;
this.workflowId = workflowId;
this.runId = runId;
this.updateId = updateId;
this.version = version;
}

/** Generate a token for a workflow run operation */
public OperationToken(OperationTokenType type, String namespace, String workflowId) {
this.type = type;
this.namespace = namespace;
this.workflowId = workflowId;
this.version = null;
this.runId = null;
this.updateId = null;
}

/** Generate a token for a workflow update operation */
public OperationToken(String namespace, String workflowId, String runId, String updateId) {
this.type = OperationTokenType.WORKFLOW_UPDATE;
this.namespace = namespace;
this.workflowId = workflowId;
this.runId = runId;
this.updateId = updateId;
this.version = null;
}

public Integer getVersion() {
Expand All @@ -51,4 +78,12 @@ public String getNamespace() {
public String getWorkflowId() {
return workflowId;
}

public String getUpdateId() {
return updateId;
}

public String getRunId() {
return runId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

public enum OperationTokenType {
UNKNOWN(0),
WORKFLOW_RUN(1);
WORKFLOW_RUN(1),
WORKFLOW_UPDATE(3);

private final int value;

Expand Down
Loading
Loading