[codex] Fix large fan-in replay joins - #18
Conversation
|
@microsoft-github-policy-service agree |
|
the real meat here is just the first commit which is the test that shows the bug - second commit is my attempt at fixing it, but it's entirely possible it's non-idiomatic or has problems I haven't seen. |
564828f to
7f69487
Compare
| return err; | ||
| } | ||
| if wake_state.is_woken() { | ||
| must_poll = true; |
There was a problem hiding this comment.
It seems that the purpose of must_poll is to avoid unnecessary extra polling, based on the assumption that only application of history events can wake up pending futures.
If that assumption is incorrect, then one naive fix would be to remove must_poll field entirely, and just always poll after applying every history event, and after applying all history. Redundant polling is always safe, as long as a completed future is not polled.
The risk is that CPU usage of repeatedly polling non-trivial futures could be wasteful.
There was a problem hiding this comment.
After digging into the implementation more, I observe that the root issue is OrchestrationContext::join is using futures::future::join_all which uses a non-deterministic scheduling strategy for >30 futures.
I provide an alternative fix here, which is to only use deterministic scheduling for OrchestrationContext combinators:
|
fantastic! thanks for the merge - i'll see how it works on my fork without the optimisation and see if performance is still a problem. (I think I only saw it because I'm using a turso provider ported from the sqlite provider, but it's a macro-driven monstrosity right now, need a bit more thought before I PR that.) |
Summary
Background
This was distilled from a spin loop bug in the wild. Parent bulk orchestrations had very large fan-in, all child
SubOrchestrationCompletedevents were already persisted, and no queue work remained, but replay still failed to produce a terminal parent event.Root Cause
There were two related issues.
First, replay polled orchestration futures with a no-op waker. For large inputs,
futures::future::join_alluses wake-driven readiness internally, so simply polling the parent future again is not enough: completed durable children need to wake the task. Without those wakes, a fully replayed large fan-in can returnContinueinstead of reachingCompleted.Second, replay completion delivery only had token-to-schedule bindings, so each completed schedule searched all bound tokens. With large fan-in, that made history replay O(n^2) just to deliver completions.
Validation