Skip to content

fix: DBObjectDirectoryAdapter lost objects written just before a read - #149

Merged
gmpassos merged 1 commit into
masterfrom
fix/object-directory-pagination-order
Aug 11, 2026
Merged

fix: DBObjectDirectoryAdapter lost objects written just before a read#149
gmpassos merged 1 commit into
masterfrom
fix/object-directory-pagination-order

Conversation

@gmpassos

Copy link
Copy Markdown
Contributor

Fixes the Pagination [objectAdapter] flake that has been failing test_vm on master.

Root cause

DBObjectDirectoryAdapter._saveObject was async and did File.writeAsString, but both callers — doInsert and doUpdate — dropped the returned Future:

_saveObject(table, id, entry);      // Future<void>, never awaited

return _finishOperation(op, id, preFinish);

Every reader in that class 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 hasn't landed yet reads back as null and is simply discarded — so selectAll returns fewer objects, with no error.

analysis_options.yaml sets discarded_futures: false, which is why the dropped Future was 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:

Run Actual Missing
master @ 4caf237 [PG-01, PG-03, PG-05] PG-02, PG-04
PR #148 [PG-01, PG-02, PG-03, PG-05] PG-04

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:

Expected: ['PG-01', 'PG-02', 'PG-03', 'PG-04', 'PG-05']
  Actual: []

The fix

Make the write synchronous, consistent with every other filesystem operation in the class (listSync, existsSync, createSync, deleteSync):

void _saveObject(String table, Object? id, Map<String, dynamic> obj) {
  var file = _resolveObjectFile(table, id);
  var enc = dart_convert.json.encode(obj);
  file.writeAsStringSync(enc);
}

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.

Gate Result
dart test --exclude-tags docker 688 passed
dart test test/bones_api_entity_db_directory_test.dart ×3 53 passed each
dart analyze --fatal-infos --fatal-warnings . clean
dart format -o none --set-exit-if-changed . clean

Docker-tagged suites not run locally (no daemon); they don't touch this adapter.

Note, not fixed here

_listTableFiles filters hidden files with !path.startsWith('.'), but path is 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

`_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

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.76%. Comparing base (4c108a0) to head (efdaa5e).
⚠️ Report is 1 commits behind head on master.

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     
Flag Coverage Δ
unittests 67.76% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gmpassos
gmpassos merged commit e92bc3e into master Aug 11, 2026
4 of 5 checks passed
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>
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.

1 participant