feat: add cluster-aware pending request tracking for pull consumers (Gap 3.14)
Adds ProposeWaitingRequest, RegisterClusterPending, RemoveClusterPending, GetClusterPendingRequests, and ClusterPendingCount to PullConsumerEngine, backed by a ConcurrentDictionary keyed by reply subject. Includes 10 xUnit tests covering quorum checks, pending tracking, and concurrent access patterns.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
using NATS.Server.JetStream.Storage;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.Subscriptions;
|
||||
@@ -93,6 +95,60 @@ public sealed class CompiledFilter
|
||||
|
||||
public sealed class PullConsumerEngine
|
||||
{
|
||||
// Go: consumer.go — cluster-wide pending pull request tracking keyed by reply subject.
|
||||
// Reference: golang/nats-server/server/consumer.go waitingRequestsPending / proposeWaitingRequest
|
||||
private readonly ConcurrentDictionary<string, PullWaitingRequest> _clusterPending =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Number of pending pull requests currently tracked across the cluster.
|
||||
/// Go reference: consumer.go — cluster pending count (waitingRequestsPending).
|
||||
/// </summary>
|
||||
public int ClusterPendingCount => _clusterPending.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Proposes a waiting pull request through the consumer's RAFT group.
|
||||
/// Returns true if quorum is available and the request was registered; false otherwise.
|
||||
/// Go reference: consumer.go proposeWaitingRequest — propose via consumer RAFT group.
|
||||
/// </summary>
|
||||
public bool ProposeWaitingRequest(PullWaitingRequest request, RaftGroup group)
|
||||
{
|
||||
if (!group.HasQuorum(group.Peers.Count))
|
||||
return false;
|
||||
|
||||
var replyKey = request.Reply ?? string.Empty;
|
||||
_clusterPending[replyKey] = request;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a pull request in the cluster pending tracker, keyed by reply subject.
|
||||
/// Go reference: consumer.go — cluster pending registration on proposal acceptance.
|
||||
/// </summary>
|
||||
public void RegisterClusterPending(PullWaitingRequest request)
|
||||
{
|
||||
var replyKey = request.Reply ?? string.Empty;
|
||||
_clusterPending[replyKey] = request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns a pending pull request by its reply subject.
|
||||
/// Returns null if no request is registered for that reply subject.
|
||||
/// Go reference: consumer.go — cluster pending removal on fulfillment or expiry.
|
||||
/// </summary>
|
||||
public PullWaitingRequest? RemoveClusterPending(string replySubject)
|
||||
{
|
||||
_clusterPending.TryRemove(replySubject, out var request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all currently pending pull requests tracked across the cluster.
|
||||
/// Go reference: consumer.go — enumerate waitingRequestsPending for expiry sweep.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<PullWaitingRequest> GetClusterPendingRequests()
|
||||
=> _clusterPending.Values.ToArray();
|
||||
|
||||
public async ValueTask<PullFetchBatch> FetchAsync(StreamHandle stream, ConsumerHandle consumer, int batch, CancellationToken ct)
|
||||
=> await FetchAsync(stream, consumer, new PullFetchRequest { Batch = batch }, ct);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user