// Go: consumer.go maxDeliver config — max delivery enforcement and advisory generation using NATS.Server.JetStream.Consumers; namespace NATS.Server.JetStream.Tests.JetStream.Consumers; public class MaxDeliveriesTests { // Test 1: MaxDeliver=0 means unlimited; many redeliveries do not terminate [Fact] public void MaxDeliver_zero_means_unlimited() { // Go: consumer.go — maxDeliver=0 disables the limit entirely var ack = new AckProcessor(); ack.MaxDeliver = 0; ack.Register(1, ackWaitMs: 5000); for (var i = 0; i < 100; i++) ack.ScheduleRedelivery(1, delayMs: 1); ack.PendingCount.ShouldBe(1); ack.ExceededCount.ShouldBe(0); ack.TerminatedCount.ShouldBe(0); } // Test 2: MaxDeliver=3 terminates on the 4th redelivery attempt [Fact] public void MaxDeliver_terminates_when_exceeded() { // Go: consumer.go — maxDeliver enforced in ScheduleRedelivery; Deliveries > maxDeliver → terminate var ack = new AckProcessor(); ack.MaxDeliver = 3; ack.Register(1, ackWaitMs: 5000); // Deliveries starts at 1 after Register; three more bumps → Deliveries reaches 4 ack.ScheduleRedelivery(1, delayMs: 1); // Deliveries = 2 (ok) ack.ScheduleRedelivery(1, delayMs: 1); // Deliveries = 3 (ok, at limit) ack.ScheduleRedelivery(1, delayMs: 1); // Deliveries = 4 (exceeded) ack.PendingCount.ShouldBe(0); ack.TerminatedCount.ShouldBe(1); } // Test 3: Exceeded sequence is added to the exceeded list [Fact] public void Exceeded_sequence_added_to_list() { // Go: consumer.go — exceeded sequences collected for advisory events var ack = new AckProcessor(); ack.MaxDeliver = 1; ack.Register(1, ackWaitMs: 5000); // Deliveries starts at 1; next call makes it 2 which exceeds maxDeliver=1 ack.ScheduleRedelivery(1, delayMs: 1); ack.ExceededCount.ShouldBe(1); ack.GetExceededSequences().ShouldContain((ulong)1); } // Test 4: DrainExceeded clears the list after returning sequences [Fact] public void DrainExceeded_clears_list() { // Go: consumer.go — drain after sending advisories to avoid duplicate events var ack = new AckProcessor(); ack.MaxDeliver = 1; ack.Register(1, ackWaitMs: 5000); ack.ScheduleRedelivery(1, delayMs: 1); ack.ExceededCount.ShouldBe(1); ack.DrainExceeded(); ack.ExceededCount.ShouldBe(0); ack.GetExceededSequences().ShouldBeEmpty(); } // Test 5: Exceeded sequence is moved to the terminated set [Fact] public void Exceeded_message_is_terminated() { // Go: consumer.go — exceeded sequence enters terminated set; cannot be redelivered var ack = new AckProcessor(); ack.MaxDeliver = 2; ack.Register(1, ackWaitMs: 5000); ack.ScheduleRedelivery(1, delayMs: 1); // Deliveries = 2 (at limit) ack.ScheduleRedelivery(1, delayMs: 1); // Deliveries = 3 (exceeded) // Sequence removed from pending and not redeliverable ack.PendingCount.ShouldBe(0); ack.TerminatedCount.ShouldBe(1); } // Test 6: ProcessNak triggers redelivery check through ScheduleRedelivery [Fact] public void ProcessNak_triggers_redelivery_check() { // Go: consumer.go — processNak calls ScheduleRedelivery which enforces maxDeliver var ack = new AckProcessor(); ack.MaxDeliver = 2; ack.Register(1, ackWaitMs: 5000); ack.ProcessNak(1); // Deliveries = 2 (at limit) ack.PendingCount.ShouldBe(1); ack.ExceededCount.ShouldBe(0); ack.ProcessNak(1); // Deliveries = 3 (exceeded) ack.PendingCount.ShouldBe(0); ack.ExceededCount.ShouldBe(1); } // Test 7: ScheduleRedelivery enforces max deliver regardless of whether it was called from expiry path [Fact] public void TryGetExpired_respects_max_deliver() { // Go: consumer.go — ScheduleRedelivery checks maxDeliver regardless of call site; // the expiry redelivery loop calls ScheduleRedelivery so maxDeliver is enforced there too. // We verify this by directly calling ScheduleRedelivery after the deadline would have passed, // without relying on wall-clock time. var ack = new AckProcessor(); ack.MaxDeliver = 1; ack.Register(1, ackWaitMs: 5000); // Deliveries starts at 1; calling ScheduleRedelivery makes it 2 which exceeds maxDeliver=1 // This is exactly what the expiry dispatch loop does: read expired seq, call ScheduleRedelivery ack.ScheduleRedelivery(1, delayMs: 1); ack.PendingCount.ShouldBe(0); ack.ExceededCount.ShouldBe(1); } // Test 8: MaxDeliver=-1 is treated as unlimited (negative values clamped to 0) [Fact] public void MaxDeliver_negative_treated_as_unlimited() { // Go: consumer.go — any non-positive maxDeliver is treated as unlimited var ack = new AckProcessor(); ack.MaxDeliver = -1; ack.Register(1, ackWaitMs: 5000); for (var i = 0; i < 50; i++) ack.ScheduleRedelivery(1, delayMs: 1); ack.PendingCount.ShouldBe(1); ack.ExceededCount.ShouldBe(0); ack.MaxDeliver.ShouldBe(0); // -1 clamped to 0 } // Test 9: ExceededPolicy defaults to Drop [Fact] public void DeliveryExceededPolicy_defaults_to_drop() { // Go: consumer.go — default behavior for exceeded messages is to drop them var ack = new AckProcessor(); ack.ExceededPolicy.ShouldBe(DeliveryExceededPolicy.Drop); } // Test 10: Multiple independent sequences can each exceed max deliveries [Fact] public void Multiple_sequences_can_exceed() { // Go: consumer.go — each sequence tracked independently; multiple can exceed in same window var ack = new AckProcessor(); ack.MaxDeliver = 1; ack.Register(1, ackWaitMs: 5000); ack.Register(2, ackWaitMs: 5000); ack.Register(3, ackWaitMs: 5000); // Each sequence starts at Deliveries=1; one call makes each exceed maxDeliver=1 ack.ScheduleRedelivery(1, delayMs: 1); ack.ScheduleRedelivery(2, delayMs: 1); ack.ScheduleRedelivery(3, delayMs: 1); ack.ExceededCount.ShouldBe(3); ack.PendingCount.ShouldBe(0); ack.TerminatedCount.ShouldBe(3); var exceeded = ack.GetExceededSequences(); exceeded.ShouldContain((ulong)1); exceeded.ShouldContain((ulong)2); exceeded.ShouldContain((ulong)3); } }