An engine and a session hold separate DynamicCollection instances: an index created through one misses the other's writes, and a session commit can erase the index definition
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
Research direction
Start with src/BLite.Core/BLiteEngine.cs, src/BLite.Core/BLiteSession.cs, and src/BLite.Core/DynamicCollection.cs, focusing on collection construction, secondary-index state, metadata persistence, and write locks. Use the issue's repro to compare engine and session collection views, then verify that index queries match full scans and index definitions survive reopening. Locate the collection and session tests and add coverage for cross-view writes, commits, index creation, and reopen behavior.
Written by the indexing model from the issue text.
Description
BLiteEngine.GetOrCreateCollection("people") and BLiteSession.GetOrCreateCollection("people") return two different DynamicCollection objects for the same collection. Each has its own list of secondary indexes and its own write lock. When the session's instance exists before an index is created through the engine:
- writes through the session are not added to the index, so an index query misses them, permanently;
- the session's commit rewrites the collection's index metadata from its own (empty) list, so unless the engine commits again afterwards, the index definition is gone after a reopen.
Found while reviewing the collection layer for #296.
Repro
using var engine = new BLiteEngine("d2.db");
var col = engine.GetOrCreateCollection("people");
await col.InsertAsync(col.CreateDocument(["age"], b => b.AddInt32("age", 1)));
using var session = engine.OpenSession();
var scol = session.GetOrCreateCollection("people"); // second instance
await col.CreateIndexAsync("age", name: "idx_age"); // index created on the engine's instance
await col.InsertAsync(col.CreateDocument(["age"], b => b.AddInt32("age", 5)));
await scol.InsertAsync(scol.CreateDocument(["age"], b => b.AddInt32("age", 5)));
await session.CommitAsync();
// col.QueryIndexAsync("idx_age", 5, 5) → 1 document; a full scan finds 2
Then dispose the engine and reopen:
| Run | After reopen |
|---|---|
no session, no engine.CommitAsync() |
index present, correct |
no session, engine.CommitAsync() |
index present, correct |
session, no engine.CommitAsync() |
ArgumentException: Index 'idx_age' not found on collection 'people' |
session, engine.CommitAsync() |
index present, but it still misses the session's document |
Expected: one collection behaves the same whichever object it is reached through: one index list, one lock, one metadata writer.
Actual: two instances with diverging index lists; the last one to persist metadata wins.
Mechanism (0450b97)
- Separate caches: the engine keeps
ConcurrentDictionary<string, DynamicCollection> _collections(src/BLite.Core/BLiteEngine.cs:31, 294); each session keeps its own and builds its own instance (src/BLite.Core/BLiteSession.cs:140-151).GdprDocumentDbContextExtensions.cs:50builds a third, short-lived one. - Each instance has its own
_secondaryIndexes(src/BLite.Core/DynamicCollection.cs:97), loaded from metadata when the instance is constructed; an index created later on another instance is never added to it, so its writes skip that index. BLiteSession.CommitAsynccallsPersistIndexMetadata()on every collection the session has touched (BLiteSession.cs:95-96).PersistIndexMetadataclears the stored index list and rewrites it from that instance's_secondaryIndexes(DynamicCollection.cs:2177-2181:metadata.Indexes.Clear();), which drops the engine's new index.BLiteEngine.CommitAsyncdoes the same from the engine's instances (BLiteEngine.cs:423-429), which is why committing the engine afterwards restores the definition.- Each instance also has its own
SemaphoreSlim(DynamicCollection.cs:49), so writes through the two instances are not serialised against each other, while they share the free-space index (_freeSpaceIndexes.GetIndex()).
Affected versions: since BLiteSession (v3.8.0) through v5.2.3.
Fix considerations
- One instance per collection name per engine: the session returns a view that carries the session's transaction holder but shares the engine's collection state (indexes, lock, metadata). The same for the GDPR helper.
PersistIndexMetadatashould never derive the stored list from one instance's memory; index creation and drop should update the catalog in the transaction that creates or drops the index.- Test: a cross-view oracle — the same operation sequence through engine and session instances in any order, checked against one model (index queries equal full scans, definitions survive reopen).
Related: #296 (one collection registry), #211 (pages of old dynamic indexes), #301 (another typed vs schema-less divergence).
- 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 3 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 3 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 65/100
KnifeLemon/Filee#363 ·
Maintainers usually reply within 1 day
-
[CI] .trgen-ignore required for several grammars.Possibly taken @kaby76 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
antlr/grammars-v4#5073 ·
Maintainers usually reply within 1 day
-
/api/oauth/introspect emits clientId and tokenType instead of RFC 7662's client_id and token_typeOpenbug P2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
nightscout/nocturne#2317 ·
Maintainers usually reply within 1 day
-
[BUG]: Position intent set on a simple order is dropped when it becomes a bracket, OCO or OTO orderPossibly taken @RaphaelFakhri claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
alpacahq/alpaca-trade-api-csharp#903 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
joinrpg/joinrpg-net#5752 ·
Maintainers usually reply within 1 day