135 lines
4.2 KiB
C#
135 lines
4.2 KiB
C#
using ZB.MOM.WW.CBDD.Bson;
|
|
using ZB.MOM.WW.CBDD.Core.Storage;
|
|
using ZB.MOM.WW.CBDD.Shared;
|
|
|
|
namespace ZB.MOM.WW.CBDD.Tests;
|
|
|
|
public class CompactionCrashRecoveryTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies compaction resumes from marker phases and preserves data.
|
|
/// </summary>
|
|
/// <param name="phase">The crash marker phase to resume from.</param>
|
|
[Theory]
|
|
[InlineData("Started")]
|
|
[InlineData("Copied")]
|
|
[InlineData("Swapped")]
|
|
public void ResumeCompaction_FromCrashMarkerPhases_ShouldFinalizeAndPreserveData(string phase)
|
|
{
|
|
var dbPath = NewDbPath();
|
|
var markerPath = MarkerPath(dbPath);
|
|
|
|
try
|
|
{
|
|
using var db = new TestDbContext(dbPath);
|
|
|
|
var ids = SeedData(db);
|
|
db.ForceCheckpoint();
|
|
WriteMarker(markerPath, dbPath, phase);
|
|
|
|
var resumed = db.Storage.ResumeCompactionIfNeeded(new CompactionOptions
|
|
{
|
|
EnableTailTruncation = true,
|
|
DefragmentSlottedPages = true,
|
|
NormalizeFreeList = true
|
|
});
|
|
|
|
resumed.ShouldNotBeNull();
|
|
resumed!.ResumedFromMarker.ShouldBeTrue();
|
|
File.Exists(markerPath).ShouldBeFalse();
|
|
|
|
db.Users.Count().ShouldBe(ids.Count);
|
|
var recoveredDoc = ids
|
|
.Select(id => db.Users.FindById(id))
|
|
.FirstOrDefault(x => x != null);
|
|
recoveredDoc.ShouldNotBeNull();
|
|
recoveredDoc!.Name.ShouldContain("user-");
|
|
|
|
db.Storage.ResumeCompactionIfNeeded().ShouldBeNull();
|
|
}
|
|
finally
|
|
{
|
|
CleanupFiles(dbPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies corrupted compaction markers are recovered deterministically.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResumeCompaction_WithCorruptedMarker_ShouldRecoverDeterministically()
|
|
{
|
|
var dbPath = NewDbPath();
|
|
var markerPath = MarkerPath(dbPath);
|
|
|
|
try
|
|
{
|
|
using var db = new TestDbContext(dbPath);
|
|
var ids = SeedData(db);
|
|
db.ForceCheckpoint();
|
|
|
|
File.WriteAllText(markerPath, "{invalid-json-marker");
|
|
|
|
var resumed = db.Storage.ResumeCompactionIfNeeded(new CompactionOptions
|
|
{
|
|
EnableTailTruncation = true
|
|
});
|
|
|
|
resumed.ShouldNotBeNull();
|
|
resumed!.ResumedFromMarker.ShouldBeTrue();
|
|
File.Exists(markerPath).ShouldBeFalse();
|
|
|
|
db.Users.Count().ShouldBe(ids.Count);
|
|
var recoveredDoc = ids
|
|
.Select(id => db.Users.FindById(id))
|
|
.FirstOrDefault(x => x != null);
|
|
recoveredDoc.ShouldNotBeNull();
|
|
recoveredDoc!.Name.ShouldContain("user-");
|
|
}
|
|
finally
|
|
{
|
|
CleanupFiles(dbPath);
|
|
}
|
|
}
|
|
|
|
private static List<ObjectId> SeedData(TestDbContext db)
|
|
{
|
|
var ids = new List<ObjectId>();
|
|
for (var i = 0; i < 120; i++)
|
|
{
|
|
ids.Add(db.Users.Insert(new User
|
|
{
|
|
Name = $"user-{i:D4}-payload-{new string('x', 120)}",
|
|
Age = i % 20
|
|
}));
|
|
}
|
|
|
|
db.SaveChanges();
|
|
return ids;
|
|
}
|
|
|
|
private static void WriteMarker(string markerPath, string dbPath, string phase)
|
|
{
|
|
var safeDbPath = dbPath.Replace("\\", "\\\\", StringComparison.Ordinal);
|
|
var now = DateTimeOffset.UtcNow.ToString("O");
|
|
var json = $$"""
|
|
{"version":1,"phase":"{{phase}}","databasePath":"{{safeDbPath}}","startedAtUtc":"{{now}}","lastUpdatedUtc":"{{now}}","onlineMode":false,"mode":"InPlace"}
|
|
""";
|
|
File.WriteAllText(markerPath, json);
|
|
}
|
|
|
|
private static string MarkerPath(string dbPath) => $"{dbPath}.compact.state";
|
|
|
|
private static string NewDbPath()
|
|
=> Path.Combine(Path.GetTempPath(), $"compaction_crash_{Guid.NewGuid():N}.db");
|
|
|
|
private static void CleanupFiles(string dbPath)
|
|
{
|
|
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
|
var markerPath = MarkerPath(dbPath);
|
|
if (File.Exists(dbPath)) File.Delete(dbPath);
|
|
if (File.Exists(walPath)) File.Delete(walPath);
|
|
if (File.Exists(markerPath)) File.Delete(markerPath);
|
|
}
|
|
}
|