feat: add atomic batch publish engine & versioning support (Tasks 9-10)

- AtomicBatchPublishEngine: stage/commit/rollback semantics for batch publish
- JsVersioning: API level negotiation and stream/consumer metadata
- Fix NormalizeConfig missing AllowAtomicPublish, Metadata, PersistMode copy
- 46 batch publish tests + 67 versioning tests, all passing
This commit is contained in:
Joseph Doherty
2026-02-24 22:05:07 -05:00
parent cd009b9342
commit b80316a42f
10 changed files with 953 additions and 5 deletions

View File

@@ -191,6 +191,60 @@ internal sealed class JetStreamApiFixture : IAsyncDisposable
return Task.FromResult(new PubAck { ErrorCode = 404 });
}
/// <summary>
/// Publishes a batch message with the Nats-Batch-Id, Nats-Batch-Sequence (and optionally
/// Nats-Batch-Commit) headers simulated via PublishOptions.
/// Returns PubAck with ErrorCode set on error, empty BatchId on staged (flow-control), or
/// full ack with BatchId+BatchSize on commit.
/// </summary>
public Task<PubAck> BatchPublishAsync(
string subject,
string payload,
string batchId,
ulong batchSeq,
string? commitValue = null,
string? msgId = null,
ulong expectedLastSeq = 0,
string? expectedLastMsgId = null)
{
var options = new PublishOptions
{
BatchId = batchId,
BatchSeq = batchSeq,
BatchCommit = commitValue,
MsgId = msgId,
ExpectedLastSeq = expectedLastSeq,
ExpectedLastMsgId = expectedLastMsgId,
};
if (_publisher.TryCaptureWithOptions(subject, Encoding.UTF8.GetBytes(payload), options, out var ack))
return Task.FromResult(ack);
return Task.FromResult(new PubAck { ErrorCode = 404 });
}
public StreamConfig? GetStreamConfig(string streamName)
{
return _streamManager.TryGet(streamName, out var handle) ? handle.Config : null;
}
public bool UpdateStream(StreamConfig config)
{
var result = _streamManager.CreateOrUpdate(config);
return result.Error == null;
}
public JetStreamApiResponse UpdateStreamWithResult(StreamConfig config)
{
return _streamManager.CreateOrUpdate(config);
}
/// <summary>
/// Exposes the underlying JetStreamPublisher for advanced test scenarios
/// (e.g. calling ClearBatches to simulate a leader change).
/// </summary>
public JetStreamPublisher GetPublisher() => _publisher;
public Task<JetStreamApiResponse> RequestLocalAsync(string subject, string payload)
{
return Task.FromResult(_router.Route(subject, Encoding.UTF8.GetBytes(payload)));