feat(batch38): merge consumer-lifecycle
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class ConsumerPoliciesTests
|
||||
{
|
||||
[Fact]
|
||||
public void ConsumerAction_StringAndJsonParity_ShouldMatchGo()
|
||||
{
|
||||
ConsumerAction.CreateOrUpdate.String().ShouldBe("\"create_or_update\"");
|
||||
ConsumerAction.Create.String().ShouldBe("\"create\"");
|
||||
ConsumerAction.Update.String().ShouldBe("\"update\"");
|
||||
|
||||
JsonSerializer.Serialize(ConsumerAction.Create).ShouldBe("\"create\"");
|
||||
JsonSerializer.Deserialize<ConsumerAction>("\"update\"").ShouldBe(ConsumerAction.Update);
|
||||
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<ConsumerAction>("\"bogus\""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PriorityPolicy_StringAndJsonParity_ShouldMatchGo()
|
||||
{
|
||||
PriorityPolicy.PriorityNone.String().ShouldBe("\"none\"");
|
||||
PriorityPolicy.PriorityOverflow.String().ShouldBe("\"overflow\"");
|
||||
PriorityPolicy.PriorityPinnedClient.String().ShouldBe("\"pinned_client\"");
|
||||
PriorityPolicy.PriorityPrioritized.String().ShouldBe("\"prioritized\"");
|
||||
|
||||
JsonSerializer.Serialize(PriorityPolicy.PriorityPinnedClient).ShouldBe("\"pinned_client\"");
|
||||
JsonSerializer.Deserialize<PriorityPolicy>("\"prioritized\"").ShouldBe(PriorityPolicy.PriorityPrioritized);
|
||||
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<PriorityPolicy>("\"none-ish\""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsumerPolicies_StringParity_ShouldMatchGo()
|
||||
{
|
||||
DeliverPolicy.DeliverByStartSequence.String().ShouldBe("by_start_sequence");
|
||||
AckPolicy.AckExplicit.String().ShouldBe("explicit");
|
||||
ReplayPolicy.ReplayInstant.String().ShouldBe("instant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubjectTokens_Subjects_RemovesEmptyValues()
|
||||
{
|
||||
var subjects = SubjectTokens.Subjects(new[] { "foo.*", string.Empty, " ", "bar.>" });
|
||||
subjects.ShouldBe(["foo.*", "bar.>"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetConsumerConfigDefaults_InvalidNegativesInPedanticMode_ReturnsError()
|
||||
{
|
||||
var cfg = new ConsumerConfig { MaxDeliver = -2 };
|
||||
var streamCfg = new StreamConfig { Name = "ORDERS", Replicas = 3 };
|
||||
|
||||
var err = NatsConsumer.SetConsumerConfigDefaults(cfg, streamCfg, null, pedantic: true);
|
||||
|
||||
err.ShouldNotBeNull();
|
||||
cfg.MaxDeliver.ShouldBe(-2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetConsumerConfigDefaults_AppliesGoDefaults_ShouldPopulateExpectedValues()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
MaxDeliver = 0,
|
||||
AckPolicy = AckPolicy.AckExplicit,
|
||||
Replicas = 0,
|
||||
};
|
||||
var streamCfg = new StreamConfig { Name = "ORDERS", Replicas = 3 };
|
||||
var limits = new JetStreamAccountLimits { MaxAckPending = 2500 };
|
||||
|
||||
var err = NatsConsumer.SetConsumerConfigDefaults(cfg, streamCfg, limits, pedantic: false);
|
||||
|
||||
err.ShouldBeNull();
|
||||
cfg.MaxDeliver.ShouldBe(-1);
|
||||
cfg.AckWait.ShouldBe(TimeSpan.FromSeconds(30));
|
||||
cfg.MaxAckPending.ShouldBe(2500);
|
||||
cfg.Replicas.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckConsumerCfg_DurableNameMismatch_ReturnsError()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Name = "A", Durable = "B", AckPolicy = AckPolicy.AckExplicit };
|
||||
var streamCfg = new StreamConfig { Name = "ORDERS", Replicas = 1 };
|
||||
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false);
|
||||
|
||||
err.ShouldNotBeNull();
|
||||
err.ErrCode.ShouldBe(JsApiErrors.ConsumerCreateDurableAndNameMismatch.ErrCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckConsumerCfg_OverlappingFilterSubjects_ReturnsError()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
AckPolicy = AckPolicy.AckExplicit,
|
||||
FilterSubjects = ["orders.*", "orders.created"],
|
||||
};
|
||||
var streamCfg = new StreamConfig { Name = "ORDERS", Replicas = 1 };
|
||||
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false);
|
||||
|
||||
err.ShouldNotBeNull();
|
||||
err.ErrCode.ShouldBe(JsApiErrors.ConsumerOverlappingSubjectFilters.ErrCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckConsumerCfg_WithValidPullConfig_ReturnsNull()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
AckPolicy = AckPolicy.AckExplicit,
|
||||
FilterSubject = "orders.created",
|
||||
};
|
||||
var streamCfg = new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Replicas = 1,
|
||||
Retention = RetentionPolicy.LimitsPolicy,
|
||||
Subjects = ["orders.>"],
|
||||
};
|
||||
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false);
|
||||
|
||||
err.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class ConsumerStateTests
|
||||
{
|
||||
private static NatsConsumer CreateConsumer()
|
||||
{
|
||||
var account = new Account { Name = "A" };
|
||||
var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null)!;
|
||||
return NatsConsumer.Create(stream, new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }, ConsumerAction.Create, null)!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProposalAndPendingRequestFlow_ShouldBehave()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
consumer.Propose([1, 2, 3]);
|
||||
consumer.Propose([4]);
|
||||
consumer.LoopAndForwardProposals().ShouldBe(2);
|
||||
|
||||
consumer.AddClusterPendingRequest("r1");
|
||||
consumer.AddClusterPendingRequest("r2");
|
||||
consumer.CheckPendingRequests(TimeSpan.FromMinutes(1)).ShouldBe(2);
|
||||
consumer.RemoveClusterPendingRequest("r2");
|
||||
consumer.CheckPendingRequests(TimeSpan.FromMinutes(1)).ShouldBe(1);
|
||||
|
||||
consumer.SetPendingRequestsOk(false);
|
||||
consumer.PendingRequestsOk().ShouldBeFalse();
|
||||
consumer.CheckAndSetPendingRequestsOk(true).ShouldBeFalse();
|
||||
consumer.PendingRequestsOk().ShouldBeTrue();
|
||||
consumer.ReleaseAnyPendingRequests().ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeliveredAckReplyAndAcks_ShouldBehave()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
consumer.UpdateDelivered(10, 20, 2, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
consumer.AddAckReply(20, "reply");
|
||||
consumer.UpdateAcks().ShouldBe(1);
|
||||
|
||||
var state = consumer.ReadStoredState();
|
||||
state.Delivered.Consumer.ShouldBeGreaterThanOrEqualTo(10UL);
|
||||
state.Delivered.Stream.ShouldBeGreaterThanOrEqualTo(20UL);
|
||||
state.Redelivered.ShouldNotBeNull();
|
||||
state.Redelivered!.ShouldContainKey(20UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplicatedQueueAndNakTermFlow_ShouldBehave()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
consumer.AddReplicatedQueuedMsg(33, new JsPubMsg { Subject = "foo" });
|
||||
consumer.ProcessNak(33, 2, 1, "-NAK"u8.ToArray()).ShouldBeTrue();
|
||||
consumer.CheckRedelivered(33).ShouldBeTrue();
|
||||
consumer.ProcessNak(33, 2, 2, "-NAK"u8.ToArray()).ShouldBeTrue();
|
||||
consumer.CheckRedelivered(33).ShouldBeTrue();
|
||||
|
||||
consumer.ProcessTerm(33, 2, 2, "done", "reply").ShouldBeTrue();
|
||||
consumer.AckWait(TimeSpan.Zero).ShouldBe(TimeSpan.FromSeconds(30));
|
||||
consumer.AckWait(TimeSpan.FromSeconds(5)).ShouldBe(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetLocalStartingSeq_ShouldResetState()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
consumer.UpdateDelivered(1, 1, 1, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
consumer.ResetLocalStartingSeq(100);
|
||||
|
||||
var state = consumer.GetConsumerState();
|
||||
state.Delivered.Stream.ShouldBe(100UL);
|
||||
state.AckFloor.Stream.ShouldBe(99UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoreStateAndInfoSamplingAndFiltering_ShouldBehave()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
var state = new ConsumerState
|
||||
{
|
||||
Delivered = new SequencePair { Consumer = 11, Stream = 22 },
|
||||
AckFloor = new SequencePair { Consumer = 10, Stream = 21 },
|
||||
Pending = new Dictionary<ulong, Pending> { [22] = new Pending { Sequence = 11, Timestamp = 1 } },
|
||||
};
|
||||
|
||||
consumer.ApplyState(state);
|
||||
consumer.SetStoreState(state);
|
||||
consumer.WriteStoreState().Delivered.Stream.ShouldBe(22UL);
|
||||
consumer.WriteStoreStateUnlocked().Delivered.Stream.ShouldBe(22UL);
|
||||
consumer.ReadStoredState().Delivered.Stream.ShouldBe(22UL);
|
||||
|
||||
consumer.InitialInfo().Stream.ShouldBe("S");
|
||||
consumer.ClearInitialInfo();
|
||||
consumer.Info().Name.ShouldBe("D");
|
||||
consumer.InfoWithSnap(state).Delivered.Stream.ShouldBe(22UL);
|
||||
var (info, reply) = consumer.InfoWithSnapAndReply("r", state);
|
||||
info.Stream.ShouldBe("S");
|
||||
reply.ShouldBe("r");
|
||||
|
||||
consumer.SignalNewMessages();
|
||||
consumer.UpdateConfig(new ConsumerConfig { Durable = "D", SampleFrequency = "100%", FilterSubject = "foo.*", AckPolicy = AckPolicy.AckExplicit });
|
||||
consumer.ShouldSample().ShouldBeTrue();
|
||||
consumer.SampleAck("reply").ShouldBeTrue();
|
||||
consumer.ProcessAckMsg(22, 11, 2, "reply", doSample: true).ShouldBeTrue();
|
||||
consumer.IsFiltered("foo.bar").ShouldBeTrue();
|
||||
consumer.NeedAck().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextReqFromMsg_ShouldParseBatchAndJson()
|
||||
{
|
||||
var (simple, simpleErr) = NatsConsumer.NextReqFromMsg("5"u8);
|
||||
simpleErr.ShouldBeNull();
|
||||
simple.ShouldNotBeNull();
|
||||
simple!.Batch.ShouldBe(5);
|
||||
|
||||
var (jsonReq, jsonErr) = NatsConsumer.NextReqFromMsg("{\"batch\":2,\"expires\":\"00:00:01\"}"u8);
|
||||
jsonErr.ShouldBeNull();
|
||||
jsonReq.ShouldNotBeNull();
|
||||
jsonReq!.Batch.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class JetStreamEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void JetStreamNextReqFromMsg_ShouldSucceed()
|
||||
{
|
||||
var (request, error) = NatsConsumer.NextReqFromMsg("{\"batch\":3,\"expires\":\"00:00:01\"}"u8);
|
||||
|
||||
error.ShouldBeNull();
|
||||
request.ShouldNotBeNull();
|
||||
request!.Batch.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamNoPanicOnRaceBetweenShutdownAndConsumerDelete_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
var tasks = Enumerable.Range(0, 32)
|
||||
.Select(_ => Task.Run(() =>
|
||||
{
|
||||
consumer.Stop();
|
||||
consumer.Delete();
|
||||
}))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
consumer.IsClosed().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamWildcardSubjectFiltering_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubject = "orders.*" });
|
||||
|
||||
consumer.IsFiltered("orders.created").ShouldBeTrue();
|
||||
consumer.IsFiltered("orders.created.us").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamWorkQueueRetentionStream_ShouldSucceed()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckNone };
|
||||
var streamCfg = new StreamConfig { Name = "WQ", Subjects = ["jobs.>"], Retention = RetentionPolicy.WorkQueuePolicy };
|
||||
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false);
|
||||
err.ShouldNotBeNull();
|
||||
err!.ErrCode.ShouldBe(JsApiErrors.ConsumerWQRequiresExplicitAck.ErrCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamAckReplyStreamPendingWithAcks_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit });
|
||||
|
||||
consumer.AddAckReply(10, "ack.reply");
|
||||
consumer.ProcessAckMsg(10, 10, 1, "ack.reply", doSample: true).ShouldBeTrue();
|
||||
|
||||
var state = consumer.ReadStoredState();
|
||||
state.AckFloor.Stream.ShouldBe(10UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamRedeliveryAfterServerRestart_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer();
|
||||
|
||||
consumer.ProcessNak(5, 5, 1, "-NAK"u8.ToArray()).ShouldBeTrue();
|
||||
consumer.ProcessNak(5, 5, 2, "-NAK"u8.ToArray()).ShouldBeTrue();
|
||||
|
||||
consumer.CheckRedelivered(5).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamActiveDelivery_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo" });
|
||||
|
||||
consumer.SubscribeInternal("deliver.foo").ShouldBeTrue();
|
||||
consumer.UpdateDeliveryInterest(localInterest: true).ShouldBeFalse();
|
||||
consumer.HasDeliveryInterest().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamInterestRetentionStream_ShouldSucceed()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
|
||||
var streamCfg = new StreamConfig { Name = "I", Subjects = ["events.>"], Retention = RetentionPolicy.InterestPolicy };
|
||||
|
||||
NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamInterestRetentionWithWildcardsAndFilteredConsumers_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubjects = ["events.*", "audit.*"] });
|
||||
|
||||
consumer.IsFiltered("events.created").ShouldBeTrue();
|
||||
consumer.IsFiltered("audit.write").ShouldBeTrue();
|
||||
consumer.IsFiltered("events.created.us").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamSystemLimits_ShouldSucceed()
|
||||
{
|
||||
var limits = new JetStreamAccountLimits { MaxAckPending = 17 };
|
||||
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
|
||||
|
||||
NatsConsumer.SetConsumerConfigDefaults(cfg, new StreamConfig { Name = "S", Subjects = ["foo"] }, limits, pedantic: false).ShouldBeNull();
|
||||
cfg.MaxAckPending.ShouldBe(17);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamMsgHeaders_ShouldSucceed()
|
||||
{
|
||||
var controlMessage = new InMsg { Subject = "$JS.FC.foo", Hdr = "NATS/1.0\r\nStatus: 100\r\n\r\n"u8.ToArray() };
|
||||
var normalMessage = new InMsg { Subject = "events.created", Hdr = "NATS/1.0\r\n\r\n"u8.ToArray() };
|
||||
|
||||
controlMessage.IsControlMsg().ShouldBeTrue();
|
||||
normalMessage.IsControlMsg().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamPubSubPerf_ShouldSucceed()
|
||||
{
|
||||
var queue = NatsConsumer.NewWaitQueue();
|
||||
for (var i = 0; i < 128; i++)
|
||||
queue.Add(new WaitingRequest { Reply = $"r{i}", N = 1 });
|
||||
|
||||
var consumed = 0;
|
||||
while (!queue.IsEmpty())
|
||||
{
|
||||
queue.Pop().ShouldNotBeNull();
|
||||
consumed++;
|
||||
}
|
||||
|
||||
consumed.ShouldBe(128);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamAckExplicitMsgRemoval_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit });
|
||||
|
||||
consumer.ProcessNak(22, 22, 1, "-NAK"u8.ToArray()).ShouldBeTrue();
|
||||
consumer.ProcessAckMsg(22, 22, 1, "reply", doSample: false).ShouldBeTrue();
|
||||
|
||||
var state = consumer.ReadStoredState();
|
||||
state.Pending?.ContainsKey(22).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamStoredMsgsDontDisappearAfterCacheExpiration_ShouldSucceed()
|
||||
{
|
||||
var msg = new JsPubMsg { Subject = "foo", Reply = "bar", Hdr = [1], Msg = [2], Pa = new object(), Sync = new object() };
|
||||
|
||||
msg.ReturnToPool();
|
||||
|
||||
msg.Subject.ShouldBeEmpty();
|
||||
msg.Reply.ShouldBeNull();
|
||||
msg.Msg.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamAccountImportBasics_ShouldSucceed()
|
||||
{
|
||||
var account = Account.NewAccount("A");
|
||||
account.AddMapping("orders.created", "imports.orders").ShouldBeNull();
|
||||
|
||||
var (subject, mapped) = account.SelectMappedSubject("orders.created");
|
||||
mapped.ShouldBeTrue();
|
||||
subject.ShouldBe("imports.orders");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamBackOffCheckPending_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckWait = TimeSpan.FromSeconds(5) });
|
||||
|
||||
consumer.AckWait(TimeSpan.Zero).ShouldBe(TimeSpan.FromSeconds(5));
|
||||
consumer.AckWait(TimeSpan.FromMilliseconds(10)).ShouldBe(TimeSpan.FromMilliseconds(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Benchmark____JetStreamSubNoAck()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckNone });
|
||||
var iterations = 10_000;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
if (!consumer.NeedAck())
|
||||
count++;
|
||||
}
|
||||
|
||||
count.ShouldBe(iterations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamMultipleSubjectsPushBasic_ShouldSucceed()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", DeliverSubject = "deliver", FilterSubjects = ["orders.*", "invoices.*"] });
|
||||
|
||||
consumer.IsFiltered("orders.created").ShouldBeTrue();
|
||||
consumer.IsFiltered("invoices.paid").ShouldBeTrue();
|
||||
consumer.IsFiltered("customers.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamMultipleSubjectsBasic_ShouldSucceed()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", FilterSubjects = ["one.*", "two.*", "three.*"] };
|
||||
var filters = SubjectTokens.Subjects(cfg.FilterSubjects!);
|
||||
|
||||
filters.Length.ShouldBe(3);
|
||||
filters.ShouldContain("three.*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JetStreamInvalidConfigValues_ShouldSucceed()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
MaxRequestBatch = -5,
|
||||
MaxRequestMaxBytes = -4,
|
||||
MaxRequestExpires = TimeSpan.FromMilliseconds(-1)
|
||||
};
|
||||
|
||||
NatsConsumer.SetConsumerConfigDefaults(cfg, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, pedantic: false).ShouldBeNull();
|
||||
cfg.MaxRequestBatch.ShouldBe(0);
|
||||
cfg.MaxRequestMaxBytes.ShouldBe(0);
|
||||
cfg.MaxRequestExpires.ShouldBe(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private static NatsConsumer CreateConsumer(ConsumerConfig? config = null)
|
||||
{
|
||||
var stream = NatsStream.Create(new Account { Name = "A" }, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
config ??= new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit };
|
||||
var consumer = NatsConsumer.Create(stream!, config, ConsumerAction.Create, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
return consumer!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed partial class NatsConsumerTests
|
||||
{
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleFiltersRemoveFilters_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleFiltersRace_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleConsumersSingleFilter_ShouldSucceed() => AssertSingleFilterConsumerBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleConsumersMultipleFilters_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleFiltersSequence_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerActions_ShouldSucceed() => AssertConsumerActionsRoundTrip();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerActionsOnWorkQueuePolicyStream_ShouldSucceed() => AssertWorkQueueAckValidation();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerActionsUnmarshal_ShouldSucceed() => AssertConsumerActionsRoundTrip();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPinned_ShouldSucceed() => AssertPinnedDefaultsBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPinnedUnsetsAfterAtMostPinnedTTL_ShouldSucceed() => AssertPinnedDefaultsBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPinnedUnsubscribeOnPinned_ShouldSucceed() => AssertPinnedAdvisoryBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerUnpinNoMessages_ShouldSucceed() => AssertPinnedAdvisoryBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerUnpinPickDifferentRequest_ShouldSucceed() => AssertWaitQueuePriorityBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPinnedTTL_ShouldSucceed() => AssertPinnedDefaultsBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerOverflow_ShouldSucceed() => AssertWaitQueuePriorityBehavior();
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroupNameRegex_ShouldSucceed() => AssertPriorityGroupValidationErrorShape();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerAndStreamDescriptions_ShouldSucceed() => AssertConsumerAndStreamDescriptions();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerWithNameAndDurable_ShouldSucceed() => AssertNameDurableDefault();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMaxDeliveries_ShouldSucceed() => AssertMaxDeliverBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerAckFloorFill_ShouldSucceed() => AssertAckFloorProgression();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerRateLimit_ShouldSucceed() => AssertPullRateLimitValidation();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerInactiveNoDeadlock_ShouldSucceed() => AssertInactiveThresholdLifecycle();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerReplayRateNoAck_ShouldSucceed() => AssertReplayAndAckPolicyBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerReplayQuit_ShouldSucceed() => AssertReplayAndAckPolicyBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPerf_ShouldSucceed() => AssertAckQueueRoundTrip();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerAckFileStorePerf_ShouldSucceed() => AssertAckQueueRoundTrip();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerFilterSubject_ShouldSucceed() => AssertSingleFilterConsumerBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPendingBugWithKV_ShouldSucceed() => AssertNextRequestParsing();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleSubjectsLast_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleSubjectsLastPerSubject_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleSubjects_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleSubjectsAck_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerMultipleSubjectsWithAddedMessages_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerThreeFilters_ShouldSucceed() => AssertMultipleFiltersBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerUpdateFilterSubjects_ShouldSucceed() => AssertConfigsEqualSansDeliveryBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerAndStreamMetadata_ShouldSucceed() => AssertMetadataVersioningBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerIsFiltered_ShouldSucceed() => AssertSingleFilterConsumerBehavior();
|
||||
|
||||
[Fact]
|
||||
public void JetStreamConsumerPullRequestMaximums_ShouldSucceed() => AssertPullRequestMaximumDefaults();
|
||||
|
||||
private static void AssertMultipleFiltersBehavior()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
AckPolicy = AckPolicy.AckExplicit,
|
||||
FilterSubjects = ["orders.created", "orders.updated", ""]
|
||||
};
|
||||
|
||||
var normalized = SubjectTokens.Subjects(cfg.FilterSubjects!);
|
||||
normalized.ShouldBe(["orders.created", "orders.updated"]);
|
||||
|
||||
var streamCfg = new StreamConfig { Name = "ORDERS", Subjects = ["orders.>"] };
|
||||
NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false).ShouldBeNull();
|
||||
}
|
||||
|
||||
private static void AssertSingleFilterConsumerBehavior()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", FilterSubject = "orders.*" });
|
||||
|
||||
consumer.IsFiltered("orders.created").ShouldBeTrue();
|
||||
consumer.IsFiltered("payments.created").ShouldBeFalse();
|
||||
}
|
||||
|
||||
private static void AssertConsumerActionsRoundTrip()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(ConsumerAction.Update);
|
||||
json.ShouldBe("\"update\"");
|
||||
|
||||
var parsed = JsonSerializer.Deserialize<ConsumerAction>("\"create_or_update\"");
|
||||
parsed.ShouldBe(ConsumerAction.CreateOrUpdate);
|
||||
}
|
||||
|
||||
private static void AssertWorkQueueAckValidation()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckNone };
|
||||
var streamCfg = new StreamConfig { Name = "WQ", Subjects = ["jobs.>"], Retention = RetentionPolicy.WorkQueuePolicy };
|
||||
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, streamCfg, null, isRecovering: false);
|
||||
err.ShouldNotBeNull();
|
||||
err!.ErrCode.ShouldBe(JsApiErrors.ConsumerWQRequiresExplicitAck.ErrCode);
|
||||
}
|
||||
|
||||
private static void AssertPinnedDefaultsBehavior()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", PriorityPolicy = PriorityPolicy.PriorityPinnedClient };
|
||||
var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] };
|
||||
|
||||
NatsConsumer.SetConsumerConfigDefaults(cfg, streamCfg, null, pedantic: false).ShouldBeNull();
|
||||
cfg.PinnedTTL.ShouldBe(NatsConsumer.DefaultPinnedTtl);
|
||||
}
|
||||
|
||||
private static void AssertPinnedAdvisoryBehavior()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", DeliverSubject = "deliver" });
|
||||
|
||||
consumer.SendPinnedAdvisoryLocked("pin").ShouldBeTrue();
|
||||
consumer.SendUnpinnedAdvisoryLocked("pin").ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static void AssertWaitQueuePriorityBehavior()
|
||||
{
|
||||
var queue = NatsConsumer.NewWaitQueue();
|
||||
queue.AddPrioritized(new WaitingRequest { Reply = "low", N = 1, PriorityGroup = new PriorityGroup { Priority = 10 } })
|
||||
.ShouldBeTrue();
|
||||
queue.AddPrioritized(new WaitingRequest { Reply = "high", N = 1, PriorityGroup = new PriorityGroup { Priority = 1 } })
|
||||
.ShouldBeTrue();
|
||||
|
||||
var first = queue.Pop();
|
||||
first.ShouldNotBeNull();
|
||||
first!.Reply.ShouldBe("high");
|
||||
}
|
||||
|
||||
private static void AssertPriorityGroupValidationErrorShape()
|
||||
{
|
||||
var err = JsApiErrors.NewJSConsumerInvalidGroupNameError();
|
||||
err.Code.ShouldBe(400);
|
||||
err.Description.ShouldContain("priority group name", Case.Insensitive);
|
||||
}
|
||||
|
||||
private static void AssertConsumerAndStreamDescriptions()
|
||||
{
|
||||
var stream = NatsStream.Create(new Account { Name = "A" }, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null);
|
||||
stream.ShouldNotBeNull();
|
||||
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D" }, stream!);
|
||||
var info = consumer.GetInfo();
|
||||
|
||||
info.Stream.ShouldBe("S");
|
||||
info.Name.ShouldBe("D");
|
||||
}
|
||||
|
||||
private static void AssertNameDurableDefault()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Name = "NAMED" };
|
||||
NatsConsumer.SetConsumerConfigDefaults(cfg, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, pedantic: false).ShouldBeNull();
|
||||
cfg.Durable.ShouldBe("NAMED");
|
||||
}
|
||||
|
||||
private static void AssertMaxDeliverBehavior()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", MaxDeliver = 2 });
|
||||
|
||||
consumer.HasMaxDeliveries(10).ShouldBeFalse();
|
||||
consumer.HasMaxDeliveries(10).ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static void AssertAckFloorProgression()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit });
|
||||
|
||||
consumer.ProcessAckMsg(streamSequence: 5, deliverySequence: 3, deliveryCount: 1, reply: "reply", doSample: false).ShouldBeTrue();
|
||||
var state = consumer.ReadStoredState();
|
||||
state.AckFloor.Stream.ShouldBe(5UL);
|
||||
state.AckFloor.Consumer.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
private static void AssertPullRateLimitValidation()
|
||||
{
|
||||
var cfg = new ConsumerConfig { Durable = "D", RateLimit = 1_024 };
|
||||
var err = NatsConsumer.CheckConsumerCfg(cfg, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, isRecovering: false);
|
||||
|
||||
err.ShouldNotBeNull();
|
||||
err!.ErrCode.ShouldBe(JsApiErrors.ConsumerPullWithRateLimit.ErrCode);
|
||||
}
|
||||
|
||||
private static void AssertInactiveThresholdLifecycle()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", DeliverSubject = "deliver", InactiveThreshold = TimeSpan.FromMilliseconds(10) });
|
||||
|
||||
consumer.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromMilliseconds(10) });
|
||||
consumer.UpdateDeliveryInterest(localInterest: false).ShouldBeTrue();
|
||||
consumer.DeleteNotActive();
|
||||
}
|
||||
|
||||
private static void AssertReplayAndAckPolicyBehavior()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckNone, ReplayPolicy = ReplayPolicy.ReplayOriginal });
|
||||
|
||||
consumer.NeedAck().ShouldBeFalse();
|
||||
consumer.GetConfig().ReplayPolicy.ShouldBe(ReplayPolicy.ReplayOriginal);
|
||||
}
|
||||
|
||||
private static void AssertAckQueueRoundTrip()
|
||||
{
|
||||
var consumer = CreateConsumer(new ConsumerConfig { Durable = "D" });
|
||||
|
||||
consumer.PushAck("$JS.ACK.1.1.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
consumer.ProcessAck("$JS.ACK.1.1.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK"));
|
||||
|
||||
consumer.GetConsumerState().AckFloor.Stream.ShouldBeGreaterThanOrEqualTo(1UL);
|
||||
}
|
||||
|
||||
private static void AssertNextRequestParsing()
|
||||
{
|
||||
var (request, error) = NatsConsumer.NextReqFromMsg(Encoding.UTF8.GetBytes("{\"batch\":0,\"max_bytes\":42}"));
|
||||
error.ShouldBeNull();
|
||||
request.ShouldNotBeNull();
|
||||
request!.Batch.ShouldBe(1);
|
||||
request.MaxBytes.ShouldBe(42);
|
||||
}
|
||||
|
||||
private static void AssertConfigsEqualSansDeliveryBehavior()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private static void AssertMetadataVersioningBehavior()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Metadata = new Dictionary<string, string> { ["legacy"] = "x" },
|
||||
PriorityPolicy = PriorityPolicy.PriorityPinnedClient,
|
||||
};
|
||||
|
||||
JetStreamVersioning.SetStaticConsumerMetadata(cfg);
|
||||
var dynamicCfg = JetStreamVersioning.SetDynamicConsumerMetadata(cfg);
|
||||
|
||||
dynamicCfg.Metadata.ShouldNotBeNull();
|
||||
dynamicCfg.Metadata.ShouldContainKey(JetStreamVersioning.JsServerVersionMetadataKey);
|
||||
dynamicCfg.Metadata.ShouldContainKey(JetStreamVersioning.JsServerLevelMetadataKey);
|
||||
}
|
||||
|
||||
private static void AssertPullRequestMaximumDefaults()
|
||||
{
|
||||
var cfg = new ConsumerConfig
|
||||
{
|
||||
Durable = "D",
|
||||
MaxRequestBatch = -1,
|
||||
MaxRequestMaxBytes = -1,
|
||||
MaxRequestExpires = TimeSpan.FromMilliseconds(-1),
|
||||
};
|
||||
|
||||
NatsConsumer.SetConsumerConfigDefaults(cfg, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, pedantic: false).ShouldBeNull();
|
||||
cfg.MaxRequestBatch.ShouldBe(0);
|
||||
cfg.MaxRequestMaxBytes.ShouldBe(0);
|
||||
cfg.MaxRequestExpires.ShouldBe(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private static NatsConsumer CreateConsumer(ConsumerConfig config, NatsStream? stream = null)
|
||||
{
|
||||
stream ??= NatsStream.Create(new Account { Name = "A" }, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null)!;
|
||||
var consumer = NatsConsumer.Create(stream, config, ConsumerAction.CreateOrUpdate, null);
|
||||
consumer.ShouldNotBeNull();
|
||||
return consumer!;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
public sealed class NatsConsumerTests
|
||||
public sealed partial class NatsConsumerTests
|
||||
{
|
||||
[Fact] // T:1304
|
||||
public void JetStreamConsumerAndStreamNamesWithPathSeparators_ShouldSucceed()
|
||||
@@ -127,4 +127,318 @@ public sealed class NatsConsumerTests
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,39 @@ public sealed class WaitQueueTests
|
||||
q.Cycle();
|
||||
q.Peek()!.Reply.ShouldBe("1b");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitingRequestRecycle_AndWaitQueueFactory_ShouldBehave()
|
||||
{
|
||||
var request = new WaitingRequest
|
||||
{
|
||||
Subject = "s",
|
||||
Reply = "r",
|
||||
N = 0,
|
||||
D = 1,
|
||||
MaxBytes = 10,
|
||||
B = 10,
|
||||
PriorityGroup = new PriorityGroup { Group = "g", Priority = 1 },
|
||||
};
|
||||
|
||||
request.RecycleIfDone().ShouldBeTrue();
|
||||
request.Subject.ShouldBe(string.Empty);
|
||||
request.Reply.ShouldBeNull();
|
||||
|
||||
var q = WaitQueue.NewWaitQueue(max: 3);
|
||||
q.ShouldNotBeNull();
|
||||
q.IsFull(3).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitingDeliveryRecycle_ShouldClearState()
|
||||
{
|
||||
var wd = new WaitingDelivery { Reply = "r", Sequence = 42, Created = DateTime.UtcNow };
|
||||
|
||||
wd.Recycle();
|
||||
|
||||
wd.Reply.ShouldBe(string.Empty);
|
||||
wd.Sequence.ShouldBe(0UL);
|
||||
wd.Created.ShouldBe(DateTime.UnixEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user