task3(batch39): implement dispatch core and ack-floor processing
This commit is contained in:
@@ -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!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user