335 lines
10 KiB
C#
335 lines
10 KiB
C#
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.>"],
|
|
});
|
|
}
|