Skip to content

Dedup retried Nexus signals - #3180

Merged
atol merged 3 commits into
mainfrom
alicelin/NEXUS-943/signal-dedup
Nov 3, 2026
Merged

atol merged 3 commits into
mainfrom
alicelin/NEXUS-943/signal-dedup

Conversation

@atol

@atol atol commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

What was changed

  • Signals and Signal-With-Starts sent through TemporalNexusClient.getWorkflowClient() in a @TemporalOperation handler now reuse the Nexus operation's request ID.
  • Breaking (experimental API): each handler run can send only one such signal; a second throws a BAD_REQUEST error. Signal-With-Start counts toward the limit; a failed signal doesn't; a retried Nexus task starts with a fresh limit.
  • Breaking (experimental API): Signal-With-Start from such a handler requires an explicit workflow ID.
  • Nexus.getOperationContext().getWorkflowClient() returns the same client, so signals sent through it from a @TemporalOperation handler are covered too. Signals from other Nexus handlers (e.g. OperationHandler.sync, WorkflowRunOperation), or from threads other than the handler thread, are unchanged.
  • Based on De-duplicate Nexus Signal and SignalWithStart operations sdk-go#2781.

Why?

When a Nexus task is retried, the handler reruns and sends its signal again with a new random request ID, so the workflow receives it twice. Reusing the operation's request ID lets the server dedupe the retry. However, every signal in one handler run then shares that ID, so a second signal to the same workflow would be silently dropped while reported as delivered. Limiting each run to one signal turns that silent loss into a clear error. Signal-With-Start also requires an explicit workflow ID, since a generated one would differ on each retry and start a second workflow instead of deduping.

Checklist

  1. How was this tested:
    Added unit and integration tests.

  2. Any docs updates needed?
    No.

@atol
atol marked this pull request as ready for review October 30, 2026 15:52
@atol
atol requested a review from a team as a code owner October 30, 2026 15:52
@Quinn-With-Two-Ns Quinn-With-Two-Ns self-assigned this Oct 30, 2026
@Quinn-With-Two-Ns

Copy link
Copy Markdown
Contributor

Breaking (experimental API): Signal-With-Start from such a handler requires an explicit workflow ID.

Can you expand on this ,, are you saying signal-with-start didn't require a workflow ID before?

/**
* Returns the underlying {@link WorkflowClient} for advanced use cases.
*
* <p>Signals sent from the operation handler carry the operation's request ID so a retried Nexus

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.

I think it would be better to move this up to the docs on TemporalNexusClient where we talk about sending a signal from a TemporalOperation, also this is quite verbose don't you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, I can move and shorten the comment.

@Quinn-With-Two-Ns

Copy link
Copy Markdown
Contributor

Breaking (experimental API): Signal-With-Start from such a handler requires an explicit workflow ID.

Can you expand on this ,, are you saying signal-with-start didn't require a workflow ID before?

@atol

atol commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Breaking (experimental API): Signal-With-Start from such a handler requires an explicit workflow ID.

Can you expand on this ,, are you saying signal-with-start didn't require a workflow ID before?

Before, the workflow ID for Signal-with-Start was derived from getWorkflowIdForStart(), which would default to a new UUID if the workflow ID is not set on WorkflowOptions:

private static String getWorkflowIdForStart(WorkflowOptions options) {
String workflowId = options.getWorkflowId();
if (workflowId == null) {
workflowId = UUID.randomUUID().toString();
}
return workflowId;
}

But since the server only matches request IDs within the same workflow run, we need a consistent workflow ID for deduplication.

Python, TypeScript and .NET already require a workflow ID for Signal-with-Start. I added a similar check in Go in temporalio/sdk-go#2781.

@atol
atol force-pushed the alicelin/NEXUS-943/signal-dedup branch from 32eb497 to e5992ee Compare November 2, 2026 19:10
@atol
atol force-pushed the alicelin/NEXUS-943/signal-dedup branch from e5992ee to 58327e3 Compare November 2, 2026 19:13
@atol
atol merged commit 94a0981 into main Nov 3, 2026
117 of 128 checks passed
@atol
atol deleted the alicelin/NEXUS-943/signal-dedup branch November 3, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants