Fix E2E test gaps and add comprehensive E2E + parity test suites

- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
This commit is contained in:
Joseph Doherty
2026-03-12 14:09:23 -04:00
parent 79c1ee8776
commit c30e67a69d
226 changed files with 17801 additions and 709 deletions

View File

@@ -26,11 +26,77 @@ public sealed class SubList : IDisposable
private ulong _inserts;
private ulong _removes;
private int _highFanoutNodes;
private Action<bool>? _interestStateNotification;
private readonly Dictionary<string, List<Action<bool>>> _queueInsertNotifications = new(StringComparer.Ordinal);
private readonly Dictionary<string, List<Action<bool>>> _queueRemoveNotifications = new(StringComparer.Ordinal);
private readonly record struct CachedResult(SubListResult Result, long Generation);
internal readonly record struct RoutedSubKeyInfo(string RouteId, string Account, string Subject, string? Queue);
public event Action<InterestChange>? InterestChanged;
public SubList()
: this(enableCache: true)
{
}
public SubList(bool enableCache)
{
if (!enableCache)
_cache = null;
}
public static SubList NewSublistNoCache() => new(enableCache: false);
public bool CacheEnabled() => _cache != null;
public void RegisterNotification(Action<bool> callback) => _interestStateNotification = callback;
public void ClearNotification() => _interestStateNotification = null;
public bool RegisterQueueNotification(string subject, string queue, Action<bool> callback)
{
if (callback == null || string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(queue))
return false;
if (SubjectMatch.SubjectHasWildcard(subject) || SubjectMatch.SubjectHasWildcard(queue))
return false;
bool hasInterest;
var key = QueueNotifyKey(subject, queue);
_lock.EnterWriteLock();
try
{
hasInterest = HasExactQueueInterestNoLock(subject, queue);
var map = hasInterest ? _queueRemoveNotifications : _queueInsertNotifications;
if (!AddQueueNotifyNoLock(map, key, callback))
return false;
}
finally
{
_lock.ExitWriteLock();
}
callback(hasInterest);
return true;
}
public bool ClearQueueNotification(string subject, string queue, Action<bool> callback)
{
var key = QueueNotifyKey(subject, queue);
_lock.EnterWriteLock();
try
{
var removed = RemoveQueueNotifyNoLock(_queueRemoveNotifications, key, callback);
removed |= RemoveQueueNotifyNoLock(_queueInsertNotifications, key, callback);
return removed;
}
finally
{
_lock.ExitWriteLock();
}
}
public void Dispose()
{
_disposed = true;
@@ -112,7 +178,7 @@ public sealed class SubList : IDisposable
_lock.EnterWriteLock();
try
{
var key = $"{sub.RouteId}|{sub.Account}|{sub.Subject}|{sub.Queue}";
var key = BuildRoutedSubKey(sub.RouteId, sub.Account, sub.Subject, sub.Queue);
var changed = false;
if (sub.IsRemoval)
{
@@ -149,6 +215,127 @@ public sealed class SubList : IDisposable
}
}
public void UpdateRemoteQSub(RemoteSubscription sub)
{
if (sub.Queue == null)
return;
_lock.EnterWriteLock();
try
{
var key = BuildRoutedSubKey(sub.RouteId, sub.Account, sub.Subject, sub.Queue);
if (!_remoteSubs.TryGetValue(key, out var existing))
return;
var nextWeight = Math.Max(1, sub.QueueWeight);
if (existing.QueueWeight == nextWeight)
return;
_remoteSubs[key] = existing with { QueueWeight = nextWeight };
Interlocked.Increment(ref _generation);
}
finally
{
_lock.ExitWriteLock();
}
}
internal static string BuildRoutedSubKey(string routeId, string account, string subject, string? queue)
=> $"{routeId}|{account}|{subject}|{queue}";
internal static string? GetAccNameFromRoutedSubKey(string routedSubKey)
=> GetRoutedSubKeyInfo(routedSubKey)?.Account;
internal static RoutedSubKeyInfo? GetRoutedSubKeyInfo(string routedSubKey)
{
if (string.IsNullOrWhiteSpace(routedSubKey))
return null;
var parts = routedSubKey.Split('|');
if (parts.Length != 4)
return null;
if (parts[0].Length == 0 || parts[1].Length == 0 || parts[2].Length == 0)
return null;
var queue = parts[3].Length == 0 ? null : parts[3];
return new RoutedSubKeyInfo(parts[0], parts[1], parts[2], queue);
}
public int RemoveRemoteSubs(string routeId)
{
_lock.EnterWriteLock();
try
{
var removed = 0;
foreach (var kvp in _remoteSubs.ToArray())
{
var info = GetRoutedSubKeyInfo(kvp.Key);
if (info == null || !string.Equals(info.Value.RouteId, routeId, StringComparison.Ordinal))
continue;
if (_remoteSubs.Remove(kvp.Key))
{
removed++;
InterestChanged?.Invoke(new InterestChange(
InterestChangeKind.RemoteRemoved,
kvp.Value.Subject,
kvp.Value.Queue,
kvp.Value.Account));
}
}
if (removed > 0)
Interlocked.Increment(ref _generation);
return removed;
}
finally
{
_lock.ExitWriteLock();
}
}
public int RemoveRemoteSubsForAccount(string routeId, string account)
{
_lock.EnterWriteLock();
try
{
var removed = 0;
foreach (var kvp in _remoteSubs.ToArray())
{
var info = GetRoutedSubKeyInfo(kvp.Key);
if (info == null)
continue;
if (!string.Equals(info.Value.RouteId, routeId, StringComparison.Ordinal)
|| !string.Equals(info.Value.Account, account, StringComparison.Ordinal))
{
continue;
}
if (_remoteSubs.Remove(kvp.Key))
{
removed++;
InterestChanged?.Invoke(new InterestChange(
InterestChangeKind.RemoteRemoved,
kvp.Value.Subject,
kvp.Value.Queue,
kvp.Value.Account));
}
}
if (removed > 0)
Interlocked.Increment(ref _generation);
return removed;
}
finally
{
_lock.ExitWriteLock();
}
}
public bool HasRemoteInterest(string subject)
=> HasRemoteInterest("$G", subject);
@@ -183,6 +370,7 @@ public sealed class SubList : IDisposable
_lock.EnterWriteLock();
try
{
var hadInterest = _count > 0;
var level = _root;
TrieNode? node = null;
bool sawFwc = false;
@@ -240,6 +428,10 @@ public sealed class SubList : IDisposable
_count++;
_inserts++;
Interlocked.Increment(ref _generation);
if (sub.Queue != null && _queueInsertNotifications.Count > 0)
CheckForQueueInsertNotificationNoLock(sub.Subject, sub.Queue);
if (!hadInterest && _count > 0)
_interestStateNotification?.Invoke(true);
InterestChanged?.Invoke(new InterestChange(
InterestChangeKind.LocalAdded,
sub.Subject,
@@ -258,10 +450,15 @@ public sealed class SubList : IDisposable
_lock.EnterWriteLock();
try
{
var hadInterest = _count > 0;
if (RemoveInternal(sub))
{
_removes++;
Interlocked.Increment(ref _generation);
if (sub.Queue != null && _queueRemoveNotifications.Count > 0)
CheckForQueueRemoveNotificationNoLock(sub.Subject, sub.Queue);
if (hadInterest && _count == 0)
_interestStateNotification?.Invoke(false);
InterestChanged?.Invoke(new InterestChange(
InterestChangeKind.LocalRemoved,
sub.Subject,
@@ -455,6 +652,90 @@ public sealed class SubList : IDisposable
}
}
private static string QueueNotifyKey(string subject, string queue) => $"{subject} {queue}";
private static bool AddQueueNotifyNoLock(Dictionary<string, List<Action<bool>>> map, string key, Action<bool> callback)
{
if (!map.TryGetValue(key, out var callbacks))
{
callbacks = [];
map[key] = callbacks;
}
else if (callbacks.Contains(callback))
{
return false;
}
callbacks.Add(callback);
return true;
}
private static bool RemoveQueueNotifyNoLock(Dictionary<string, List<Action<bool>>> map, string key, Action<bool> callback)
{
if (!map.TryGetValue(key, out var callbacks))
return false;
var removed = callbacks.Remove(callback);
if (callbacks.Count == 0)
map.Remove(key);
return removed;
}
private bool HasExactQueueInterestNoLock(string subject, string queue)
{
var subs = new List<Subscription>();
CollectAll(_root, subs);
foreach (var sub in subs)
{
if (sub.Queue != null
&& string.Equals(sub.Subject, subject, StringComparison.Ordinal)
&& string.Equals(sub.Queue, queue, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private void CheckForQueueInsertNotificationNoLock(string subject, string queue)
{
var key = QueueNotifyKey(subject, queue);
if (!_queueInsertNotifications.TryGetValue(key, out var callbacks) || callbacks.Count == 0)
return;
foreach (var callback in callbacks)
callback(true);
if (!_queueRemoveNotifications.TryGetValue(key, out var removeCallbacks))
{
removeCallbacks = [];
_queueRemoveNotifications[key] = removeCallbacks;
}
removeCallbacks.AddRange(callbacks);
_queueInsertNotifications.Remove(key);
}
private void CheckForQueueRemoveNotificationNoLock(string subject, string queue)
{
var key = QueueNotifyKey(subject, queue);
if (!_queueRemoveNotifications.TryGetValue(key, out var callbacks) || callbacks.Count == 0)
return;
if (HasExactQueueInterestNoLock(subject, queue))
return;
foreach (var callback in callbacks)
callback(false);
if (!_queueInsertNotifications.TryGetValue(key, out var insertCallbacks))
{
insertCallbacks = [];
_queueInsertNotifications[key] = insertCallbacks;
}
insertCallbacks.AddRange(callbacks);
_queueRemoveNotifications.Remove(key);
}
private void SweepCache()
{
_lock.EnterWriteLock();
@@ -632,6 +913,9 @@ public sealed class SubList : IDisposable
CacheHitRate = hitRate,
MaxFanout = maxFanout,
AvgFanout = cacheEntries > 0 ? (double)totalFanout / cacheEntries : 0.0,
TotalFanout = (int)totalFanout,
CacheEntries = cacheEntries,
CacheHits = cacheHits,
};
}
@@ -695,7 +979,11 @@ public sealed class SubList : IDisposable
foreach (var sub in subs)
{
if (RemoveInternal(sub))
{
_removes++;
if (sub.Queue != null && _queueRemoveNotifications.Count > 0)
CheckForQueueRemoveNotificationNoLock(sub.Subject, sub.Queue);
}
}
Interlocked.Increment(ref _generation);
@@ -724,6 +1012,34 @@ public sealed class SubList : IDisposable
return subs;
}
public IReadOnlyList<Subscription> LocalSubs(bool includeLeafHubs = false)
{
var subs = new List<Subscription>();
_lock.EnterReadLock();
try
{
CollectLocalSubs(_root, subs, includeLeafHubs);
}
finally
{
_lock.ExitReadLock();
}
return subs;
}
internal int NumLevels()
{
_lock.EnterReadLock();
try
{
return VisitLevel(_root, 0);
}
finally
{
_lock.ExitReadLock();
}
}
public SubListResult ReverseMatch(string subject)
{
var tokens = Tokenize(subject);
@@ -857,6 +1173,82 @@ public sealed class SubList : IDisposable
}
}
private static void CollectLocalSubs(TrieLevel level, List<Subscription> subs, bool includeLeafHubs)
{
foreach (var (_, node) in level.Nodes)
{
AddNodeLocalSubs(node, subs, includeLeafHubs);
if (node.Next != null)
CollectLocalSubs(node.Next, subs, includeLeafHubs);
}
if (level.Pwc != null)
{
AddNodeLocalSubs(level.Pwc, subs, includeLeafHubs);
if (level.Pwc.Next != null)
CollectLocalSubs(level.Pwc.Next, subs, includeLeafHubs);
}
if (level.Fwc != null)
{
AddNodeLocalSubs(level.Fwc, subs, includeLeafHubs);
if (level.Fwc.Next != null)
CollectLocalSubs(level.Fwc.Next, subs, includeLeafHubs);
}
}
private static void AddNodeLocalSubs(TrieNode node, List<Subscription> subs, bool includeLeafHubs)
{
foreach (var sub in node.PlainSubs)
AddLocalSub(sub, subs, includeLeafHubs);
foreach (var (_, qset) in node.QueueSubs)
foreach (var sub in qset)
AddLocalSub(sub, subs, includeLeafHubs);
}
private static void AddLocalSub(Subscription sub, List<Subscription> subs, bool includeLeafHubs)
{
if (sub.Client == null)
return;
var kind = sub.Client.Kind;
if (kind is global::NATS.Server.ClientKind.Client
or global::NATS.Server.ClientKind.System
or global::NATS.Server.ClientKind.JetStream
or global::NATS.Server.ClientKind.Account
|| (includeLeafHubs && kind == global::NATS.Server.ClientKind.Leaf))
{
subs.Add(sub);
}
}
private static int VisitLevel(TrieLevel? level, int depth)
{
if (level == null || (level.Nodes.Count == 0 && level.Pwc == null && level.Fwc == null))
return depth;
depth++;
var maxDepth = depth;
foreach (var (_, node) in level.Nodes)
{
var childDepth = VisitLevel(node.Next, depth);
if (childDepth > maxDepth)
maxDepth = childDepth;
}
if (level.Pwc != null)
{
var pwcDepth = VisitLevel(level.Pwc.Next, depth);
if (pwcDepth > maxDepth)
maxDepth = pwcDepth;
}
if (level.Fwc != null)
{
var fwcDepth = VisitLevel(level.Fwc.Next, depth);
if (fwcDepth > maxDepth)
maxDepth = fwcDepth;
}
return maxDepth;
}
private static void ReverseMatchLevel(TrieLevel? level, string[] tokens, int tokenIndex,
List<Subscription> plainSubs, List<List<Subscription>> queueSubs)
{