Skip to content

Commit 02c85d0

Browse files
committed
Fix rollback cleanup for safepoint-flushed buffers
Port the LiteDB PR litedb-org#2657 change that avoids discarding snapshot buffers after they have already been transitioned out of writable state by a safepoint/WAL flush. Update TransactionService rollback/dispose cleanup to discard only buffers whose ShareCounter is still BUFFER_WRITABLE. This covers the async rollback, sync rollback, and dispose paths. Add focused regression tests for readable dirty collection-page buffers to verify rollback and disposal no longer fail after safepoint-style buffer transitions. Validated with targeted transaction tests, engine tests, and the full LiteDbX.Tests suite. 290 passing tests.
1 parent 7324329 commit 02c85d0

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

LiteDBX.Tests/Engine/Transactions_Tests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ public class Transactions_Tests
1313
{
1414
private static readonly TimeSpan CoordinationTimeout = TimeSpan.FromSeconds(5);
1515

16+
private static async Task<(LiteTransaction transaction, TransactionService service)> CreateTransactionWithReadableDirtyCollectionPage(
17+
LiteDatabase db,
18+
string collectionName,
19+
string indexName)
20+
{
21+
var collection = db.GetCollection<Person>(collectionName);
22+
await collection.Insert(new Person { Id = 1, Name = "seed" });
23+
24+
var transaction = (LiteTransaction)await db.BeginTransaction();
25+
var service = transaction.Service;
26+
var snapshot = await service.CreateSnapshotAsync(LockMode.Write, collectionName, addIfNotExists: false);
27+
28+
snapshot.CollectionPage.InsertCollectionIndex(indexName, "$.Name", unique: false);
29+
snapshot.CollectionPage.IsDirty.Should().BeTrue();
30+
31+
// Simulate a safepoint-flushed page buffer: the page is still tracked by the snapshot,
32+
// but the underlying buffer is no longer writable.
33+
snapshot.CollectionPage.Buffer.ShareCounter = 0;
34+
35+
return (transaction, service);
36+
}
37+
1638
private static Task RunIsolated(Func<Task> action)
1739
{
1840
using (ExecutionContext.SuppressFlow())
@@ -242,4 +264,55 @@ public async Task Test_Transaction_Rollback_On_Dispose()
242264

243265
(await col.Count()).Should().Be(0);
244266
}
267+
268+
[Fact]
269+
public async Task Transaction_RollbackAsync_Ignores_Readable_Safepoint_Buffers()
270+
{
271+
await using var db = new LiteDatabase(new MemoryStream());
272+
var (transaction, service) = await CreateTransactionWithReadableDirtyCollectionPage(db, "rollback_async", "idx_async");
273+
274+
try
275+
{
276+
await FluentActions.Invoking(() => service.RollbackAsync().AsTask()).Should().NotThrowAsync();
277+
service.State.Should().Be(TransactionState.Aborted);
278+
}
279+
finally
280+
{
281+
await transaction.DisposeAsync();
282+
}
283+
}
284+
285+
[Fact]
286+
public async Task Transaction_Rollback_Ignores_Readable_Safepoint_Buffers()
287+
{
288+
await using var db = new LiteDatabase(new MemoryStream());
289+
var (transaction, service) = await CreateTransactionWithReadableDirtyCollectionPage(db, "rollback_sync", "idx_sync");
290+
291+
try
292+
{
293+
service.Invoking(x => x.Rollback()).Should().NotThrow();
294+
service.State.Should().Be(TransactionState.Aborted);
295+
}
296+
finally
297+
{
298+
await transaction.DisposeAsync();
299+
}
300+
}
301+
302+
[Fact]
303+
public async Task Transaction_Dispose_Ignores_Readable_Safepoint_Buffers()
304+
{
305+
await using var db = new LiteDatabase(new MemoryStream());
306+
var (transaction, service) = await CreateTransactionWithReadableDirtyCollectionPage(db, "rollback_dispose", "idx_dispose");
307+
308+
try
309+
{
310+
service.Invoking(x => x.Dispose()).Should().NotThrow();
311+
service.State.Should().Be(TransactionState.Disposed);
312+
}
313+
finally
314+
{
315+
await transaction.DisposeAsync();
316+
}
317+
}
245318
}

LiteDBX/Engine/Services/TransactionService.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,14 @@ public void Commit()
385385
State = TransactionState.Committed;
386386
}
387387

388+
private static IEnumerable<PageBuffer> GetStillWritableBuffers(Snapshot snapshot, bool dirty)
389+
{
390+
return snapshot
391+
.GetWritablePages(dirty, true)
392+
.Select(x => x.Buffer)
393+
.Where(x => x.ShareCounter == BUFFER_WRITABLE);
394+
}
395+
388396
/// <summary>
389397
/// Asynchronously rollback the transaction: discard dirty pages, return new pages, dispose snapshots.
390398
///
@@ -403,8 +411,8 @@ public async ValueTask RollbackAsync(CancellationToken ct = default)
403411
{
404412
if (snapshot.Mode == LockMode.Write)
405413
{
406-
_disk.DiscardDirtyPages(snapshot.GetWritablePages(true, true).Select(x => x.Buffer));
407-
_disk.DiscardCleanPages(snapshot.GetWritablePages(false, true).Select(x => x.Buffer));
414+
_disk.DiscardDirtyPages(GetStillWritableBuffers(snapshot, true));
415+
_disk.DiscardCleanPages(GetStillWritableBuffers(snapshot, false));
408416
}
409417

410418
snapshot.Dispose();
@@ -430,8 +438,8 @@ public void Rollback()
430438
{
431439
if (snapshot.Mode == LockMode.Write)
432440
{
433-
_disk.DiscardDirtyPages(snapshot.GetWritablePages(true, true).Select(x => x.Buffer));
434-
_disk.DiscardCleanPages(snapshot.GetWritablePages(false, true).Select(x => x.Buffer));
441+
_disk.DiscardDirtyPages(GetStillWritableBuffers(snapshot, true));
442+
_disk.DiscardCleanPages(GetStillWritableBuffers(snapshot, false));
435443
}
436444

437445
snapshot.Dispose();
@@ -577,8 +585,8 @@ protected virtual void Dispose(bool dispose)
577585
{
578586
if (snapshot.Mode == LockMode.Write)
579587
{
580-
_disk.DiscardDirtyPages(snapshot.GetWritablePages(true, true).Select(x => x.Buffer));
581-
_disk.DiscardCleanPages(snapshot.GetWritablePages(false, true).Select(x => x.Buffer));
588+
_disk.DiscardDirtyPages(GetStillWritableBuffers(snapshot, true));
589+
_disk.DiscardCleanPages(GetStillWritableBuffers(snapshot, false));
582590
}
583591

584592
// Always dispose snapshots so write snapshots release collection locks.

0 commit comments

Comments
 (0)