feat: add entry application pipeline for meta and stream RAFT groups (Gap 2.7)
Extend ApplyEntry in JetStreamMetaGroup with PeerAdd/PeerRemove dispatch to the existing Task 12 peer management methods. Add StreamMsgOp (Store, Remove, Purge) and ConsumerOp (Ack, Nak, Deliver, Term, Progress) enums plus ApplyStreamMsgOp and ApplyConsumerEntry methods to StreamReplicaGroup. Extend ApplyCommittedEntriesAsync to parse smsg:/centry: command prefixes and route to the new apply methods. Add MetaEntryType.PeerAdd/PeerRemove enum values. 35 new tests in EntryApplicationTests.cs, all passing.
This commit is contained in:
@@ -11,6 +11,15 @@ public sealed class StreamReplicaGroup
|
||||
private long _messageCount;
|
||||
private long _lastSequence;
|
||||
|
||||
// Track ack/nak/deliver counts for consumer entry apply.
|
||||
// Go reference: jetstream_cluster.go processConsumerEntries.
|
||||
private long _ackCount;
|
||||
private long _nakCount;
|
||||
private long _deliverCount;
|
||||
|
||||
// Last consumer op applied (used for diagnostics / unknown-op logging).
|
||||
private string _lastUnknownCommand = string.Empty;
|
||||
|
||||
public string StreamName { get; }
|
||||
public IReadOnlyList<RaftNode> Nodes => _nodes;
|
||||
public RaftNode Leader { get; private set; }
|
||||
@@ -27,6 +36,30 @@ public sealed class StreamReplicaGroup
|
||||
/// </summary>
|
||||
public long LastSequence => Interlocked.Read(ref _lastSequence);
|
||||
|
||||
/// <summary>
|
||||
/// Number of consumer acknowledgement ops applied.
|
||||
/// Go reference: jetstream_cluster.go processConsumerEntries — ack tracking.
|
||||
/// </summary>
|
||||
public long AckCount => Interlocked.Read(ref _ackCount);
|
||||
|
||||
/// <summary>
|
||||
/// Number of consumer negative-acknowledgement (nak) ops applied.
|
||||
/// Go reference: jetstream_cluster.go processConsumerEntries — nak tracking.
|
||||
/// </summary>
|
||||
public long NakCount => Interlocked.Read(ref _nakCount);
|
||||
|
||||
/// <summary>
|
||||
/// Number of consumer deliver ops applied.
|
||||
/// Go reference: jetstream_cluster.go processConsumerEntries — deliver tracking.
|
||||
/// </summary>
|
||||
public long DeliverCount => Interlocked.Read(ref _deliverCount);
|
||||
|
||||
/// <summary>
|
||||
/// The last command string that did not match any known entry type.
|
||||
/// Used to verify graceful unknown-entry handling.
|
||||
/// </summary>
|
||||
public string LastUnknownCommand => _lastUnknownCommand;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when leadership transfers to a new node.
|
||||
/// Go reference: jetstream_cluster.go leader change notification.
|
||||
@@ -187,6 +220,8 @@ public sealed class StreamReplicaGroup
|
||||
/// processes each one:
|
||||
/// "+peer:<id>" — adds the peer via ProposeAddPeerAsync
|
||||
/// "-peer:<id>" — removes the peer via ProposeRemovePeerAsync
|
||||
/// "smsg:<op>[,...]" — dispatches a stream message op (store/remove/purge)
|
||||
/// "centry:<op>" — dispatches a consumer op (ack/nak/deliver/term/progress)
|
||||
/// anything else — marks the entry as processed via MarkProcessed
|
||||
/// Go reference: jetstream_cluster.go:processStreamEntries (apply loop).
|
||||
/// </summary>
|
||||
@@ -207,13 +242,115 @@ public sealed class StreamReplicaGroup
|
||||
var peerId = entry.Command["-peer:".Length..];
|
||||
await Leader.ProposeRemovePeerAsync(peerId, ct);
|
||||
}
|
||||
else if (entry.Command.StartsWith("smsg:", StringComparison.Ordinal))
|
||||
{
|
||||
var opStr = entry.Command["smsg:".Length..];
|
||||
if (TryParseStreamMsgOp(opStr, out var msgOp))
|
||||
ApplyStreamMsgOp(msgOp, entry.Index);
|
||||
else
|
||||
_lastUnknownCommand = entry.Command;
|
||||
Leader.MarkProcessed(entry.Index);
|
||||
}
|
||||
else if (entry.Command.StartsWith("centry:", StringComparison.Ordinal))
|
||||
{
|
||||
var opStr = entry.Command["centry:".Length..];
|
||||
if (TryParseConsumerOp(opStr, out var consumerOp))
|
||||
ApplyConsumerEntry(consumerOp);
|
||||
else
|
||||
_lastUnknownCommand = entry.Command;
|
||||
Leader.MarkProcessed(entry.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastUnknownCommand = entry.Command;
|
||||
Leader.MarkProcessed(entry.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a stream-level message operation (Store, Remove, Purge) to the local state.
|
||||
/// Go reference: jetstream_cluster.go:2474-4261 processStreamEntries — per-message ops.
|
||||
/// </summary>
|
||||
public void ApplyStreamMsgOp(StreamMsgOp op, long index = 0)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case StreamMsgOp.Store:
|
||||
// Increment message count and track the sequence.
|
||||
ApplyMessage(index > 0 ? index : Interlocked.Read(ref _messageCount) + 1);
|
||||
break;
|
||||
|
||||
case StreamMsgOp.Remove:
|
||||
// Decrement message count; clamp to zero.
|
||||
long current;
|
||||
do
|
||||
{
|
||||
current = Interlocked.Read(ref _messageCount);
|
||||
if (current <= 0)
|
||||
return;
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _messageCount, current - 1, current) != current);
|
||||
break;
|
||||
|
||||
case StreamMsgOp.Purge:
|
||||
// Clear all messages: reset count and last sequence.
|
||||
Interlocked.Exchange(ref _messageCount, 0);
|
||||
Interlocked.Exchange(ref _lastSequence, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a consumer state entry (Ack, Nak, Deliver, Term, Progress).
|
||||
/// Go reference: jetstream_cluster.go processConsumerEntries.
|
||||
/// </summary>
|
||||
public void ApplyConsumerEntry(ConsumerOp op)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case ConsumerOp.Ack:
|
||||
Interlocked.Increment(ref _ackCount);
|
||||
break;
|
||||
case ConsumerOp.Nak:
|
||||
Interlocked.Increment(ref _nakCount);
|
||||
break;
|
||||
case ConsumerOp.Deliver:
|
||||
Interlocked.Increment(ref _deliverCount);
|
||||
break;
|
||||
// Term and Progress are no-ops in the model but are valid ops.
|
||||
case ConsumerOp.Term:
|
||||
case ConsumerOp.Progress:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseStreamMsgOp(string s, out StreamMsgOp op)
|
||||
{
|
||||
op = s switch
|
||||
{
|
||||
"store" => StreamMsgOp.Store,
|
||||
"remove" => StreamMsgOp.Remove,
|
||||
"purge" => StreamMsgOp.Purge,
|
||||
_ => (StreamMsgOp)(-1),
|
||||
};
|
||||
return (int)op >= 0;
|
||||
}
|
||||
|
||||
private static bool TryParseConsumerOp(string s, out ConsumerOp op)
|
||||
{
|
||||
op = s switch
|
||||
{
|
||||
"ack" => ConsumerOp.Ack,
|
||||
"nak" => ConsumerOp.Nak,
|
||||
"deliver" => ConsumerOp.Deliver,
|
||||
"term" => ConsumerOp.Term,
|
||||
"progress" => ConsumerOp.Progress,
|
||||
_ => (ConsumerOp)(-1),
|
||||
};
|
||||
return (int)op >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot of the current state at the leader's applied index and compacts
|
||||
/// the log up to that point.
|
||||
@@ -298,3 +435,35 @@ public sealed class LeaderChangedEventArgs(string previousLeaderId, string newLe
|
||||
public string NewLeaderId { get; } = newLeaderId;
|
||||
public int NewTerm { get; } = newTerm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message-level operation types applied to a stream's RAFT group.
|
||||
/// Go reference: jetstream_cluster.go:2474-4261 processStreamEntries — op constants.
|
||||
/// </summary>
|
||||
public enum StreamMsgOp
|
||||
{
|
||||
/// <summary>Store a new message; increments the message count and advances the sequence.</summary>
|
||||
Store,
|
||||
/// <summary>Remove a message by sequence; decrements the message count.</summary>
|
||||
Remove,
|
||||
/// <summary>Purge all messages; resets message count and last sequence to zero.</summary>
|
||||
Purge,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumer-state operation types applied to a consumer's RAFT group.
|
||||
/// Go reference: jetstream_cluster.go processConsumerEntries — op constants.
|
||||
/// </summary>
|
||||
public enum ConsumerOp
|
||||
{
|
||||
/// <summary>Consumer acknowledged a message.</summary>
|
||||
Ack,
|
||||
/// <summary>Consumer negatively acknowledged a message (redelivery scheduled).</summary>
|
||||
Nak,
|
||||
/// <summary>A message was delivered to the consumer.</summary>
|
||||
Deliver,
|
||||
/// <summary>Consumer terminated a message (no redelivery).</summary>
|
||||
Term,
|
||||
/// <summary>Consumer reported progress on a slow-ack message.</summary>
|
||||
Progress,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user