Files
natsdotnet/tests/NATS.Server.Tests/JetStream/Consumers/AckProcessorEnhancedTests.cs
Joseph Doherty 3183fd2dc7 feat(consumer): enhance AckProcessor with NAK delay, TERM, WPI, and maxAckPending
Add RedeliveryTracker constructor overload, Register(ulong, string) for
tracker-based ack-wait, ProcessAck(ulong) payload-free overload, GetDeadline,
CanRegister for maxAckPending enforcement, ParseAckType static parser, and
AckType enum. All existing API signatures are preserved; 9 new tests added in
AckProcessorEnhancedTests.cs with no regressions to existing 10 tests.
2026-02-25 02:15:46 -05:00

119 lines
4.0 KiB
C#

using NATS.Server.JetStream.Consumers;
namespace NATS.Server.Tests.JetStream.Consumers;
/// <summary>
/// Tests for enhanced AckProcessor with RedeliveryTracker integration.
/// Go reference: consumer.go:4854 (processInboundAcks).
/// </summary>
public class AckProcessorEnhancedTests
{
[Fact]
public void ProcessAck_removes_from_pending()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000);
var processor = new AckProcessor(tracker);
processor.Register(1, "deliver.subj");
processor.PendingCount.ShouldBe(1);
processor.ProcessAck(1);
processor.PendingCount.ShouldBe(0);
}
[Fact]
public void ProcessNak_schedules_redelivery()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000);
var processor = new AckProcessor(tracker);
processor.Register(1, "deliver.subj");
processor.ProcessNak(1, delayMs: 500);
processor.PendingCount.ShouldBe(1); // still pending until redelivered
}
[Fact]
public void ProcessTerm_removes_permanently()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000);
var processor = new AckProcessor(tracker);
processor.Register(1, "deliver.subj");
processor.ProcessTerm(1);
processor.PendingCount.ShouldBe(0);
processor.TerminatedCount.ShouldBe(1);
}
[Fact]
public void ProcessProgress_resets_deadline_to_full_ack_wait()
{
// Go: consumer.go — processAckProgress (+WPI): resets deadline to UtcNow + ackWait
// Verify the invariant: after ProcessProgress, the deadline is strictly in the future
// by at least (ackWait - epsilon) milliseconds, without relying on wall-clock delays.
var ackWaitMs = 1000;
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: ackWaitMs);
var processor = new AckProcessor(tracker);
processor.Register(1, "deliver.subj");
var before = DateTimeOffset.UtcNow;
processor.ProcessProgress(1);
var after = DateTimeOffset.UtcNow;
var deadline = processor.GetDeadline(1);
// Deadline must be at least (before + ackWait) and at most (after + ackWait + epsilon)
deadline.ShouldBeGreaterThanOrEqualTo(before.AddMilliseconds(ackWaitMs));
deadline.ShouldBeLessThanOrEqualTo(after.AddMilliseconds(ackWaitMs + 50));
}
[Fact]
public void MaxAckPending_blocks_new_registrations()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000);
var processor = new AckProcessor(tracker, maxAckPending: 2);
processor.Register(1, "d.1");
processor.Register(2, "d.2");
processor.CanRegister().ShouldBeFalse();
}
[Fact]
public void CanRegister_true_when_unlimited()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000);
var processor = new AckProcessor(tracker); // maxAckPending=0 means unlimited
processor.Register(1, "d.1");
processor.CanRegister().ShouldBeTrue();
}
[Fact]
public void ParseAckType_identifies_all_types()
{
AckProcessor.ParseAckType("+ACK"u8).ShouldBe(AckType.Ack);
AckProcessor.ParseAckType("-NAK"u8).ShouldBe(AckType.Nak);
AckProcessor.ParseAckType("+TERM"u8).ShouldBe(AckType.Term);
AckProcessor.ParseAckType("+WPI"u8).ShouldBe(AckType.Progress);
}
[Fact]
public void ParseAckType_returns_unknown_for_invalid()
{
AckProcessor.ParseAckType("GARBAGE"u8).ShouldBe(AckType.Unknown);
AckProcessor.ParseAckType(""u8).ShouldBe(AckType.Unknown);
}
[Fact]
public void GetDeadline_returns_min_for_unknown_sequence()
{
var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 1000);
var processor = new AckProcessor(tracker);
// Unknown sequence should return DateTimeOffset.MinValue
processor.GetDeadline(999).ShouldBe(DateTimeOffset.MinValue);
}
}