184 lines
5.9 KiB
C#
Executable File
184 lines
5.9 KiB
C#
Executable File
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.Indexing;
|
|
using ZB.MOM.WW.CBDD.Core.Storage;
|
|
using ZB.MOM.WW.CBDD.Core.Transactions;
|
|
using ZB.MOM.WW.CBDD.Shared;
|
|
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
|
|
using System.Buffers;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.CBDD.Tests;
|
|
|
|
public class WalIndexTests : IDisposable
|
|
{
|
|
private readonly string _dbPath;
|
|
private readonly string _walPath;
|
|
private readonly Shared.TestDbContext _db;
|
|
private readonly ITestOutputHelper _output;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WalIndexTests"/> class.
|
|
/// </summary>
|
|
/// <param name="output">Test output sink.</param>
|
|
public WalIndexTests(ITestOutputHelper output)
|
|
{
|
|
_output = output;
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_index_{Guid.NewGuid()}.db");
|
|
// WAL defaults to .wal next to db
|
|
_walPath = Path.ChangeExtension(_dbPath, ".wal");
|
|
|
|
_db = new Shared.TestDbContext(_dbPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies index writes are recorded in the WAL.
|
|
/// </summary>
|
|
[Fact]
|
|
public void IndexWritesAreLoggedToWal()
|
|
{
|
|
// 2. Start a transaction
|
|
using var txn = _db.BeginTransaction();
|
|
_output.WriteLine($"Started Transaction: {txn.TransactionId}");
|
|
|
|
// 3. Insert a user
|
|
var user = new User { Name = "Alice", Age = 30 };
|
|
_db.Users.Insert(user);
|
|
|
|
// 4. Commit
|
|
txn.Commit();
|
|
_output.WriteLine("Committed Transaction");
|
|
|
|
// 5. Verify WAL
|
|
// Dispose current storage to release file locks, BUT skip checkpoint/truncate
|
|
_db.Dispose();
|
|
|
|
File.Exists(_walPath).ShouldBeTrue("WAL file should exist");
|
|
|
|
using var walReader = new WriteAheadLog(_walPath);
|
|
var records = walReader.ReadAll();
|
|
|
|
_output.WriteLine($"Found {records.Count} WAL records");
|
|
|
|
// Filter for this transaction
|
|
var txnRecords = records.Where(r => r.TransactionId == txn.TransactionId).ToList();
|
|
|
|
txnRecords.ShouldContain(r => r.Type == WalRecordType.Begin);
|
|
txnRecords.ShouldContain(r => r.Type == WalRecordType.Commit);
|
|
|
|
var writeRecords = txnRecords.Where(r => r.Type == WalRecordType.Write).ToList();
|
|
_output.WriteLine($"Found {writeRecords.Count} Write records for Txn {txn.TransactionId}");
|
|
|
|
// Analyze pages
|
|
int indexPageCount = 0;
|
|
int dataPageCount = 0;
|
|
|
|
foreach (var record in writeRecords)
|
|
{
|
|
var pageType = ParsePageType(record.AfterImage);
|
|
_output.WriteLine($"Page {record.PageId}: Type={pageType}, Size={record.AfterImage?.Length}");
|
|
|
|
if (pageType == PageType.Index) indexPageCount++;
|
|
else if (pageType == PageType.Data) dataPageCount++;
|
|
}
|
|
|
|
(indexPageCount > 0).ShouldBeTrue($"Expected at least 1 Index page write, found {indexPageCount}");
|
|
(dataPageCount > 0).ShouldBeTrue($"Expected at least 1 Data page write, found {dataPageCount}");
|
|
}
|
|
|
|
private PageType ParsePageType(byte[]? pageData)
|
|
{
|
|
if (pageData == null || pageData.Length < 32) return (PageType)0;
|
|
// PageType is at offset 4 (1 byte)
|
|
return (PageType)pageData[4]; // Casting byte to PageType
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies offline compaction leaves the WAL empty.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun()
|
|
{
|
|
for (var i = 0; i < 100; i++)
|
|
{
|
|
_db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 });
|
|
}
|
|
|
|
_db.SaveChanges();
|
|
_db.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
|
|
|
var stats = _db.Compact(new CompactionOptions
|
|
{
|
|
EnableTailTruncation = true,
|
|
NormalizeFreeList = true,
|
|
DefragmentSlottedPages = true
|
|
});
|
|
|
|
stats.OnlineMode.ShouldBeFalse();
|
|
_db.Storage.GetWalSize().ShouldBe(0);
|
|
new FileInfo(_walPath).Length.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies WAL recovery followed by compaction preserves data.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db");
|
|
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
|
var markerPath = $"{dbPath}.compact.state";
|
|
var expectedIds = new List<ObjectId>();
|
|
|
|
try
|
|
{
|
|
using (var writer = new Shared.TestDbContext(dbPath))
|
|
{
|
|
for (var i = 0; i < 48; i++)
|
|
{
|
|
expectedIds.Add(writer.Users.Insert(new User { Name = $"recover-{i:D3}", Age = i % 10 }));
|
|
}
|
|
|
|
writer.SaveChanges();
|
|
writer.Storage.GetWalSize().ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
new FileInfo(walPath).Length.ShouldBeGreaterThan(0);
|
|
|
|
using (var recovered = new Shared.TestDbContext(dbPath))
|
|
{
|
|
recovered.Users.Count().ShouldBe(expectedIds.Count);
|
|
recovered.Compact();
|
|
recovered.Storage.GetWalSize().ShouldBe(0);
|
|
|
|
foreach (var id in expectedIds)
|
|
{
|
|
recovered.Users.FindById(id).ShouldNotBeNull();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(dbPath)) File.Delete(dbPath);
|
|
if (File.Exists(walPath)) File.Delete(walPath);
|
|
if (File.Exists(markerPath)) File.Delete(markerPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases test resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
_db?.Dispose(); // Safe to call multiple times
|
|
}
|
|
catch { }
|
|
|
|
try { if (File.Exists(_dbPath)) File.Delete(_dbPath); } catch { }
|
|
try { if (File.Exists(_walPath)) File.Delete(_walPath); } catch { }
|
|
}
|
|
}
|