Files
CBDD/tests/CBDD.Tests/CompactionOnlineConcurrencyTests.cs
Joseph Doherty 3ffd468c79
All checks were successful
NuGet Publish / build-and-pack (push) Successful in 45s
NuGet Publish / publish-to-gitea (push) Successful in 52s
Fix audit findings for coverage, architecture checks, and XML docs
2026-02-20 15:43:25 -05:00

141 lines
4.7 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 CompactionOnlineConcurrencyTests
{
/// <summary>
/// Verifies online compaction completes without deadlock under concurrent workload.
/// </summary>
[Fact]
public async Task OnlineCompaction_WithConcurrentishWorkload_ShouldCompleteWithoutDeadlock()
{
var dbPath = NewDbPath();
var activeIds = new List<ObjectId>();
var sync = new object();
var completedOps = 0;
try
{
using var db = new TestDbContext(dbPath);
var testCancellation = TestContext.Current.CancellationToken;
for (var i = 0; i < 120; i++)
{
var id = db.Users.Insert(new User { Name = $"seed-{i:D4}", Age = i % 40 });
activeIds.Add(id);
}
db.SaveChanges();
db.ForceCheckpoint();
var workloadTask = Task.Run(() =>
{
for (var i = 0; i < 150; i++)
{
if (i % 3 == 0)
{
var id = db.Users.Insert(new User { Name = $"insert-{i:D4}", Age = i % 60 });
lock (sync)
{
activeIds.Add(id);
}
}
else if (i % 3 == 1)
{
ObjectId? candidate = null;
lock (sync)
{
if (activeIds.Count > 0)
{
candidate = activeIds[i % activeIds.Count];
}
}
if (candidate.HasValue)
{
var entity = db.Users.FindById(candidate.Value);
if (entity != null)
{
entity.Age += 1;
db.Users.Update(entity).ShouldBeTrue();
}
}
}
else
{
ObjectId? candidate = null;
lock (sync)
{
if (activeIds.Count > 60)
{
candidate = activeIds[^1];
activeIds.RemoveAt(activeIds.Count - 1);
}
}
if (candidate.HasValue)
{
db.Users.Delete(candidate.Value);
}
}
db.SaveChanges();
_ = db.Users.Count();
db.SaveChanges();
Interlocked.Increment(ref completedOps);
}
}, testCancellation);
var compactionTask = Task.Run(() => db.Compact(new CompactionOptions
{
OnlineMode = true,
OnlineBatchPageLimit = 4,
OnlineBatchDelay = TimeSpan.FromMilliseconds(2),
MaxOnlineDuration = TimeSpan.FromMilliseconds(400),
EnableTailTruncation = true
}), testCancellation);
await Task.WhenAll(workloadTask, compactionTask).WaitAsync(TimeSpan.FromSeconds(20), testCancellation);
var stats = await compactionTask;
stats.OnlineMode.ShouldBeTrue();
completedOps.ShouldBeGreaterThanOrEqualTo(100);
var allUsers = db.Users.FindAll().ToList();
allUsers.Count.ShouldBeGreaterThan(0);
db.SaveChanges();
List<ObjectId> snapshotIds;
lock (sync)
{
snapshotIds = activeIds.ToList();
}
var actualIds = allUsers.Select(x => x.Id).ToHashSet();
foreach (var id in snapshotIds)
{
actualIds.ShouldContain(id);
}
}
finally
{
CleanupFiles(dbPath);
}
}
private static string NewDbPath()
=> Path.Combine(Path.GetTempPath(), $"compaction_online_{Guid.NewGuid():N}.db");
private static void CleanupFiles(string dbPath)
{
var walPath = Path.ChangeExtension(dbPath, ".wal");
var markerPath = $"{dbPath}.compact.state";
if (File.Exists(dbPath)) File.Delete(dbPath);
if (File.Exists(walPath)) File.Delete(walPath);
if (File.Exists(markerPath)) File.Delete(markerPath);
}
}