Implement checkpoint modes with docs/tests and reorganize project file layout
This commit is contained in:
@@ -2,9 +2,10 @@ using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Core;
|
||||
using ZB.MOM.WW.CBDD.Core.Collections;
|
||||
using ZB.MOM.WW.CBDD.Core.Compression;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Core.Metadata;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Core.Metadata;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Shared;
|
||||
|
||||
@@ -235,16 +236,25 @@ public partial class TestDbContext : DocumentDbContext
|
||||
modelBuilder.Entity<TemporalEntity>().ToCollection("temporal_entities").HasKey(e => e.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes ForceCheckpoint.
|
||||
/// </summary>
|
||||
public void ForceCheckpoint()
|
||||
{
|
||||
Engine.Checkpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Storage.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Executes ForceCheckpoint.
|
||||
/// </summary>
|
||||
public void ForceCheckpoint()
|
||||
{
|
||||
Engine.Checkpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes ForceCheckpoint with the requested checkpoint mode.
|
||||
/// </summary>
|
||||
/// <param name="mode">Checkpoint mode to execute.</param>
|
||||
public CheckpointResult ForceCheckpoint(CheckpointMode mode)
|
||||
{
|
||||
return Engine.Checkpoint(mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Storage.
|
||||
/// </summary>
|
||||
public StorageEngine Storage => Engine;
|
||||
}
|
||||
228
tests/CBDD.Tests/Storage/CheckpointModeTests.cs
Normal file
228
tests/CBDD.Tests/Storage/CheckpointModeTests.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System.Reflection;
|
||||
using ZB.MOM.WW.CBDD.Bson;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
using ZB.MOM.WW.CBDD.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CheckpointModeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies default checkpoint mode truncates WAL.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Checkpoint_Default_ShouldUseTruncate()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
try
|
||||
{
|
||||
using var db = new TestDbContext(dbPath);
|
||||
db.Users.Insert(new User { Name = "checkpoint-default", Age = 42 });
|
||||
db.SaveChanges();
|
||||
|
||||
db.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
||||
var result = db.Checkpoint();
|
||||
|
||||
result.Mode.ShouldBe(CheckpointMode.Truncate);
|
||||
result.Executed.ShouldBeTrue();
|
||||
result.Truncated.ShouldBeTrue();
|
||||
db.Storage.GetWalSize().ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies passive mode skips when checkpoint lock is contended.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Checkpoint_Passive_ShouldSkip_WhenLockIsContended()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
try
|
||||
{
|
||||
using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
|
||||
var gate = GetCommitGate(storage);
|
||||
gate.Wait(TestContext.Current.CancellationToken);
|
||||
try
|
||||
{
|
||||
var result = storage.Checkpoint(CheckpointMode.Passive);
|
||||
result.Mode.ShouldBe(CheckpointMode.Passive);
|
||||
result.Executed.ShouldBeFalse();
|
||||
result.Truncated.ShouldBeFalse();
|
||||
result.Restarted.ShouldBeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies full checkpoint applies data and appends a checkpoint marker without truncating WAL.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Checkpoint_Full_ShouldAppendMarker_AndPreserveWal()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new TestDbContext(dbPath))
|
||||
{
|
||||
db.Users.Insert(new User { Name = "checkpoint-full", Age = 50 });
|
||||
db.SaveChanges();
|
||||
|
||||
var walBefore = db.Storage.GetWalSize();
|
||||
walBefore.ShouldBeGreaterThan(0);
|
||||
|
||||
var result = db.Checkpoint(CheckpointMode.Full);
|
||||
result.Mode.ShouldBe(CheckpointMode.Full);
|
||||
result.Executed.ShouldBeTrue();
|
||||
result.Truncated.ShouldBeFalse();
|
||||
result.WalBytesAfter.ShouldBeGreaterThan(0);
|
||||
db.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
using var wal = new WriteAheadLog(walPath);
|
||||
wal.ReadAll().Any(r => r.Type == WalRecordType.Checkpoint).ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies restart checkpoint clears WAL and allows subsequent writes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Checkpoint_Restart_ShouldResetWal_AndAcceptNewWrites()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
try
|
||||
{
|
||||
using var db = new TestDbContext(dbPath);
|
||||
db.Users.Insert(new User { Name = "restart-before", Age = 30 });
|
||||
db.SaveChanges();
|
||||
|
||||
db.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
||||
var result = db.Checkpoint(CheckpointMode.Restart);
|
||||
result.Mode.ShouldBe(CheckpointMode.Restart);
|
||||
result.Executed.ShouldBeTrue();
|
||||
result.Truncated.ShouldBeTrue();
|
||||
result.Restarted.ShouldBeTrue();
|
||||
db.Storage.GetWalSize().ShouldBe(0);
|
||||
|
||||
db.Users.Insert(new User { Name = "restart-after", Age = 31 });
|
||||
db.SaveChanges();
|
||||
db.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies recovery remains deterministic after a full checkpoint boundary.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Recover_AfterFullCheckpoint_ShouldApplyLatestCommitDeterministically()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
try
|
||||
{
|
||||
uint pageId;
|
||||
|
||||
using (var storage = new StorageEngine(dbPath, PageFileConfig.Default))
|
||||
{
|
||||
pageId = storage.AllocatePage();
|
||||
|
||||
using (var tx1 = storage.BeginTransaction())
|
||||
{
|
||||
var first = new byte[storage.PageSize];
|
||||
first[0] = 1;
|
||||
storage.WritePage(pageId, tx1.TransactionId, first);
|
||||
tx1.Commit();
|
||||
}
|
||||
|
||||
storage.Checkpoint(CheckpointMode.Full);
|
||||
|
||||
using (var tx2 = storage.BeginTransaction())
|
||||
{
|
||||
var second = new byte[storage.PageSize];
|
||||
second[0] = 2;
|
||||
storage.WritePage(pageId, tx2.TransactionId, second);
|
||||
tx2.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
using (var recovered = new StorageEngine(dbPath, PageFileConfig.Default))
|
||||
{
|
||||
var buffer = new byte[recovered.PageSize];
|
||||
recovered.ReadPage(pageId, 0, buffer);
|
||||
buffer[0].ShouldBe((byte)2);
|
||||
recovered.GetWalSize().ShouldBe(0);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies asynchronous mode-based checkpoints return expected result metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CheckpointAsync_Full_ShouldReturnResult()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
try
|
||||
{
|
||||
using var db = new TestDbContext(dbPath);
|
||||
db.Users.Insert(new User { Name = "checkpoint-async", Age = 38 });
|
||||
db.SaveChanges();
|
||||
|
||||
var result = await db.CheckpointAsync(CheckpointMode.Full, TestContext.Current.CancellationToken);
|
||||
result.Mode.ShouldBe(CheckpointMode.Full);
|
||||
result.Executed.ShouldBeTrue();
|
||||
result.Truncated.ShouldBeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static SemaphoreSlim GetCommitGate(StorageEngine storage)
|
||||
{
|
||||
var field = typeof(StorageEngine).GetField("_commitLock", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
field.ShouldNotBeNull();
|
||||
return (SemaphoreSlim)field!.GetValue(storage)!;
|
||||
}
|
||||
|
||||
private static string NewDbPath()
|
||||
=> Path.Combine(Path.GetTempPath(), $"checkpoint_mode_{Guid.NewGuid():N}.db");
|
||||
|
||||
private static void CleanupFiles(string dbPath)
|
||||
{
|
||||
if (File.Exists(dbPath)) File.Delete(dbPath);
|
||||
|
||||
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
||||
if (File.Exists(walPath)) File.Delete(walPath);
|
||||
|
||||
var markerPath = $"{dbPath}.compact.state";
|
||||
if (File.Exists(markerPath)) File.Delete(markerPath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user