Skip to content

fix(workflow): use server-blocking WaitForInstance* RPCs instead of client polling#1839

Closed
paulstadler-mesh wants to merge 2 commits into
dapr:masterfrom
paulstadler-mesh:fix/workflow-wait-server-blocking
Closed

fix(workflow): use server-blocking WaitForInstance* RPCs instead of client polling#1839
paulstadler-mesh wants to merge 2 commits into
dapr:masterfrom
paulstadler-mesh:fix/workflow-wait-server-blocking

Conversation

@paulstadler-mesh

Copy link
Copy Markdown
Contributor

Description

WorkflowGrpcClient.WaitForWorkflowStartAsync and WaitForWorkflowCompletionAsync implemented the wait as a client-side polling loop over the unary GetInstance RPC (500 ms / 1 s cadence) rather than calling the server-side blocking RPCs the sidecar already exposes for exactly this purpose:

rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse);
rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse);

These return the moment the instance reaches the target state, so no client polling is needed. The generated WaitForInstance*Async stubs existed but were never called.

This PR switches both methods to the blocking RPCs, wrapped in an exponential-backoff retry that re-issues on transient interruptions (DeadlineExceeded / Unavailable) and propagates cancellation — mirroring the dapr/durabletask-go client (WaitForOrchestrationStart / WaitForOrchestrationCompletion), which is what the Dapr go-sdk uses. Calls continue to flow through CreateCallOptions(...) so the dapr-api-token is honored.

No public API change; behavior stays compatible (still returns WorkflowMetadata, still throws InvalidOperationException if the instance doesn't exist).

Issue reference

Fixes #1838

Impact / motivation

For short-lived workflows the fixed poll cadence dominated wall-clock latency. Local benchmark against Dapr 1.17.3 with a workflow that completes server-side in ~28 ms:

Path p50 p95
Old WaitForWorkflowCompletionAsync (poll) 1066 ms 1078 ms
Server-blocking WaitForInstanceCompletion 49 ms 133 ms

It also removes needless repeated GetInstance load on the sidecar for long-running instances.

go-sdk parity

dapr/durabletask-go client/client_grpc.go already does this — single blocking call wrapped in a backoff retry that stops on context cancellation. The .NET SDK was the lone outlier still client-polling. This brings it in line.

How tested

  • Updated WorkflowGrpcClientTests to drive the blocking RPCs.
  • Added coverage for: blocking-RPC happy path (start + completion), transient-error retry-then-succeed, non-transient error propagation (no retry), and cancellation.
  • Removed the now-obsolete client-poll tests.
  • dotnet test test/Dapr.Workflow.Test — 398 passing (net8.0 / net10.0).

Checklist

  • Code compiles correctly
  • Created/updated tests
  • Tests pass locally
  • Extended the documentation, if applicable

…lient polling

WaitForWorkflowStartAsync and WaitForWorkflowCompletionAsync polled GetInstance
on a fixed 500ms/1s cadence. The sidecar already exposes server-blocking
WaitForInstanceStart / WaitForInstanceCompletion RPCs that return the moment the
instance reaches the target state, and the go-sdk (via durabletask-go) already
uses them. This switches both methods to the blocking RPCs, wrapped in an
exponential-backoff retry that re-issues on transient interruptions
(DeadlineExceeded / Unavailable) and propagates cancellation, mirroring
durabletask-go's client. Calls continue to flow through CreateCallOptions so the
dapr-api-token is honored.

Removes the now-dead client-poll tests and adds coverage for the blocking RPC
path, transient-error retry, non-transient propagation, and cancellation.

Fixes dapr#1838

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Stadler <paul.stadler@meshconnect.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates workflow wait operations to use the sidecar’s server-blocking WaitForInstanceStart and WaitForInstanceCompletion RPCs instead of client-side polling, reducing latency and sidecar load.

Changes:

  • Replaced polling loops in workflow start/completion waits with blocking RPC calls.
  • Added exponential backoff retries for transient wait interruptions.
  • Updated unit tests to verify blocking RPC usage and retry behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Dapr.Workflow/Client/WorkflowGrpcClient.cs Switches wait logic to blocking RPCs with retry handling.
src/Dapr.Workflow/Logging.cs Adds debug logging for retrying interrupted wait RPCs.
test/Dapr.Workflow.Test/Client/WorkflowGrpcClientTests.cs Updates wait tests from polling expectations to blocking RPC behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +360 to +369
return await waitCall(request, grpcCallOptions);
}
catch (RpcException ex) when (
!cancellationToken.IsCancellationRequested &&
ex.StatusCode is StatusCode.DeadlineExceeded or StatusCode.Unavailable)
{
logger.LogWaitForInstanceRetry(ex, instanceId, ex.StatusCode, delay);
await Task.Delay(delay, cancellationToken);
delay = TimeSpan.FromMilliseconds(
Math.Min(delay.TotalMilliseconds * 2, MaxWaitRetryDelay.TotalMilliseconds));

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.

@paulstadler-mesh If you could please make this change, I'd appreciate it. If you're able to do this within the next 12 hours, I can get it into the Dapr GA release (aiming for Tuesday).

This is an excellent perf improvement - thank you for spotting it and submitting a PR!

@WhitWaldo

Copy link
Copy Markdown
Contributor

@paulstadler-mesh I'm seeing some non-transient errors in the 1.17 and 1.18 RC runtimes regarding exception issues. Can you please review your changes and correct as necessary? If you're able to do this in the next 8 hours, I can get it into the GA release - otherwise, it'll have to wait for a patch release.

@WhitWaldo

Copy link
Copy Markdown
Contributor

Closing in favor of #1843 so we can get this into the next major release

@WhitWaldo WhitWaldo closed this Jun 5, 2026
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.

WaitForWorkflowCompletionAsync / WaitForWorkflowStartAsync client-poll instead of using the server-blocking WaitForInstance* RPCs (go-sdk does)

3 participants