Version: BLite 5.1.1 (NuGet), .NET 10.0.12, macOS 27 arm64. Default PageFileConfig, single process.
A. Rollback leaves the collection unreadable
Insert N documents into an empty collection inside an explicit transaction, then roll back. For N ≤ 64 everything is fine (count 0 afterwards). For N ≥ 65, every later read of the collection spins at 100 % CPU and never returns, also after reopening the file in a new process. The file stays unreadable.
public sealed class Figure { public ObjectId Id { get; set; } public decimal Amount { get; set; } }
public partial class ReproDbContext : DocumentDbContext
{
public DocumentCollection<ObjectId, Figure> Figures { get; set; } = null!;
public ReproDbContext(string path) : base(path) => InitializeCollections();
protected override void OnModelCreating(ModelBuilder mb) => mb.Entity<Figure>();
}
using (var db = new ReproDbContext(path))
{
using (var txn = db.BeginTransaction())
{
for (var i = 0; i < 65; i++)
await db.Figures.InsertAsync(new Figure { Amount = i }, txn);
await txn.RollbackAsync();
}
var all = await db.Figures.AsQueryable().ToListAsync(); // never returns, 100 % CPU
}
// new process:
using var db2 = new ReproDbContext(path);
var all2 = await db2.Figures.AsQueryable().ToListAsync(); // never returns either
| N rolled back |
read after rollback |
read after reopen (new process) |
| 10, 50, 64 |
0 documents |
0 documents |
| 65, 79, 80, 100, 500 |
hangs (100 % CPU) |
hangs (100 % CPU) |
The threshold of 65 looks like the moment the transaction allocates a second data page for the collection. In practice this means one failed first import leaves a database that can never be read again.
B. Reads outside an open transaction hang
Same threshold, no rollback needed. One document is committed. A transaction then inserts N more without committing, and a read that does not use that transaction runs:
await db.Figures.InsertAsync(new Figure { Amount = -1 }); // auto-commit
var txn = db.BeginTransaction();
for (var i = 0; i < 64; i++)
await db.Figures.InsertAsync(new Figure { Amount = i }, txn);
var all = await db.Figures.AsQueryable().ToListAsync(); // hangs, 100 % CPU
| committed + uncommitted |
read outside the transaction |
| 1 + 10, 1 + 62, 1 + 63 |
returns 1 document (correct) |
| 1 + 64, 1 + 65, 1 + 100, 1 + 500 |
hangs (100 % CPU) |
Expected: the read returns the 1 committed document (read committed). AsQueryable() has no overload that takes a transaction, so there is no way around this for LINQ reads while a larger write transaction is open.
A and B are probably the same root cause: a page allocated by an uncommitted (or rolled-back) transaction becomes visible in the collection's page chain, and the scan follows a link it cannot resolve.
Side note on the README
The README's explicit-transaction example calls db.Users.InsertAsync(new User { … }) inside using (var txn = db.BeginTransaction()) without passing txn. In 5.1.1 such inserts auto-commit, so RollbackAsync() in that pattern silently rolls back nothing (the documents are still there after a reopen). The BeginTransaction XML doc says the transaction must be passed to every collection method; the README example should do the same.
Version: BLite 5.1.1 (NuGet), .NET 10.0.12, macOS 27 arm64. Default
PageFileConfig, single process.A. Rollback leaves the collection unreadable
Insert N documents into an empty collection inside an explicit transaction, then roll back. For N ≤ 64 everything is fine (count 0 afterwards). For N ≥ 65, every later read of the collection spins at 100 % CPU and never returns, also after reopening the file in a new process. The file stays unreadable.
The threshold of 65 looks like the moment the transaction allocates a second data page for the collection. In practice this means one failed first import leaves a database that can never be read again.
B. Reads outside an open transaction hang
Same threshold, no rollback needed. One document is committed. A transaction then inserts N more without committing, and a read that does not use that transaction runs:
Expected: the read returns the 1 committed document (read committed).
AsQueryable()has no overload that takes a transaction, so there is no way around this for LINQ reads while a larger write transaction is open.A and B are probably the same root cause: a page allocated by an uncommitted (or rolled-back) transaction becomes visible in the collection's page chain, and the scan follows a link it cannot resolve.
Side note on the README
The README's explicit-transaction example calls
db.Users.InsertAsync(new User { … })insideusing (var txn = db.BeginTransaction())without passingtxn. In 5.1.1 such inserts auto-commit, soRollbackAsync()in that pattern silently rolls back nothing (the documents are still there after a reopen). TheBeginTransactionXML doc says the transaction must be passed to every collection method; the README example should do the same.