feat(batch14): complete group2 lifecycle checks and wave2 tests

This commit is contained in:
Joseph Doherty
2026-02-28 15:27:32 -05:00
parent 4f0a7f40fc
commit 9a35e2dfc5
5 changed files with 856 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Reflection;
using Shouldly;
using ZB.MOM.NatsNet.Server;
@@ -59,6 +60,38 @@ public sealed partial class ConcurrencyTests2
}
}
[Fact] // T:2476
public void NoRaceFilestoreBinaryStreamSnapshotEncodingLargeGaps_ShouldSucceed()
{
WithStore((fs, _) =>
{
const int numMsgs = 5000;
var payload = new byte[128];
fs.StoreMsg("zzz", null, payload, 0).Seq.ShouldBe(1UL);
for (var i = 2; i < numMsgs; i++)
{
var (seq, _) = fs.StoreMsg("zzz", null, null, 0);
seq.ShouldBeGreaterThan(1UL);
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
}
fs.StoreMsg("zzz", null, payload, 0).Seq.ShouldBe((ulong)numMsgs);
Should.NotThrow(() => InvokePrivate(fs, "SyncBlocks"));
var (snapshot, err) = fs.EncodedStreamState(0);
err.ShouldBeNull();
StoreParity.IsEncodedStreamState(snapshot).ShouldBeTrue();
snapshot.Length.ShouldBeLessThan(2048);
var state = fs.State();
state.FirstSeq.ShouldBe(1UL);
state.LastSeq.ShouldBe((ulong)numMsgs);
state.Msgs.ShouldBe(2UL);
state.NumDeleted.ShouldBe(numMsgs - 2);
});
}
private static void WithStore(Action<JetStreamFileStore, string> action, StreamConfig? cfg = null)
{
var root = NewRoot();
@@ -94,5 +127,12 @@ public sealed partial class ConcurrencyTests2
};
}
private static void InvokePrivate(object target, string methodName, params object[] args)
{
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
method.ShouldNotBeNull();
method!.Invoke(target, args);
}
private static string NewRoot() => Path.Combine(Path.GetTempPath(), $"impl-fs-c2-{Guid.NewGuid():N}");
}

View File

