BLite.Wasm: a committed write is lost on a page reload unless a checkpoint ran, and CheckpointAsync does not reliably prevent it
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- csharp, wasm
- Domain
- databases, testing-qa
Research direction
Start with the browser harness tests in tests/BLite.Wasm.BrowserTests, especially the Committed_write_survives_reload rows and the large-database checkpoint case. Trace recovery ordering through src/BLite.Wasm/BLiteWasm.cs, browser page flushing in src/BLite.Wasm/Storage/IndexedDbPageStorage.cs and OpfsPageStorage.cs, and checkpoint coordination in src/BLite.Core/Storage/StorageEngine.Recovery.cs. Done means acknowledged IndexedDB and OPFS writes survive reloads, including while a background checkpoint is running, and the affected known-candidate tests pass.
Written by the indexing model from the issue text.
Description
A document inserted through the auto-commit API on IndexedDB or OPFS, with the engine then left open or disposed, is gone after a page reload. The WAL still holds the transaction's records; the reopen replays them, truncates the WAL, and the collection holds 0 documents. A second reopen finds 0 too, so the loss is permanent. Only CheckpointAsync before the reload keeps the document, and past 16 MB of WAL even that fails in half the runs, because CheckpointAsync returns at once while a background checkpoint is running.
WASM_SUPPORT.md calls the browser WALs "WAL for crash recovery in browser contexts" and the sample's counter "persistent": "Reload the page to verify that the count survives browser restarts." BLiteWasm.CreateAsync says the same of its recovery call (src/BLite.Wasm/BLiteWasm.cs:121-123: "recover committed-but-not-checkpointed transactions so data survives page reloads"). README "Transactions & ACID": "Durable: WAL ensures data safety even in power loss."
Found by the browser harness (tests/BLite.Wasm.BrowserTests, https://github.com/EntglDb/BLite/pull/295), which carries the affected rows as known candidate BW1 until this is fixed. Same result in all four publish variants (trimmed, TrimMode=full, untrimmed, AOT), so it is not a trimming problem.
Repro
Rows Committed_write_survives_reload IndexedDb/commit-only, IndexedDb/dispose, Opfs/commit-only, Opfs/dispose and Typed_write_survives_reload */checkpoint=False/indexed=False (6 of 6 fail every run); Large_database_open_time_and_memory */5MB (fails in 6 of 12 runs).
// tab, first load
var engine = await BLiteWasm.CreateAsync("durable", WasmStorageBackend.IndexedDb);
await engine.InsertAsync("docs", engine.CreateDocument(["name", "v"], b => b.AddString("name", "m1").AddInt32("v", 1)));
// leave it open (or engine.Dispose()); reload the page
// tab, second load
var engine = await BLiteWasm.CreateAsync("durable", WasmStorageBackend.IndexedDb);
await foreach (var doc in engine.FindAllAsync("docs")) ... // nothing
dotnet test tests/BLite.Wasm.BrowserTests --filter "FullyQualifiedName~Committed_write_survives_reload"
The IndexedDB object-store counts the test prints: meta=1 pages=3 wal=4 before the reload, pages=5 wal=0 after the reopen, 0 documents. With CheckpointAsync before the reload the marker survives.
For the 5 MB rows: 5,242 documents of 1,000 bytes through InsertBulkAsync, CheckpointAsync, reload, reopen. In 6 of 12 runs CheckpointAsync returned in 0 ms with 1,089–1,119 records still in the WAL, and the reopened database held 0 documents; in the 6 runs where it took 95–135 ms all documents were there.
Expected: what InsertAsync acknowledged is there after the reload, as on desktop, where the same sequence survives a process exit.
Actual: the WAL is replayed and truncated, and the data is gone.
Mechanism (verified on 0450b97; the first part is an inference consistent with the page and WAL counts, no test isolates it)
- Both browser page stores keep every page in a managed cache. Their synchronous
Flush()is a no-op (src/BLite.Wasm/Storage/IndexedDbPageStorage.cs:216,OpfsPageStorage.cs:217); pages reach IndexedDB or OPFS only inFlushAsync, which only a checkpoint calls. - The engine writes state outside the WAL and relies on it being durable at once: page allocation and the next page id, the header, the key dictionary, through
WritePageImmediate(src/BLite.Core/Storage/StorageEngine.Pages.cs:135). On desktopPageFile.WritePageis durable when it returns; in the browser these writes stay in the cache until the next checkpoint. After a reload the replayed page images refer to pages and dictionary keys that were never persisted (keyMapHasName=Falsein the test's output after the reopen), andRecoverAsyncthen truncates the WAL that held the data. StorageEngine(IPageStorage, IWriteAheadLog)documents that "the caller is responsible for replaying WAL before constructing the engine" (src/BLite.Core/Storage/StorageEngine.cs:420-422) and reads the dictionary and KV roots in its constructor;BLiteWasm.CreateAsyncconstructs first and callsRecoverAsyncafter (src/BLite.Wasm/BLiteWasm.cs:118-125).Disposedrains the group-commit writer and checkpoints only when pages are queued to be freed (StorageEngine.cs:462-498); it does not flush committed pages, so a disposed engine loses them the same way.- Past
MaxWalSize(16 MB,StorageEngine.cs:124) a commit starts a background checkpoint (_ = Task.Run(() => CheckpointAsync()),StorageEngine.GroupCommit.cs:292). While it runs, an explicitCheckpointAsyncreturns at once (StorageEngine.Recovery.cs:59,_checkpointRunning), althoughBLiteEngine.CheckpointAsyncpromises to "guarantee that the page file is fully up-to-date on disk" (src/BLite.Core/BLiteEngine.cs:440-442). That is the 0 ms checkpoint of the 5 MB rows; the memory side of it is #292.
This is the browser face of the class behind #260, #261 and #262: writes the engine makes outside the WAL. On desktop they are durable at once, so a crash leaks pages (#261) or splits a two-write update (#262); in the browser they are not durable at all until a checkpoint, so a reload loses the committed data they describe.
Affected versions: BLite.Wasm since it was added in #63 (830e83e, v4.4.0) through v5.2.3. The background checkpoint since group commit (d72b73a, v3.6.2).
Fix considerations
- Make the browser WAL the only thing a reload needs: either replay the WAL before the engine constructor reads anything (the order the constructor documents), and make every write that bypasses the WAL (
WritePageImmediate, allocation, header) go through the WAL or throughFlushAsyncbefore the commit is acknowledged; or checkpoint on every commit in the browser (the WAL is in IndexedDB already, so a commit costs one IndexedDB transaction either way). CheckpointAsyncshould wait for a running checkpoint and then run its own (or wait and return once the running one covers its commits), so that its documented guarantee holds; that applies on desktop too.Disposeon a browser backend should flush committed pages, or the docs should say that a tab must callCheckpointAsyncbefore it closes.- Test: the harness's reload rows, which stay KNOWN until the entry is removed.
Related: #260, #261, #262 (the same class on desktop, found by the WAL crash test), #209 / #242 (checkpoint on dispose for queued frees), #57 / #63 (BLite.Wasm). Found with #290, #291, #292 and #293 by the same harness.
- Dominant language
- C#
- Stars
- 66
- Forks
- 6
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 58
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from EntglDb/BLite
-
GdprMode.Strict warns that deleted documents are not zeroed, but every delete zero-fills themPossibly taken @CaffeinatedCoder claimed this 4 days ago. Openbug gdpr package/core severity/low
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
EntglDb/BLite#299 · 1 comment ·
Maintainers usually reply within 2 days
-
bug package/core severity/medium triage
Difficulty 4/5 3-5 days Newbie friendliness 54/100
Maintainers usually reply within 2 days
-
bug package/core severity/medium
Difficulty 4/5 3-5 days Newbie friendliness 48/100
EntglDb/BLite#322 · 1 comment ·
Maintainers usually reply within 2 days
-
enhancement package/core
Difficulty 4/5 3-5 days Newbie friendliness 48/100
EntglDb/BLite#320 · 1 comment ·
Maintainers usually reply within 2 days
-
BLiteSession.Rollback() does not await the rollback: it returns while the transaction is still active, and failures go unobservedPossibly taken @CaffeinatedCoder claimed this 4 days ago. Openbug package/core severity/medium
Difficulty 3/5 1-2 days Newbie friendliness 78/100
EntglDb/BLite#307 · 1 comment ·
Maintainers usually reply within 2 days
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
AvaloniaUI/Avalonia#22568 ·
Maintainers usually reply within 2 days
-
[bug] 截图失败日志冲爆硬盘Open
Difficulty 2/5 1-3 hours Newbie friendliness 63/100
babalae/better-genshin-impact#3846 · 2 comments ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
lugassawan/swe-workbench#823 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
microsoft/fluentui-blazor#5518 ·
Maintainers usually reply within 1 day
-
devtools
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day