229 lines
7.1 KiB
C#
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);
|
|
}
|
|
} |