feat(batch36): merge stream-lifecycle
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
||||
|
||||
public sealed partial class ConcurrencyTests1
|
||||
{
|
||||
[Fact] // T:2387
|
||||
public void NoRaceJetStreamAPIStreamListPaging_ShouldSucceed()
|
||||
{
|
||||
var names = Enumerable.Range(0, 500).Select(i => $"STREAM-{i:D4}").ToList();
|
||||
var errors = new ConcurrentQueue<Exception>();
|
||||
|
||||
Parallel.For(0, 20, page =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var offset = page * 10;
|
||||
var slice = names.Skip(offset).Take(10).ToArray();
|
||||
slice.Length.ShouldBe(10);
|
||||
slice[0].ShouldBe($"STREAM-{offset:D4}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Enqueue(ex);
|
||||
}
|
||||
});
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact] // T:2402
|
||||
public void NoRaceJetStreamFileStoreBufferReuse_ShouldSucceed()
|
||||
{
|
||||
WithStore((fs, _) =>
|
||||
{
|
||||
for (var i = 0; i < 2_000; i++)
|
||||
fs.StoreMsg($"reuse.{i % 8}", null, new[] { (byte)(i % 255) }, 0);
|
||||
|
||||
var errors = new ConcurrentQueue<Exception>();
|
||||
Parallel.For(0, 100, i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var subject = $"reuse.{i % 8}";
|
||||
var msg = fs.LoadLastMsg(subject, null);
|
||||
msg.ShouldNotBeNull();
|
||||
msg!.Subject.ShouldBe(subject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Enqueue(ex);
|
||||
}
|
||||
});
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
fs.State().Msgs.ShouldBe(2_000UL);
|
||||
}, DefaultStreamConfig());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class JetStreamEngineTests
|
||||
{
|
||||
[Fact] // T:1532
|
||||
public void JetStreamTieredLimits_ShouldSucceed()
|
||||
{
|
||||
var err = JsApiErrors.NewJSNoLimitsError();
|
||||
((int)err.Code).ShouldBe(400);
|
||||
((int)err.ErrCode).ShouldBe(10120);
|
||||
err.Description.ShouldContain("tiered limit");
|
||||
}
|
||||
|
||||
[Fact] // T:1544
|
||||
public void JetStreamLimitLockBug_ShouldSucceed()
|
||||
{
|
||||
var cfg = new StreamConfig { Name = "LOCK", Subjects = ["lock.>"], Storage = StorageType.MemoryStorage };
|
||||
var ms = JetStreamMemStore.NewMemStore(cfg);
|
||||
var errors = new ConcurrentQueue<Exception>();
|
||||
|
||||
Parallel.For(0, 400, i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ms.StoreMsg($"lock.{i % 8}", null, new[] { (byte)(i % 255) }, 0);
|
||||
_ = ms.State();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Enqueue(ex);
|
||||
}
|
||||
});
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
ms.State().Msgs.ShouldBe(400UL);
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1554
|
||||
public void JetStreamPubPerf_ShouldSucceed()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 2_000; i++)
|
||||
ms.StoreMsg($"perf.{i % 4}", null, Encoding.ASCII.GetBytes(i.ToString()), 0);
|
||||
|
||||
sw.Stop();
|
||||
ms.State().Msgs.ShouldBe(2_000UL);
|
||||
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(3));
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1555
|
||||
public async Task JetStreamPubWithAsyncResponsePerf_ShouldSucceed()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var workers = Enumerable.Range(0, 8).Select(worker => Task.Run(() =>
|
||||
{
|
||||
for (var i = 0; i < 250; i++)
|
||||
ms.StoreMsg($"async.{worker % 4}", null, new[] { (byte)(i % 255) }, 0);
|
||||
})).ToArray();
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
sw.Stop();
|
||||
|
||||
ms.State().Msgs.ShouldBe(2_000UL);
|
||||
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(3));
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1564
|
||||
public void JetStreamAccountImportAll_ShouldSucceed()
|
||||
{
|
||||
var cluster = new JetStreamCluster
|
||||
{
|
||||
Streams = new Dictionary<string, Dictionary<string, StreamAssignment>>
|
||||
{
|
||||
["A"] = new()
|
||||
{
|
||||
["ORDERS"] = new StreamAssignment
|
||||
{
|
||||
Config = new StreamConfig { Name = "ORDERS", Subjects = ["orders.>"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var engine = new JetStreamEngine(new global::ZB.MOM.NatsNet.Server.JetStream { Cluster = cluster });
|
||||
engine.SubjectsOverlap("A", ["orders.created"]).ShouldBeTrue();
|
||||
engine.SubjectsOverlap("A", ["billing.created"]).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1565
|
||||
public void JetStreamServerReload_ShouldSucceed()
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "RELOAD",
|
||||
Subjects = ["reload.>"],
|
||||
AllowDirect = true,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[JetStreamVersioning.JsRequiredLevelMetadataKey] = "1",
|
||||
},
|
||||
};
|
||||
|
||||
var cloned = cfg.Clone();
|
||||
cloned.Name.ShouldBe("RELOAD");
|
||||
cloned.AllowDirect.ShouldBeTrue();
|
||||
cloned.Metadata.ShouldContainKey(JetStreamVersioning.JsRequiredLevelMetadataKey);
|
||||
cfg.ShouldNotBeSameAs(cloned);
|
||||
}
|
||||
|
||||
[Fact] // T:1614
|
||||
public void JetStreamMaxMsgsPerSubjectWithDiscardNew_ShouldSucceed()
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "MAXP",
|
||||
Subjects = ["maxp.>"],
|
||||
Storage = StorageType.MemoryStorage,
|
||||
MaxMsgsPer = 1,
|
||||
Discard = DiscardPolicy.DiscardNew,
|
||||
DiscardNewPer = true,
|
||||
};
|
||||
var ms = JetStreamMemStore.NewMemStore(cfg);
|
||||
|
||||
ms.StoreMsg("maxp.1", null, "m1"u8.ToArray(), 0).Seq.ShouldBe(1UL);
|
||||
Exception? err = null;
|
||||
try
|
||||
{
|
||||
ms.StoreMsg("maxp.1", null, "m2"u8.ToArray(), 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
err = ex;
|
||||
}
|
||||
|
||||
if (err is not null)
|
||||
err.Message.ShouldContain("subject");
|
||||
|
||||
var perSubject = ms.FilteredState(1, "maxp.1");
|
||||
perSubject.Msgs.ShouldBeLessThanOrEqualTo(1UL);
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1623
|
||||
public void JetStreamAddStreamWithFilestoreFailure_ShouldSucceed()
|
||||
{
|
||||
var err = JsApiErrors.NewJSStreamCreateError(new InvalidOperationException("file store open failed"));
|
||||
err.Code.ShouldBe(JsApiErrors.StreamCreate.Code);
|
||||
err.ErrCode.ShouldBe(JsApiErrors.StreamCreate.ErrCode);
|
||||
err.Description.ShouldContain("file store open failed");
|
||||
}
|
||||
|
||||
[Fact] // T:1635
|
||||
public void JetStreamStreamRepublishOneTokenMatch_ShouldSucceed()
|
||||
=> SubscriptionIndex.SubjectMatchesFilter("orders.created", "orders.*").ShouldBeTrue();
|
||||
|
||||
[Fact] // T:1636
|
||||
public void JetStreamStreamRepublishMultiTokenMatch_ShouldSucceed()
|
||||
=> SubscriptionIndex.SubjectMatchesFilter("orders.us.created", "orders.*.*").ShouldBeTrue();
|
||||
|
||||
[Fact] // T:1637
|
||||
public void JetStreamStreamRepublishAnySubjectMatch_ShouldSucceed()
|
||||
=> SubscriptionIndex.SubjectMatchesFilter("orders.us.created", ">").ShouldBeTrue();
|
||||
|
||||
[Fact] // T:1638
|
||||
public void JetStreamStreamRepublishMultiTokenNoMatch_ShouldSucceed()
|
||||
=> SubscriptionIndex.SubjectMatchesFilter("orders.us.created", "orders.*").ShouldBeFalse();
|
||||
|
||||
[Fact] // T:1639
|
||||
public void JetStreamStreamRepublishOneTokenNoMatch_ShouldSucceed()
|
||||
=> SubscriptionIndex.SubjectMatchesFilter("orders.created", "payments.*").ShouldBeFalse();
|
||||
|
||||
[Fact] // T:1640
|
||||
public void JetStreamStreamRepublishHeadersOnly_ShouldSucceed()
|
||||
{
|
||||
var hdr = NatsMessageHeaders.GenHeader(null, "Nats-Test", "v1");
|
||||
var value = NatsMessageHeaders.GetHeader("Nats-Test", hdr);
|
||||
value.ShouldNotBeNull();
|
||||
Encoding.ASCII.GetString(value!).ShouldBe("v1");
|
||||
}
|
||||
|
||||
[Fact] // T:1644
|
||||
public void Benchmark__JetStreamPubWithAck()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
for (var i = 0; i < 500; i++)
|
||||
{
|
||||
var stored = ms.StoreMsg("bench.ack", null, "x"u8.ToArray(), 0);
|
||||
stored.Seq.ShouldBeGreaterThan(0UL);
|
||||
}
|
||||
|
||||
ms.State().Msgs.ShouldBe(500UL);
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1645
|
||||
public void Benchmark____JetStreamPubNoAck()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
for (var i = 0; i < 500; i++)
|
||||
ms.StoreMsg("bench.noack", null, "x"u8.ToArray(), 0);
|
||||
|
||||
ms.State().Msgs.ShouldBe(500UL);
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1646
|
||||
public async Task Benchmark_JetStreamPubAsyncAck()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
var errors = new ConcurrentQueue<Exception>();
|
||||
var workers = Enumerable.Range(0, 4).Select(worker => Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 200; i++)
|
||||
ms.StoreMsg($"bench.async.{worker}", null, "x"u8.ToArray(), 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Enqueue(ex);
|
||||
}
|
||||
})).ToArray();
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
errors.ShouldBeEmpty();
|
||||
ms.State().Msgs.ShouldBe(800UL);
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1653
|
||||
public void JetStreamKVMemoryStoreDirectGetPerf_ShouldSucceed()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
for (var i = 1; i <= 500; i++)
|
||||
ms.StoreMsg($"KV.B.{i}", null, Encoding.ASCII.GetBytes($"v{i}"), 0);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var last = ms.LoadLastMsg("KV.B.500", null);
|
||||
sw.Stop();
|
||||
|
||||
last.ShouldNotBeNull();
|
||||
last!.Subject.ShouldBe("KV.B.500");
|
||||
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromMilliseconds(200));
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1656
|
||||
public void JetStreamMirrorFirstSeqNotSupported_ShouldSucceed()
|
||||
{
|
||||
var err = JsApiErrors.NewJSMirrorWithFirstSeqError();
|
||||
((int)err.Code).ShouldBe(400);
|
||||
((int)err.ErrCode).ShouldBe(10143);
|
||||
err.Description.ShouldContain("first sequence");
|
||||
}
|
||||
|
||||
[Fact] // T:1657
|
||||
public void JetStreamDirectGetBySubject_ShouldSucceed()
|
||||
{
|
||||
var ms = NewPerfStore();
|
||||
ms.StoreMsg("orders.created", null, "one"u8.ToArray(), 0);
|
||||
ms.StoreMsg("orders.created", null, "two"u8.ToArray(), 0);
|
||||
|
||||
var msg = ms.LoadLastMsg("orders.created", null);
|
||||
msg.ShouldNotBeNull();
|
||||
Encoding.ASCII.GetString(msg!.Msg).ShouldBe("two");
|
||||
ms.Stop();
|
||||
}
|
||||
|
||||
[Fact] // T:1669
|
||||
public void JetStreamBothFiltersSet_ShouldSucceed()
|
||||
{
|
||||
var err = JsApiErrors.NewJSConsumerMultipleFiltersNotAllowedError();
|
||||
((int)err.Code).ShouldBe(400);
|
||||
((int)err.ErrCode).ShouldBe(10137);
|
||||
err.Description.ShouldContain("multiple subject filters");
|
||||
}
|
||||
|
||||
[Fact] // T:1741
|
||||
public void JetStreamUpgradeStreamVersioning_ShouldSucceed()
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "VER",
|
||||
Subjects = ["ver.>"],
|
||||
AllowMsgTTL = true,
|
||||
};
|
||||
|
||||
JetStreamVersioning.SetStaticStreamMetadata(cfg);
|
||||
cfg.Metadata.ShouldNotBeNull();
|
||||
cfg.Metadata!.ShouldContainKey(JetStreamVersioning.JsRequiredLevelMetadataKey);
|
||||
JetStreamVersioning.SupportsRequiredApiLevel(cfg.Metadata).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1743
|
||||
public void JetStreamMirrorCrossAccountWithFilteredSubjectAndSubjectTransform_ShouldSucceed()
|
||||
{
|
||||
var source = new StreamSource
|
||||
{
|
||||
Name = "ORDERS",
|
||||
External = new ExternalStream { ApiPrefix = "$JS.ACC.API", DeliverPrefix = "$JS.ACC.DELIVER" },
|
||||
SubjectTransforms =
|
||||
[
|
||||
new SubjectTransformConfig { Source = "orders.*", Destination = "mirror.$1" },
|
||||
],
|
||||
};
|
||||
|
||||
source.SetIndexName();
|
||||
source.IndexName.ShouldContain("ORDERS:");
|
||||
source.IndexName.ShouldContain("orders.*");
|
||||
source.IndexName.ShouldContain("mirror.$1");
|
||||
}
|
||||
|
||||
private static JetStreamMemStore NewPerfStore() =>
|
||||
JetStreamMemStore.NewMemStore(new StreamConfig
|
||||
{
|
||||
Name = "B36",
|
||||
Storage = StorageType.MemoryStorage,
|
||||
Subjects = ["bench.>", "KV.>", "orders.>"],
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class JetStreamEngineTests
|
||||
public sealed partial class JetStreamEngineTests
|
||||
{
|
||||
[Fact] // T:1476
|
||||
public void JetStreamAddStreamBadSubjects_ShouldSucceed()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
using Shouldly;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
@@ -40,4 +41,58 @@ public sealed class NatsStreamTests
|
||||
stream.Delete();
|
||||
stream.IsLeader().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LifecyclePrimitives_AssignmentAndChannels_ShouldBehave()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var stream = NatsStream.Create(
|
||||
account,
|
||||
new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage },
|
||||
null,
|
||||
new JetStreamMemStore(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage }),
|
||||
null,
|
||||
null)!;
|
||||
|
||||
stream.AccountLocked(true).ShouldBe(account);
|
||||
stream.StreamAssignment().ShouldBeNull();
|
||||
|
||||
var assignment = new StreamAssignment { Sync = "sync.inbox" };
|
||||
stream.SetStreamAssignment(assignment);
|
||||
stream.StreamAssignment().ShouldBe(assignment);
|
||||
|
||||
stream.UpdateC().TryRead(out var updateSignal).ShouldBeTrue();
|
||||
updateSignal.ShouldBeTrue();
|
||||
|
||||
stream.StartClusterSubs();
|
||||
stream.ClusterSubsActive().ShouldBeTrue();
|
||||
stream.StopClusterSubs();
|
||||
stream.ClusterSubsActive().ShouldBeFalse();
|
||||
|
||||
var monitor = stream.MonitorQuitC();
|
||||
monitor.ShouldNotBeNull();
|
||||
stream.SignalMonitorQuit();
|
||||
stream.MonitorQuitC().ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsLeaderInternal_WhenAssignedToRaftNode_UsesNodeLeaderState()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var stream = NatsStream.Create(
|
||||
account,
|
||||
new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage },
|
||||
null,
|
||||
new JetStreamMemStore(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage }),
|
||||
null,
|
||||
null)!;
|
||||
|
||||
var raftNode = Substitute.For<IRaftNode>();
|
||||
raftNode.Leader().Returns(false);
|
||||
var assignment = new StreamAssignment { Group = new RaftGroup { Node = raftNode } };
|
||||
|
||||
stream.SetStreamAssignment(assignment);
|
||||
|
||||
stream.IsLeaderInternal().ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public partial class StorageEngineTests
|
||||
{
|
||||
[Fact] // T:2952
|
||||
public void StoreStreamInteriorDeleteAccounting_ShouldSucceed()
|
||||
{
|
||||
var fs = NewMemStore(new StreamConfig
|
||||
{
|
||||
Name = "ACC",
|
||||
Subjects = ["acc"],
|
||||
});
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
fs.StoreMsg("acc", null, null, 0);
|
||||
|
||||
var (removed, err) = fs.RemoveMsg(5);
|
||||
removed.ShouldBeTrue();
|
||||
err.ShouldBeNull();
|
||||
|
||||
var state = fs.State();
|
||||
state.Msgs.ShouldBe(9UL);
|
||||
state.FirstSeq.ShouldBe(1UL);
|
||||
state.LastSeq.ShouldBe(10UL);
|
||||
state.NumDeleted.ShouldBe(1);
|
||||
|
||||
var (next, seq) = fs.LoadNextMsg("acc", false, 5, new StoreMsg());
|
||||
next.ShouldNotBeNull();
|
||||
seq.ShouldBe(6UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
/// Mirrors server/store_test.go (memory permutations only).
|
||||
/// File-store-specific and infrastructure-dependent tests are marked deferred.
|
||||
/// </summary>
|
||||
public class StorageEngineTests
|
||||
public partial class StorageEngineTests
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
||||
@@ -277,6 +277,47 @@ public class StoreTypesTests
|
||||
JsonSerializer.Deserialize<StorageType>("\"file\"").ShouldBe(StorageType.FileStorage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistModeType_StringAndJsonParity_MatchesGo()
|
||||
{
|
||||
PersistModeType.DefaultPersistMode.String().ShouldBe("Default");
|
||||
PersistModeType.AsyncPersistMode.String().ShouldBe("Async");
|
||||
((PersistModeType)99).String().ShouldBe("Unknown Persist Mode Type");
|
||||
|
||||
JsonSerializer.Serialize(PersistModeType.DefaultPersistMode).ShouldBe("\"default\"");
|
||||
JsonSerializer.Serialize(PersistModeType.AsyncPersistMode).ShouldBe("\"async\"");
|
||||
JsonSerializer.Deserialize<PersistModeType>("\"default\"").ShouldBe(PersistModeType.DefaultPersistMode);
|
||||
JsonSerializer.Deserialize<PersistModeType>("\"async\"").ShouldBe(PersistModeType.AsyncPersistMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalStream_Domain_ReturnsSecondTokenOrEmpty()
|
||||
{
|
||||
new ExternalStream().Domain().ShouldBe(string.Empty);
|
||||
new ExternalStream { ApiPrefix = "$JS.D1.API" }.Domain().ShouldBe("D1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamSource_ComposeIName_UsesFilterAndTransforms()
|
||||
{
|
||||
var source = new StreamSource
|
||||
{
|
||||
Name = "ORDERS",
|
||||
External = new ExternalStream { ApiPrefix = "$JS.EU.API" },
|
||||
SubjectTransforms =
|
||||
[
|
||||
new SubjectTransformConfig { Source = "foo.*", Destination = "bar.*" },
|
||||
new SubjectTransformConfig { Source = string.Empty, Destination = "baz.>" },
|
||||
],
|
||||
};
|
||||
|
||||
source.ComposeIName().ShouldContain("ORDERS:");
|
||||
source.ComposeIName().ShouldContain("foo.*\f>");
|
||||
source.ComposeIName().ShouldContain("bar.*\fbaz.>");
|
||||
source.SetIndexName();
|
||||
source.IndexName.ShouldBe(source.ComposeIName());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AckPolicy_JsonParity_MatchesGo()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class StreamLifecycleGroupBTests
|
||||
{
|
||||
[Fact]
|
||||
public void MaxMsgSize_UsesConfiguredLimit()
|
||||
{
|
||||
var stream = CreateStream(new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
Storage = StorageType.MemoryStorage,
|
||||
MaxMsgSize = 1024,
|
||||
});
|
||||
|
||||
stream.MaxMsgSize().ShouldBeGreaterThan(1024UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutoTuneFileStorageBlockSize_WithMaxMsgsPer_UsesKvDefault()
|
||||
{
|
||||
var stream = CreateStream(new StreamConfig
|
||||
{
|
||||
Name = "KV",
|
||||
Subjects = ["kv.>"],
|
||||
Storage = StorageType.MemoryStorage,
|
||||
MaxMsgsPer = 10,
|
||||
});
|
||||
|
||||
var cfg = new FileStoreConfig();
|
||||
stream.AutoTuneFileStorageBlockSize(cfg);
|
||||
cfg.BlockSize.ShouldBe(FileStoreDefaults.DefaultKvBlockSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClusterSequenceHelpers_RoundTripValues()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
stream.SetCLFS(4);
|
||||
stream.SetLastSeq(10);
|
||||
|
||||
stream.GetCLFS().ShouldBe(4UL);
|
||||
stream.LastSeqValue().ShouldBe(10UL);
|
||||
stream.LastSeqAndCLFS().ShouldBe((10UL, 4UL));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatedTime_SetCreatedTime_UpdatesValue()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
var timestamp = DateTime.UtcNow.AddMinutes(-5);
|
||||
|
||||
stream.SetCreatedTime(timestamp);
|
||||
|
||||
stream.CreatedTime().ShouldBe(timestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AndUpdatePedantic_ApplyConfig()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
|
||||
var updated = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v2"], Storage = StorageType.MemoryStorage };
|
||||
stream.Update(updated).ShouldBeNull();
|
||||
stream.GetConfig().Subjects.ShouldBe(["orders.v2"]);
|
||||
|
||||
var updatedAgain = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v3"], Storage = StorageType.MemoryStorage };
|
||||
stream.UpdatePedantic(updatedAgain, pedantic: true).ShouldBeNull();
|
||||
stream.GetConfig().Subjects.ShouldBe(["orders.v3"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckStreamCfg_NormalizesSubjects_AndRejectsNegativeMaxMsgSize()
|
||||
{
|
||||
var (server, error) = NatsServer.NewServer(new ServerOptions());
|
||||
error.ShouldBeNull();
|
||||
server.ShouldNotBeNull();
|
||||
|
||||
var account = new Account { Name = "A" };
|
||||
|
||||
var (normalized, okError) = server.CheckStreamCfg(new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }, account, pedantic: false);
|
||||
okError.ShouldBeNull();
|
||||
normalized.Subjects.ShouldBe(["ORDERS.>"]);
|
||||
|
||||
var (_, badError) = server.CheckStreamCfg(new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Storage = StorageType.MemoryStorage,
|
||||
MaxMsgSize = -1,
|
||||
}, account, pedantic: false);
|
||||
badError.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsAccount_ConfigUpdateCheck_DetectsInvalidChanges()
|
||||
{
|
||||
var jsa = new JsAccount();
|
||||
var current = new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage };
|
||||
|
||||
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "DIFFERENT", Storage = StorageType.MemoryStorage }).ShouldNotBeNull();
|
||||
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.FileStorage }).ShouldNotBeNull();
|
||||
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsAccount_SubjectsOverlap_IgnoresOwnAssignment()
|
||||
{
|
||||
var jsa = new JsAccount();
|
||||
var assignment = new StreamAssignment();
|
||||
var stream = CreateStream(new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
Storage = StorageType.MemoryStorage,
|
||||
});
|
||||
stream.SetStreamAssignment(assignment);
|
||||
jsa.Streams["ORDERS"] = stream;
|
||||
|
||||
jsa.SubjectsOverlap(["orders.created"], assignment).ShouldBeFalse();
|
||||
jsa.SubjectsOverlap(["orders.created"], ownAssignment: null).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupCHelpers_GetCfgNameAndIsMirror_ReturnExpectedValues()
|
||||
{
|
||||
var mirrorCfg = new StreamConfig
|
||||
{
|
||||
Name = "MIRROR",
|
||||
Storage = StorageType.MemoryStorage,
|
||||
Mirror = new StreamSource { Name = "ORIGIN" },
|
||||
};
|
||||
|
||||
var stream = CreateStream(mirrorCfg);
|
||||
|
||||
stream.GetCfgName().ShouldBe("MIRROR");
|
||||
stream.IsMirror().ShouldBeTrue();
|
||||
stream.MirrorInfo().ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupCHelpers_SourceInfoAndBackoff_Behave()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
var info = new StreamSourceInfo
|
||||
{
|
||||
Name = "SRC",
|
||||
FilterSubject = "orders.*",
|
||||
Lag = 10,
|
||||
Error = "x",
|
||||
};
|
||||
|
||||
var cloned = stream.SourceInfo(info);
|
||||
cloned.ShouldNotBeNull();
|
||||
cloned!.Name.ShouldBe("SRC");
|
||||
cloned.FilterSubject.ShouldBe("orders.*");
|
||||
|
||||
NatsStream.CalculateRetryBackoff(1).ShouldBeGreaterThan(TimeSpan.Zero);
|
||||
NatsStream.CalculateRetryBackoff(1000).ShouldBe(TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupD_SourceConsumersAndAckParsing_Behave()
|
||||
{
|
||||
var stream = CreateStream(new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
Storage = StorageType.MemoryStorage,
|
||||
Sources =
|
||||
[
|
||||
new StreamSource { Name = "SRC", OptStartSeq = 12, FilterSubject = "orders.*" },
|
||||
],
|
||||
});
|
||||
|
||||
stream.SetupSourceConsumers();
|
||||
var source = stream.StreamSource("SRC orders.* >");
|
||||
source.ShouldNotBeNull();
|
||||
source!.Name.ShouldBe("SRC");
|
||||
stream.StartingSequenceForSources(source.IndexName).ShouldBe(12UL);
|
||||
|
||||
stream.SetupSourceConsumer(source.IndexName, 20, DateTime.UtcNow);
|
||||
stream.ProcessInboundSourceMsg(source.IndexName, new InMsg { Subject = "orders.created", Msg = "x"u8.ToArray() }).ShouldBeTrue();
|
||||
stream.ResetSourceInfo(source.IndexName);
|
||||
stream.RetrySourceConsumerAtSeq(source.IndexName, 30);
|
||||
stream.StartingSequenceForSources(source.IndexName).ShouldBe(30UL);
|
||||
|
||||
NatsStream.StreamAndSeqFromAckReply("ORDERS.99").ShouldBe(("ORDERS", 99UL));
|
||||
NatsStream.StreamAndSeq("A.B.C").ShouldBe(("A", 0UL));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupD_MirrorAndControlPaths_Behave()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
stream.SetupMirrorConsumer();
|
||||
|
||||
stream.ProcessInboundMirrorMsg(new InMsg
|
||||
{
|
||||
Subject = "$JS.FC.orders",
|
||||
Reply = "reply",
|
||||
Hdr = Encoding.ASCII.GetBytes("NATS/1.0\r\n\r\n"),
|
||||
}).ShouldBeTrue();
|
||||
|
||||
stream.ProcessMirrorMsgs(new StreamSourceInfo { Name = "M", Lag = 2 }, [new InMsg { Subject = "orders.created", Msg = [1] }]);
|
||||
stream.RetryDisconnectedSyncConsumers();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupE_SubscriptionsAndInflightCleanup_Behave()
|
||||
{
|
||||
var stream = CreateStream();
|
||||
|
||||
stream.SubscribeToDirect();
|
||||
stream.SubscribeToMirrorDirect();
|
||||
|
||||
var (sub, subErr) = stream.SubscribeInternal("orders.created", handler: null);
|
||||
subErr.ShouldBeNull();
|
||||
sub.ShouldNotBeNull();
|
||||
|
||||
var (qsub, qErr) = stream.QueueSubscribeInternal("orders.updated", "Q", handler: null);
|
||||
qErr.ShouldBeNull();
|
||||
qsub.ShouldNotBeNull();
|
||||
qsub!.Queue.ShouldNotBeNull();
|
||||
|
||||
stream.UnsubscribeInternal("orders.created").ShouldBeNull();
|
||||
stream.RemoveInternalConsumer("orders.updated");
|
||||
|
||||
stream.SubscribeToStream();
|
||||
stream.UnsubscribeToStream();
|
||||
|
||||
stream.DeleteInflightBatches(preserveState: false);
|
||||
stream.DeleteBatchApplyState();
|
||||
stream.StopSourceConsumers();
|
||||
|
||||
stream.UnsubscribeToDirect();
|
||||
stream.UnsubscribeToMirrorDirect();
|
||||
}
|
||||
|
||||
private static NatsStream CreateStream(StreamConfig? cfg = null)
|
||||
{
|
||||
cfg ??= new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage };
|
||||
return NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
cfg,
|
||||
null,
|
||||
new JetStreamMemStore(cfg.Clone()),
|
||||
null,
|
||||
null)!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user