Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1317 +/- ##
==========================================
+ Coverage 87.35% 87.62% +0.26%
==========================================
Files 163 163
Lines 13069 13046 -23
==========================================
+ Hits 11416 11431 +15
+ Misses 1653 1615 -38 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The worker replaced its gRPC channel every time the work-item stream ended. That forced the retire/publish handshake, per-channel call counts and the deferred-close threads, because results and other calls could still be using the old channel when it was closed. A grpc.Channel reconnects on its own, so only the GetWorkItems call needs to be re-issued. start() now opens one channel and stop() closes it. The run loop only sends Hello and opens a new stream on it. Result sends and the history fetch use wait_for_ready=True within their 30s deadline, so a result that is ready during a reconnect waits for the channel instead of a separate reconnect wait. The connect Hello waits for the channel too, so a returning sidecar is seen as soon as gRPC reconnects rather than after the run loop's retry delay on top of gRPC's own backoff, which is capped at 15s (default 120s). stop() still drains calls in flight for up to 5s and closes the channel only once no call is in flight, since grpcio crashes on a call started on a closed channel with interceptors. Closing the channel is also what ends the work-item stream: through an interceptor it is a generator with no cancel(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
3669201 to
d58bd60
Compare
CasperGN
left a comment
There was a problem hiding this comment.
Thanks @famarting for this contribution and for following up on the #1312 review so quickly! 🙌 Keeping one channel for the worker's lifetime makes the code much simpler. The PR description explains clearly why the old design existed, and checking against a real gRPC server with the interceptor in place was a nice touch.
I left one comment inline about how long stop() can take while the sidecar is down, please take a look. 👀
A few more notes:
- CI is red on this head:
build (windows-latest, 3.13),validate (3.14),validate-completeand both codecov checks fail. Could you check whether any of them come from this change?
Drafted with AI assistance and reviewed by me.
The connect Hello holds the channel while it waits for the channel to be ready, and stop() closes the channel only once no call is in flight. During a sidecar outage that Hello sat on its 30s deadline, so stop() did too. Wait for the channel in 1s Hellos that check for shutdown between them, still within the 30s budget, and end with one Hello that fails fast so the log says why the channel is not ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
Each start() created an event loop and never closed it, leaving the GC to close it later. On Windows the proactor's close calls time.monotonic(), so a loop collected during a test that mocks time.monotonic used up the mock's values: test_mcp_client's budget tests failed with StopIteration on the Windows jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
…paths Only a timed-out connect Hello is retried: a refusal from the sidecar must fail at once rather than be retried for the whole connect timeout. Also covers the Hello on a closed channel and the warning stop() logs when the run loop outlives its timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
The reconnect tests only ever ended the work-item stream cleanly, so the paths that disconnect after a broken stream or a failed stream open never ran. Cover both: the worker drops its history cache and reopens the stream on the one channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
Description
This follows @JoshVanL's review suggestion on #1312: keep one gRPC channel for the worker's lifetime instead of replacing it every time the work-item stream ends.
Why the worker replaced its channel. Recreating the channel on every reconnect came from microsoft/durabletask-python#50 (2025), which was vendored here in #963. Neither PR says why. Upstream has since moved toward this design: microsoft/durabletask-python#135 keeps the channel, re-opens only the stream, and recreates the channel only after 5 transport failures in a row. A
grpc.Channelreconnects by itself, re-resolving DNS when its connection drops, so onlyGetWorkItemshas to be re-sent.What changes
start()opens one channel andstop()closes it. On reconnect the run loop only sendsHelloand a newGetWorkItemson that channel._RESULT_RECONNECT_WAITand the stream-reconnect wait on result sends.wait_for_ready=Truewith their existing 30s deadline. A result that is ready during a reconnect waits for the channel, instead of the worker tracking connections.Helloalso useswait_for_ready=True. Without it, aHellosent while gRPC is between reconnect attempts fails at once. The run loop's retry delay then adds to gRPC's own backoff, and in testing the worker took 8.7s to reconnect after the sidecar came back, against 0.7s on fix(workflow): keep the workflow worker working across sidecar reconnects #1312.grpc.max_reconnect_backoff_ms), matching the run loop's retry cap. The default is 120s, which could leave a worker idle for 2 minutes after a long outage. A caller'schannel_optionsstill override it.What stays
_sidecar_stubwith one in-flight counter) sostop()never closes the channel while a call is starting on it. With interceptors, grpcio crashes with SIGSEGV on a call started on a closed channel (see fix(workflow): keep the workflow worker working across sidecar reconnects #1312), and one channel does not remove that risk at shutdown.stop()keeps fix(workflow): keep the workflow worker working across sidecar reconnects #1312's order: wait up to 5s for calls in flight, close the channel, then join the run loop. The channel has to close first because closing it is the only way to end the work-item stream. Through an interceptor,GetWorkItemsreturns a plain generator with nocancel().Found along the way, not fixed here: for the same reason,
_make_stream_teardown(used when a history fetch fails) does nothing when the channel has interceptors, which is always the case underWorkflowRuntime. Itscancel()raisesAttributeError, which is logged at debug level. This predates both PRs, and I can open an issue for it.Checked against a real gRPC server (in-process, with the SDK's metadata interceptor): start the worker, kill the sidecar while an activity runs, let the activity finish while the sidecar is down, then restart the sidecar on the same port after 6s.
stop()On #1312 the 0.7s depends on when the run loop's retry happens to land. On both branches the worst case is about 15s.
Issue reference
No issue filed. This is a follow-up to review feedback on #1312.
Checklist
dapr/ext/workflow/AGENTS.md)🤖 Generated with Claude Code