Files
natsdotnet/docs/plans/2026-02-23-sections3-6-gaps-design.md
2026-02-23 00:17:24 -05:00

8.3 KiB

Sections 3-6 Gaps Implementation Design

Approved 2026-02-23. Implements all remaining gaps in Protocol Parsing, Subscriptions & Subject Matching, Authentication & Authorization, and Configuration.


Section 3 — Protocol Parsing

3a. INFO Serialization Caching

Add byte[] _infoJsonCache on NatsServer. Build once in StartAsync() after ServerInfo is populated. Rebuild only on config reload.

NatsClient.SendInfo() uses cached bytes for non-nonce connections. For NKey connections (which need a per-connection nonce), clone ServerInfo, set nonce, serialize fresh.

Files: NatsServer.cs, NatsClient.cs

3b. Protocol Tracing

Add ILogger to NatsParser. Add TraceInOp(ReadOnlySpan<byte> op, ReadOnlySpan<byte> arg) that logs at LogLevel.Trace after each command dispatch in TryParse().

Controlled by NatsOptions.Trace (which sets log level filter).

Files: NatsParser.cs, NatsOptions.cs

3c. MIME Header Parsing

New NatsHeaderParser static class in Protocol/. Parses NATS/1.0 <status> <description>\r\n status line + Key: Value\r\n pairs.

Returns NatsHeaders readonly struct with Status (int), Description (string), and key-value Dictionary<string, string[]> lookup.

Used in ProcessMessage() for header inspection (no-responders status, future message tracing).

Files: New Protocol/NatsHeaderParser.cs, NatsServer.cs

3d. MSG/HMSG Construction Optimization

Pre-allocate buffers for MSG/HMSG prefix using Span<byte> and Utf8Formatter instead of string interpolation + Encoding.UTF8.GetBytes(). Reduces per-message allocations.

Files: NatsClient.cs


Section 4 — Subscriptions & Subject Matching

4a. Atomic Generation ID for Cache Invalidation

Add long _generation to SubList. Increment via Interlocked.Increment on every Insert() and Remove(). Cache entries store generation at computation time. On Match(), stale generation = cache miss.

Replaces current explicit per-key cache removal. O(1) invalidation.

Files: SubList.cs

4b. Async Background Cache Sweep

Replace inline sweep (runs under write lock) with PeriodicTimer-based background task. Acquires write lock briefly to snapshot + evict stale entries. Triggered when cache count > 1024, sweeps to 256.

Files: SubList.cs

4c. plist Optimization for High-Fanout Nodes

On TrieNode, when PlainSubs.Count > 256, convert HashSet<Subscription> to Subscription[] flat array in PList field. Match() iterates PList when present. On count < 128, convert back to HashSet.

Files: SubList.cs

4d. SubList Utility Methods

Method Description
Stats() Returns SubListStats record with NumSubs, NumCache, NumInserts, NumRemoves, NumMatches, CacheHitRate, MaxFanout. Track via Interlocked counters.
HasInterest(string) Walk trie without building result, return true on first hit.
NumInterest(string) Walk trie, count without allocating result arrays.
ReverseMatch(string) Walk trie with literal tokens, collect all subscription patterns matching the literal.
RemoveBatch(IEnumerable<Subscription>) Single write-lock, batch removes, single generation bump.
All() Depth-first trie walk, yield all subscriptions. Returns IReadOnlyList<Subscription>.
MatchBytes(ReadOnlySpan<byte>) Zero-copy match using byte-based TokenEnumerator.

Files: SubList.cs, new SubListStats.cs

4e. SubjectMatch Utilities

Method Description
SubjectsCollide(string, string) Token-by-token comparison handling * and >. Two patterns collide if any literal could match both.
TokenAt(string, int) Return nth dot-delimited token as ReadOnlySpan<char>.
NumTokens(string) Count dots + 1.
UTF-8/null validation Add checkRunes parameter to IsValidSubject(). When true, scan for \0 bytes and validate UTF-8.

Files: SubjectMatch.cs

4f. Per-Account Subscription Limits

Add MaxSubscriptions to Account. Track SubscriptionCount via Interlocked. Enforce in ProcessSub(). Close with ClientClosedReason.MaxSubscriptionsExceeded.

Files: Account.cs, NatsClient.cs


