Wait on the held save itself for optimistic persistence - #6140
Merged
Merged
Conversation
…tence
The optimistic-persistence test polled `waitUntil(() => saves > 0)` at
@ember/test-helpers' 1000ms default for a background save that is a real
HTTP write to the test realm plus the store's post-save update. On a
loaded CI runner that round-trip regularly exceeds a second, so the poll
expired while the save was still legitimately in flight.
The window the test checks was luck as well. `withSlowSave` restores the
original `persistAndUpdate` at the callback's first `await` — in an async
function `try { return cb() } finally {}` runs the finally before the
returned promise settles — and `store.patch` reaches the persist well
after that, so the artificial delay never covered it.
`withHeldSave` holds the saves its callback triggers until the callback
returns, then releases them and waits for them to land. The optimistic
window is guaranteed rather than raced, and the persisted result is ready
when the helper resolves, so no wall clock is left in the test. The
store's save state rides along in the assertion message, naming the cause
when a save fails rather than merely running long.
`withSlowSave` keeps its current behavior: its remaining callers observe
the document a save serializes at the moment the save is initiated, and
holding entry into `persistAndUpdate` changes what they see.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Contributor
lukemelia
approved these changes
Sep 15, 2026
A patch that stops handing its save to the background is caught by the held-save test only as a stall: that test holds the save such a patch awaits, so the two wait on each other and the suite reports a 60s timeout naming nothing about persistence. The handoff now has its own assertion, against a save that runs normally. Dropping `doNotWaitForPersist` fails it in seconds on the option itself. Bounding the held-save test instead would put back the wall clock that made it flaky, so the guarantee is asserted where no hold is in play. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spy stands in for `store.patch`, so its options parameter has to accept every option that overload set declares, not only the one the test reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lukemelia
approved these changes
Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration | Command | patch-fields > Optimistic persistence behavior: patches do not await persistencefails intermittently in host CI withError: waitUntil timed out.What the test was waiting on
The wait was
waitUntil(() => saves > 0)at@ember/test-helpers' 1000ms default.store.patchis called withdoNotWaitForPersist, so the persist runs in the background: a request through the virtual network to the in-browser realm, the realm's write, its index update, and the store folding the result back in. None of that is bounded by anything the test controls, and on a loaded runner it routinely exceeds a second — so the poll expired while the save was still legitimately in flight. On the shard that failed, the runner sat at 90–100% CPU across 4 cores.The optimistic window the test checks was riding on luck too.
withSlowSaverestores the originalpersistAndUpdateat its callback's firstawait— in an async functiontry { return cb() } finally {}runs the finally before the returned promise settles — andstore.patchreaches the persist well after that. The artificial 100ms delay never covered the save, soassert.strictEqual(saves, 0, 'no persistence yet')was really asserting that the save is slower than a microtask.The change
withHeldSaveholds every save its callback triggers until the callback returns, then releases them and waits for them to land. The window where a mutation is applied locally and no save can have reached the realm is guaranteed rather than raced, and the persisted result is ready the moment the helper resolves — so the test has no wall clock in it at all. The store's save state rides along in the assertion message, so a save that fails rather than merely running long names its own cause.withSlowSaveis left as it is. Its remaining callers observe the document a save serializes at the moment that save is initiated; holding entry intopersistAndUpdatechanges what they see. Running them with the hold made real turnedIntegration | Store: an instance can debounce auto savesred — the leading-edge save then writes the final value instead of the intermediate one. That is a question about what those tests should assert, separate from this failure.Two tests, because one of them cannot report its own regression
withHeldSaveholds the save a non-optimistic patch would await, so if the patch ever stops handing the save to the background the two wait on each other and the suite reportsTest took longer than 60000ms; test timed out— true, but naming nothing about persistence. Bounding the hold with a timer would put back the wall clock that made this flaky in the first place, so the handoff is asserted separately, against a save that runs normally.Verified by building with
doNotWaitForPersistremoved from the tool and running each against it:patches do not await persistenceTest took longer than 60000ms; test timed outthe patch hands the save to the backgroundstore.patch receives doNotWaitForPersist optionBoth go red, so neither passes for free; the second one says why.
Verification
The timing pair on CI is the direct evidence that the deadline, not the save, was the discriminator:
waitUntilwaitUntil timed outNear-identical slowness, opposite outcomes. Raising the timeout would have bought room until the next slow shard; removing it makes the test indifferent to runner load.
patch-fieldstests present and the shard at 290 tests / 0 fail.patch-fieldsmodule passes 44/44 across two full runs.withHeldSave— okmain(withSlowSave+ barewaitUntil) — not ok,Error: waitUntil timed out, the same signature as the reported job.withSlowSavecall sites (store-test ×2, operator-mode, code-submode editor, patch-instance) pass unchanged.eslintandember-tsc --noEmitclean forpackages/host.🤖 Generated with Claude Code