// Go: consumer.go (trackPending ~line 5540, processNak, rdq/rdc map, // addToRedeliverQueue, maxdeliver check) using NATS.Server.JetStream.Consumers; namespace NATS.Server.JetStream.Tests.JetStream.Consumers; public class RedeliveryTrackerTests { // ------------------------------------------------------------------------- // Test 1 — Backoff array clamping at last entry for high delivery counts // // Go reference: consumer.go — backoff index = min(deliveries-1, len(backoff)-1) // so that sequences with delivery counts past the array length use the last // backoff value rather than going out of bounds. // ------------------------------------------------------------------------- [Fact] public async Task Schedule_clamps_backoff_at_last_entry_for_high_delivery_count() { var tracker = new RedeliveryTracker([1, 5000]); // delivery 1 → backoff[0] = 1ms tracker.Schedule(seq: 1, deliveryCount: 1); await Task.Delay(10); tracker.GetDue().ShouldContain(1UL); tracker.Acknowledge(1); // delivery 3 → index clamps to 1 → backoff[1] = 5000ms tracker.Schedule(seq: 1, deliveryCount: 3); tracker.GetDue().ShouldNotContain(1UL); } // ------------------------------------------------------------------------- // Test 2 — GetDue returns only entries whose deadline has passed // // Go reference: consumer.go — rdq items are eligible for redelivery only // once their scheduled deadline has elapsed. // ------------------------------------------------------------------------- [Fact] public async Task GetDue_returns_only_expired_entries() { var tracker = new RedeliveryTracker([1, 5000]); // 1ms backoff → will expire quickly tracker.Schedule(seq: 10, deliveryCount: 1); // 5000ms backoff → will not expire in test window tracker.Schedule(seq: 20, deliveryCount: 2); // Neither should be due yet immediately after scheduling tracker.GetDue().ShouldNotContain(10UL); await Task.Delay(15); var due = tracker.GetDue(); due.ShouldContain(10UL); due.ShouldNotContain(20UL); } // ------------------------------------------------------------------------- // Test 3 — Acknowledge removes the sequence from tracking // // Go reference: consumer.go — acking a sequence removes it from pending map // so it is never surfaced by GetDue again. // ------------------------------------------------------------------------- [Fact] public async Task Acknowledge_removes_sequence_from_tracking() { var tracker = new RedeliveryTracker([1]); tracker.Schedule(seq: 5, deliveryCount: 1); await Task.Delay(10); tracker.GetDue().ShouldContain(5UL); tracker.Acknowledge(5); tracker.IsTracking(5).ShouldBeFalse(); tracker.GetDue().ShouldNotContain(5UL); tracker.TrackedCount.ShouldBe(0); } // ------------------------------------------------------------------------- // Test 4 — IsMaxDeliveries returns true when threshold is reached // // Go reference: consumer.go — when rdc[sseq] >= MaxDeliver the sequence is // dropped from redelivery and never surfaced again. // ------------------------------------------------------------------------- [Fact] public void IsMaxDeliveries_returns_true_when_delivery_count_meets_threshold() { var tracker = new RedeliveryTracker([100]); tracker.Schedule(seq: 7, deliveryCount: 3); tracker.IsMaxDeliveries(7, maxDeliver: 3).ShouldBeTrue(); tracker.IsMaxDeliveries(7, maxDeliver: 4).ShouldBeFalse(); tracker.IsMaxDeliveries(7, maxDeliver: 2).ShouldBeTrue(); } // ------------------------------------------------------------------------- // Test 5 — IsMaxDeliveries returns false when maxDeliver is 0 (unlimited) // // Go reference: consumer.go — MaxDeliver <= 0 means unlimited redeliveries. // ------------------------------------------------------------------------- [Fact] public void IsMaxDeliveries_returns_false_when_maxDeliver_is_zero() { var tracker = new RedeliveryTracker([100]); tracker.Schedule(seq: 99, deliveryCount: 1000); tracker.IsMaxDeliveries(99, maxDeliver: 0).ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 6 — Empty backoff falls back to ackWait // // Go reference: consumer.go — when BackOff is empty the ack-wait duration is // used as the redelivery delay. // ------------------------------------------------------------------------- [Fact] public async Task Schedule_with_empty_backoff_falls_back_to_ackWait() { // Empty backoff array → fall back to ackWaitMs var tracker = new RedeliveryTracker([]); tracker.Schedule(seq: 1, deliveryCount: 1, ackWaitMs: 1); await Task.Delay(10); tracker.GetDue().ShouldContain(1UL); } // ------------------------------------------------------------------------- // Test 7 — Empty backoff with large ackWait does not expire prematurely // ------------------------------------------------------------------------- [Fact] public void Schedule_with_empty_backoff_and_large_ackWait_does_not_expire() { var tracker = new RedeliveryTracker([]); tracker.Schedule(seq: 2, deliveryCount: 1, ackWaitMs: 5000); tracker.GetDue().ShouldNotContain(2UL); } // ------------------------------------------------------------------------- // Test 8 — Schedule returns the deadline UTC time // // Go reference: consumer.go:5540 — trackPending stores the computed deadline. // ------------------------------------------------------------------------- [Fact] public void Schedule_returns_deadline_in_the_future() { var tracker = new RedeliveryTracker([100]); var before = DateTime.UtcNow; var deadline = tracker.Schedule(seq: 3, deliveryCount: 1); var after = DateTime.UtcNow; deadline.ShouldBeGreaterThanOrEqualTo(before); // Deadline should be ahead of scheduling time by at least the backoff value (deadline - after).TotalMilliseconds.ShouldBeGreaterThan(0); } // ------------------------------------------------------------------------- // Test 9 — Multiple sequences tracked independently // ------------------------------------------------------------------------- [Fact] public async Task Multiple_sequences_are_tracked_independently() { var tracker = new RedeliveryTracker([1, 5000]); tracker.Schedule(seq: 1, deliveryCount: 1); // 1ms → expires soon tracker.Schedule(seq: 2, deliveryCount: 2); // 5000ms → won't expire tracker.TrackedCount.ShouldBe(2); await Task.Delay(15); var due = tracker.GetDue(); due.ShouldContain(1UL); due.ShouldNotContain(2UL); tracker.Acknowledge(1); tracker.TrackedCount.ShouldBe(1); } // ------------------------------------------------------------------------- // Test 10 — IsMaxDeliveries returns false for untracked sequence // ------------------------------------------------------------------------- [Fact] public void IsMaxDeliveries_returns_false_for_untracked_sequence() { var tracker = new RedeliveryTracker([100]); tracker.IsMaxDeliveries(999, maxDeliver: 1).ShouldBeFalse(); } }