Merge branch 'feature/sections-7-10-gaps' into main

This commit is contained in:
Joseph Doherty
2026-02-23 03:34:00 -05:00
28 changed files with 1165 additions and 62 deletions

View File

@@ -34,6 +34,13 @@ public sealed class AuthService
var nonceRequired = false;
Dictionary<string, User>? usersMap = null;
// TLS certificate mapping (highest priority when enabled)
if (options.TlsMap && options.TlsVerify && options.Users is { Count: > 0 })
{
authenticators.Add(new TlsMapAuthenticator(options.Users));
authRequired = true;
}
// Priority order (matching Go): NKeys > Users > Token > SimpleUserPassword
if (options.NKeys is { Count: > 0 })

View File

@@ -1,3 +1,4 @@
using System.Security.Cryptography.X509Certificates;
using NATS.Server.Protocol;
namespace NATS.Server.Auth;
@@ -11,4 +12,5 @@ public sealed class ClientAuthContext
{
public required ClientOptions Opts { get; init; }
public required byte[] Nonce { get; init; }
public X509Certificate2? ClientCertificate { get; init; }
}

View File

@@ -0,0 +1,67 @@
using System.Security.Cryptography.X509Certificates;
namespace NATS.Server.Auth;
/// <summary>
/// Authenticates clients by mapping TLS certificate subject DN to configured users.
/// Corresponds to Go server/auth.go checkClientTLSCertSubject.
/// </summary>
public sealed class TlsMapAuthenticator : IAuthenticator
{
private readonly Dictionary<string, User> _usersByDn;
private readonly Dictionary<string, User> _usersByCn;
public TlsMapAuthenticator(IReadOnlyList<User> users)
{
_usersByDn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
_usersByCn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
foreach (var user in users)
{
_usersByDn[user.Username] = user;
_usersByCn[user.Username] = user;
}
}
public AuthResult? Authenticate(ClientAuthContext context)
{
var cert = context.ClientCertificate;
if (cert == null)
return null;
var dn = cert.SubjectName;
var dnString = dn.Name; // RFC 2253 format
// Try exact DN match first
if (_usersByDn.TryGetValue(dnString, out var user))
return BuildResult(user);
// Try CN extraction
var cn = ExtractCn(dn);
if (cn != null && _usersByCn.TryGetValue(cn, out user))
return BuildResult(user);
return null;
}
private static string? ExtractCn(X500DistinguishedName dn)
{
var dnString = dn.Name;
foreach (var rdn in dnString.Split(',', StringSplitOptions.TrimEntries))
{
if (rdn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
return rdn[3..];
}
return null;
}
private static AuthResult BuildResult(User user)
{
return new AuthResult
{
Identity = user.Username,
AccountName = user.Account,
Permissions = user.Permissions,
Expiry = user.ConnectionDeadline,
};
}
}

View File

@@ -0,0 +1,25 @@
namespace NATS.Server.Monitoring;
/// <summary>
/// Snapshot of a closed client connection for /connz reporting.
/// </summary>
public sealed record ClosedClient
{
public required ulong Cid { get; init; }
public string Ip { get; init; } = "";
public int Port { get; init; }
public DateTime Start { get; init; }
public DateTime Stop { get; init; }
public string Reason { get; init; } = "";
public string Name { get; init; } = "";
public string Lang { get; init; } = "";
public string Version { get; init; } = "";
public long InMsgs { get; init; }
public long OutMsgs { get; init; }
public long InBytes { get; init; }
public long OutBytes { get; init; }
public uint NumSubs { get; init; }
public TimeSpan Rtt { get; init; }
public string TlsVersion { get; init; } = "";
public string TlsCipherSuite { get; init; } = "";
}

View File

@@ -168,6 +168,9 @@ public enum SortOpt
ByLast,
ByIdle,
ByUptime,
ByRtt,
ByStop,
ByReason,
}
/// <summary>

View File

