C3 – PushConsumerEngine delivery dispatch: - Add DeliverSubject property (mirrors consumer.go:1131 dsubj field) - Add StartDeliveryLoop / StopDeliveryLoop: background Task that drains ConsumerHandle.PushFrames and calls a sendMessage delegate per frame - Delivery loop honours AvailableAtUtc for rate-limiting (consumer.go:5120) - Data frames: HMSG headers Nats-Sequence, Nats-Time-Stamp, Nats-Subject (stream.go:586 JSSequence / JSTimeStamp / JSSubject constants) - Flow-control frames: "NATS/1.0 100 FlowControl Request" (consumer.go:5501) - Heartbeat frames: "NATS/1.0 100 Idle Heartbeat" (consumer.go:5222) - Add DeliverSubject field to ConsumerConfig (consumer.go:115) C4 – RedeliveryTracker with backoff schedules: - Schedule(seq, deliveryCount, ackWaitMs): computes deadline using backoff array indexed by (deliveryCount-1), clamped at last entry (consumer.go:5540) - GetDue(): returns sequences whose deadline has passed - Acknowledge(seq): removes sequence from tracking - IsMaxDeliveries(seq, maxDeliver): checks threshold for drop decision - Empty backoff array falls back to ackWaitMs Tests: 7 PushConsumerDelivery tests + 10 RedeliveryTracker tests (17 total)
199 lines
7.5 KiB
C#
199 lines
7.5 KiB
C#
// Go: consumer.go (trackPending ~line 5540, processNak, rdq/rdc map,
|
|
// addToRedeliverQueue, maxdeliver check)
|
|
using NATS.Server.JetStream.Consumers;
|
|
|
|
namespace NATS.Server.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();
|
|
}
|
|
}
|