Files
CBDD/tests/CBDD.Tests/Storage/CheckpointModeTests.cs
Joseph Doherty a70d8befae
All checks were successful
NuGet Publish / build-and-pack (push) Successful in 46s
NuGet Publish / publish-to-gitea (push) Successful in 56s
Reformat / cleanup
2026-02-21 08:10:36 -05:00

229 lines
7.1 KiB
C#

using System.Reflection;
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()
{
string 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()
{
string 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()
{
string dbPath = NewDbPath();
string walPath = Path.ChangeExtension(dbPath, ".wal");
try
{
using (var db = new TestDbContext(dbPath))
{
db.Users.Insert(new User { Name = "checkpoint-full", Age = 50 });
db.SaveChanges();
long 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()
{
string 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()
{
string 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()
{
string 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()
{
return Path.Combine(Path.GetTempPath(), $"checkpoint_mode_{Guid.NewGuid():N}.db");
}
private static void CleanupFiles(string dbPath)
{
if (File.Exists(dbPath)) File.Delete(dbPath);
string walPath = Path.ChangeExtension(dbPath, ".wal");
if (File.Exists(walPath)) File.Delete(walPath);
var markerPath = $"{dbPath}.compact.state";
if (File.Exists(markerPath)) File.Delete(markerPath);
}
}