Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/NatsConsumerTests.cs

445 lines
18 KiB
C#

// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed partial class NatsConsumerTests
{
[Fact] // T:1304
public void JetStreamConsumerAndStreamNamesWithPathSeparators_ShouldSucceed()
{
var streamErr = JsApiErrors.NewJSStreamNameContainsPathSeparatorsError();
streamErr.Code.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.Code);
streamErr.ErrCode.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.ErrCode);
streamErr.Description.ShouldBe("Stream name can not contain path separators");
var consumerErr = JsApiErrors.NewJSConsumerNameContainsPathSeparatorsError();
consumerErr.Code.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.Code);
consumerErr.ErrCode.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.ErrCode);
consumerErr.Description.ShouldBe("Consumer name can not contain path separators");
}
[Fact]
public void Create_SetLeader_UpdateConfig_AndStop_ShouldBehave()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Storage = StorageType.FileStorage };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.CreateOrUpdate, null);
consumer.ShouldNotBeNull();
consumer!.IsLeader().ShouldBeFalse();
consumer.SetLeader(true, 3);
consumer.IsLeader().ShouldBeTrue();
var updated = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll };
consumer.UpdateConfig(updated);
consumer.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll);
var info = consumer.GetInfo();
info.Stream.ShouldBe("S");
info.Name.ShouldBe("D");
consumer.Stop();
consumer.IsLeader().ShouldBeFalse();
}
[Fact] // T:1364
public void SortingConsumerPullRequests_ShouldSucceed()
{
var q = new WaitQueue(max: 100);
q.AddPrioritized(new WaitingRequest { Reply = "1a", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "2a", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "1b", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "2b", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "1c", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "3a", PriorityGroup = new PriorityGroup { Priority = 3 }, N = 1 })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "2c", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 })
.ShouldBeTrue();
var expectedOrder = new[]
{
("1a", 1),
("1b", 1),
("1c", 1),
("2a", 2),
("2b", 2),
("2c", 2),
("3a", 3),
};
q.Len.ShouldBe(expectedOrder.Length);
foreach (var (reply, priority) in expectedOrder)
{
var current = q.Peek();
current.ShouldNotBeNull();
current!.Reply.ShouldBe(reply);
current.PriorityGroup.ShouldNotBeNull();
current.PriorityGroup!.Priority.ShouldBe(priority);
q.RemoveCurrent();
}
q.IsEmpty().ShouldBeTrue();
}
[Fact] // T:1365
public void WaitQueuePopAndRequeue_ShouldSucceed()
{
var q = new WaitQueue(max: 100);
q.AddPrioritized(new WaitingRequest { Reply = "1a", N = 2, PriorityGroup = new PriorityGroup { Priority = 1 } })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "1b", N = 1, PriorityGroup = new PriorityGroup { Priority = 1 } })
.ShouldBeTrue();
q.AddPrioritized(new WaitingRequest { Reply = "2a", N = 3, PriorityGroup = new PriorityGroup { Priority = 2 } })
.ShouldBeTrue();
var wr = q.PopAndRequeue();
wr.ShouldNotBeNull();
wr!.Reply.ShouldBe("1a");
wr.N.ShouldBe(1);
q.Len.ShouldBe(3);
wr = q.PopAndRequeue();
wr.ShouldNotBeNull();
wr!.Reply.ShouldBe("1b");
wr.N.ShouldBe(0);
q.Len.ShouldBe(2);
wr = q.PopAndRequeue();
wr.ShouldNotBeNull();
wr!.Reply.ShouldBe("1a");
wr.N.ShouldBe(0);
q.Len.ShouldBe(1);
q.Peek()!.Reply.ShouldBe("2a");
q.Peek()!.N.ShouldBe(3);
}
[Fact]
public void AddConsumerWithAction_CreateThenUpdate_ShouldRespectActions()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var (created, createErr) = stream!.AddConsumerWithAction(cfg, "D", ConsumerAction.Create, pedantic: false);
createErr.ShouldBeNull();
created.ShouldNotBeNull();
var updateCfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll };
var (updated, updateErr) = stream.AddConsumerWithAction(updateCfg, "D", ConsumerAction.Update, pedantic: false);
updateErr.ShouldBeNull();
updated.ShouldNotBeNull();
updated!.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll);
}
[Fact]
public void AddConsumer_WithAssignment_ShouldAttachAssignment()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var assignment = new ConsumerAssignment
{
Name = "D",
Stream = "S",
Group = new RaftGroup { Name = "RG", Peers = ["N1"] },
};
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var (consumer, err) = stream!.AddConsumerWithAssignment(cfg, "D", assignment, isRecovering: false, ConsumerAction.Create, pedantic: false);
err.ShouldBeNull();
consumer.ShouldNotBeNull();
consumer!.ConsumerAssignment().ShouldBeSameAs(assignment);
}
[Fact]
public void UpdateInactiveThreshold_AndPauseState_ShouldTrackConfigValues()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromSeconds(30) });
consumer.GetConfig().InactiveThreshold.ShouldBe(TimeSpan.FromSeconds(30));
var pauseUntil = DateTime.UtcNow.AddMinutes(1);
consumer.UpdatePauseState(new ConsumerConfig { PauseUntil = pauseUntil });
consumer.GetConfig().PauseUntil.ShouldBe(pauseUntil);
}
[Fact]
public void ConsumerAssignment_GetSet_ShouldRoundTrip()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
var assignment = new ConsumerAssignment
{
Name = "D",
Stream = "S",
Group = new RaftGroup { Name = "RG", Peers = ["N1"] },
};
consumer!.SetConsumerAssignment(assignment);
consumer.ConsumerAssignment().ShouldBeSameAs(assignment);
}
[Fact]
public async Task MonitorQuitC_AndSignalMonitorQuit_ShouldPublishQuitSignal()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
var monitor = consumer!.MonitorQuitC();
monitor.ShouldNotBeNull();
monitor!.TryRead(out _).ShouldBeFalse();
consumer.SignalMonitorQuit();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var signal = await monitor.ReadAsync(cts.Token);
signal.ShouldBeTrue();
}
[Fact]
public void SubscribeInternal_Unsubscribe_AndHasDeliveryInterest_ShouldTrackState()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(
stream!,
new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo" },
ConsumerAction.Create,
null);
consumer.ShouldNotBeNull();
consumer!.HasDeliveryInterest().ShouldBeFalse();
consumer.SubscribeInternal("deliver.foo").ShouldBeTrue();
consumer.CheckQueueInterest("deliver.foo").ShouldBeTrue();
consumer.HasDeliveryInterest().ShouldBeTrue();
consumer.Unsubscribe("deliver.foo").ShouldBeTrue();
consumer.HasDeliveryInterest().ShouldBeFalse();
}
[Fact]
public void AdvisoryHelpers_AndCreatedTime_ShouldBehave()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
consumer!.SendCreateAdvisory().ShouldBeTrue();
consumer.SendDeleteAdvisoryLocked().ShouldBeTrue();
consumer.SendPinnedAdvisoryLocked("pin-1").ShouldBeTrue();
consumer.SendUnpinnedAdvisoryLocked("pin-1").ShouldBeTrue();
consumer.SendPauseAdvisoryLocked(DateTime.UtcNow.AddMinutes(1)).ShouldBeTrue();
var created = DateTime.UtcNow.AddHours(-1);
consumer.SetCreatedTime(created);
consumer.CreatedTime().ShouldBe(created);
}
[Fact]
public void HandleClusterConsumerInfoRequest_WhenLeader_ReturnsInfo()
{
var account = new Account { Name = "A" };
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
var stream = NatsStream.Create(account, streamCfg, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
consumer!.HandleClusterConsumerInfoRequest().ShouldBeNull();
consumer.SetLeader(true, 1);
consumer.IsLeaderInternal().ShouldBeTrue();
consumer.HandleClusterConsumerInfoRequest().ShouldNotBeNull();
consumer.ClearNode();
}
[Fact]
public void UpdateDeliveryInterest_AndDeleteNotActive_ShouldReflectInterestState()
{
var account = new Account { Name = "A" };
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(
stream!,
new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", InactiveThreshold = TimeSpan.FromMilliseconds(20) },
ConsumerAction.Create,
null);
consumer.ShouldNotBeNull();
consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromMilliseconds(20) });
consumer.UpdateDeliveryInterest(localInterest: false).ShouldBeTrue();
Thread.Sleep(40);
consumer.IsClosed().ShouldBeTrue();
}
[Fact]
public void WatchGWinterest_AndRateLimit_ShouldExecuteWithoutErrors()
{
var account = new Account { Name = "A" };
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"], MaxMsgSize = 4096 }, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(
stream!,
new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", RateLimit = 8_000 },
ConsumerAction.Create,
null);
consumer.ShouldNotBeNull();
consumer!.SetRateLimitNeedsLocks();
consumer.WatchGWinterest();
consumer.SubscribeInternal("deliver.foo").ShouldBeTrue();
consumer.UpdateDeliveryInterest(localInterest: true).ShouldBeFalse();
consumer.HasDeliveryInterest().ShouldBeTrue();
}
[Fact]
public void AccountCheckNewConsumerConfig_InvalidPolicyChanges_ShouldFail()
{
var account = new Account { Name = "A" };
var current = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, DeliverPolicy = DeliverPolicy.DeliverAll };
var next = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll, DeliverPolicy = DeliverPolicy.DeliverAll };
var err = account.CheckNewConsumerConfig(current, next);
err.ShouldNotBeNull();
err.Message.ShouldContain("ack policy");
}
[Fact]
public void UpdateDeliverSubject_AndConfigsEqualSansDelivery_ShouldBehave()
{
var account = new Account { Name = "A" };
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
stream.ShouldNotBeNull();
var cfg = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit };
var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
consumer!.SubscribeInternal("deliver.a");
consumer.UpdateDeliverSubject("deliver.b").ShouldBeTrue();
consumer.SubscribeInternal("deliver.b");
consumer.HasDeliveryInterest().ShouldBeTrue();
var left = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit };
var right = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.b", AckPolicy = AckPolicy.AckExplicit };
NatsConsumer.ConfigsEqualSansDelivery(left, right).ShouldBeTrue();
}
[Fact]
public void AckFlow_NewMessage_Push_Process_Progress_UpdateSkipped_ShouldBehave()
{
var account = new Account { Name = "A" };
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }, ConsumerAction.Create, null);
consumer.ShouldNotBeNull();
var ack = NatsConsumer.NewJSAckMsg("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
ack.Subject.ShouldBe("a.b.10.20.1");
consumer!.PushAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
consumer.ProcessAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray());
consumer.ProgressUpdate(10);
consumer.UpdateSkipped(25);
var state = consumer.GetConsumerState();
state.AckFloor.Stream.ShouldBeGreaterThanOrEqualTo(10UL);
}
[Fact]
public void HasMaxDeliveries_ForceExpirePending_AndResetStartingSeq_ShouldBehave()
{
var account = new Account { Name = "A" };
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
stream.ShouldNotBeNull();
var consumer = NatsConsumer.Create(
stream!,
new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, MaxDeliver = 2, DeliverPolicy = DeliverPolicy.DeliverAll },
ConsumerAction.Create,
null);
consumer.ShouldNotBeNull();
consumer!.ProcessAck("a.b.5.7.1", "reply", 0, "-NAK"u8.ToArray());
consumer.HasMaxDeliveries(5).ShouldBeFalse();
consumer.HasMaxDeliveries(5).ShouldBeTrue();
consumer.ForceExpirePending();
var (seq, canRespond, err) = consumer.ResetStartingSeq(10, "reply");
err.ShouldBeNull();
seq.ShouldBe(10UL);
canRespond.ShouldBeTrue();
}
[Fact]
public void JsPubMsg_ReturnToPool_ShouldResetState()
{
var msg = new JsPubMsg
{
Subject = "foo",
Reply = "bar",
Hdr = [1, 2],
Msg = [3, 4],
Pa = new object(),
Sync = new object(),
};
msg.ReturnToPool();
msg.Subject.ShouldBe(string.Empty);
msg.Reply.ShouldBeNull();
msg.Hdr.ShouldBeNull();
msg.Msg.ShouldBeNull();
msg.Pa.ShouldBeNull();
msg.Sync.ShouldBeNull();
}
}