diff --git a/docs/plans/2026-02-23-config-parsing-design.md b/docs/plans/2026-02-23-config-parsing-design.md new file mode 100644 index 0000000..351110e --- /dev/null +++ b/docs/plans/2026-02-23-config-parsing-design.md @@ -0,0 +1,184 @@ +# Config File Parsing & Hot Reload Design + +> Resolves the two remaining high-priority gaps in differences.md. + +## Goals + +1. Port the Go NATS config file parser (`conf/lex.go` + `conf/parse.go`) to .NET +2. Map parsed config to existing + new `NatsOptions` fields (single-server scope) +3. Implement SIGHUP hot reload matching Go's reloadable option set +4. Add unit tests for lexer, parser, config processor, and reload + +## Architecture + +``` +Config File + → NatsConfLexer (state-machine tokenizer) + → NatsConfParser (builds Dictionary) + → ConfigProcessor.Apply(dict, NatsOptions) + → NatsOptions populated + +SIGHUP + → NatsServer.ReloadConfig() + → Re-parse config file + → Merge with CLI flag snapshot + → ConfigReloader.Diff(old, new) → IConfigChange[] + → Validate (reject non-reloadable) + → Apply each change to running server +``` + +## Component 1: Lexer (`NatsConfLexer.cs`) + +Direct port of Go `conf/lex.go` (~1320 lines Go → ~400 lines C#). + +State-machine tokenizer producing typed tokens: +- `Key`, `String`, `Bool`, `Integer`, `Float`, `DateTime` +- `ArrayStart`/`ArrayEnd`, `MapStart`/`MapEnd` +- `Variable`, `Include`, `Comment`, `EOF`, `Error` + +Supported syntax: +- Key separators: `=`, `:`, or whitespace +- Comments: `#` and `//` +- Strings: `"double"`, `'single'`, raw (unquoted) +- Booleans: `true/false`, `yes/no`, `on/off` +- Integers with size suffixes: `1k`, `2mb`, `1gb` +- Floats, ISO8601 datetimes (`2006-01-02T15:04:05Z`) +- Block strings: `(` multi-line raw text `)` +- Hex escapes: `\x##`, plus `\t`, `\n`, `\r`, `\"`, `\\` + +## Component 2: Parser (`NatsConfParser.cs`) + +Direct port of Go `conf/parse.go` (~529 lines Go → ~300 lines C#). + +Consumes token stream, produces `Dictionary`: +- Stack-based context tracking for nested maps/arrays +- Variable resolution: `$VAR` searches current context stack, then environment +- Cycle detection for variable references +- `include "path"` resolves relative to current config file +- Pedantic mode with line/column tracking for error messages +- SHA256 digest of parsed content for reload change detection + +## Component 3: Config Processor (`ConfigProcessor.cs`) + +Maps parsed dictionary keys to `NatsOptions` fields. Port of Go `processConfigFileLine` in `opts.go`. + +Key categories handled: +- **Network**: `listen`, `port`, `host`/`net`, `client_advertise`, `max_connections`/`max_conn` +- **Logging**: `debug`, `trace`, `trace_verbose`, `logtime`, `logtime_utc`, `logfile`/`log_file`, `log_size_limit`, `log_max_num`, `syslog`, `remote_syslog` +- **Auth**: `authorization { ... }` block (username, password, token, users, nkeys, timeout), `no_auth_user` +- **Accounts**: `accounts { ... }` block, `system_account`, `no_system_account` +- **TLS**: `tls { ... }` block (cert_file, key_file, ca_file, verify, verify_and_map, timeout, pinned_certs, handshake_first, handshake_first_fallback), `allow_non_tls` +- **Monitoring**: `http_port`/`monitor_port`, `https_port`, `http`/`https` (combined), `http_base_path` +- **Limits**: `max_payload`, `max_control_line`, `max_pending`, `max_subs`, `max_sub_tokens`, `max_traced_msg_len`, `write_deadline` +- **Ping**: `ping_interval`, `ping_max`/`ping_max_out` +- **Lifecycle**: `lame_duck_duration`, `lame_duck_grace_period` +- **Files**: `pidfile`/`pid_file`, `ports_file_dir` +- **Misc**: `server_name`, `server_tags`, `disable_sublist_cache`, `max_closed_clients`, `prof_port` + +Error handling: accumulate all errors, report together (not fail-fast). Unknown keys silently ignored (allows cluster/JetStream configs to coexist). + +## Component 4: Hot Reload (`ConfigReloader.cs`) + +### Reloadable Options (matching Go) + +- **Logging**: Debug, Trace, TraceVerbose, Logtime, LogtimeUTC, LogFile, LogSizeLimit, LogMaxFiles, Syslog, RemoteSyslog +- **Auth**: Username, Password, Authorization, Users, NKeys, NoAuthUser, AuthTimeout +- **Limits**: MaxConnections, MaxPayload, MaxPending, WriteDeadline, PingInterval, MaxPingsOut, MaxControlLine, MaxSubs, MaxSubTokens, MaxTracedMsgLen +- **TLS**: cert/key/CA file paths (reload certs without restart) +- **Misc**: Tags, LameDuckDuration, LameDuckGracePeriod, ClientAdvertise, MaxClosedClients + +### Non-Reloadable (error if changed) + +- Host, Port, ServerName + +### IConfigChange Interface + +```csharp +interface IConfigChange +{ + string Name { get; } + void Apply(NatsServer server); + bool IsLoggingChange { get; } + bool IsAuthChange { get; } + bool IsTlsChange { get; } +} +``` + +### Reload Flow + +1. SIGHUP → `NatsServer.ReloadConfig()` +2. Re-parse config file via `ConfigProcessor.ProcessConfigFile()` +3. Merge with CLI flag snapshot (CLI always wins) +4. `ConfigReloader.Diff(oldOpts, newOpts)` → list of `IConfigChange` +5. Validate: reject if non-reloadable options changed +6. Apply each change to running server (logging, auth, limits, TLS grouped) +7. Log applied changes at Information level, errors at Warning + +## New NatsOptions Fields + +Added for single-server parity with Go: + +| Field | Type | Default | Go equivalent | +|-------|------|---------|---------------| +| `ClientAdvertise` | string? | null | `client_advertise` | +| `TraceVerbose` | bool | false | `trace_verbose` | +| `MaxTracedMsgLen` | int | 0 | `max_traced_msg_len` | +| `DisableSublistCache` | bool | false | `disable_sublist_cache` | +| `ConnectErrorReports` | int | 3600 | `connect_error_reports` | +| `ReconnectErrorReports` | int | 1 | `reconnect_error_reports` | +| `NoHeaderSupport` | bool | false | `no_header_support` | +| `MaxClosedClients` | int | 10000 | `max_closed_clients` | +| `NoSystemAccount` | bool | false | `no_system_account` | +| `SystemAccount` | string? | null | `system_account` | + +## Integration Points + +### NatsServer.cs + +- Constructor: if `ConfigFile` set, parse before startup +- SIGHUP handler: call `ReloadConfig()` instead of warning log +- New `ReloadConfig()` method for reload orchestration +- Store CLI flag snapshot (`HashSet InCmdLine`) + +### Program.cs + +- Parse config file after defaults, before CLI args +- Track CLI-set options in `InCmdLine` +- Rebuild Serilog config on logging reload + +## File Layout + +``` +src/NATS.Server/Configuration/ + NatsConfLexer.cs (~400 lines) + NatsConfParser.cs (~300 lines) + NatsConfToken.cs (~30 lines) + ConfigProcessor.cs (~350 lines) + ConfigReloader.cs (~250 lines) + IConfigChange.cs (~15 lines) +``` + +## Test Plan + +### Test Files + +- `NatsConfLexerTests.cs` — all token types, comments, escapes, edge cases +- `NatsConfParserTests.cs` — nested blocks, arrays, variables, includes, errors +- `ConfigProcessorTests.cs` — all option key mappings, type coercion, error collection +- `ConfigReloadTests.cs` — reload flow, reloadable vs non-reloadable, CLI precedence + +### Test Data + +``` +tests/NATS.Server.Tests/TestData/ + basic.conf — minimal server config + auth.conf — authorization block with users/nkeys + tls.conf — TLS configuration + full.conf — all supported options + includes/ — include directive tests + invalid.conf — error case configs +``` + +## Task Reference + +Implementation tasks will be created via the writing-plans skill.