using System.Collections.Concurrent; using System.Reflection; using Shouldly; using ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed partial class ConcurrencyTests2 { [Fact] // T:2491 public void NoRaceFileStoreMsgLoadNextMsgMultiPerf_ShouldSucceed() { WithStore((fs, _) => { for (var i = 0; i < 150; i++) fs.StoreMsg($"ln.{i % 6}", null, "x"u8.ToArray(), 0); var errors = new ConcurrentQueue(); Parallel.For(0, 400, _ => { try { var (sm, _) = fs.LoadNextMsgMulti(new[] { "ln.1", "ln.*" }, 1, null); if (sm != null) sm.Subject.ShouldStartWith("ln."); } catch (Exception ex) { errors.Enqueue(ex); } }); errors.ShouldBeEmpty(); fs.State().Msgs.ShouldBeGreaterThan(0UL); }); } [Fact] // T:2501 public void NoRaceFileStoreMsgLimitsAndOldRecoverState_ShouldSucceed() { var root = NewRoot(); Directory.CreateDirectory(root); try { var cfg = DefaultStreamConfig(maxMsgs: 60); var fs1 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg); Parallel.For(0, 180, i => fs1.StoreMsg($"lm.{i % 4}", null, "x"u8.ToArray(), 0)); fs1.Stop(); var fs2 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg); var (seq, _) = fs2.StoreMsg("lm.tail", null, "tail"u8.ToArray(), 0); seq.ShouldBeGreaterThan(0UL); fs2.State().Msgs.ShouldBeLessThanOrEqualTo((ulong)cfg.MaxMsgs); fs2.Stop(); } finally { Directory.Delete(root, recursive: true); } } [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 action, StreamConfig? cfg = null) { var root = NewRoot(); Directory.CreateDirectory(root); JetStreamFileStore? fs = null; try { fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg ?? DefaultStreamConfig()); action(fs, root); } finally { fs?.Stop(); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } private static StreamConfig DefaultStreamConfig(long maxMsgs = -1) { return new StreamConfig { Name = "TEST", Storage = StorageType.FileStorage, Subjects = ["test.>"], MaxMsgs = maxMsgs, MaxBytes = -1, MaxAge = TimeSpan.Zero, MaxMsgsPer = -1, Discard = DiscardPolicy.DiscardOld, Retention = RetentionPolicy.LimitsPolicy, }; } 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}"); }