Section 5 — Authentication & Authorization

5a. Deny List Enforcement at Delivery Time

In NatsServer.DeliverMessage(), before sending MSG, check subscriber.Client.Permissions?.IsDeliveryAllowed(subject). New method checks publish deny list for the receiving client ("msg delivery filter"). Cache results in pub cache.

Files: NatsServer.cs, ClientPermissions.cs

5b. Permission Caching with 128-Entry LRU

Replace ConcurrentDictionary<string, bool> with custom PermissionLruCache (128 entries). Dictionary<string, LinkedListNode> + LinkedList for LRU ordering. Lock-protected (per-client, low contention).

Files: ClientPermissions.cs, new Auth/PermissionLruCache.cs

5c. Subscribe Deny Queue-Group Checking

IsSubscribeAllowed(subject, queue) checks queue group against subscribe deny list (currently ignores queue parameter).

Files: ClientPermissions.cs

5d. Response Permissions (Reply Tracking)

New ResponseTracker class on NatsClient. Created when Permissions.Response is non-null. Tracks reply subjects with TTL (ResponsePermission.Expires) and max count (ResponsePermission.MaxMsgs). IsPublishAllowed() consults tracker for reply subjects not in static allow list. Expired entries cleaned lazily + in ping timer.

Files: New Auth/ResponseTracker.cs, ClientPermissions.cs, NatsClient.cs

5e. Per-Account Connection Limits

Add MaxConnections to Account. Enforce in ProcessConnectAsync() after account assignment. Reject with -ERR maximum connections for account exceeded.

Files: Account.cs, NatsClient.cs

5f. Multi-Account User Resolution

Add NatsOptions.Accounts as Dictionary<string, AccountConfig> with per-account MaxConnections, MaxSubscriptions, DefaultPermissions. AuthService resolves account name to configured Account with limits.

Files: NatsOptions.cs, new Auth/AccountConfig.cs, AuthService.cs, NatsServer.cs

5g. Auth Expiry Enforcement

In ProcessConnectAsync(), if AuthResult.Expiry is set, start CancellationTokenSource.CancelAfter(expiry - now). Link to client lifetime. On fire, close with ClientClosedReason.AuthenticationExpired.

Files: NatsClient.cs

5h. Auto-Unsub Cleanup

In DeliverMessage(), when sub.MessageCount >= sub.MaxMessages, call sub.Client.RemoveSubscription(sub.Sid) and subList.Remove(sub) to clean up both tracking dict and trie. Currently only skips delivery.

Files: NatsServer.cs, NatsClient.cs


Section 6 — Configuration

6a. Debug/Trace CLI Flags

Add -D/--debug, -V/--trace, -DV to Program.cs. Set NatsOptions.Debug and NatsOptions.Trace. Wire into Serilog minimum level.

Files: Program.cs

6b. New NatsOptions Fields

Field Type Default Purpose
MaxSubs int 0 (unlimited) Per-connection subscription limit
MaxSubTokens int 0 (unlimited) Max tokens in a subject
Debug bool false Enable debug-level logging
Trace bool false Enable trace-level protocol logging
LogFile string? null Log to file instead of console
LogSizeLimit long 0 Max log file size before rotation
Tags Dictionary<string, string>? null Server metadata tags

Files: NatsOptions.cs

6c. Logging Options Wiring

In Program.cs, if LogFile is set, add Serilog File sink with LogSizeLimit. If Debug/Trace, override Serilog minimum level.

Files: Program.cs


Implementation Strategy

Execute in a git worktree. Parallelize where files don't overlap:

  • Parallel batch 1: SubjectMatch utilities (4e) | NatsOptions + CLI flags (6a, 6b) | NatsHeaderParser (3c) | PermissionLruCache (5b) | SubListStats (4d stats class)
  • Parallel batch 2: SubList overhaul (4a, 4b, 4c, 4d methods) | Account limits + config (4f, 5e, 5f, 5g) | Response tracker (5d)
  • Parallel batch 3: Protocol tracing (3b) | INFO caching (3a) | MSG optimization (3d)
  • Sequential: Delivery-time enforcement (5a, 5c, 5h) — touches NatsServer.cs + ClientPermissions.cs, must be coordinated
  • Final: Logging wiring (6c) | differences.md update

Tests added alongside each feature in the appropriate test file.