feat: add filter skip tracking using SubjectMatch (Gap 3.10)
Add FilterSkipTracker using SubjectMatch.MatchLiteral() for NATS token-based filter matching with sorted-set skip sequence gap tracking. Includes 14 tests covering exact match, star/gt wildcards, multi-filter, counters, RecordSkip, NextUnskippedSequence, PurgeBelow, and Reset.
This commit is contained in:
115
src/NATS.Server/JetStream/Consumers/FilterSkipTracker.cs
Normal file
115
src/NATS.Server/JetStream/Consumers/FilterSkipTracker.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
// Go: consumer.go isFilteredMatch, skipMsgs tracking (~line 4200)
|
||||
namespace NATS.Server.JetStream.Consumers;
|
||||
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks filter matching for consumer message delivery.
|
||||
/// Uses SubjectMatch.MatchLiteral() for NATS token-based filter matching (not Regex).
|
||||
/// Maintains a sorted set of skipped sequences for gap tracking.
|
||||
/// Go reference: consumer.go isFilteredMatch, skipMsgs tracking.
|
||||
/// </summary>
|
||||
public sealed class FilterSkipTracker
|
||||
{
|
||||
private readonly string? _filterSubject;
|
||||
private readonly IReadOnlyList<string> _filterSubjects;
|
||||
private readonly SortedSet<ulong> _skippedSequences = new();
|
||||
private long _matchCount;
|
||||
private long _skipCount;
|
||||
|
||||
public FilterSkipTracker(string? filterSubject = null, IReadOnlyList<string>? filterSubjects = null)
|
||||
{
|
||||
_filterSubject = filterSubject;
|
||||
_filterSubjects = filterSubjects ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Number of messages that matched the filter.</summary>
|
||||
public long MatchCount => Interlocked.Read(ref _matchCount);
|
||||
|
||||
/// <summary>Number of messages that were skipped (didn't match filter).</summary>
|
||||
public long SkipCount => Interlocked.Read(ref _skipCount);
|
||||
|
||||
/// <summary>Number of currently tracked skipped sequences.</summary>
|
||||
public int SkippedSequenceCount => _skippedSequences.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the filter is active (has at least one filter subject).
|
||||
/// </summary>
|
||||
public bool HasFilter => !string.IsNullOrEmpty(_filterSubject) || _filterSubjects.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a message subject matches the filter.
|
||||
/// Returns true (should deliver) when:
|
||||
/// - No filter is configured
|
||||
/// - Subject matches the single filter
|
||||
/// - Subject matches any of the multiple filters
|
||||
/// Uses SubjectMatch.MatchLiteral for NATS token-based matching.
|
||||
/// Go reference: consumer.go isFilteredMatch.
|
||||
/// </summary>
|
||||
public bool ShouldDeliver(string subject)
|
||||
{
|
||||
if (!HasFilter)
|
||||
{
|
||||
Interlocked.Increment(ref _matchCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_filterSubject))
|
||||
{
|
||||
if (SubjectMatch.MatchLiteral(subject, _filterSubject))
|
||||
{
|
||||
Interlocked.Increment(ref _matchCount);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var filter in _filterSubjects)
|
||||
{
|
||||
if (SubjectMatch.MatchLiteral(subject, filter))
|
||||
{
|
||||
Interlocked.Increment(ref _matchCount);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _skipCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a skipped sequence for gap tracking.
|
||||
/// </summary>
|
||||
public void RecordSkip(ulong sequence)
|
||||
{
|
||||
_skippedSequences.Add(sequence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next unskipped sequence >= startSeq.
|
||||
/// Used to find the next deliverable message efficiently.
|
||||
/// </summary>
|
||||
public ulong NextUnskippedSequence(ulong startSeq)
|
||||
{
|
||||
var seq = startSeq;
|
||||
while (_skippedSequences.Contains(seq))
|
||||
seq++;
|
||||
return seq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears skipped sequences below the given floor (e.g., ack floor).
|
||||
/// Prevents unbounded growth.
|
||||
/// </summary>
|
||||
public void PurgeBelow(ulong floor)
|
||||
{
|
||||
_skippedSequences.RemoveWhere(s => s < floor);
|
||||
}
|
||||
|
||||
/// <summary>Resets all state.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_skippedSequences.Clear();
|
||||
Interlocked.Exchange(ref _matchCount, 0);
|
||||
Interlocked.Exchange(ref _skipCount, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user