Files
natsdotnet/docs/plans/2026-02-22-base-server-design.md
Joseph Doherty 0ea71ace79 Add CLAUDE.md and base server design document
Design covers the minimal NATS server port: pub/sub with wildcards
and queue groups over System.IO.Pipelines, targeting .NET 10.
2026-02-22 19:37:32 -05:00

6.1 KiB

NATS .NET Base Server Design

Date: 2026-02-22 Scope: Minimal single-node NATS server — pub/sub, queue groups, wildcards. No auth, clustering, JetStream, or monitoring. Target: .NET 10, C# Approach: Bottom-up (Sublist → Parser → Client → Server)

Decisions

  • Framework: .NET 10
  • I/O: System.IO.Pipelines for zero-copy protocol parsing
  • Structure: Library (NATS.Server) + Host (NATS.Server.Host) + Tests (NATS.Server.Tests)
  • Test framework: xUnit
  • Queue group selection: Round-robin with atomic counter

Solution Layout

natsdotnet/
├── NatsDotNet.sln
├── src/
│   ├── NATS.Server/                 # Core library
│   │   ├── NatsServer.cs            # Server orchestrator
│   │   ├── NatsClient.cs            # Client connection
│   │   ├── NatsOptions.cs           # Configuration
│   │   ├── Protocol/
│   │   │   ├── NatsProtocol.cs      # Constants, op codes
│   │   │   └── NatsParser.cs        # Pipeline-based parser
│   │   └── Subscriptions/
│   │       ├── Subscription.cs
│   │       ├── SubjectMatch.cs
│   │       └── SubList.cs           # Trie + cache
│   └── NATS.Server.Host/            # Console app
│       └── Program.cs
└── tests/
    └── NATS.Server.Tests/

Component Designs

1. SubList (subject matching trie)

Reference: golang/nats-server/server/sublist.go

Structures:

  • SubList — root trie + cache (Dictionary<string, SubListResult>) + ReaderWriterLockSlim + atomic genId
  • TrieLevelDictionary<string, TrieNode> for literal tokens, nullable pwc (*) and fwc (>) pointers
  • TrieNode — optional next: TrieLevel, HashSet<Subscription> for plain subs, Dictionary<string, HashSet<Subscription>> for queue subs
  • Subscription — client ref, subject, queue, sid, atomic message count, max messages
  • SubListResultSubscription[] plain subs, Subscription[][] queue subs (grouped)

Operations:

  • Insert(Subscription) — walk trie by .-split tokens, create nodes, add sub, increment genId (clears cache)
  • Remove(Subscription) — reverse, prune empty nodes, increment genId
  • Match(ReadOnlySpan<byte>) — cache check (convert to string key on miss), walk trie collecting matches from literal + * + > nodes
  • Thread safety: read lock for Match, write lock for Insert/Remove

2. Protocol Parser

Reference: golang/nats-server/server/parser.go

Operates on PipeReader. Scans for \r\n to delimit control lines. Identifies command by first bytes. For PUB/HPUB, reads control line then exactly size bytes of payload + \r\n.

Commands (base server):

Command Direction Control line format
INFO S→C INFO {json}\r\n
CONNECT C→S CONNECT {json}\r\n
PUB C→S PUB subject [reply] size\r\n[payload]\r\n
HPUB C→S HPUB subject [reply] hdr_size total_size\r\n[hdrs+payload]\r\n
SUB C→S SUB subject [queue] sid\r\n
UNSUB C→S UNSUB sid [max_msgs]\r\n
MSG S→C MSG subject sid [reply] size\r\n[payload]\r\n
HMSG S→C HMSG subject sid [reply] hdr_size total_size\r\n[hdrs+payload]\r\n
PING/PONG Both PING\r\n / PONG\r\n
+OK / -ERR S→C +OK\r\n / -ERR 'msg'\r\n

Design choices:

  • Parser returns parsed command structs, does not dispatch
  • No string allocation on hot path — subject matching uses Span<byte>
  • Max control line: 4096 bytes, max payload: 1MB default (configurable)

3. NatsClient (connection handler)

Reference: golang/nats-server/server/client.go

Fields: unique id, Socket, IDuplexPipe, back-ref to server, subs dict (sid→Subscription), ClientOptions (from CONNECT json), stats (Interlocked counters), SemaphoreSlim for write serialization.

Lifecycle:

  1. Server accepts socket, creates NatsClient
  2. RunAsync(CancellationToken): send INFO → read loop (parse + dispatch commands)
  3. Dispatch: CONNECT→validate, SUB→insert into SubList, UNSUB→remove, PUB→server.ProcessMessage(), PING→PONG
  4. On disconnect: remove all subs, remove from server clients dict

Writing: SendMessageAsync() formats MSG/HMSG, acquires write lock, writes to PipeWriter, flushes.

4. NatsServer (orchestrator)

Reference: golang/nats-server/server/server.go

Fields: NatsOptions, TCP listener Socket, ConcurrentDictionary<ulong, NatsClient>, SubList, atomic next client ID, ServerInfo struct, CancellationTokenSource.

Core methods:

  • StartAsync() — bind, accept loop
  • ProcessMessage(subject, reply, headers, payload, sender) — SubList.Match → deliver to plain subs + pick one per queue group (round-robin)
  • RemoveClient(client) — cleanup subs and client dict
  • Shutdown() — cancel, close listener, drain

5. NatsOptions

Reference: golang/nats-server/server/opts.go

Minimal for base: Host, Port (default 4222), ServerName, MaxPayload (1MB), MaxControlLine (4096), MaxConnections, PingInterval, MaxPingsOut.

Testing Strategy

Unit tests:

  • SubList: insert/remove/match with literals, *, >, queue groups, cache invalidation. Port Go test cases.
  • Parser: each command type, malformed input, partial reads, boundary sizes.
  • Client protocol: CONNECT handshake, SUB/UNSUB lifecycle, PUB delivery, PING/PONG via loopback sockets.

Integration tests (using NATS.Client.Core NuGet):

  • Basic pub/sub, wildcard matching, queue group distribution, fan-out, UNSUB + auto-unsub, PING/PONG.

Module Boundaries (future)

These are explicitly excluded from this design and will be added as separate modules later:

Module Key files in Go reference
Authentication auth.go, auth_callout.go, nkey.go, jwt.go
Monitoring HTTP monitor.go
Clustering/Routes route.go
Gateways gateway.go
Leaf Nodes leafnode.go
JetStream jetstream.go, stream.go, consumer.go, filestore.go, memstore.go, raft.go
WebSocket websocket.go
MQTT mqtt.go
TLS TLS config in opts.go, various TLS setup in server.go