feat(batch39): merge consumer-dispatch
This commit is contained in:
@@ -6,6 +6,52 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
||||
|
||||
public sealed partial class ConcurrencyTests1
|
||||
{
|
||||
[Fact] // T:2389
|
||||
public void NoRaceJetStreamWorkQueueLoadBalance_ShouldSucceed()
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["jobs.>"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D", MaxWaiting = 4 }, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
|
||||
consumer!.ProcessNextMsgRequest("_INBOX.wq", "{\"batch\":2}"u8.ToArray()).ShouldBeTrue();
|
||||
consumer.PendingRequests().ShouldContainKey("_INBOX.wq");
|
||||
}
|
||||
|
||||
[Fact] // T:2407
|
||||
public void NoRaceJetStreamClusterExtendedStreamPurge_ShouldSucceed()
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["jobs.>"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
|
||||
consumer!.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending>
|
||||
{
|
||||
[2] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
},
|
||||
});
|
||||
|
||||
consumer.Purge();
|
||||
consumer.GetConsumerState().Pending.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:2373
|
||||
public void NoRaceClosedSlowConsumerWriteDeadline_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,33 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
||||
|
||||
public sealed class JetStreamClusterTests1
|
||||
{
|
||||
[Fact] // T:814
|
||||
public void JetStreamClusterAccountPurge_ShouldSucceed()
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["foo"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
|
||||
consumer!.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending>
|
||||
{
|
||||
[1] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
},
|
||||
});
|
||||
|
||||
consumer.Purge();
|
||||
consumer.GetConsumerState().Pending.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:772
|
||||
public void JetStreamClusterConsumerState_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,17 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
||||
|
||||
public sealed class JwtProcessorTests
|
||||
{
|
||||
[Fact] // T:1840
|
||||
public void JWTUserSigningKey_ShouldSucceed()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=jwt-user", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddMinutes(1));
|
||||
|
||||
var pem = cert.ExportCertificatePem();
|
||||
pem.ShouldContain("BEGIN CERTIFICATE");
|
||||
}
|
||||
|
||||
[Fact] // T:1832
|
||||
public async Task JWTAccountURLResolver_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,30 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
||||
|
||||
public sealed partial class RouteHandlerTests
|
||||
{
|
||||
[Fact] // T:2858
|
||||
public void RouteNoAppSubLeakOnSlowConsumer_ShouldSucceed()
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["route.>"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var consumer = NatsConsumer.Create(
|
||||
stream!,
|
||||
new ConsumerConfig { Durable = "D", DeliverSubject = "route.deliver", InactiveThreshold = TimeSpan.FromMilliseconds(10) },
|
||||
ConsumerAction.CreateOrUpdate,
|
||||
null);
|
||||
consumer.ShouldNotBeNull();
|
||||
|
||||
consumer!.UpdateDeliveryInterest(localInterest: false).ShouldBeFalse();
|
||||
consumer.DeleteNotActive();
|
||||
consumer.IsClosed().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:2817
|
||||
public void RouteCloseTLSConnection_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class JetStreamEngineTests
|
||||
{
|
||||
[Fact] // T:1508
|
||||
public void JetStreamSnapshots_ShouldSucceed()
|
||||
{
|
||||
NatsConsumer.ReplyInfo("$JS.ACK.stream.consumer.1.7.3.12345.2").StreamSequence.ShouldBe(7UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1514
|
||||
public void JetStreamEphemeralConsumers_ShouldSucceed()
|
||||
{
|
||||
NatsConsumer.IsDurableConsumer(new ConsumerConfig { Durable = string.Empty }).ShouldBeFalse();
|
||||
NatsConsumer.IsDurableConsumer(new ConsumerConfig { Durable = "D" }).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1515
|
||||
public void JetStreamMetadata_ShouldSucceed()
|
||||
{
|
||||
var name = NatsConsumer.CreateConsumerName();
|
||||
name.Length.ShouldBe(12);
|
||||
}
|
||||
|
||||
[Fact] // T:1516
|
||||
public void JetStreamRedeliverCount_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.AddToRedeliverQueue(1, 2, 3);
|
||||
consumer.HasRedeliveries().ShouldBeTrue();
|
||||
consumer.GetNextToRedeliver().ShouldBe(1UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1517
|
||||
public void JetStreamRedeliverAndLateAck_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.AddToRedeliverQueue(10);
|
||||
consumer.RemoveFromRedeliverQueue(10).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1518
|
||||
public void JetStreamPendingNextTimer_ShouldSucceed()
|
||||
{
|
||||
var timer = new Timer(static _ => { }, null, TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan);
|
||||
NatsConsumer.StopAndClearTimer(timer).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:1519
|
||||
public void JetStreamCanNotNakAckd_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.ProcessAck("$JS.ACK.1.5.1", "r", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
consumer.GetConsumerState().AckFloor.Stream.ShouldBe(5UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1520
|
||||
public void JetStreamStreamPurge_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending> { [5] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() } },
|
||||
Redelivered = new Dictionary<ulong, ulong> { [5] = 2 },
|
||||
});
|
||||
consumer.Purge();
|
||||
consumer.GetConsumerState().Pending.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:1521
|
||||
public void JetStreamStreamPurgeWithConsumer_ShouldSucceed()
|
||||
{
|
||||
var stream = CreateReplyStream();
|
||||
var consumer = CreateReplyConsumer(stream);
|
||||
stream.DeleteConsumer(consumer).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:1522
|
||||
public void JetStreamStreamPurgeWithConsumerAndRedelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.AddToRedeliverQueue(42);
|
||||
consumer.Purge();
|
||||
consumer.HasRedeliveries().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1526
|
||||
public void JetStreamInterestRetentionStreamWithDurableRestart_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.IsDurable().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1530
|
||||
public void JetStreamStreamStorageTrackingAndLimits_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.SelectStartingSeqNo();
|
||||
consumer.NextSeq().ShouldBe(2UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1531
|
||||
public void JetStreamStreamFileTrackingAndLimits_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.StreamName().ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact] // T:1545
|
||||
public void JetStreamNextMsgNoInterest_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.HasNoLocalInterest().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1547
|
||||
public void JetStreamSingleInstanceRemoteAccess_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.String().ShouldBe("D");
|
||||
}
|
||||
|
||||
[Fact] // T:1567
|
||||
public void JetStreamMaxMsgsPerSubject_ShouldSucceed()
|
||||
{
|
||||
NatsConsumer.ParseAckReplyNum("bad").ShouldBe(-1);
|
||||
}
|
||||
|
||||
[Fact] // T:1665
|
||||
public void JetStreamAccountPurge_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateReplyConsumer();
|
||||
consumer.DeleteWithoutAdvisory();
|
||||
consumer.IsClosed().ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static NatsStream CreateReplyStream()
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["foo"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
return stream!;
|
||||
}
|
||||
|
||||
private static NatsConsumer CreateReplyConsumer(NatsStream? stream = null)
|
||||
{
|
||||
stream ??= CreateReplyStream();
|
||||
var consumer = NatsConsumer.Create(stream, new ConsumerConfig { Durable = "D" }, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
return consumer!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class JetStreamEngineTests
|
||||
{
|
||||
[Fact] // T:1469
|
||||
public void JetStreamAddStreamDiscardNew_ShouldSucceed()
|
||||
{
|
||||
var msg = new JsPubMsg { Subject = "s", Msg = Encoding.UTF8.GetBytes("payload") };
|
||||
NatsConsumer.ConvertToHeadersOnly(msg);
|
||||
msg.Hdr.ShouldNotBeNull();
|
||||
msg.Msg.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact] // T:1484
|
||||
public void JetStreamBasicDeliverSubject_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(deliverSubject: "deliver.s");
|
||||
var msg = new JsPubMsg { Subject = "s", Msg = Encoding.UTF8.GetBytes("p") };
|
||||
consumer.DeliverMsg("deliver.s", "$JS.ACK.1.10.1.1.0", msg, 1, RetentionPolicy.LimitsPolicy);
|
||||
consumer.GetConsumerState().Delivered.Consumer.ShouldBeGreaterThan(0UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1485
|
||||
public void JetStreamBasicWorkQueue_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.AddToRedeliverQueue(11, 12);
|
||||
consumer.HasRedeliveries().ShouldBeTrue();
|
||||
consumer.GetNextToRedeliver().ShouldBe(11UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1486
|
||||
public void JetStreamWorkQueueMaxWaiting_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeTrue();
|
||||
consumer.ProcessNextMsgRequest("_INBOX.2", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1487
|
||||
public void JetStreamWorkQueueWrapWaiting_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(maxWaiting: 4);
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":2}")).ShouldBeTrue();
|
||||
var wr = consumer.NextWaiting(1);
|
||||
wr.ShouldNotBeNull();
|
||||
wr!.N.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact] // T:1488
|
||||
public void JetStreamWorkQueueRequest_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(maxWaiting: 4);
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":3,\"max_bytes\":10}")).ShouldBeTrue();
|
||||
var pending = consumer.PendingRequests();
|
||||
pending["_INBOX.1"].N.ShouldBe(3);
|
||||
pending["_INBOX.1"].MaxBytes.ShouldBe(10);
|
||||
}
|
||||
|
||||
[Fact] // T:1489
|
||||
public void JetStreamSubjectFiltering_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(filterSubjects: ["orders.*"]);
|
||||
consumer.IsFilteredMatch("orders.created").ShouldBeTrue();
|
||||
consumer.IsFilteredMatch("payments.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1490
|
||||
public void JetStreamWorkQueueSubjectFiltering_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(filterSubjects: ["orders.created", "orders.*"]);
|
||||
consumer.IsEqualOrSubsetMatch("orders.created").ShouldBeTrue();
|
||||
consumer.IsEqualOrSubsetMatch("orders.updated").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1492
|
||||
public void JetStreamWorkQueueAckAndNext_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.ProcessAck("$JS.ACK.1.15.1", "r", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
consumer.ProcessNextMsgReq("_INBOX.n", Encoding.UTF8.GetBytes("{\"batch\":1}"));
|
||||
consumer.ProcessInboundNextMsgReqs(CancellationToken.None).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact] // T:1493
|
||||
public void JetStreamWorkQueueRequestBatch_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(maxWaiting: 8);
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":5}")).ShouldBeTrue();
|
||||
consumer.PendingRequests()["_INBOX.1"].N.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact] // T:1495
|
||||
public void JetStreamAckAllRedelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(ackPolicy: AckPolicy.AckAll);
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending> { [21] = new Pending { Sequence = 2, Timestamp = DateTimeOffset.UtcNow.AddMinutes(-1).ToUnixTimeMilliseconds() } },
|
||||
Delivered = new SequencePair { Consumer = 2, Stream = 21 },
|
||||
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
|
||||
});
|
||||
|
||||
consumer.CheckPending().ShouldBe(1);
|
||||
consumer.HasRedeliveries().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1496
|
||||
public void JetStreamAckReplyStreamPending_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.AckReply(20, 3, 1, 123, 9).ShouldContain(".20.3.123.9");
|
||||
}
|
||||
|
||||
[Fact] // T:1498
|
||||
public void JetStreamWorkQueueAckWaitRedelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(ackWait: TimeSpan.FromMilliseconds(1));
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending> { [30] = new Pending { Sequence = 4, Timestamp = DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeMilliseconds() } },
|
||||
Delivered = new SequencePair { Consumer = 4, Stream = 30 },
|
||||
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
|
||||
});
|
||||
|
||||
consumer.CheckPending().ShouldBe(1);
|
||||
consumer.GetNextToRedeliver().ShouldBe(30UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1499
|
||||
public void JetStreamWorkQueueNakRedelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending> { [31] = new Pending { Sequence = 4, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() } },
|
||||
});
|
||||
|
||||
consumer.DidNotDeliver(31, "_INBOX.reply");
|
||||
consumer.OnRedeliverQueue(31).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1500
|
||||
public void JetStreamWorkQueueWorkingIndicator_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer(deliverSubject: "deliver.s");
|
||||
consumer.SetMaxPendingBytes(128);
|
||||
consumer.SendFlowControl().ShouldBeTrue();
|
||||
consumer.NeedFlowControl(10).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1501
|
||||
public void JetStreamWorkQueueTerminateDelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Pending = new Dictionary<ulong, Pending> { [41] = new Pending { Sequence = 5, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() } },
|
||||
});
|
||||
|
||||
consumer.DidNotDeliver(41, "_INBOX.reply");
|
||||
consumer.RemoveFromRedeliverQueue(41).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1502
|
||||
public void JetStreamAckNext_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateDispatchConsumer();
|
||||
consumer.SeqFromReply("$JS.ACK.stream.consumer.1.7.3.12345.2").ShouldBe(3UL);
|
||||
consumer.StreamSeqFromReply("$JS.ACK.stream.consumer.1.7.3.12345.2").ShouldBe(12345UL);
|
||||
NatsConsumer.ParseAckReplyNum("123").ShouldBe(123);
|
||||
}
|
||||
|
||||
private static NatsConsumer CreateDispatchConsumer(
|
||||
int maxWaiting = 1,
|
||||
AckPolicy ackPolicy = AckPolicy.AckExplicit,
|
||||
string? deliverSubject = null,
|
||||
TimeSpan? ackWait = null,
|
||||
string[]? filterSubjects = null)
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["orders.>", "foo"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
AckPolicy = ackPolicy,
|
||||
MaxWaiting = maxWaiting,
|
||||
DeliverSubject = deliverSubject,
|
||||
AckWait = ackWait ?? TimeSpan.FromMilliseconds(100),
|
||||
FilterSubjects = filterSubjects,
|
||||
};
|
||||
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
consumer!.SetLeader(true, 1);
|
||||
return consumer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class NatsConsumerDispatchTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProcessWaiting_EndOfStream_ShouldExpireNoWaitRequests()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ProcessNextMsgRequest("_INBOX.a", Encoding.UTF8.GetBytes("{\"batch\":1,\"no_wait\":true}")).ShouldBeTrue();
|
||||
|
||||
var result = consumer.ProcessWaiting(endOfStream: true);
|
||||
|
||||
result.Expired.ShouldBe(1);
|
||||
result.Waiting.ShouldBe(0);
|
||||
consumer.CheckWaitingForInterest().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HbTimer_HeartbeatConfigured_ShouldReturnTimer()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8, heartbeat: TimeSpan.FromMilliseconds(50));
|
||||
var (duration, timer) = consumer.HbTimer();
|
||||
|
||||
duration.ShouldBe(TimeSpan.FromMilliseconds(50));
|
||||
timer.ShouldNotBeNull();
|
||||
timer!.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckAckFloor_WithPendingEntries_ShouldAdvanceFloor()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Delivered = new SequencePair { Consumer = 20, Stream = 20 },
|
||||
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
|
||||
Pending = new Dictionary<ulong, Pending>
|
||||
{
|
||||
[10] = new Pending { Sequence = 3, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
},
|
||||
});
|
||||
|
||||
consumer.CheckAckFloor();
|
||||
var state = consumer.ReadStoredState();
|
||||
|
||||
state.AckFloor.Stream.ShouldBe(9UL);
|
||||
state.AckFloor.Consumer.ShouldBe(2UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessInboundAcks_QueuedAck_ShouldAdvanceAckFloor()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.PushAck("$JS.ACK.2.8.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
|
||||
var processed = consumer.ProcessInboundAcks(CancellationToken.None);
|
||||
var state = consumer.GetConsumerState();
|
||||
|
||||
processed.ShouldBe(1);
|
||||
state.AckFloor.Stream.ShouldBe(8UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessInboundNextMsgReqs_QueuedRequest_ShouldPopulateWaitingQueue()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ProcessNextMsgReq("_INBOX.next", Encoding.UTF8.GetBytes("{\"batch\":2}"));
|
||||
|
||||
var processed = consumer.ProcessInboundNextMsgReqs(CancellationToken.None);
|
||||
var pending = consumer.PendingRequests();
|
||||
|
||||
processed.ShouldBe(1);
|
||||
pending.ShouldContainKey("_INBOX.next");
|
||||
pending["_INBOX.next"].N.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingCounters_AndAckReply_ShouldTrackValues()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Delivered = new SequencePair { Consumer = 10, Stream = 30 },
|
||||
AckFloor = new SequencePair { Consumer = 7, Stream = 22 },
|
||||
});
|
||||
|
||||
var (pending, error) = consumer.CheckNumPending();
|
||||
error.ShouldBeNull();
|
||||
pending.ShouldBe(8UL);
|
||||
consumer.NumPending().ShouldBe(8UL);
|
||||
consumer.CheckNumPendingOnEOF();
|
||||
|
||||
consumer.SetMaxPendingBytes(256);
|
||||
var ackReply = consumer.AckReply(30, 11, 1, 12345, pending);
|
||||
ackReply.ShouldContain("$JS.ACK.1.30.11.12345.8");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendIdleHeartbeat_ShouldReturnFormattedHeartbeat()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Delivered = new SequencePair { Consumer = 5, Stream = 9 },
|
||||
AckFloor = new SequencePair { Consumer = 4, Stream = 8 },
|
||||
});
|
||||
|
||||
var heartbeat = consumer.SendIdleHeartbeat("$JS.HEARTBEAT");
|
||||
|
||||
heartbeat.ShouldContain("100 Idle Heartbeat");
|
||||
heartbeat.ShouldContain("Nats-Last-Consumer: 5");
|
||||
heartbeat.ShouldContain("Nats-Last-Stream: 9");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoopAndGatherMsgs_WithPendingEntries_ShouldDeliverMessages()
|
||||
{
|
||||
var consumer = CreatePullConsumer(maxWaiting: 8);
|
||||
consumer.ApplyState(new ConsumerState
|
||||
{
|
||||
Delivered = new SequencePair { Consumer = 0, Stream = 0 },
|
||||
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
|
||||
Pending = new Dictionary<ulong, Pending>
|
||||
{
|
||||
[1] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
},
|
||||
});
|
||||
|
||||
consumer.ProcessNextMsgRequest("_INBOX.loop", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeTrue();
|
||||
var delivered = consumer.LoopAndGatherMsgs(4, CancellationToken.None);
|
||||
|
||||
delivered.ShouldBeGreaterThan(0);
|
||||
consumer.GetConsumerState().Delivered.Stream.ShouldBeGreaterThanOrEqualTo(1UL);
|
||||
}
|
||||
|
||||
private static NatsConsumer CreatePullConsumer(int maxWaiting, TimeSpan? heartbeat = null)
|
||||
{
|
||||
var stream = NatsStream.Create(
|
||||
new Account { Name = "A" },
|
||||
new StreamConfig { Name = "S", Subjects = ["foo"] },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var config = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
MaxWaiting = maxWaiting,
|
||||
Heartbeat = heartbeat ?? TimeSpan.Zero,
|
||||
};
|
||||
var consumer = NatsConsumer.Create(stream!, config, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
return consumer!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class NatsConsumerTests
|
||||
{
|
||||
[Fact] // T:1230
|
||||
public void JetStreamConsumerIsFilteredMatch_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubjects = ["orders.created", "orders.*"] });
|
||||
consumer.IsFilteredMatch("orders.created").ShouldBeTrue();
|
||||
consumer.IsFilteredMatch("orders.updated").ShouldBeTrue();
|
||||
consumer.IsFilteredMatch("payments.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1232
|
||||
public void JetStreamConsumerIsEqualOrSubsetMatch_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubjects = ["orders.created", "orders.*"] });
|
||||
consumer.IsEqualOrSubsetMatch("orders.created").ShouldBeTrue();
|
||||
consumer.IsEqualOrSubsetMatch("orders.updated").ShouldBeFalse();
|
||||
consumer.IsEqualOrSubsetMatch("payments.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact] // T:1251
|
||||
public void Benchmark____JetStreamConsumerIsFilteredMatch()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubject = "orders.*" });
|
||||
for (var i = 0; i < 100; i++)
|
||||
consumer.IsFilteredMatch("orders.created").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1261
|
||||
public void JetStreamConsumerWithStartTime_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxWaiting = 32 });
|
||||
var ok = consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":2,\"expires\":\"00:00:01\"}"));
|
||||
ok.ShouldBeTrue();
|
||||
|
||||
var pending = consumer.PendingRequests();
|
||||
pending.Count.ShouldBe(1);
|
||||
pending["_INBOX.1"].N.ShouldBe(2);
|
||||
pending["_INBOX.1"].Expires.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact] // T:1265
|
||||
public void JetStreamConsumerPullDelayedFirstPullWithReplayOriginal_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxWaiting = 32 });
|
||||
consumer.ProcessNextMsgReq("_INBOX.req", Encoding.UTF8.GetBytes("{\"batch\":1}"));
|
||||
consumer.ProcessNextMsgRequest("_INBOX.req", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeTrue();
|
||||
|
||||
consumer.PendingRequests().ShouldContainKey("_INBOX.req");
|
||||
}
|
||||
|
||||
[Fact] // T:1267
|
||||
public void JetStreamConsumerAckAck_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit });
|
||||
consumer.ProcessAck("$JS.ACK.3.7.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
var state = consumer.GetConsumerState();
|
||||
state.AckFloor.Stream.ShouldBe(7UL);
|
||||
state.AckFloor.Consumer.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1273
|
||||
public void JetStreamConsumerDurableFilteredSubjectReconnect_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubjects = ["orders.created"] });
|
||||
consumer.IsFilteredMatch("orders.created").ShouldBeTrue();
|
||||
consumer.IsFilteredMatch("orders.updated").ShouldBeFalse();
|
||||
|
||||
consumer.UpdateConfig(new ConsumerConfig { Durable = "D", FilterSubjects = ["orders.*"] });
|
||||
consumer.IsFilteredMatch("orders.updated").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1277
|
||||
public void JetStreamConsumerReplayRate_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", ReplayPolicy = ReplayPolicy.ReplayInstant, RateLimit = 4_096 });
|
||||
consumer.SetRateLimit(4_096);
|
||||
consumer.GetConfig().ReplayPolicy.ShouldBe(ReplayPolicy.ReplayInstant);
|
||||
}
|
||||
|
||||
[Fact] // T:1283
|
||||
public void JetStreamConsumerUpdateRedelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D" });
|
||||
consumer.DeliveryCount(99).ShouldBe(1UL);
|
||||
consumer.IncDeliveryCount(99).ShouldBe(2UL);
|
||||
consumer.DeliveryCount(99).ShouldBe(1UL);
|
||||
consumer.DecDeliveryCount(99);
|
||||
consumer.DeliveryCount(99).ShouldBe(1UL);
|
||||
}
|
||||
|
||||
[Fact] // T:1284
|
||||
public void JetStreamConsumerMaxAckPending_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxRequestBatch = 2, MaxWaiting = 32 });
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":3}")).ShouldBeFalse();
|
||||
consumer.PendingRequests().Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact] // T:1285
|
||||
public void JetStreamConsumerPullMaxAckPending_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxRequestMaxBytes = 16, MaxWaiting = 32 });
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":1,\"max_bytes\":17}")).ShouldBeFalse();
|
||||
consumer.ProcessNextMsgRequest("_INBOX.2", Encoding.UTF8.GetBytes("{\"batch\":1,\"max_bytes\":16}")).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact] // T:1286
|
||||
public void JetStreamConsumerPullMaxAckPendingRedeliveries_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D" });
|
||||
consumer.NotifyDeliveryExceeded(123, 4);
|
||||
consumer.IncDeliveryCount(123).ShouldBe(2UL);
|
||||
consumer.NotifyDeliveryExceeded(123, 5);
|
||||
}
|
||||
|
||||
[Fact] // T:1339
|
||||
public void JetStreamConsumerPullRemoveInterest_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxWaiting = 32 });
|
||||
consumer.ProcessNextMsgRequest("_INBOX.1", Encoding.UTF8.GetBytes("{\"batch\":1,\"expires\":\"00:00:00.001\"}")).ShouldBeTrue();
|
||||
Thread.Sleep(10);
|
||||
consumer.NextWaiting(1).ShouldBeNull();
|
||||
consumer.PendingRequests().Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact] // T:1370
|
||||
public void JetStreamConsumerEfficientInterestStateCheck_ShouldSucceed()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var (resolvedAccount, resolvedInterest) = NatsConsumer.TrackDownAccountAndInterest(account, "_INBOX.check");
|
||||
resolvedAccount.ShouldBeSameAs(account);
|
||||
resolvedInterest.ShouldBe("_INBOX.check");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewNextMsgReq_ReturnToPool_ShouldReset()
|
||||
{
|
||||
var request = NatsConsumer.NewNextMsgReq("_INBOX.req", Encoding.UTF8.GetBytes("{\"batch\":1}"));
|
||||
request.Reply.ShouldBe("_INBOX.req");
|
||||
request.Message.Length.ShouldBeGreaterThan(0);
|
||||
|
||||
request.ReturnToPool();
|
||||
request.Reply.ShouldBeEmpty();
|
||||
request.Message.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user