feat(jetstream): add API leader forwarding and stream purge options (C7+C8)
C7: JetStreamApiRouter now checks leadership before mutating operations. Non-leader nodes return error code 10003 with a leader_hint field. JetStreamMetaGroup gains IsLeader() and Leader for cluster-aware routing. C8: StreamApiHandlers.HandlePurge accepts PurgeRequest options (filter, seq, keep). StreamManager.PurgeEx implements subject-filtered purge, sequence-based purge, keep-last-N, and filter+keep combinations.
This commit is contained in:
@@ -103,6 +103,97 @@ public sealed class StreamManager
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended purge with optional subject filter, sequence cutoff, and keep-last-N.
|
||||
/// Returns the number of messages purged, or -1 if the stream was not found.
|
||||
/// Go reference: jetstream_api.go:1200-1350 — purge options: filter, seq, keep.
|
||||
/// </summary>
|
||||
public long PurgeEx(string name, string? filter, ulong? seq, ulong? keep)
|
||||
{
|
||||
if (!_streams.TryGetValue(name, out var stream))
|
||||
return -1;
|
||||
if (stream.Config.Sealed || stream.Config.DenyPurge)
|
||||
return -1;
|
||||
|
||||
// No options — purge everything (backward-compatible with the original Purge).
|
||||
if (filter is null && seq is null && keep is null)
|
||||
{
|
||||
var stateBefore = stream.Store.GetStateAsync(default).GetAwaiter().GetResult();
|
||||
var count = stateBefore.Messages;
|
||||
stream.Store.PurgeAsync(default).GetAwaiter().GetResult();
|
||||
return (long)count;
|
||||
}
|
||||
|
||||
var messages = stream.Store.ListAsync(default).GetAwaiter().GetResult();
|
||||
long purged = 0;
|
||||
|
||||
// Filter + Keep: keep last N per matching subject.
|
||||
if (filter is not null && keep is not null)
|
||||
{
|
||||
var matching = messages
|
||||
.Where(m => SubjectMatch.MatchLiteral(m.Subject, filter))
|
||||
.GroupBy(m => m.Subject, StringComparer.Ordinal);
|
||||
|
||||
foreach (var group in matching)
|
||||
{
|
||||
var ordered = group.OrderByDescending(m => m.Sequence).ToList();
|
||||
foreach (var msg in ordered.Skip((int)keep.Value))
|
||||
{
|
||||
if (stream.Store.RemoveAsync(msg.Sequence, default).GetAwaiter().GetResult())
|
||||
purged++;
|
||||
}
|
||||
}
|
||||
|
||||
return purged;
|
||||
}
|
||||
|
||||
// Filter only: remove all messages matching the subject pattern.
|
||||
if (filter is not null)
|
||||
{
|
||||
// If seq is also set, only purge matching messages below that sequence.
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (!SubjectMatch.MatchLiteral(msg.Subject, filter))
|
||||
continue;
|
||||
if (seq is not null && msg.Sequence >= seq.Value)
|
||||
continue;
|
||||
if (stream.Store.RemoveAsync(msg.Sequence, default).GetAwaiter().GetResult())
|
||||
purged++;
|
||||
}
|
||||
|
||||
return purged;
|
||||
}
|
||||
|
||||
// Seq only: remove all messages with sequence < seq.
|
||||
if (seq is not null)
|
||||
{
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (msg.Sequence >= seq.Value)
|
||||
continue;
|
||||
if (stream.Store.RemoveAsync(msg.Sequence, default).GetAwaiter().GetResult())
|
||||
purged++;
|
||||
}
|
||||
|
||||
return purged;
|
||||
}
|
||||
|
||||
// Keep only (no filter): keep the last N messages globally, delete the rest.
|
||||
if (keep is not null)
|
||||
{
|
||||
var ordered = messages.OrderByDescending(m => m.Sequence).ToList();
|
||||
foreach (var msg in ordered.Skip((int)keep.Value))
|
||||
{
|
||||
if (stream.Store.RemoveAsync(msg.Sequence, default).GetAwaiter().GetResult())
|
||||
purged++;
|
||||
}
|
||||
|
||||
return purged;
|
||||
}
|
||||
|
||||
return purged;
|
||||
}
|
||||
|
||||
public StoredMessage? GetMessage(string name, ulong sequence)
|
||||
{
|
||||
if (!_streams.TryGetValue(name, out var stream))
|
||||
|
||||
Reference in New Issue
Block a user