Repository navigation
fix(storage): serialise multi-process create/open and remap grown files (#216) - #221
Open
CaffeinatedCoder wants to merge 2 commits into
Open
CaffeinatedCoder wants to merge 2 commits into
CaffeinatedCoder wants to merge 2 commits into
Conversation
…glDb#216) Adds MultiProcessConcurrentCreateTests, which fail on main: - two and four engines open the same, not yet existing database at the same moment with AllowMultiProcessAccess (10 rounds each); one of them fails with UnauthorizedAccessException (page write past its stale memory map) or "Page size mismatch" (header read before it was written) - a zero-filled file left by a creator that died between sizing the file and writing its header must be initialised on open Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es (EntglDb#216) Two processes opening the same, not yet existing database with AllowMultiProcessAccess crashed one of them: the file creation, header write, dictionary init and collection creation ran outside any cross-process lock, and each process kept a memory map sized at open while the other one grew the file. - WalShmFcntl: lock slots. Slot 0 is the existing WAL writer lock, slot 1 a new open lock (Windows/Linux: next byte of the SHM file; macOS: its own <shm>.open.lock file), with the same primitives. - WalSharedMemory: AcquireOpenLock/ReleaseOpenLock; the SHM header is now validated / initialised under it. - StorageEngine: the SHM is opened first and the constructor holds the open lock from page-file creation through recovery, dictionary and KV init, so a second opener waits and then opens the initialised files. EnterOpenLock (reentrant per engine) is also taken around collection creation (DocumentCollection / DynamicCollection constructors, CreateCollection, CreateSessionCollection, DropOrphanCollections), around adding a dictionary key (which first reloads the dictionary from disk in multi-process mode), and around opening a collection file. - PageFile: page accesses are bounds-checked against the mapped length instead of the file length; a read or write past the map but inside the file remaps first; growing no longer resizes a file another process has already grown. Under the open lock, RefreshAllocationState moves allocation past blocks other processes added and re-reads the free-list head. A zero-filled file of the initial size (creator died before writing the header) is initialised instead of failing with a page-size mismatch. Single-process mode takes no lock; its only new per-access cost is a comparison against the mapped length. Tests: remap of pages another engine added, dictionary ids across engines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder
had a problem deploying
to
Production
October 13, 2026 17:56 — with
GitHub Actions
Failure
This branch had an error being deployed
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 #216
With
AllowMultiProcessAccess, two processes opening the same new database at once crashed one of them. File creation, the header write, dictionary init and collection creation all ran outside any cross-process lock. Each process also kept a memory map sized at open time while another process grew the file.Changes
WalShmFcntl: lock slots. Slot 0 is the WAL writer lock; slot 1 is a new open lock built from the same primitives. Windows and Linux lock the next byte of the SHM file; macOS usesflockon<shm>.open.lock.WalSharedMemory:AcquireOpenLock/ReleaseOpenLock. The SHM header is now validated and initialised under the open lock.StorageEngine: the SHM is opened first, and the constructor holds the open lock through page-file create/open, recovery and dictionary/KV init. A second opener waits, then opens the initialised files.EnterOpenLock()(reentrant per engine, a no-op in single-process mode) also covers collection creation (DocumentCollection/DynamicCollection constructors,CreateCollection,CreateSessionCollection,DropOrphanCollections), adding dictionary keys (the dictionary is reloaded from disk first) and opening collection files.PageFile: accesses are checked against the mapped length, not the file length.Single-process mode takes no lock. Open time is unchanged (4.1–4.8 ms before and after), and
ReadPagegot faster (≈510 → ≈240 ns) because it no longer callsfstatper read.Tests (
MultiProcessConcurrentCreateTests, separate engines on the same files, as inMultiProcessWalSharedMemoryTests):All of them fail on
main. The first three are in a separate commit (c7ceb06) that shows the failure. The full suite passes (2528 / 0 / 4 skipped), and the multi-process filter passed 5 runs with no hangs.BLite.NetStandard21.Testswasn't run locally (no .NET 6 runtime).Repro harness (posted on #216). Rounds with a crashed process, before → after, macOS 27 arm64 / .NET 10:
A Linux container (Ubuntu 24.04 arm64) gives the same results.
Not covered
MULTI_PROCESS_WAL.mdlists simultaneous writers as out of scope, but nothing enforces it, andmainshows the same loss when the database already exists. I suggest a follow-up that makes a second writer process fail fast.EnsureIndex,DropCollection, KV root creation andTruncateToMinimumAsync. On open,RecoverAsyncstill truncates a non-empty shared WAL without the writer lock (Multi-process: RecoverAsync on open truncates the shared WAL without the SHM lock and leaves the SHM end offset stale #200).Merge note: this conflicts with #201 in
StorageEngine.cs. I'll resolve it in whichever lands second.🤖 Generated with Claude Code