@@ -12,9 +12,25 @@ public sealed class ConnzHandler(NatsServer server)
{
var opts = ParseQueryParams(ctx);
var now = DateTime.UtcNow;
var clients = server.GetClients().ToArray();
var connInfos = clients.Select(c => BuildConnInfo(c, now, opts)).ToList();
var connInfos = new List<ConnInfo>();
// Collect open connections
if (opts.State is ConnState.Open or ConnState.All)
{
var clients = server.GetClients().ToArray();
connInfos.AddRange(clients.Select(c => BuildConnInfo(c, now, opts)));
}
// Collect closed connections
if (opts.State is ConnState.Closed or ConnState.All)
{
connInfos.AddRange(server.GetClosedClients().Select(c => BuildClosedConnInfo(c, now, opts)));
}
// Validate sort options that require closed state
if (opts.Sort is SortOpt.ByStop or SortOpt.ByReason && opts.State == ConnState.Open)
opts.Sort = SortOpt.ByCid; // Fallback
// Sort
connInfos = opts.Sort switch
@@ -30,6 +46,9 @@ public sealed class ConnzHandler(NatsServer server)
SortOpt.ByLast => connInfos.OrderByDescending(c => c.LastActivity).ToList(),
SortOpt.ByIdle => connInfos.OrderByDescending(c => now - c.LastActivity).ToList(),
SortOpt.ByUptime => connInfos.OrderByDescending(c => now - c.Start).ToList(),
SortOpt.ByStop => connInfos.OrderByDescending(c => c.Stop ?? DateTime.MinValue).ToList(),
SortOpt.ByReason => connInfos.OrderBy(c => c.Reason).ToList(),
SortOpt.ByRtt => connInfos.OrderBy(c => c.Rtt).ToList(),
_ => connInfos.OrderBy(c => c.Cid).ToList(),
};
@@ -73,6 +92,7 @@ public sealed class ConnzHandler(NatsServer server)
Reason = client.CloseReason.ToReasonString(),
TlsVersion = client.TlsState?.TlsVersion ?? "",
TlsCipherSuite = client.TlsState?.CipherSuite ?? "",
Rtt = FormatRtt(client.Rtt),
};
if (opts.Subscriptions)
@@ -96,6 +116,35 @@ public sealed class ConnzHandler(NatsServer server)
return info;
}
private static ConnInfo BuildClosedConnInfo(ClosedClient closed, DateTime now, ConnzOptions opts)
{
return new ConnInfo
{
Cid = closed.Cid,
Kind = "Client",
Type = "Client",
Ip = closed.Ip,
Port = closed.Port,
Start = closed.Start,
Stop = closed.Stop,
LastActivity = closed.Stop,
Uptime = FormatDuration(closed.Stop - closed.Start),
Idle = FormatDuration(now - closed.Stop),
InMsgs = closed.InMsgs,
OutMsgs = closed.OutMsgs,
InBytes = closed.InBytes,
OutBytes = closed.OutBytes,
NumSubs = closed.NumSubs,
Name = closed.Name,
Lang = closed.Lang,
Version = closed.Version,
Reason = closed.Reason,
Rtt = FormatRtt(closed.Rtt),
TlsVersion = closed.TlsVersion,
TlsCipherSuite = closed.TlsCipherSuite,
};
}
private static ConnzOptions ParseQueryParams(HttpContext ctx)
{
var q = ctx.Request.Query;
@@ -116,6 +165,9 @@ public sealed class ConnzHandler(NatsServer server)
"last" => SortOpt.ByLast,
"idle" => SortOpt.ByIdle,
"uptime" => SortOpt.ByUptime,
"rtt" => SortOpt.ByRtt,
"stop" => SortOpt.ByStop,
"reason" => SortOpt.ByReason,
_ => SortOpt.ByCid,
};
}
@@ -128,6 +180,17 @@ public sealed class ConnzHandler(NatsServer server)
opts.Subscriptions = true;
}
if (q.TryGetValue("state", out var state))
{
opts.State = state.ToString().ToLowerInvariant() switch
{
"open" => ConnState.Open,
"closed" => ConnState.Closed,
"all" => ConnState.All,
_ => ConnState.Open,
};
}
if (q.TryGetValue("offset", out var offset) && int.TryParse(offset, out var o))
opts.Offset = o;
@@ -137,6 +200,16 @@ public sealed class ConnzHandler(NatsServer server)
return opts;
}
private static string FormatRtt(TimeSpan rtt)
{
if (rtt == TimeSpan.Zero) return "";
if (rtt.TotalMilliseconds < 1)
return $"{rtt.TotalMicroseconds:F3}\u00b5s";
if (rtt.TotalSeconds < 1)
return $"{rtt.TotalMilliseconds:F3}ms";
return $"{rtt.TotalSeconds:F3}s";
}
private static string FormatDuration(TimeSpan ts)
{
if (ts.TotalDays >= 1)

View File

@@ -15,6 +15,7 @@ public sealed class MonitorServer : IAsyncDisposable
private readonly ILogger<MonitorServer> _logger;
private readonly VarzHandler _varzHandler;
private readonly ConnzHandler _connzHandler;
private readonly SubszHandler _subszHandler;
public MonitorServer(NatsServer server, NatsOptions options, ServerStats stats, ILoggerFactory loggerFactory)
{
@@ -29,6 +30,7 @@ public sealed class MonitorServer : IAsyncDisposable
_varzHandler = new VarzHandler(server, options);
_connzHandler = new ConnzHandler(server);
_subszHandler = new SubszHandler(server);
_app.MapGet(basePath + "/", () =>
{
@@ -75,15 +77,15 @@ public sealed class MonitorServer : IAsyncDisposable
stats.HttpReqStats.AddOrUpdate("/leafz", 1, (_, v) => v + 1);
return Results.Ok(new { });
});
_app.MapGet(basePath + "/subz", () =>
_app.MapGet(basePath + "/subz", (HttpContext ctx) =>
{
stats.HttpReqStats.AddOrUpdate("/subz", 1, (_, v) => v + 1);
return Results.Ok(new { });
return Results.Ok(_subszHandler.HandleSubsz(ctx));
});
_app.MapGet(basePath + "/subscriptionsz", () =>
_app.MapGet(basePath + "/subscriptionsz", (HttpContext ctx) =>
{
stats.HttpReqStats.AddOrUpdate("/subscriptionsz", 1, (_, v) => v + 1);
return Results.Ok(new { });
return Results.Ok(_subszHandler.HandleSubsz(ctx));
});
_app.MapGet(basePath + "/accountz", () =>
{

View File

@@ -0,0 +1,45 @@
using System.Text.Json.Serialization;
namespace NATS.Server.Monitoring;
/// <summary>
/// Subscription information response. Corresponds to Go server/monitor.go Subsz struct.
/// </summary>
public sealed class Subsz
{
[JsonPropertyName("server_id")]
public string Id { get; set; } = "";
[JsonPropertyName("now")]
public DateTime Now { get; set; }
[JsonPropertyName("num_subscriptions")]
public uint NumSubs { get; set; }
[JsonPropertyName("num_cache")]
public int NumCache { get; set; }
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("offset")]
public int Offset { get; set; }
[JsonPropertyName("limit")]
public int Limit { get; set; }
[JsonPropertyName("subscriptions")]
public SubDetail[] Subs { get; set; } = [];
}
/// <summary>
/// Options passed to Subsz() for filtering.
/// </summary>
public sealed class SubszOptions
{
public int Offset { get; set; }
public int Limit { get; set; } = 1024;
public bool Subscriptions { get; set; }
public string Account { get; set; } = "";
public string Test { get; set; } = "";
}

View File

@@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Http;
using NATS.Server.Subscriptions;
namespace NATS.Server.Monitoring;
/// <summary>
/// Handles /subz endpoint requests, returning subscription information.
/// Corresponds to Go server/monitor.go handleSubsz.
/// </summary>
public sealed class SubszHandler(NatsServer server)
{
public Subsz HandleSubsz(HttpContext ctx)
{
var opts = ParseQueryParams(ctx);
var now = DateTime.UtcNow;
// Collect subscriptions from all accounts (or filtered)
var allSubs = new List<Subscription>();
foreach (var account in server.GetAccounts())
{
if (!string.IsNullOrEmpty(opts.Account) && account.Name != opts.Account)
continue;
allSubs.AddRange(account.SubList.GetAllSubscriptions());
}
// Filter by test subject if provided
if (!string.IsNullOrEmpty(opts.Test))
{
allSubs = allSubs.Where(s => SubjectMatch.MatchLiteral(opts.Test, s.Subject)).ToList();
}
var total = allSubs.Count;
var numSubs = server.GetAccounts()
.Where(a => string.IsNullOrEmpty(opts.Account) || a.Name == opts.Account)
.Aggregate(0u, (sum, a) => sum + a.SubList.Count);
var numCache = server.GetAccounts()
.Where(a => string.IsNullOrEmpty(opts.Account) || a.Name == opts.Account)
.Sum(a => a.SubList.CacheCount);
SubDetail[] details = [];
if (opts.Subscriptions)
{
details = allSubs
.Skip(opts.Offset)
.Take(opts.Limit)
.Select(s => new SubDetail
{
Subject = s.Subject,
Queue = s.Queue ?? "",
Sid = s.Sid,
Msgs = Interlocked.Read(ref s.MessageCount),
Max = s.MaxMessages,
Cid = s.Client?.Id ?? 0,
})
.ToArray();
}
return new Subsz
{
Id = server.ServerId,
Now = now,
NumSubs = numSubs,
NumCache = numCache,
Total = total,
Offset = opts.Offset,
Limit = opts.Limit,
Subs = details,
};
}
private static SubszOptions ParseQueryParams(HttpContext ctx)
{
var q = ctx.Request.Query;
var opts = new SubszOptions();
if (q.TryGetValue("subs", out var subs))
opts.Subscriptions = subs == "true" || subs == "1" || subs == "detail";
if (q.TryGetValue("offset", out var offset) && int.TryParse(offset, out var o))
opts.Offset = o;
if (q.TryGetValue("limit", out var limit) && int.TryParse(limit, out var l))
opts.Limit = l;
if (q.TryGetValue("acc", out var acc))
opts.Account = acc.ToString();
if (q.TryGetValue("test", out var test))
opts.Test = test.ToString();
return opts;
}
}

View File

@@ -157,6 +157,12 @@ public sealed class Varz
[JsonPropertyName("slow_consumer_stats")]
public SlowConsumersStats SlowConsumerStats { get; set; } = new();
[JsonPropertyName("stale_connections")]
public long StaleConnections { get; set; }
[JsonPropertyName("stale_connection_stats")]
public StaleConnectionStats StaleConnectionStatsDetail { get; set; } = new();
[JsonPropertyName("subscriptions")]
public uint Subscriptions { get; set; }
@@ -219,6 +225,25 @@ public sealed class SlowConsumersStats
public ulong Leafs { get; set; }
}
/// <summary>
/// Statistics about stale connections by connection type.
/// Corresponds to Go server/monitor.go StaleConnectionStats struct.
/// </summary>
public sealed class StaleConnectionStats
{
[JsonPropertyName("clients")]
public ulong Clients { get; set; }
[JsonPropertyName("routes")]
public ulong Routes { get; set; }
[JsonPropertyName("gateways")]
public ulong Gateways { get; set; }
[JsonPropertyName("leafs")]
public ulong Leafs { get; set; }
}
/// <summary>
/// Cluster configuration monitoring information.
/// Corresponds to Go server/monitor.go ClusterOptsVarz struct.

View File

@@ -91,6 +91,14 @@ public sealed class VarzHandler : IDisposable
Gateways = (ulong)Interlocked.Read(ref stats.SlowConsumerGateways),
Leafs = (ulong)Interlocked.Read(ref stats.SlowConsumerLeafs),
},
StaleConnections = Interlocked.Read(ref stats.StaleConnections),
StaleConnectionStatsDetail = new StaleConnectionStats
{
Clients = (ulong)Interlocked.Read(ref stats.StaleConnectionClients),
Routes = (ulong)Interlocked.Read(ref stats.StaleConnectionRoutes),
Gateways = (ulong)Interlocked.Read(ref stats.StaleConnectionGateways),
Leafs = (ulong)Interlocked.Read(ref stats.StaleConnectionLeafs),
},
Subscriptions = _server.SubList.Count,
ConfigLoadTime = _server.StartTime,
HttpReqStats = stats.HttpReqStats.ToDictionary(kv => kv.Key, kv => (ulong)kv.Value),

View File

@@ -74,6 +74,11 @@ public sealed class NatsClient : IDisposable
private int _pingsOut;
private long _lastIn;
// RTT tracking
private long _rttStartTicks;
private long _rtt;
public TimeSpan Rtt => new(Interlocked.Read(ref _rtt));
public TlsConnectionState? TlsState { get; set; }
public bool InfoAlreadySent { get; set; }
@@ -322,6 +327,14 @@ public sealed class NatsClient : IDisposable
case CommandType.Pong:
Interlocked.Exchange(ref _pingsOut, 0);
var rttStart = Interlocked.Read(ref _rttStartTicks);
if (rttStart > 0)
{
var elapsed = DateTime.UtcNow.Ticks - rttStart;
if (elapsed <= 0) elapsed = 1; // min 1 tick for Windows granularity
Interlocked.Exchange(ref _rtt, elapsed);
}
_flags.SetFlag(ClientFlags.FirstPongSent);
break;
case CommandType.Sub:
@@ -356,6 +369,7 @@ public sealed class NatsClient : IDisposable
{
Opts = ClientOpts,
Nonce = _nonce ?? [],
ClientCertificate = TlsState?.PeerCert,
};
authResult = _authService.Authenticate(context);
@@ -733,6 +747,13 @@ public sealed class NatsClient : IDisposable
{
while (await timer.WaitForNextTickAsync(ct))
{
// Delay first PING until client has responded with PONG or 2 seconds elapsed
if (!_flags.HasFlag(ClientFlags.FirstPongSent)
&& (DateTime.UtcNow - StartTime).TotalSeconds < 2)
{
continue;
}
var elapsed = Environment.TickCount64 - Interlocked.Read(ref _lastIn);
if (elapsed < (long)_options.PingInterval.TotalMilliseconds)
{
@@ -744,6 +765,8 @@ public sealed class NatsClient : IDisposable
if (Volatile.Read(ref _pingsOut) + 1 > _options.MaxPingsOut)
{
_logger.LogDebug("Client {ClientId} stale connection -- closing", Id);
Interlocked.Increment(ref _serverStats.StaleConnections);
Interlocked.Increment(ref _serverStats.StaleConnectionClients);
await SendErrAndCloseAsync(NatsProtocol.ErrStaleConnection, ClientClosedReason.StaleConnection);
return;
}
@@ -751,6 +774,7 @@ public sealed class NatsClient : IDisposable
var currentPingsOut = Interlocked.Increment(ref _pingsOut);
_logger.LogDebug("Client {ClientId} sending PING ({PingsOut}/{MaxPingsOut})",
Id, currentPingsOut, _options.MaxPingsOut);
Interlocked.Exchange(ref _rttStartTicks, DateTime.UtcNow.Ticks);
WriteProtocol(NatsProtocol.PingBytes);
}
}

View File

@@ -20,12 +20,6 @@ public sealed class NatsOptions
public int MaxSubs { get; set; } // 0 = unlimited (per-connection)
public int MaxSubTokens { get; set; } // 0 = unlimited
// Logging / diagnostics
public bool Debug { get; set; }
public bool Trace { get; set; }
public string? LogFile { get; set; }
public long LogSizeLimit { get; set; }
// Server tags (exposed via /varz)
public Dictionary<string, string>? Tags { get; set; }
@@ -63,6 +57,17 @@ public sealed class NatsOptions
public string? PortsFileDir { get; set; }
public string? ConfigFile { get; set; }
// Logging
public string? LogFile { get; set; }
public long LogSizeLimit { get; set; }
public int LogMaxFiles { get; set; }
public bool Debug { get; set; }
public bool Trace { get; set; }
public bool Logtime { get; set; } = true;
public bool LogtimeUTC { get; set; }
public bool Syslog { get; set; }
public string? RemoteSyslog { get; set; }
// Profiling (0 = disabled)
public int ProfPort { get; set; }

View File

@@ -19,6 +19,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
{
private readonly NatsOptions _options;
private readonly ConcurrentDictionary<ulong, NatsClient> _clients = new();
private readonly ConcurrentQueue<ClosedClient> _closedClients = new();
private const int MaxClosedClients = 10_000;
private readonly ServerInfo _serverInfo;
private readonly ILogger<NatsServer> _logger;
private readonly ILoggerFactory _loggerFactory;
@@ -64,8 +66,13 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
public string ServerNKey { get; }
public bool IsShuttingDown => Volatile.Read(ref _shutdown) != 0;
public bool IsLameDuckMode => Volatile.Read(ref _lameDuck) != 0;
public Action? ReOpenLogFile { get; set; }
public IEnumerable<NatsClient> GetClients() => _clients.Values;
public IEnumerable<ClosedClient> GetClosedClients() => _closedClients;
public IEnumerable<Auth.Account> GetAccounts() => _accounts.Values;
public Task WaitForReadyAsync() => _listeningStarted.Task;
public void WaitForShutdown() => _shutdownComplete.Task.GetAwaiter().GetResult();
@@ -195,7 +202,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
/// <summary>
/// Registers Unix signal handlers.
/// SIGTERM → shutdown, SIGUSR2 → lame duck, SIGUSR1 → log reopen (stub), SIGHUP → reload (stub).
/// SIGTERM → shutdown, SIGUSR2 → lame duck, SIGUSR1 → log reopen, SIGHUP → reload (stub).
/// </summary>
public void HandleSignals()
{
@@ -225,7 +232,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
_signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)10, ctx =>
{
ctx.Cancel = true;
_logger.LogWarning("Trapped SIGUSR1 signal — log reopen not yet supported");
_logger.LogInformation("Trapped SIGUSR1 signal — reopening log file");
ReOpenLogFile?.Invoke();
}));
_signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)12, ctx =>
@@ -615,6 +623,33 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
{
_clients.TryRemove(client.Id, out _);
_logger.LogDebug("Removed client {ClientId}", client.Id);
// Snapshot for closed-connections tracking
_closedClients.Enqueue(new ClosedClient
{
Cid = client.Id,
Ip = client.RemoteIp ?? "",
Port = client.RemotePort,
Start = client.StartTime,
Stop = DateTime.UtcNow,
Reason = client.CloseReason.ToReasonString(),
Name = client.ClientOpts?.Name ?? "",
Lang = client.ClientOpts?.Lang ?? "",
Version = client.ClientOpts?.Version ?? "",
InMsgs = Interlocked.Read(ref client.InMsgs),
OutMsgs = Interlocked.Read(ref client.OutMsgs),
InBytes = Interlocked.Read(ref client.InBytes),
OutBytes = Interlocked.Read(ref client.OutBytes),
NumSubs = (uint)client.Subscriptions.Count,
Rtt = client.Rtt,
TlsVersion = client.TlsState?.TlsVersion ?? "",
TlsCipherSuite = client.TlsState?.CipherSuite ?? "",
});
// Cap closed clients list
while (_closedClients.Count > MaxClosedClients)
_closedClients.TryDequeue(out _);
var subList = client.Account?.SubList ?? _globalAccount.SubList;
client.RemoveAllSubscriptions(subList);
client.Account?.RemoveClient(client.Id);

View File

@@ -16,5 +16,9 @@ public sealed class ServerStats
public long SlowConsumerRoutes;
public long SlowConsumerLeafs;
public long SlowConsumerGateways;
public long StaleConnectionClients;
public long StaleConnectionRoutes;
public long StaleConnectionLeafs;
public long StaleConnectionGateways;
public readonly ConcurrentDictionary<string, long> HttpReqStats = new();
}

View File

@@ -40,6 +40,62 @@ public sealed class SubList : IDisposable
}
}
/// <summary>
/// Returns all subscriptions in the trie. For monitoring only.
/// </summary>
public List<Subscription> GetAllSubscriptions()
{
_lock.EnterReadLock();
try
{
var result = new List<Subscription>();
CollectAll(_root, result);
return result;
}
finally
{
_lock.ExitReadLock();
}
}
private static void CollectAll(TrieLevel level, List<Subscription> result)
{
foreach (var (_, node) in level.Nodes)
{
foreach (var sub in node.PlainSubs) result.Add(sub);
foreach (var (_, qset) in node.QueueSubs)
foreach (var sub in qset) result.Add(sub);
if (node.Next != null) CollectAll(node.Next, result);
}
if (level.Pwc != null)
{
foreach (var sub in level.Pwc.PlainSubs) result.Add(sub);
foreach (var (_, qset) in level.Pwc.QueueSubs)
foreach (var sub in qset) result.Add(sub);
if (level.Pwc.Next != null) CollectAll(level.Pwc.Next, result);
}
if (level.Fwc != null)
{
foreach (var sub in level.Fwc.PlainSubs) result.Add(sub);
foreach (var (_, qset) in level.Fwc.QueueSubs)
foreach (var sub in qset) result.Add(sub);
if (level.Fwc.Next != null) CollectAll(level.Fwc.Next, result);
}
}
/// <summary>
/// Returns the current number of entries in the cache.
/// </summary>
public int CacheCount
{
get
{
_lock.EnterReadLock();
try { return _cache?.Count ?? 0; }
finally { _lock.ExitReadLock(); }
}
}
public void Insert(Subscription sub)
{
var subject = sub.Subject;