task3(batch39): implement dispatch core and ack-floor processing

This commit is contained in:
Joseph Doherty
2026-03-01 01:16:27 -05:00
parent 0760c550b4
commit f537612d7c
4 changed files with 478 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class NatsConsumerDispatchTests
{
[Fact]
public void ProcessWaiting_EndOfStream_ShouldExpireNoWaitRequests()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ProcessNextMsgRequest("_INBOX.a", Encoding.UTF8.GetBytes("{\"batch\":1,\"no_wait\":true}")).ShouldBeTrue();
var result = consumer.ProcessWaiting(endOfStream: true);
result.Expired.ShouldBe(1);
result.Waiting.ShouldBe(0);
consumer.CheckWaitingForInterest().ShouldBeFalse();
}
[Fact]
public void HbTimer_HeartbeatConfigured_ShouldReturnTimer()
{
var consumer = CreatePullConsumer(maxWaiting: 8, heartbeat: TimeSpan.FromMilliseconds(50));
var (duration, timer) = consumer.HbTimer();
duration.ShouldBe(TimeSpan.FromMilliseconds(50));
timer.ShouldNotBeNull();
timer!.Dispose();
}
[Fact]
public void CheckAckFloor_WithPendingEntries_ShouldAdvanceFloor()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ApplyState(new ConsumerState
{
Delivered = new SequencePair { Consumer = 20, Stream = 20 },
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
Pending = new Dictionary<ulong, Pending>
{
[10] = new Pending { Sequence = 3, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
},
});
consumer.CheckAckFloor();
var state = consumer.ReadStoredState();
state.AckFloor.Stream.ShouldBe(9UL);
state.AckFloor.Consumer.ShouldBe(2UL);
}
[Fact]
public void ProcessInboundAcks_QueuedAck_ShouldAdvanceAckFloor()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.PushAck("$JS.ACK.2.8.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK"));
var processed = consumer.ProcessInboundAcks(CancellationToken.None);
var state = consumer.GetConsumerState();
processed.ShouldBe(1);
state.AckFloor.Stream.ShouldBe(8UL);
}
[Fact]
public void ProcessInboundNextMsgReqs_QueuedRequest_ShouldPopulateWaitingQueue()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ProcessNextMsgReq("_INBOX.next", Encoding.UTF8.GetBytes("{\"batch\":2}"));
var processed = consumer.ProcessInboundNextMsgReqs(CancellationToken.None);
var pending = consumer.PendingRequests();
processed.ShouldBe(1);
pending.ShouldContainKey("_INBOX.next");
pending["_INBOX.next"].N.ShouldBe(2);
}
[Fact]
public void PendingCounters_AndAckReply_ShouldTrackValues()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ApplyState(new ConsumerState
{
Delivered = new SequencePair { Consumer = 10, Stream = 30 },
AckFloor = new SequencePair { Consumer = 7, Stream = 22 },
});
var (pending, error) = consumer.CheckNumPending();
error.ShouldBeNull();
pending.ShouldBe(8UL);
consumer.NumPending().ShouldBe(8UL);
consumer.CheckNumPendingOnEOF();
consumer.SetMaxPendingBytes(256);
var ackReply = consumer.AckReply(30, 11, 1, 12345, pending);
ackReply.ShouldContain("$JS.ACK.1.30.11.12345.8");
}
[Fact]
public void SendIdleHeartbeat_ShouldReturnFormattedHeartbeat()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ApplyState(new ConsumerState
{
Delivered = new SequencePair { Consumer = 5, Stream = 9 },
AckFloor = new SequencePair { Consumer = 4, Stream = 8 },
});
var heartbeat = consumer.SendIdleHeartbeat("$JS.HEARTBEAT");
heartbeat.ShouldContain("100 Idle Heartbeat");
heartbeat.ShouldContain("Nats-Last-Consumer: 5");
heartbeat.ShouldContain("Nats-Last-Stream: 9");
}
[Fact]
public void LoopAndGatherMsgs_WithPendingEntries_ShouldDeliverMessages()
{
var consumer = CreatePullConsumer(maxWaiting: 8);
consumer.ApplyState(new ConsumerState
{
Delivered = new SequencePair { Consumer = 0, Stream = 0 },
AckFloor = new SequencePair { Consumer = 0, Stream = 0 },
Pending = new Dictionary<ulong, Pending>
{
[1] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
},
});
consumer.ProcessNextMsgRequest("_INBOX.loop", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeTrue();
var delivered = consumer.LoopAndGatherMsgs(4, CancellationToken.None);
delivered.ShouldBeGreaterThan(0);
consumer.GetConsumerState().Delivered.Stream.ShouldBeGreaterThanOrEqualTo(1UL);
}
private static NatsConsumer CreatePullConsumer(int maxWaiting, TimeSpan? heartbeat = null)
{
var stream = NatsStream.Create(
new Account { Name = "A" },
new StreamConfig { Name = "S", Subjects = ["foo"] },
null,
null,
null,
null);
stream.ShouldNotBeNull();
var config = new ConsumerConfig
{
Durable = "D",
MaxWaiting = maxWaiting,
Heartbeat = heartbeat ?? TimeSpan.Zero,
};
var consumer = NatsConsumer.Create(stream!, config, ConsumerAction.CreateOrUpdate, null);
consumer.ShouldNotBeNull();
return consumer!;
}
}