Skip to content
Open
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 @@ -64,14 +64,21 @@ public StartActivityOutput startActivity(StartActivityInput input) {
NexusOperationMetadata nexusOperationMetadata =
nexusContext == null ? null : nexusContext.getNexusOperationMetadata();

String requestId;
if (nexusOperationMetadata != null
&& !Strings.isNullOrEmpty(nexusOperationMetadata.requestId)) {
requestId = nexusOperationMetadata.requestId;
} else if (nexusContext != null && !Strings.isNullOrEmpty(nexusContext.getRequestId())) {
requestId = nexusContext.getRequestId();
} else {
requestId = UUID.randomUUID().toString();
}

StartActivityExecutionRequest.Builder request =
StartActivityExecutionRequest.newBuilder()
.setNamespace(clientOptions.getNamespace())
.setIdentity(clientOptions.getIdentity())
.setRequestId(
nexusOperationMetadata == null
? UUID.randomUUID().toString()
: nexusOperationMetadata.requestId)
.setRequestId(requestId)
.setActivityId(options.getId())
.setActivityType(ActivityType.newBuilder().setName(input.getActivityType()).build())
.setTaskQueue(TaskQueue.newBuilder().setName(options.getTaskQueue()).build())
Expand Down Expand Up @@ -121,14 +128,30 @@ public StartActivityOutput startActivity(StartActivityInput input) {
io.temporal.api.common.v1.Header grpcHeader = HeaderUtils.toHeaderGrpc(input.getHeader(), null);
request.setHeader(grpcHeader);

if (nexusOperationMetadata != null) {
List<Link> protoLinks = nexusContext.getRequestLinks();
List<Link> protoLinks = Collections.emptyList();
if (nexusContext != null) {
// Propagate the inbound Nexus request ID and links to every activity start on the
// operation-handler thread, including starts through a raw ActivityClient.
// Completion callbacks remain limited to the metadata-backed start because only it
// completes the Nexus operation.
protoLinks = nexusContext.getRequestLinks();
request.addAllLinks(protoLinks);
}

boolean willAttachCompletionCallback =
nexusOperationMetadata != null
&& !Strings.isNullOrEmpty(nexusOperationMetadata.callbackUrl);
if (nexusContext != null && (!protoLinks.isEmpty() || willAttachCompletionCallback)) {
// The server rejects attach_request_id unless the request also carries at least one link
// or completion callback to attach on conflict.
request.setOnConflictOptions(
io.temporal.api.common.v1.OnConflictOptions.newBuilder()
.setAttachRequestId(true)
.setAttachLinks(true)
.setAttachCompletionCallbacks(true));
.setAttachLinks(!protoLinks.isEmpty())
.setAttachCompletionCallbacks(willAttachCompletionCallback));
}

if (nexusOperationMetadata != null) {
// Generate the operation token from the user-supplied activity ID and namespace so the
// dual OPERATION_ID + OPERATION_TOKEN headers can be injected before the start RPC fires.
try {
Expand All @@ -141,7 +164,7 @@ public StartActivityOutput startActivity(StartActivityInput input) {
"failed to generate activity operation token",
e);
}
if (!Strings.isNullOrEmpty(nexusOperationMetadata.callbackUrl)) {
if (willAttachCompletionCallback) {
Callback cb =
InternalUtils.buildNexusCallback(
nexusOperationMetadata.callbackUrl,
Expand All @@ -168,7 +191,7 @@ public StartActivityOutput startActivity(StartActivityInput input) {
throw e;
}

if (nexusOperationMetadata != null && response.hasLink()) {
if (nexusContext != null && response.hasLink()) {
nexusContext.addResponseLink(response.getLink());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public class InternalNexusOperationContext {
// workflow client can attach them to the outgoing requests it issues (e.g. signal,
// signalWithStart) via the request's links field.
private List<Link> requestLinks = Collections.emptyList();
// The inbound Nexus task's request ID, captured at the task-handler boundary and available to
// clients executing on the operation-handler thread. RootActivityClientInvoker reuses it for
// redelivery-safe activity-start deduplication. It is deliberately independent of
// nexusOperationMetadata, which is scoped to the single backing start because it carries
// completion-callback semantics.
private String requestId;
// Links returned by outbound RPCs the operation handler issues (such as
// SignalWorkflowExecutionResponse.link or SignalWithStartWorkflowExecutionResponse.signal_link).
// One entry per outbound RPC that returned a link. Drained
Expand Down Expand Up @@ -106,6 +112,16 @@ public void setRequestLinks(List<Link> links) {
return Collections.unmodifiableList(requestLinks);
}

/** Set the request ID of the inbound Nexus task, ambient for the whole invocation. */
public void setRequestId(String requestId) {
this.requestId = requestId;
}

/** The inbound Nexus task's request ID; {@code null} if not set. */
public String getRequestId() {
return requestId;
}

public void setStartWorkflowResponseLink(Link link) {
this.startWorkflowResponseLink = link;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ private StartOperationResponse handleStartOperation(
}
});
CurrentNexusOperationContext.get().setRequestLinks(inboundCommonLinks);
// Ambient for the whole operation-handler invocation, independent of NexusOperationMetadata.
// see InternalNexusOperationContext.requestId.
CurrentNexusOperationContext.get().setRequestId(task.getRequestId());

HandlerInputContent.Builder input =
HandlerInputContent.newBuilder().setDataStream(task.getPayload().toByteString().newInput());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand All @@ -22,6 +23,7 @@
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Assert;
Expand Down Expand Up @@ -106,6 +108,21 @@ public void nexusMetadataAddsCallbackLinksAndRequestId() {
Assert.assertEquals(Collections.singletonList(activityLink()), nexusContext.getResponseLinks());
}

@Test
public void metadataRequestIdTakesPrecedenceOverAmbientRequestId() {
NexusOperationMetadata metadata =
new NexusOperationMetadata(
"nexus-request-id", "http://localhost/callback", Collections.emptyMap());
nexusContext.setNexusOperationMetadata(metadata);

invoker.startActivity(newStartActivityInput());

ArgumentCaptor<StartActivityExecutionRequest> captor =
ArgumentCaptor.forClass(StartActivityExecutionRequest.class);
verify(genericClient).startActivity(captor.capture());
Assert.assertEquals("nexus-request-id", captor.getValue().getRequestId());
}

@Test
public void nexusMetadataWithEmptyCallbackUrlOmitsCompletionCallback() {
NexusOperationMetadata metadata =
Expand All @@ -126,14 +143,84 @@ public void nexusMetadataWithEmptyCallbackUrlOmitsCompletionCallback() {
Assert.assertEquals(0, request.getCompletionCallbacksCount());
Assert.assertTrue(request.getOnConflictOptions().getAttachRequestId());
Assert.assertTrue(request.getOnConflictOptions().getAttachLinks());
Assert.assertTrue(request.getOnConflictOptions().getAttachCompletionCallbacks());
Assert.assertFalse(request.getOnConflictOptions().getAttachCompletionCallbacks());
Assert.assertNotNull(metadata.operationToken);
Assert.assertEquals(Collections.singletonList(activityLink()), nexusContext.getResponseLinks());
}

@Test
public void nexusContextWithoutMetadataStartsOrdinaryActivity() {
nexusContext.setRequestLinks(Collections.singletonList(workflowEventLink()));
public void metadataWithEmptyCallbackUrlAndNoLinksOmitsOnConflictOptions() {
NexusOperationMetadata metadata =
new NexusOperationMetadata(
"nexus-request-id", "", Collections.singletonMap("Custom-Header", "value"));
nexusContext.setNexusOperationMetadata(metadata);

invoker.startActivity(newStartActivityInput());

ArgumentCaptor<StartActivityExecutionRequest> captor =
ArgumentCaptor.forClass(StartActivityExecutionRequest.class);
verify(genericClient).startActivity(captor.capture());
StartActivityExecutionRequest request = captor.getValue();
Assert.assertEquals("nexus-request-id", request.getRequestId());
Assert.assertEquals(0, request.getLinksCount());
Assert.assertEquals(0, request.getCompletionCallbacksCount());
Assert.assertFalse(request.hasOnConflictOptions());
}

@Test
public void nexusContextWithoutMetadataGetsAmbientLinksAndAmbientRequestIdButNoCallback() {
Link link = workflowEventLink();
nexusContext.setRequestLinks(Collections.singletonList(link));
nexusContext.setRequestId("ambient-nexus-request-id");

invoker.startActivity(newStartActivityInput());

ArgumentCaptor<StartActivityExecutionRequest> captor =
ArgumentCaptor.forClass(StartActivityExecutionRequest.class);
verify(genericClient).startActivity(captor.capture());
StartActivityExecutionRequest request = captor.getValue();
Assert.assertEquals("ambient-nexus-request-id", request.getRequestId());
Assert.assertEquals(Collections.singletonList(link), request.getLinksList());
Assert.assertEquals(0, request.getCompletionCallbacksCount());
Assert.assertTrue(request.getOnConflictOptions().getAttachRequestId());
Assert.assertTrue(request.getOnConflictOptions().getAttachLinks());
Assert.assertFalse(request.getOnConflictOptions().getAttachCompletionCallbacks());
Assert.assertEquals(Collections.singletonList(activityLink()), nexusContext.getResponseLinks());
}

@Test
public void twoStartsInTheSameInvocationShareTheAmbientRequestId() {
nexusContext.setRequestId("ambient-nexus-request-id");

invoker.startActivity(newStartActivityInput());
invoker.startActivity(newStartActivityInput());

ArgumentCaptor<StartActivityExecutionRequest> captor =
ArgumentCaptor.forClass(StartActivityExecutionRequest.class);
verify(genericClient, times(2)).startActivity(captor.capture());
List<StartActivityExecutionRequest> requests = captor.getAllValues();
Assert.assertEquals("ambient-nexus-request-id", requests.get(0).getRequestId());
Assert.assertEquals("ambient-nexus-request-id", requests.get(1).getRequestId());
}

@Test
public void nexusContextWithoutAmbientStateStartsOrdinaryActivity() {
invoker.startActivity(newStartActivityInput());

ArgumentCaptor<StartActivityExecutionRequest> captor =
ArgumentCaptor.forClass(StartActivityExecutionRequest.class);
verify(genericClient).startActivity(captor.capture());
StartActivityExecutionRequest request = captor.getValue();
Assert.assertFalse(request.getRequestId().isEmpty());
Assert.assertEquals(0, request.getLinksCount());
Assert.assertEquals(0, request.getCompletionCallbacksCount());
Assert.assertFalse(request.hasOnConflictOptions());
Assert.assertEquals(Collections.singletonList(activityLink()), nexusContext.getResponseLinks());
}

@Test
public void outsideNexusContextStartsOrdinaryActivity() {
CurrentNexusOperationContext.unset();

invoker.startActivity(newStartActivityInput());

Expand Down
Loading
Loading