@@ -1,3 +1,5 @@
using System.Reflection;
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
@@ -374,6 +376,201 @@ public sealed partial class JetStreamFileStoreTests
});
}
[Fact] // T:354
public void FileStoreSelectNextFirst_ShouldSucceed()
{
WithStore((fs, _) =>
{
for (var i = 0; i < 10; i++)
fs.StoreMsg("zzz", null, "Hello World"u8.ToArray(), 0);
for (ulong seq = 2; seq <= 7; seq++)
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
var state = fs.State();
state.Msgs.ShouldBe(4UL);
state.FirstSeq.ShouldBe(1UL);
fs.RemoveMsg(1).Removed.ShouldBeTrue();
state = fs.State();
state.Msgs.ShouldBe(3UL);
state.FirstSeq.ShouldBe(8UL);
});
}
[Fact] // T:365
public void FileStoreCompactLastPlusOne_ShouldSucceed()
{
WithStore((fs, _) =>
{
var payload = new byte[512];
for (var i = 0; i < 1000; i++)
fs.StoreMsg("foo", null, payload, 0);
Should.NotThrow(() => InvokePrivateVoid(fs, "CheckAndFlushLastBlock"));
var state = fs.State();
state.Msgs.ShouldBe(1000UL);
var (purged, err) = fs.Compact(state.LastSeq + 1);
err.ShouldBeNull();
purged.ShouldBe(1000UL);
fs.State().Msgs.ShouldBe(0UL);
fs.StoreMsg("foo", null, payload, 0).Seq.ShouldBeGreaterThan(0UL);
fs.State().Msgs.ShouldBe(1UL);
}, fcfg: new FileStoreConfig { BlockSize = 8192, AsyncFlush = true });
}
[Fact] // T:372
public void FileStoreBitRot_ShouldSucceed()
{
WithStore((fs, root) =>
{
for (var i = 0; i < 100; i++)
fs.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
var blockPath = CreateBlock(root, 1, Encoding.ASCII.GetBytes("abcdefgh12345678"));
var mb = fs.RecoverMsgBlock(1);
mb.Mfn.ShouldBe(blockPath);
File.Exists(blockPath).ShouldBeTrue();
var contents = File.ReadAllBytes(blockPath);
contents.Length.ShouldBeGreaterThan(0);
contents[contents.Length / 2] ^= 0xFF;
File.WriteAllBytes(blockPath, contents);
Should.NotThrow(() => InvokePrivate<LostStreamData?>(fs, "CheckMsgs"));
var state = fs.State();
state.LastSeq.ShouldBeGreaterThanOrEqualTo(state.FirstSeq);
});
}
[Fact] // T:435
public void FileStoreMsgBlkFailOnKernelFaultLostDataReporting_ShouldSucceed()
{
var root = NewRoot();
Directory.CreateDirectory(root);
JetStreamFileStore? fs = null;
JetStreamFileStore? reopened = null;
try
{
var cfg = DefaultStreamConfig();
var fcfg = new FileStoreConfig { StoreDir = root, BlockSize = 4096 };
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
for (var i = 0; i < 500; i++)
fs.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
var mfn = CreateBlock(root, 1, Encoding.ASCII.GetBytes("kernel-fault-simulated-block"));
fs.RecoverMsgBlock(1).Mfn.ShouldBe(mfn);
fs.Stop();
fs = null;
File.Delete(mfn);
reopened = JetStreamFileStore.NewFileStore(fcfg, cfg);
Should.NotThrow(() => InvokePrivate<LostStreamData?>(reopened, "CheckMsgs"));
reopened.StoreMsg("foo", null, "restart"u8.ToArray(), 0).Seq.ShouldBe(1UL);
reopened.State().Msgs.ShouldBe(1UL);
}
finally
{
reopened?.Stop();
fs?.Stop();
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
[Fact] // T:593
public void FileStoreSelectMsgBlockBinarySearch_ShouldSucceed()
{
WithStore((fs, _) =>
{
var blks = new List<MessageBlock>();
var bim = new Dictionary<uint, MessageBlock>();
ulong seq = 1;
for (uint index = 1; index <= 34; index++)
{
var mb = fs.InitMsgBlock(index);
mb.First = new MsgId { Seq = seq, Ts = (long)seq };
mb.Last = new MsgId { Seq = seq + 1, Ts = (long)(seq + 1) };
mb.Msgs = 2;
blks.Add(mb);
bim[index] = mb;
seq += 2;
}
SetPrivateField(fs, "_blks", blks);
SetPrivateField(fs, "_bim", bim);
SetPrivateField(fs, "_lmb", blks[^1]);
SetPrivateField(fs, "_state", new StreamState
{
Msgs = 68,
FirstSeq = 1,
LastSeq = 68,
FirstTime = DateTime.UtcNow,
LastTime = DateTime.UtcNow,
});
var first = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 2UL);
first.Index.ShouldBe(0);
first.Block.ShouldNotBeNull();
first.Block!.Index.ShouldBe(1U);
var deleted = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 5UL);
deleted.Index.ShouldBe(2);
deleted.Block.ShouldNotBeNull();
deleted.Block!.Index.ShouldBe(3U);
var tail = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 64UL);
tail.Index.ShouldBe(31);
tail.Block.ShouldNotBeNull();
tail.Block!.Index.ShouldBe(32U);
var last = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 68UL);
last.Index.ShouldBe(33);
last.Block.ShouldNotBeNull();
last.Block!.Index.ShouldBe(34U);
var oob = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 69UL);
oob.Index.ShouldBe(-1);
oob.Block.ShouldBeNull();
});
}
private static T InvokePrivate<T>(object target, string methodName, params object[] args)
{
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
method.ShouldNotBeNull();
var result = method!.Invoke(target, args);
if (result == null)
return default!;
return (T)result;
}
private static void InvokePrivateVoid(object target, string methodName, params object[] args)
{
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
method.ShouldNotBeNull();
method!.Invoke(target, args);
}
private static T GetPrivateField<T>(object target, string name)
{
var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
field.ShouldNotBeNull();
return (T)field!.GetValue(target)!;
}
private static void SetPrivateField<T>(object target, string name, T value)
{
var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
field.ShouldNotBeNull();
field!.SetValue(target, value);
}
private static void RunWaveBScenario(string scenario)
{
switch (scenario)