Repository navigation
fix(storage): let a damaged record cost only that record - #782
Merged
Ishaan Gangwani (ishaan1124) merged 2 commits intoSep 29, 2026
Conversation
ANIRUDDHA ADAK (aniruddhaadak80)
requested review from
Aayam Bansal (aayambansal) and
Ishaan Gangwani (ishaan1124)
as code owners
September 28, 2026 08:36
|
ANIRUDDHA ADAK (@aniruddhaadak80) is attempting to deploy a commit to the InkVell Team on Vercel. A member of the Team first needs to authorize it. |
ANIRUDDHA ADAK (aniruddhaadak80)
force-pushed
the
fix/storage-migration-damaged-record
branch
from
September 29, 2026 13:32
bf9a56d to
54f3aa5
Compare
The clash was CHANGELOG.md alone: main added several bullets at the anchor this branch also inserted at. The changelog is rebuilt from main's copy with this branch's entry kept, and the code files are carried over unchanged. Rebased as a single parent on main so a later rebase carries it.
ANIRUDDHA ADAK (aniruddhaadak80)
force-pushed
the
fix/storage-migration-damaged-record
branch
from
September 29, 2026 14:04
54f3aa5 to
3fa18fd
Compare
# Conflicts: # CHANGELOG.md
Ishaan Gangwani (ishaan1124)
merged commit Sep 29, 2026
cd73014
into
synthetic-sciences:main
8 of 9 checks passed
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.
What does this PR do, and why?
The storage migration chain advances a marker only past a migration that
completes:
and both migrations read their input with an unguarded
await Bun.file(item).json(),which throws on a zero-length or malformed record. So one damaged file fails the
migration, the loop breaks, and the marker never advances — and since
MIGRATIONSis append-only, every migration added in every future release thensilently never runs for that user, for good.
Nothing surfaces it.
state()still resolves and returns{ dir }, so reads,writes and
listkeep working and the CLI looks completely healthy. The onlyevidence is one
log.errorline in a rotating log.A zero-length record is exactly what an interrupted write leaves behind, and
Storage.publishis the one record writer in the tree that never fsyncs (everysibling does), so this is reachable rather than theoretical.
The fix is that a damaged record costs that record:
Applied to all five unguarded parses in the two migrations, so the chain always
makes progress.
The same lesson is already applied elsewhere in the tree and was worth following:
data-dir.tscarries the note "A corrupt legacy store must cost that store, notthe import. Left unguarded this threw past the marker write, so every later boot
re-ran the whole import and failed at the same byte, forever."
Linked issue
Self-identified. My issue budget this turn went to the two
apply_patchdefectsand the two
localStoragecopies; happy to file one for this if you would rathertrack it separately.
How did you verify it?
test/storage/migration-damaged-record.test.tsruns the migration against athrowaway directory containing a zero-length record and a healthy one, and
asserts both that it resolves and that the healthy record is still migrated. It
calls the migration directly rather than through
state(), because the marker isprocess-global and a marker-based test would depend on file order in the run.
That is also why
Storage.MIGRATIONSis now exported — the alternative was atest that only passes when it happens to run first.
I could not run the new test against unmodified
main, because the export doesnot exist there. Instead I proved the two mechanisms it depends on, each in
isolation:
The first is
Bun.file(<empty>).json()— main's exact expression. The secondruns main's loop shape with a throwing migration at index 1: the marker is still
1afterwards, so index 2 never runs. Together with reading the loop, that is thewhole causal chain, and it is why the test's
resolves.toBeUndefined()is theassertion that matters.
Commands run:
bun test --timeout 60000 ./test/storage/migration-damaged-record.test.ts→ 1 passbun test --timeout 120000 ./test/storage→ 17 pass, 3 failcompeting reclaimers cannot remove a newly acquired storage lock,a stale storage observer revalidates before replacing a new live owner,storage mutations and authority signals cross real process boundaries) arepre-existing: with my change stashed the same directory reports them, plus
my own test failing to import. They are interprocess file-lock tests, which is
the area most sensitive to Windows semantics.
bun run --cwd backend/cli typecheck→ exit 0One thing I deliberately did not do
There is a related durability gap:
Storage.publishrenames a record into placewithout ever fsyncing the file or its directory, while
JsonStore.replace,Config,DataRelocationandCredentialLifecycleall do both. Fixing itproperly needs a crash-injection test, and the only assertion I could construct
was one that reads the source and looks for
sync()— whichAGENTS.mdrulesout. I would rather raise it than smuggle in a test the conventions forbid.
Checklist
bun run checkis green (format, typecheck, backend + frontend/ui + SDK tests) — blocked on this Windows checkout by the CRLF and symlink artifacts in my other pull requests; backend typecheck is clean and the storage suite adds no new failurebun run --cwd frontend/workspace buildsucceeds if I touchedfrontend/workspaceorfrontend/ui— not touched./tooling/repo/generate.tswas run and thetooling/sdkoutput committed if I changedbackend/cli/src/server— not touchedfrontend/docs/src/content/openscience/is updated if behavior changed — no doc change needed: this is invisible either way except that migrations now completepackage.jsonversions and tags are written by the release workflow)installandfrontend/landing/public/installare still byte-identical if I touched either — not touched