feat(batch29): implement jetstream batching group-b validation and apply state

This commit is contained in:
Joseph Doherty
2026-03-01 01:51:17 -05:00
parent 02d3b610a1
commit 6ae023d4a7
3 changed files with 729 additions and 3 deletions

View File

@@ -143,4 +143,165 @@ public sealed class JetStreamBatchingCoreTests
store.Received(1).Stop();
JetStreamBatching.GlobalInflightBatches.ShouldBe(0);
}
[Fact]
public void Commit_DiffContainsState_UpdatesRuntimeState()
{
var diff = new BatchStagedDiff
{
MsgIds = new Dictionary<string, object?> { ["id-1"] = null },
Counter = new Dictionary<string, MsgCounterRunningTotal>
{
["foo"] = new() { Ops = 2, Total = new System.Numerics.BigInteger(5) },
},
Inflight = new Dictionary<string, InflightSubjectRunningTotal>
{
["foo"] = new() { Bytes = 10, Ops = 1 },
},
ExpectedPerSubject = new Dictionary<string, BatchExpectedPerSubject>
{
["foo"] = new() { SSeq = 3, ClSeq = 9 },
},
};
var state = new BatchRuntimeState();
JetStreamBatching.Commit(diff, state);
state.MsgIds.ContainsKey("id-1").ShouldBeTrue();
state.ClusteredCounterTotal["foo"].Total.ShouldBe(new System.Numerics.BigInteger(5));
state.Inflight["foo"].Ops.ShouldBe((ulong)1);
state.ExpectedPerSubjectSequence[9].ShouldBe("foo");
state.ExpectedPerSubjectInProcess.Contains("foo").ShouldBeTrue();
}
[Fact]
public void BatchApplyWrappers_ClearAndRejectPaths_UpdateState()
{
var entry = new TestCommittedEntry();
var apply = new BatchApply
{
Id = "batch-A",
Count = 3,
EntryStart = 4,
MaxApplied = 8,
Entries = [entry],
};
var context = new BatchApplyContext { Clfs = 10 };
JetStreamBatching.RejectBatchStateLocked(apply, context);
context.Clfs.ShouldBe((ulong)13);
entry.Returned.ShouldBeTrue();
apply.Id.ShouldBe(string.Empty);
apply.Count.ShouldBe((ulong)0);
apply.Id = "batch-B";
apply.Count = 2;
JetStreamBatching.ClearBatchStateLocked(apply);
apply.Id.ShouldBe(string.Empty);
apply.Count.ShouldBe((ulong)0);
}
[Fact]
public void CheckMsgHeadersPreClusteredProposal_ExpectedStreamMismatch_ReturnsNotMatchError()
{
var store = new JetStreamMemStore(new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage });
try
{
var hdr = NatsMessageHeaders.GenHeader(null, NatsHeaderConstants.JsExpectedStream, "OTHER");
var diff = new BatchStagedDiff();
var context = new BatchHeaderCheckContext
{
Store = store,
ClSeq = 1,
Clfs = 0,
MaxPayload = 1024,
StreamSubjects = ["ORDERS.>"],
};
var (_, _, _, apiError, error) = JetStreamBatching.CheckMsgHeadersPreClusteredProposal(
diff,
context,
"ORDERS.created",
hdr,
[],
sourced: false,
name: "ORDERS",
allowRollup: true,
denyPurge: false,
allowTtl: true,
allowMsgCounter: false,
allowMsgSchedules: false,
discard: DiscardPolicy.DiscardOld,
discardNewPer: false,
maxMsgSize: -1,
maxMsgs: -1,
maxMsgsPer: -1,
maxBytes: -1);
apiError.ShouldNotBeNull();
apiError!.ErrCode.ShouldBe(JsApiErrors.StreamNotMatch.ErrCode);
error.ShouldNotBeNull();
}
finally
{
store.Stop();
}
}
[Fact]
public void CheckMsgHeadersPreClusteredProposal_DuplicateMsgIdInBatch_ReturnsDuplicateError()
{
var store = new JetStreamMemStore(new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage });
try
{
var hdr = NatsMessageHeaders.GenHeader(null, NatsHeaderConstants.JsMsgId, "msg-1");
var diff = new BatchStagedDiff
{
MsgIds = new Dictionary<string, object?> { ["msg-1"] = null },
};
var context = new BatchHeaderCheckContext
{
Store = store,
ClSeq = 1,
MaxPayload = 1024,
};
var (_, _, _, apiError, error) = JetStreamBatching.CheckMsgHeadersPreClusteredProposal(
diff,
context,
"ORDERS.created",
hdr,
[],
sourced: false,
name: "ORDERS",
allowRollup: true,
denyPurge: false,
allowTtl: true,
allowMsgCounter: false,
allowMsgSchedules: false,
discard: DiscardPolicy.DiscardOld,
discardNewPer: false,
maxMsgSize: -1,
maxMsgs: -1,
maxMsgsPer: -1,
maxBytes: -1);
apiError.ShouldNotBeNull();
apiError!.ErrCode.ShouldBe(JsApiErrors.AtomicPublishContainsDuplicateMessage.ErrCode);
error.ShouldNotBeNull();
}
finally
{
store.Stop();
}
}
}
internal sealed class TestCommittedEntry : ICommittedEntry
{
public bool Returned { get; private set; }
public void ReturnToPool() => Returned = true;
}