Skip to content

Collection-per-file mode: creating a collection can route another collection's page to the main file (lock-free read of a Dictionary) #312

Closed

Description

@CaffeinatedCoder

In collection-per-file mode (PageFileConfig.Server), creating a collection while other collections read or write their pages can send one of those pages to the main file. The operation then fails with:

InvalidDataException: Page 3254779953 is outside the page file (file holds 65 pages): the file is corrupt.

3254779953 is 0xC2000031: the collection-page marker, slot 2, local page 49. Nothing on disk is corrupt. Slot 2's file exists, but the slot lookup missed.

Mechanism (0450b97)

  • _collectionSlotToName is a plain Dictionary<int, string> (src/BLite.Core/Storage/StorageEngine.cs:79).
  • It is written under _collectionSlotLock when a collection gets its slot (GetOrAssignCollectionSlot, StorageEngine.Memory.cs:663-683).
  • It is read without that lock by GetCollectionNameBySlot (StorageEngine.Memory.cs:333-335). GetPageFile calls that for every read, write and free of a collection page.
  • Dictionary does not support a read that runs concurrently with a write. When the 4th and the 8th collection get their slots, the dictionary resizes (capacity 3 → 7 → 17). Resize assigns a new, still empty _buckets array before it re-links the entries, so a concurrent TryGetValue for an existing slot can return false.
  • On a miss, GetPageFile falls through to the main file and passes the whole encoded id as the physical page number (StorageEngine.Memory.cs:327-331).

A related ordering detail: GetOrAssignCollectionSlot publishes name → slot before slot → name (lines 679-680). A thread that finds the slot through _collectionNameToSlot can route a page before its name is in _collectionSlotToName.

Reads fail with the exception above. That is the only outcome observed so far. Writes use the same routing: WritePage on the main file would take its grow path (PageFile.EnsureCapacityCore) and try to extend the main file to the page's offset. With 16 KB pages that is tens of TB. The collection file would never get that page. I have not observed this.

Reproduction

The error appeared once while running ConcurrentConsistencyTests 60 times under load (DOTNET_PROCESSOR_COUNT=2, two full-suite copies running alongside). It happened in the workload of HighContention_ManySmallTransactions_ConsistentFinalState: 8 threads each create their own collection at the same moment and insert into it. It hit writer 2 in its second transaction, while the other collections were still being created. The race window is the few instructions of each resize, which happen only at the 4th and the 8th collection of an engine. So it is rare, but it hits exactly the workload that test runs on a loaded CI runner.

Fix considerations

  • Make _collectionSlotToName safe for lock-free readers: a ConcurrentDictionary<int, string>, or a string?[MaxCollections] array (64 entries) written with Volatile.Write. The array fits because slots are dense and bounded.
  • Publish slot → name before name → slot.
  • LoadCollectionSlots writes the same maps (at open, before any reader). It should use the same types.

Found while investigating #311 (the other failure of that test). Related: #288.

Activity

  1. added
    bugSomething isn't working
    severity/mediumSpace leak, inconsistency or failure in a narrow scenario
    on Oct 17, 2026
  2. moved this from Todo to In Progress in BLiteon Oct 17, 2026
  3. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    CollaboratorAuthor

    Reproduced reliably: the test in #314 races slot lookups and page reads against all 63 slot assignments of an engine. On main it fails in 10 of 10 runs within about a second (also with DOTNET_PROCESSOR_COUNT=2), and a variant that reads through ReadPage hit the exact error from this issue: Page 3221225536 is outside the page file (file holds 65 pages). Fix in #314.

  4. mrdevrobot commented on Oct 18, 2026

    @mrdevrobot
    Contributor

    Reproduced on 0450b97 (3 of 3) with the test from #314. Collection-per-file mode only: medium.

  5. moved this from In Progress to Done in BLiteon Oct 18, 2026
  6. added this to the 5.3.0 milestone on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpackage/coreAffects BLite.Coreseverity/mediumSpace leak, inconsistency or failure in a narrow scenario

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions