fix: DBObjectDirectoryAdapter lost objects written just before a read - #149
Merged
Conversation
`_saveObject` was `async` and performed `File.writeAsString`, but both callers — `doInsert` and `doUpdate` — dropped the returned `Future`. Every reader in the adapter inspects the filesystem *synchronously* (`listSync`, `existsSync`), so there was no happens-before relationship between a `store` completing and its object being on disk. The failure is silent rather than loud: `_doSelectAllImpl` lists the directory, reads each entry, and passes the result through `resolveAllNotNull()`. A file that has not landed yet reads back as `null` and is simply discarded, so `selectAll` returns fewer objects with no error. This is what made `Pagination [objectAdapter]` flaky on CI. The evidence fits: the entries that went missing were always the most recently stored ones (the test stores in the order PG-03, PG-01, PG-05, PG-02, PG-04, and the failures dropped PG-04, or PG-04 and PG-02), and which ones varied per run. It does not reproduce on a fast local disk, so it only ever showed up on CI. Confirmed by experiment: inserting a 30ms delay before the un-awaited write reproduces that exact assertion failure locally. Fixed by making the write synchronous, consistent with every other filesystem operation in this class. Note `analysis_options.yaml` sets `discarded_futures: false`, which is why the dropped `Future` was never flagged. Adds a test asserting that a stored object is immediately readable. It only fails where the write is slow enough to lose the race, so it is a statement of the invariant rather than a sensitive guard — `Pagination [objectAdapter]` remains the test that actually catches this, and it should now be stable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #149 +/- ##
==========================================
- Coverage 67.77% 67.76% -0.01%
==========================================
Files 64 64
Lines 21607 21607
==========================================
- Hits 14644 14642 -2
- Misses 6963 6965 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
gmpassos
added a commit
that referenced
this pull request
Aug 11, 2026
The fix from #149 ships in 1.14.0 but was missing from its CHANGELOG section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Fixes the
Pagination [objectAdapter]flake that has been failingtest_vmonmaster.Root cause
DBObjectDirectoryAdapter._saveObjectwasasyncand didFile.writeAsString, but both callers —doInsertanddoUpdate— dropped the returnedFuture:Every reader in that class inspects the filesystem synchronously (
listSync,existsSync), so there was no happens-before relationship between astorecompleting and its object being on disk.The failure is silent rather than loud.
_doSelectAllImpllists the directory, reads each entry, and passes the result throughresolveAllNotNull(). A file that hasn't landed yet reads back asnulland is simply discarded — soselectAllreturns fewer objects, with no error.analysis_options.yamlsetsdiscarded_futures: false, which is why the droppedFuturewas never flagged.Why this is the flake
The test stores in the order
PG-03, PG-01, PG-05, PG-02, PG-04, then asserts all five come back. Observed CI failures:master@4caf237[PG-01, PG-03, PG-05][PG-01, PG-02, PG-03, PG-05]The entries that vanish are always the most recently stored ones, and which ones vary per run — exactly what an unfinished write produces. It never reproduces on a fast local disk, which is why it only ever showed up on CI.
Confirmed by experiment: inserting a 30 ms delay before the un-awaited write reproduces that exact assertion failure locally, on every group:
The fix
Make the write synchronous, consistent with every other filesystem operation in the class (
listSync,existsSync,createSync,deleteSync):This is a correctness fix, not just a test fix: any caller that stored an object and then read it back could miss it.
Testing
Adds
DBObjectDirectoryAdapter > a stored object is immediately visible, asserting the store→read invariant.Being straight about its limits: it only fails where the write is slow enough to lose the race, so it does not fail on a fast local disk. It is a cheap statement of the invariant;
Pagination [objectAdapter]remains the test that actually catches this, and it should now be stable.dart test --exclude-tags dockerdart test test/bones_api_entity_db_directory_test.dart×3dart analyze --fatal-infos --fatal-warnings .dart format -o none --set-exit-if-changed .Docker-tagged suites not run locally (no daemon); they don't touch this adapter.
Note, not fixed here
_listTableFilesfilters hidden files with!path.startsWith('.'), butpathis the absolute path, so that check is always false — it was presumably meant to test the basename. Harmless today (a hidden file would still need to end in.json), and unrelated to this bug, so I left it alone rather than mix an unrelated behavior change into a flake fix.🤖 Generated with Claude Code