From 88a82ee8608fccdab3faf4ec23fdf0b2d886e219 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 13 Mar 2026 18:47:48 -0400 Subject: [PATCH] docs: add XML doc comments to server types and fix flaky test timings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add XML doc comments to public properties across EventTypes, Connz, Varz, NatsOptions, StreamConfig, IStreamStore, FileStore, MqttListener, MqttSessionStore, MessageTraceContext, and JetStreamApiResponse. Fix flaky tests by increasing timing margins (ResponseTracker expiry 1ms→50ms, sleep 50ms→200ms) and document known flaky test patterns in tests.md. --- src/NATS.Server/Events/EventTypes.cs | 630 ++++++++++++++++++ .../Internal/MessageTraceContext.cs | 138 ++++ .../JetStream/Api/JetStreamApiResponse.cs | 183 ++++- .../JetStream/Models/StreamConfig.cs | 223 +++++-- .../JetStream/Storage/FileStore.cs | 15 +- .../JetStream/Storage/IStreamStore.cs | 297 +++++++-- src/NATS.Server/Monitoring/Connz.cs | 335 ++++++++++ src/NATS.Server/Monitoring/Varz.cs | 417 +++++++++++- src/NATS.Server/Mqtt/MqttListener.cs | 99 +++ src/NATS.Server/Mqtt/MqttSessionStore.cs | 77 +++ src/NATS.Server/NatsOptions.cs | 574 +++++++++++++++- tests.md | 10 + tests/NATS.E2E.Tests/AccountIsolationTests.cs | 6 +- tests/NATS.E2E.Tests/CoreMessagingTests.cs | 4 +- .../ConcurrencyStressTests.cs | 2 +- .../ResponseTrackerTests.cs | 8 +- .../Stress/ClusterStressTests.cs | 2 +- .../Gateways/GatewayGoParityTests.cs | 6 +- .../Gateways/ReplyMapCacheTests.cs | 10 +- .../Storage/FileStoreGoParityTests.cs | 4 +- .../Storage/FileStoreTombstoneTests.cs | 16 +- .../Storage/MemStoreGoParityTests.cs | 14 +- .../JetStream/Storage/StoreInterfaceTests.cs | 16 +- .../Raft/RaftElectionTimerTests.cs | 4 +- 24 files changed, 2874 insertions(+), 216 deletions(-) diff --git a/src/NATS.Server/Events/EventTypes.cs b/src/NATS.Server/Events/EventTypes.cs index bcff575..ef4d485 100644 --- a/src/NATS.Server/Events/EventTypes.cs +++ b/src/NATS.Server/Events/EventTypes.cs @@ -21,12 +21,21 @@ public enum ServerCapability : ulong /// public sealed class ServerID { + /// + /// Gets or sets the server name returned by the IDZ endpoint. + /// [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// + /// Gets or sets the server host returned by the IDZ endpoint. + /// [JsonPropertyName("host")] public string Host { get; set; } = string.Empty; + /// + /// Gets or sets the unique server ID returned by the IDZ endpoint. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; } @@ -37,70 +46,127 @@ public sealed class ServerID /// public sealed class EventServerInfo { + /// + /// Gets or sets the server name that emitted the advisory. + /// [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// + /// Gets or sets the server host for the emitting server. + /// [JsonPropertyName("host")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Host { get; set; } + /// + /// Gets or sets the unique server ID for the emitting server. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets the cluster name the server belongs to. + /// [JsonPropertyName("cluster")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Cluster { get; set; } + /// + /// Gets or sets the JetStream domain associated with the server. + /// [JsonPropertyName("domain")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Domain { get; set; } + /// + /// Gets or sets the server version string. + /// [JsonPropertyName("ver")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Version { get; set; } + /// + /// Gets or sets configured server tags included in advisories. + /// [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[]? Tags { get; set; } + /// + /// Gets or sets arbitrary server metadata published with advisories. + /// [JsonPropertyName("metadata")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Metadata { get; set; } + /// + /// Gets or sets a value indicating whether JetStream is enabled on the server. + /// [JsonPropertyName("jetstream")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool JetStream { get; set; } + /// + /// Gets or sets server capability flags bitmask. + /// [JsonPropertyName("flags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ulong Flags { get; set; } + /// + /// Gets or sets advisory sequence number for this server. + /// [JsonPropertyName("seq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ulong Seq { get; set; } + /// + /// Gets or sets advisory timestamp for this server info block. + /// [JsonPropertyName("time")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public DateTime Time { get; set; } + /// + /// Marks JetStream capability as enabled in this server info. + /// public void SetJetStreamEnabled() { JetStream = true; Flags |= (ulong)ServerCapability.JetStreamEnabled; } + /// + /// Returns whether the JetStream capability flag is set. + /// + /// when JetStream is flagged as enabled. public bool JetStreamEnabled() => (Flags & (ulong)ServerCapability.JetStreamEnabled) != 0; + /// + /// Marks binary stream snapshot capability as enabled. + /// public void SetBinaryStreamSnapshot() => Flags |= (ulong)ServerCapability.BinaryStreamSnapshot; + /// + /// Returns whether binary stream snapshot capability is set. + /// + /// when binary snapshots are supported. public bool BinaryStreamSnapshot() => (Flags & (ulong)ServerCapability.BinaryStreamSnapshot) != 0; + /// + /// Marks account NRG capability as enabled. + /// public void SetAccountNRG() => Flags |= (ulong)ServerCapability.AccountNRG; + /// + /// Returns whether account NRG capability is set. + /// + /// when account NRG replication is supported. public bool AccountNRG() => (Flags & (ulong)ServerCapability.AccountNRG) != 0; } @@ -111,89 +177,155 @@ public sealed class EventServerInfo /// public sealed class EventClientInfo { + /// + /// Gets or sets when the client connection started. + /// [JsonPropertyName("start")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public DateTime Start { get; set; } + /// + /// Gets or sets when the client connection stopped. + /// [JsonPropertyName("stop")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public DateTime Stop { get; set; } + /// + /// Gets or sets the client remote host address. + /// [JsonPropertyName("host")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Host { get; set; } + /// + /// Gets or sets the client connection identifier. + /// [JsonPropertyName("id")] public ulong Id { get; set; } + /// + /// Gets or sets the account associated with the client. + /// [JsonPropertyName("acc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Account { get; set; } + /// + /// Gets or sets the service account or service identity, when present. + /// [JsonPropertyName("svc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Service { get; set; } + /// + /// Gets or sets the authenticated user. + /// [JsonPropertyName("user")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? User { get; set; } + /// + /// Gets or sets the client-provided connection name. + /// [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } + /// + /// Gets or sets the client library language. + /// [JsonPropertyName("lang")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Lang { get; set; } + /// + /// Gets or sets the client library version. + /// [JsonPropertyName("ver")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Version { get; set; } + /// + /// Gets or sets client round-trip latency in nanoseconds. + /// [JsonPropertyName("rtt")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public long RttNanos { get; set; } + /// + /// Gets or sets the server ID the client is currently connected to. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Server { get; set; } + /// + /// Gets or sets the cluster name visible to the client. + /// [JsonPropertyName("cluster")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Cluster { get; set; } + /// + /// Gets or sets advertised alternate connect URLs. + /// [JsonPropertyName("alts")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[]? Alternates { get; set; } + /// + /// Gets or sets the client JWT used for authentication. + /// [JsonPropertyName("jwt")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Jwt { get; set; } + /// + /// Gets or sets the issuer key for JWT-authenticated clients. + /// [JsonPropertyName("issuer_key")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? IssuerKey { get; set; } + /// + /// Gets or sets a name tag attached during authentication. + /// [JsonPropertyName("name_tag")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? NameTag { get; set; } + /// + /// Gets or sets tags associated with the client identity. + /// [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[]? Tags { get; set; } + /// + /// Gets or sets the connection kind classification string. + /// [JsonPropertyName("kind")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Kind { get; set; } + /// + /// Gets or sets protocol-specific client type classification. + /// [JsonPropertyName("client_type")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ClientType { get; set; } + /// + /// Gets or sets MQTT client ID when this connection is MQTT. + /// [JsonPropertyName("client_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? MqttClient { get; set; } + /// + /// Gets or sets auth nonce value used during challenge/response flows. + /// [JsonPropertyName("nonce")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Nonce { get; set; } @@ -205,20 +337,35 @@ public sealed class EventClientInfo /// public sealed class DataStats { + /// + /// Gets or sets message count for this traffic bucket. + /// [JsonPropertyName("msgs")] public long Msgs { get; set; } + /// + /// Gets or sets byte count for this traffic bucket. + /// [JsonPropertyName("bytes")] public long Bytes { get; set; } + /// + /// Gets or sets gateway-specific message/byte counters. + /// [JsonPropertyName("gateways")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public MsgBytesStats? Gateways { get; set; } + /// + /// Gets or sets route-specific message/byte counters. + /// [JsonPropertyName("routes")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public MsgBytesStats? Routes { get; set; } + /// + /// Gets or sets leaf-node-specific message/byte counters. + /// [JsonPropertyName("leafs")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public MsgBytesStats? Leafs { get; set; } @@ -230,9 +377,15 @@ public sealed class DataStats /// public sealed class MsgBytesStats { + /// + /// Gets or sets message count. + /// [JsonPropertyName("msgs")] public long Msgs { get; set; } + /// + /// Gets or sets byte count. + /// [JsonPropertyName("bytes")] public long Bytes { get; set; } } @@ -242,18 +395,33 @@ public sealed class ConnectEventMsg { public const string EventType = "io.nats.server.advisory.v1.client_connect"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets connected client identity. + /// [JsonPropertyName("client")] public EventClientInfo Client { get; set; } = new(); } @@ -263,27 +431,51 @@ public sealed class DisconnectEventMsg { public const string EventType = "io.nats.server.advisory.v1.client_disconnect"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets disconnected client identity. + /// [JsonPropertyName("client")] public EventClientInfo Client { get; set; } = new(); + /// + /// Gets or sets bytes/messages sent to the client before disconnect. + /// [JsonPropertyName("sent")] public DataStats Sent { get; set; } = new(); + /// + /// Gets or sets bytes/messages received from the client before disconnect. + /// [JsonPropertyName("received")] public DataStats Received { get; set; } = new(); + /// + /// Gets or sets disconnect reason. + /// [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; } @@ -296,15 +488,27 @@ public sealed class AccountNumConns { public const string EventType = "io.nats.server.advisory.v1.account_connections"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); @@ -334,9 +538,15 @@ public sealed class AccountNumConns [JsonPropertyName("num_subscriptions")] public uint NumSubscriptions { get; set; } + /// + /// Gets or sets sent traffic counters for the account. + /// [JsonPropertyName("sent")] public DataStats Sent { get; set; } = new(); + /// + /// Gets or sets received traffic counters for the account. + /// [JsonPropertyName("received")] public DataStats Received { get; set; } = new(); @@ -352,19 +562,34 @@ public sealed class AccountNumConns /// public sealed class RouteStat { + /// + /// Gets or sets route connection ID. + /// [JsonPropertyName("rid")] public ulong Id { get; set; } + /// + /// Gets or sets route remote name, when known. + /// [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } + /// + /// Gets or sets sent traffic counters for this route. + /// [JsonPropertyName("sent")] public DataStats Sent { get; set; } = new(); + /// + /// Gets or sets received traffic counters for this route. + /// [JsonPropertyName("received")] public DataStats Received { get; set; } = new(); + /// + /// Gets or sets pending bytes queued to this route. + /// [JsonPropertyName("pending")] public int Pending { get; set; } } @@ -375,18 +600,33 @@ public sealed class RouteStat /// public sealed class GatewayStat { + /// + /// Gets or sets gateway connection ID. + /// [JsonPropertyName("gwid")] public ulong Id { get; set; } + /// + /// Gets or sets gateway name. + /// [JsonPropertyName("name")] public string Name { get; set; } = ""; + /// + /// Gets or sets sent traffic counters for this gateway. + /// [JsonPropertyName("sent")] public DataStats Sent { get; set; } = new(); + /// + /// Gets or sets received traffic counters for this gateway. + /// [JsonPropertyName("received")] public DataStats Received { get; set; } = new(); + /// + /// Gets or sets number of inbound gateway connections. + /// [JsonPropertyName("inbound_connections")] public int InboundConnections { get; set; } } @@ -397,15 +637,27 @@ public sealed class GatewayStat /// public sealed class SlowConsumersStats { + /// + /// Gets or sets slow-consumer count for client connections. + /// [JsonPropertyName("clients")] public long Clients { get; set; } + /// + /// Gets or sets slow-consumer count for route connections. + /// [JsonPropertyName("routes")] public long Routes { get; set; } + /// + /// Gets or sets slow-consumer count for gateway connections. + /// [JsonPropertyName("gateways")] public long Gateways { get; set; } + /// + /// Gets or sets slow-consumer count for leaf connections. + /// [JsonPropertyName("leafs")] public long Leafs { get; set; } } @@ -416,15 +668,27 @@ public sealed class SlowConsumersStats /// public sealed class StaleConnectionStats { + /// + /// Gets or sets stale-connection count for client connections. + /// [JsonPropertyName("clients")] public long Clients { get; set; } + /// + /// Gets or sets stale-connection count for route connections. + /// [JsonPropertyName("routes")] public long Routes { get; set; } + /// + /// Gets or sets stale-connection count for gateway connections. + /// [JsonPropertyName("gateways")] public long Gateways { get; set; } + /// + /// Gets or sets stale-connection count for leaf connections. + /// [JsonPropertyName("leafs")] public long Leafs { get; set; } } @@ -432,9 +696,15 @@ public sealed class StaleConnectionStats /// Server stats broadcast. Go events.go:150-153. public sealed class ServerStatsMsg { + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets aggregated server runtime statistics. + /// [JsonPropertyName("statsz")] public ServerStatsData Stats { get; set; } = new(); } @@ -444,31 +714,55 @@ public sealed class ServerStatsMsg /// public sealed class ServerStatsData { + /// + /// Gets or sets server start timestamp. + /// [JsonPropertyName("start")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public DateTime Start { get; set; } + /// + /// Gets or sets current process memory usage in bytes. + /// [JsonPropertyName("mem")] public long Mem { get; set; } + /// + /// Gets or sets number of CPU cores available. + /// [JsonPropertyName("cores")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int Cores { get; set; } + /// + /// Gets or sets current CPU usage. + /// [JsonPropertyName("cpu")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public double Cpu { get; set; } + /// + /// Gets or sets active client connection count. + /// [JsonPropertyName("connections")] public int Connections { get; set; } + /// + /// Gets or sets cumulative client connection count. + /// [JsonPropertyName("total_connections")] public long TotalConnections { get; set; } + /// + /// Gets or sets number of active accounts. + /// [JsonPropertyName("active_accounts")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int ActiveAccounts { get; set; } + /// + /// Gets or sets active subscription count. + /// [JsonPropertyName("subscriptions")] public long Subscriptions { get; set; } @@ -480,44 +774,77 @@ public sealed class ServerStatsData [JsonPropertyName("received")] public DataStats Received { get; set; } = new(); + /// + /// Gets or sets total slow-consumer events. + /// [JsonPropertyName("slow_consumers")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public long SlowConsumers { get; set; } + /// + /// Gets or sets slow-consumer breakdown by connection class. + /// [JsonPropertyName("slow_consumer_stats")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SlowConsumersStats? SlowConsumerStats { get; set; } + /// + /// Gets or sets total stale-connection events. + /// [JsonPropertyName("stale_connections")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public long StaleConnections { get; set; } + /// + /// Gets or sets stale-connection breakdown by connection class. + /// [JsonPropertyName("stale_connection_stats")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public StaleConnectionStats? StaleConnectionStats { get; set; } + /// + /// Gets or sets per-route traffic statistics. + /// [JsonPropertyName("routes")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public RouteStat[]? Routes { get; set; } + /// + /// Gets or sets per-gateway traffic statistics. + /// [JsonPropertyName("gateways")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public GatewayStat[]? Gateways { get; set; } + /// + /// Gets or sets number of active servers in the cluster. + /// [JsonPropertyName("active_servers")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int ActiveServers { get; set; } // Kept for backward compat — flat counters that mirror Sent/Received. + /// + /// Gets or sets flat inbound message counter for compatibility clients. + /// [JsonPropertyName("in_msgs")] public long InMsgs { get; set; } + /// + /// Gets or sets flat outbound message counter for compatibility clients. + /// [JsonPropertyName("out_msgs")] public long OutMsgs { get; set; } + /// + /// Gets or sets flat inbound byte counter for compatibility clients. + /// [JsonPropertyName("in_bytes")] public long InBytes { get; set; } + /// + /// Gets or sets flat outbound byte counter for compatibility clients. + /// [JsonPropertyName("out_bytes")] public long OutBytes { get; set; } } @@ -525,9 +852,15 @@ public sealed class ServerStatsData /// Server shutdown notification. public sealed class ShutdownEventMsg { + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets shutdown reason text. + /// [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; } @@ -535,6 +868,9 @@ public sealed class ShutdownEventMsg /// Lame duck mode notification. public sealed class LameDuckEventMsg { + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); } @@ -544,21 +880,39 @@ public sealed class AuthErrorEventMsg { public const string EventType = "io.nats.server.advisory.v1.client_auth"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets client identity associated with the auth error. + /// [JsonPropertyName("client")] public EventClientInfo Client { get; set; } = new(); + /// + /// Gets or sets authentication failure reason. + /// [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; } @@ -571,25 +925,46 @@ public sealed class OcspPeerRejectEventMsg { public const string EventType = "io.nats.server.advisory.v1.ocsp_peer_reject"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets connection kind (client, route, gateway, or leaf). + /// [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets rejected peer certificate identity. + /// [JsonPropertyName("peer")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventCertInfo? Peer { get; set; } + /// + /// Gets or sets OCSP rejection reason. + /// [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; } @@ -600,18 +975,30 @@ public sealed class OcspPeerRejectEventMsg /// public sealed class EventCertInfo { + /// + /// Gets or sets certificate subject distinguished name. + /// [JsonPropertyName("subject")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Subject { get; set; } + /// + /// Gets or sets certificate issuer distinguished name. + /// [JsonPropertyName("issuer")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Issuer { get; set; } + /// + /// Gets or sets certificate fingerprint. + /// [JsonPropertyName("fingerprint")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Fingerprint { get; set; } + /// + /// Gets or sets raw certificate representation. + /// [JsonPropertyName("raw")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Raw { get; set; } @@ -625,22 +1012,40 @@ public sealed class OcspPeerChainlinkInvalidEventMsg { public const string EventType = "io.nats.server.advisory.v1.ocsp_peer_link_invalid"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; set; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// + /// Gets or sets advisory timestamp. + /// [JsonPropertyName("timestamp")] public DateTime Time { get; set; } + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets certificate in the invalid OCSP chain link. + /// [JsonPropertyName("link")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventCertInfo? Link { get; set; } + /// + /// Gets or sets peer certificate that referenced the invalid chain link. + /// [JsonPropertyName("peer")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventCertInfo? Peer { get; set; } @@ -655,27 +1060,48 @@ public sealed class OcspChainValidationEvent { public const string EventType = "io.nats.server.advisory.v1.ocsp_chain_validation"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; init; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; init; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets advisory timestamp in ISO-8601 format. + /// [JsonPropertyName("time")] public string Time { get; init; } = DateTime.UtcNow.ToString("O"); + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets certificate subject that was validated. + /// [JsonPropertyName("cert_subject")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CertSubject { get; set; } + /// + /// Gets or sets certificate issuer that was validated. + /// [JsonPropertyName("cert_issuer")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CertIssuer { get; set; } + /// + /// Gets or sets certificate serial number that was validated. + /// [JsonPropertyName("serial_number")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? SerialNumber { get; set; } @@ -685,10 +1111,16 @@ public sealed class OcspChainValidationEvent [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? OcspStatus { get; set; } + /// + /// Gets or sets when the OCSP check was performed. + /// [JsonPropertyName("checked_at")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DateTime? CheckedAt { get; set; } + /// + /// Gets or sets OCSP validation error details, when validation failed. + /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Error { get; set; } @@ -715,6 +1147,11 @@ public static class OcspEventBuilder /// Build an OcspPeerRejectEventMsg for a rejected peer certificate. /// Go reference: ocsp.go — postOCSPPeerRejectEvent. /// + /// Emitting server ID. + /// Emitting server name. + /// Connection kind under validation. + /// OCSP rejection reason. + /// A populated OCSP peer rejection advisory. public static OcspPeerRejectEventMsg BuildPeerReject( string serverId, string serverName, string kind, string reason) => @@ -731,6 +1168,14 @@ public static class OcspEventBuilder /// Build an OcspChainValidationEvent for a certificate OCSP check. /// Go reference: ocsp.go — OCSP chain validation advisory. /// + /// Emitting server ID. + /// Emitting server name. + /// Validated certificate subject. + /// Validated certificate issuer. + /// Validated certificate serial number. + /// Validation status string. + /// Optional validation error details. + /// A populated OCSP chain validation advisory. public static OcspChainValidationEvent BuildChainValidation( string serverId, string serverName, string certSubject, string certIssuer, string serialNumber, @@ -750,6 +1195,8 @@ public static class OcspEventBuilder /// Parse an OCSP status string into the enum. /// Go reference: ocsp.go — ocspStatusGood / ocspStatusRevoked string constants. /// + /// Status string to parse. + /// The parsed OCSP status enum value. public static OcspStatus ParseStatus(string? status) => status?.ToLowerInvariant() switch { @@ -767,27 +1214,48 @@ public sealed class RemoteServerShutdownEvent { public const string EventType = "io.nats.server.advisory.v1.remote_shutdown"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; init; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; init; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets advisory timestamp in ISO-8601 format. + /// [JsonPropertyName("time")] public string Time { get; init; } = DateTime.UtcNow.ToString("O"); + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets remote server ID that shut down. + /// [JsonPropertyName("remote_server_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RemoteServerId { get; set; } + /// + /// Gets or sets remote server name that shut down. + /// [JsonPropertyName("remote_server_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RemoteServerName { get; set; } + /// + /// Gets or sets remote shutdown reason. + /// [JsonPropertyName("reason")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Reason { get; set; } @@ -801,23 +1269,41 @@ public sealed class RemoteServerUpdateEvent { public const string EventType = "io.nats.server.advisory.v1.remote_update"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; init; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; init; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets advisory timestamp in ISO-8601 format. + /// [JsonPropertyName("time")] public string Time { get; init; } = DateTime.UtcNow.ToString("O"); + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets remote server ID that was updated. + /// [JsonPropertyName("remote_server_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RemoteServerId { get; set; } + /// + /// Gets or sets remote server name that was updated. + /// [JsonPropertyName("remote_server_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RemoteServerName { get; set; } @@ -836,31 +1322,55 @@ public sealed class LeafNodeConnectEvent { public const string EventType = "io.nats.server.advisory.v1.leafnode_connect"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; init; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; init; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets advisory timestamp in ISO-8601 format. + /// [JsonPropertyName("time")] public string Time { get; init; } = DateTime.UtcNow.ToString("O"); + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets leaf node connection ID. + /// [JsonPropertyName("leaf_node_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? LeafNodeId { get; set; } + /// + /// Gets or sets leaf node connection name. + /// [JsonPropertyName("leaf_node_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? LeafNodeName { get; set; } + /// + /// Gets or sets remote URL used by the leaf connection. + /// [JsonPropertyName("remote_url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RemoteUrl { get; set; } + /// + /// Gets or sets account bound to the leaf node connection. + /// [JsonPropertyName("account")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Account { get; set; } @@ -874,23 +1384,41 @@ public sealed class LeafNodeDisconnectEvent { public const string EventType = "io.nats.server.advisory.v1.leafnode_disconnect"; + /// + /// Gets or sets advisory schema type identifier. + /// [JsonPropertyName("type")] public string Type { get; init; } = EventType; + /// + /// Gets or sets unique advisory ID. + /// [JsonPropertyName("id")] public string Id { get; init; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets advisory timestamp in ISO-8601 format. + /// [JsonPropertyName("time")] public string Time { get; init; } = DateTime.UtcNow.ToString("O"); + /// + /// Gets or sets emitting server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets leaf node connection ID. + /// [JsonPropertyName("leaf_node_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? LeafNodeId { get; set; } + /// + /// Gets or sets disconnect reason text. + /// [JsonPropertyName("reason")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Reason { get; set; } @@ -902,9 +1430,15 @@ public sealed class LeafNodeDisconnectEvent /// public sealed class AccNumConnsReq { + /// + /// Gets or sets requesting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets account to query. + /// [JsonPropertyName("acc")] public string Account { get; set; } = string.Empty; } @@ -915,9 +1449,15 @@ public sealed class AccNumConnsReq /// public sealed class AccNumSubsReq { + /// + /// Gets or sets requesting server identity. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets account to query. + /// [JsonPropertyName("acc")] public string Account { get; set; } = string.Empty; } @@ -928,22 +1468,37 @@ public sealed class AccNumSubsReq /// public class EventFilterOptions { + /// + /// Gets or sets server-name filter. + /// [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } + /// + /// Gets or sets cluster-name filter. + /// [JsonPropertyName("cluster")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Cluster { get; set; } + /// + /// Gets or sets host filter. + /// [JsonPropertyName("host")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Host { get; set; } + /// + /// Gets or sets tag filter list. + /// [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[]? Tags { get; set; } + /// + /// Gets or sets domain filter. + /// [JsonPropertyName("domain")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Domain { get; set; } @@ -972,10 +1527,16 @@ public sealed class RaftzEventOptions : EventFilterOptions; /// public sealed class ServerAPIError { + /// + /// Gets or sets server API error code. + /// [JsonPropertyName("code")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int Code { get; set; } + /// + /// Gets or sets server API error description. + /// [JsonPropertyName("description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Description { get; set; } @@ -987,14 +1548,23 @@ public sealed class ServerAPIError /// public class ServerAPIResponse { + /// + /// Gets or sets responding server identity. + /// [JsonPropertyName("server")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EventServerInfo? Server { get; set; } + /// + /// Gets or sets response payload. + /// [JsonPropertyName("data")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object? Data { get; set; } + /// + /// Gets or sets response error payload when the request failed. + /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ServerAPIError? Error { get; set; } @@ -1019,6 +1589,9 @@ public sealed class ServerAPIRaftzResponse : ServerAPIResponse; /// public sealed class KickClientReq { + /// + /// Gets or sets client ID to disconnect. + /// [JsonPropertyName("cid")] public ulong ClientId { get; set; } } @@ -1029,6 +1602,9 @@ public sealed class KickClientReq /// public sealed class LDMClientReq { + /// + /// Gets or sets client ID to move into lame-duck handling. + /// [JsonPropertyName("cid")] public ulong ClientId { get; set; } } @@ -1039,14 +1615,23 @@ public sealed class LDMClientReq /// public sealed class UserInfo { + /// + /// Gets or sets user identifier. + /// [JsonPropertyName("user")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? User { get; set; } + /// + /// Gets or sets account identifier. + /// [JsonPropertyName("acc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Account { get; set; } + /// + /// Gets or sets permission summary for the user. + /// [JsonPropertyName("permissions")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Permissions { get; set; } @@ -1064,6 +1649,17 @@ public static class EventBuilder /// Build a ConnectEventMsg with all required fields. /// Go reference: events.go sendConnectEventMsg (around line 636). /// + /// Emitting server ID. + /// Emitting server name. + /// Optional cluster name. + /// Connecting client ID. + /// Connecting client host. + /// Connecting client account. + /// Connecting client user. + /// Connecting client name. + /// Connecting client library language. + /// Connecting client library version. + /// A populated connect advisory. public static ConnectEventMsg BuildConnectEvent( string serverId, string serverName, string? cluster, ulong clientId, string host, string? account, string? user, string? name, @@ -1095,6 +1691,17 @@ public static class EventBuilder /// Build a DisconnectEventMsg with all required fields. /// Go reference: events.go sendDisconnectEventMsg (around line 668). /// + /// Emitting server ID. + /// Emitting server name. + /// Optional cluster name. + /// Disconnecting client ID. + /// Disconnecting client host. + /// Disconnecting client account. + /// Disconnecting client user. + /// Disconnect reason. + /// Traffic sent to the client. + /// Traffic received from the client. + /// A populated disconnect advisory. public static DisconnectEventMsg BuildDisconnectEvent( string serverId, string serverName, string? cluster, ulong clientId, string host, string? account, string? user, @@ -1126,6 +1733,17 @@ public static class EventBuilder /// Build an AccountNumConns event. /// Go reference: events.go sendAccountNumConns (around line 714). /// + /// Emitting server ID. + /// Emitting server name. + /// Account name being reported. + /// Active account connection count. + /// Active leaf-node count for the account. + /// Total account connections over time. + /// Active subscription count for the account. + /// Traffic sent by the account. + /// Traffic received by the account. + /// Slow-consumer count for the account. + /// A populated account-connections advisory. public static AccountNumConns BuildAccountConnsEvent( string serverId, string serverName, string accountName, int connections, int leafNodes, @@ -1154,6 +1772,18 @@ public static class EventBuilder /// Build a ServerStatsMsg. /// Go reference: events.go sendStatsz (around line 742). /// + /// Emitting server ID. + /// Emitting server name. + /// Current memory usage in bytes. + /// Available CPU core count. + /// Current CPU usage. + /// Active client connections. + /// Total client connections since start. + /// Active account count. + /// Active subscription count. + /// Total outbound traffic counters. + /// Total inbound traffic counters. + /// A populated server statistics advisory. public static ServerStatsMsg BuildServerStats( string serverId, string serverName, long mem, int cores, double cpu, diff --git a/src/NATS.Server/Internal/MessageTraceContext.cs b/src/NATS.Server/Internal/MessageTraceContext.cs index 26f8793..5a85ea5 100644 --- a/src/NATS.Server/Internal/MessageTraceContext.cs +++ b/src/NATS.Server/Internal/MessageTraceContext.cs @@ -55,16 +55,28 @@ public static class MsgTraceErrors /// public sealed class MsgTraceEvent { + /// + /// Gets or sets server identity details for the hop that emitted this trace. + /// [JsonPropertyName("server")] public EventServerInfo Server { get; set; } = new(); + /// + /// Gets or sets request-level metadata captured at ingress time. + /// [JsonPropertyName("request")] public MsgTraceRequest Request { get; set; } = new(); + /// + /// Gets or sets the number of inter-server hops observed for this trace. + /// [JsonPropertyName("hops")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int Hops { get; set; } + /// + /// Gets or sets the ordered list of trace events recorded along the path. + /// [JsonPropertyName("events")] public List Events { get; set; } = []; } @@ -75,10 +87,16 @@ public sealed class MsgTraceEvent /// public sealed class MsgTraceRequest { + /// + /// Gets or sets captured trace-relevant request headers. + /// [JsonPropertyName("header")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Header { get; set; } + /// + /// Gets or sets message size in bytes for the traced publish. + /// [JsonPropertyName("msgsize")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int MsgSize { get; set; } @@ -96,9 +114,15 @@ public sealed class MsgTraceRequest [JsonDerivedType(typeof(MsgTraceEgress))] public class MsgTraceEntry { + /// + /// Gets or sets the event type token (for example in, eg, or js). + /// [JsonPropertyName("type")] public string Type { get; set; } = ""; + /// + /// Gets or sets when this trace event was recorded. + /// [JsonPropertyName("ts")] public DateTime Timestamp { get; set; } = DateTime.UtcNow; } @@ -109,22 +133,40 @@ public class MsgTraceEntry /// public sealed class MsgTraceIngress : MsgTraceEntry { + /// + /// Gets or sets connection kind for the publisher (client, route, gateway, or leaf). + /// [JsonPropertyName("kind")] public int Kind { get; set; } + /// + /// Gets or sets source connection ID. + /// [JsonPropertyName("cid")] public ulong Cid { get; set; } + /// + /// Gets or sets optional source connection name. + /// [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } + /// + /// Gets or sets source account name. + /// [JsonPropertyName("acc")] public string Account { get; set; } = ""; + /// + /// Gets or sets original published subject. + /// [JsonPropertyName("subj")] public string Subject { get; set; } = ""; + /// + /// Gets or sets ingress error text when the publish was rejected. + /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Error { get; set; } @@ -136,6 +178,9 @@ public sealed class MsgTraceIngress : MsgTraceEntry /// public sealed class MsgTraceSubjectMapping : MsgTraceEntry { + /// + /// Gets or sets the remapped destination subject. + /// [JsonPropertyName("to")] public string MappedTo { get; set; } = ""; } @@ -146,9 +191,15 @@ public sealed class MsgTraceSubjectMapping : MsgTraceEntry /// public sealed class MsgTraceStreamExport : MsgTraceEntry { + /// + /// Gets or sets account that exported the message. + /// [JsonPropertyName("acc")] public string Account { get; set; } = ""; + /// + /// Gets or sets export destination subject. + /// [JsonPropertyName("to")] public string To { get; set; } = ""; } @@ -159,12 +210,21 @@ public sealed class MsgTraceStreamExport : MsgTraceEntry /// public sealed class MsgTraceServiceImport : MsgTraceEntry { + /// + /// Gets or sets account that imported the service. + /// [JsonPropertyName("acc")] public string Account { get; set; } = ""; + /// + /// Gets or sets original subject before import remap. + /// [JsonPropertyName("from")] public string From { get; set; } = ""; + /// + /// Gets or sets rewritten service subject. + /// [JsonPropertyName("to")] public string To { get; set; } = ""; } @@ -175,17 +235,29 @@ public sealed class MsgTraceServiceImport : MsgTraceEntry /// public sealed class MsgTraceJetStreamEntry : MsgTraceEntry { + /// + /// Gets or sets the JetStream stream that handled the message. + /// [JsonPropertyName("stream")] public string Stream { get; set; } = ""; + /// + /// Gets or sets stored subject after any JetStream subject transform. + /// [JsonPropertyName("subject")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Subject { get; set; } + /// + /// Gets or sets a value indicating whether no consumer interest was present. + /// [JsonPropertyName("nointerest")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool NoInterest { get; set; } + /// + /// Gets or sets JetStream storage/delivery error text. + /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Error { get; set; } @@ -197,32 +269,56 @@ public sealed class MsgTraceJetStreamEntry : MsgTraceEntry /// public sealed class MsgTraceEgress : MsgTraceEntry { + /// + /// Gets or sets target connection kind for this delivery attempt. + /// [JsonPropertyName("kind")] public int Kind { get; set; } + /// + /// Gets or sets target connection ID. + /// [JsonPropertyName("cid")] public ulong Cid { get; set; } + /// + /// Gets or sets optional target connection name. + /// [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } + /// + /// Gets or sets hop identifier used for forwarded deliveries. + /// [JsonPropertyName("hop")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Hop { get; set; } + /// + /// Gets or sets target account when it differs from ingress account. + /// [JsonPropertyName("acc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Account { get; set; } + /// + /// Gets or sets delivered subscription subject for client egress. + /// [JsonPropertyName("sub")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Subscription { get; set; } + /// + /// Gets or sets queue group name for queue deliveries. + /// [JsonPropertyName("queue")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Queue { get; set; } + /// + /// Gets or sets egress error text for failed delivery attempts. + /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Error { get; set; } @@ -302,6 +398,14 @@ public sealed class MsgTraceContext /// Parses Nats-Trace-Dest, Nats-Trace-Only, and Nats-Trace-Hop headers. /// Go reference: msgtrace.go:332-492 /// + /// Raw protocol headers from the inbound publish. + /// Source client connection ID. + /// Optional source client name. + /// Source account name. + /// Published subject. + /// Message payload size in bytes. + /// Source connection kind constant. + /// A trace context when tracing is enabled; otherwise . public static MsgTraceContext? Create( ReadOnlyMemory headers, ulong clientId, @@ -377,6 +481,7 @@ public sealed class MsgTraceContext /// Sets an error on the ingress event. /// Go reference: msgtrace.go:657-661 /// + /// Ingress failure reason text. public void SetIngressError(string error) { if (Event.Events.Count > 0 && Event.Events[0] is MsgTraceIngress ingress) @@ -389,6 +494,7 @@ public sealed class MsgTraceContext /// Adds a subject mapping trace event. /// Go reference: msgtrace.go:663-674 /// + /// Mapped subject after subject transformation. public void AddSubjectMappingEvent(string mappedTo) { Event.Events.Add(new MsgTraceSubjectMapping @@ -403,6 +509,13 @@ public sealed class MsgTraceContext /// Adds an egress trace event for a delivery target. /// Go reference: msgtrace.go:676-711 /// + /// Target client ID. + /// Optional target client name. + /// Target connection kind constant. + /// Subscription subject used for client delivery. + /// Queue group name for queue subscriptions. + /// Target account name when applicable. + /// Delivery error text, when delivery failed. public void AddEgressEvent(ulong clientId, string? clientName, int clientKind, string? subscriptionSubject = null, string? queue = null, string? account = null, string? error = null) { @@ -442,6 +555,8 @@ public sealed class MsgTraceContext /// Adds a stream export trace event. /// Go reference: msgtrace.go:713-728 /// + /// Exporting account name. + /// Export destination subject. public void AddStreamExportEvent(string accountName, string to) { Event.Events.Add(new MsgTraceStreamExport @@ -457,6 +572,9 @@ public sealed class MsgTraceContext /// Adds a service import trace event. /// Go reference: msgtrace.go:730-743 /// + /// Importing account name. + /// Original service subject. + /// Mapped service subject. public void AddServiceImportEvent(string accountName, string from, string to) { Event.Events.Add(new MsgTraceServiceImport @@ -473,6 +591,7 @@ public sealed class MsgTraceContext /// Adds a JetStream trace event for stream storage. /// Go reference: msgtrace.go:745-757 /// + /// Stream name selected for storage. public void AddJetStreamEvent(string streamName) { _js = new MsgTraceJetStreamEntry @@ -488,6 +607,8 @@ public sealed class MsgTraceContext /// Updates the JetStream trace event with subject and interest info. /// Go reference: msgtrace.go:759-772 /// + /// Resolved subject stored in the stream. + /// Whether downstream consumer interest was absent. public void UpdateJetStreamEvent(string subject, bool noInterest) { if (_js == null) return; @@ -514,6 +635,7 @@ public sealed class MsgTraceContext /// Delegates to SendEvent for the two-phase ready logic. /// Go reference: msgtrace.go:774-786 /// + /// Optional JetStream error to attach before publishing. public void SendEventFromJetStream(string? error = null) { if (_js == null) return; @@ -546,6 +668,8 @@ public sealed class MsgTraceContext /// Returns null if no trace headers found. /// Go reference: msgtrace.go:509-591 /// + /// Raw header block from a NATS message. + /// Trace-relevant headers when present; otherwise . internal static Dictionary? ParseTraceHeaders(ReadOnlySpan hdr) { // Must start with NATS/1.0 header line @@ -705,6 +829,10 @@ public sealed class TraceContextPropagator /// /// Creates a new trace context for an origin message. /// + /// Trace identifier shared across all hops. + /// Initial span identifier for this hop. + /// Optional trace destination subject. + /// A new trace context for propagation. public static TraceContext CreateTrace(string traceId, string spanId, string? destination = null) => new(traceId, spanId, destination, TraceOnly: false, DateTime.UtcNow); @@ -713,6 +841,8 @@ public sealed class TraceContextPropagator /// Parses "Nats-Trace-Parent: {traceId}-{spanId}" from the header block. /// Returns null if the header is absent or malformed. /// + /// Raw NATS header block. + /// The extracted trace context, or when missing or invalid. public static TraceContext? ExtractTrace(ReadOnlySpan headers) { if (headers.IsEmpty) @@ -803,6 +933,9 @@ public sealed class TraceContextPropagator /// "Nats-Trace-Parent: {traceId}-{spanId}\r\n". /// If existingHeaders is empty a minimal NATS/1.0 header block is created. /// + /// Trace context values to inject. + /// Existing NATS header block to augment. + /// A header block containing trace propagation headers. public static byte[] InjectTrace(TraceContext context, ReadOnlySpan existingHeaders) { var headerLine = $"{TraceParentHeader}: {context.TraceId}-{context.SpanId}\r\n"; @@ -843,6 +976,9 @@ public sealed class TraceContextPropagator /// Creates a child span that preserves the parent TraceId but /// uses a new SpanId for this hop. /// + /// Parent trace context. + /// New span ID for the child hop. + /// A child trace context with inherited trace metadata. public static TraceContext CreateChildSpan(TraceContext parent, string newSpanId) => new(parent.TraceId, newSpanId, parent.Destination, parent.TraceOnly, DateTime.UtcNow); @@ -850,6 +986,8 @@ public sealed class TraceContextPropagator /// Returns true if the header block contains a Nats-Trace-Parent header, /// indicating the message should be traced. /// + /// Raw message headers to inspect. + /// when tracing headers are present. public static bool ShouldTrace(ReadOnlySpan headers) { if (headers.IsEmpty) diff --git a/src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs b/src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs index 7ac16c3..157636f 100644 --- a/src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs +++ b/src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs @@ -2,21 +2,79 @@ using NATS.Server.JetStream.Models; namespace NATS.Server.JetStream.Api; +/// +/// Represents normalized response data for JetStream API handlers before wire shaping. +/// public sealed class JetStreamApiResponse { + /// + /// Gets the error payload when the API call fails. + /// public JetStreamApiError? Error { get; init; } + + /// + /// Gets stream info payload for single-stream responses. + /// public JetStreamStreamInfo? StreamInfo { get; init; } + + /// + /// Gets consumer info payload for single-consumer responses. + /// public JetStreamConsumerInfo? ConsumerInfo { get; init; } + + /// + /// Gets account info payload for account info responses. + /// public JetStreamAccountInfo? AccountInfo { get; init; } + + /// + /// Gets stream-name list payload for stream name listing. + /// public IReadOnlyList? StreamNames { get; init; } + + /// + /// Gets stream-info list payload for stream listing. + /// public IReadOnlyList? StreamInfoList { get; init; } + + /// + /// Gets consumer-name list payload for consumer name listing. + /// public IReadOnlyList? ConsumerNames { get; init; } + + /// + /// Gets consumer-info list payload for consumer listing. + /// public IReadOnlyList? ConsumerInfoList { get; init; } + + /// + /// Gets stream message payload for message-get APIs. + /// public JetStreamStreamMessage? StreamMessage { get; init; } + + /// + /// Gets direct message payload for direct get/fetch APIs. + /// public JetStreamDirectMessage? DirectMessage { get; init; } + + /// + /// Gets snapshot payload for snapshot APIs. + /// public JetStreamSnapshot? Snapshot { get; init; } + + /// + /// Gets pull-batch payload for pull APIs. + /// public JetStreamPullBatch? PullBatch { get; init; } + + /// + /// Gets a value indicating whether the operation succeeded. + /// public bool Success { get; init; } + + /// + /// Gets number of purged messages for purge responses. + /// public ulong Purged { get; init; } /// @@ -44,9 +102,9 @@ public sealed class JetStreamApiResponse /// /// Returns a wire-format object for JSON serialization matching the Go server's - /// flat response structure (e.g., config/state at root level for stream responses, - /// not nested under a wrapper property). + /// flat response structure. /// + /// Anonymous object shaped to match Go JetStream API response JSON. public object ToWireFormat() { if (StreamInfo != null) @@ -122,9 +180,9 @@ public sealed class JetStreamApiResponse /// /// Creates a Go-compatible wire format for StreamConfig. - /// Only includes fields the Go server sends, with enums as lowercase strings. - /// Go reference: server/stream.go StreamConfig JSON marshaling. /// + /// Stream configuration. + /// Anonymous object matching Go stream config JSON fields. private static object ToWireConfig(StreamConfig c) => new { name = c.Name, @@ -147,6 +205,11 @@ public sealed class JetStreamApiResponse first_seq = c.FirstSeq, }; + /// + /// Creates a Go-compatible wire format for stream state. + /// + /// API stream state. + /// Anonymous object matching Go stream state JSON fields. private static object ToWireState(ApiStreamState s) => new { messages = s.Messages, @@ -156,6 +219,11 @@ public sealed class JetStreamApiResponse consumer_count = 0, }; + /// + /// Creates a Go-compatible wire format for consumer config. + /// + /// Consumer configuration. + /// Anonymous object matching Go consumer config JSON fields. private static object ToWireConsumerConfig(ConsumerConfig c) => new { durable_name = string.IsNullOrEmpty(c.DurableName) ? null : c.DurableName, @@ -167,10 +235,14 @@ public sealed class JetStreamApiResponse max_deliver = c.MaxDeliver, max_ack_pending = c.MaxAckPending, filter_subject = c.FilterSubject, - // Go: consumer.go — deliver_subject present for push consumers deliver_subject = string.IsNullOrEmpty(c.DeliverSubject) ? null : c.DeliverSubject, }; + /// + /// Creates a not-found error response for unknown API subjects. + /// + /// Unknown API subject. + /// Not-found response payload. public static JetStreamApiResponse NotFound(string subject) => new() { Error = new JetStreamApiError @@ -180,13 +252,27 @@ public sealed class JetStreamApiResponse }, }; + /// + /// Creates an empty success response. + /// + /// Empty response. public static JetStreamApiResponse Ok() => new(); + /// + /// Creates a success response. + /// + /// Success response payload. public static JetStreamApiResponse SuccessResponse() => new() { Success = true, }; + /// + /// Creates an error response. + /// + /// Error code. + /// Error description. + /// Error response payload. public static JetStreamApiResponse ErrorResponse(int code, string description) => new() { Error = new JetStreamApiError @@ -198,9 +284,9 @@ public sealed class JetStreamApiResponse /// /// Returns a not-leader error with code 10003 and a leader_hint. - /// Go reference: jetstream_api.go:200-300 — non-leader nodes return this error - /// for mutating operations so clients can redirect. /// + /// Leader redirect hint. + /// Not-leader error response payload. public static JetStreamApiResponse NotLeader(string leaderHint) => new() { Error = new JetStreamApiError @@ -212,9 +298,10 @@ public sealed class JetStreamApiResponse }; /// - /// Returns a purge success response with the number of messages purged. - /// Go reference: jetstream_api.go:1200-1350 — purge response includes purged count. + /// Returns a purge success response with number of purged messages. /// + /// Purged message count. + /// Purge success response payload. public static JetStreamApiResponse PurgeResponse(ulong purged) => new() { Success = true, @@ -223,8 +310,10 @@ public sealed class JetStreamApiResponse /// /// Returns a pause/resume success response with current pause state. - /// Go reference: server/consumer.go jsConsumerPauseResponse — returned after pause/resume API call. /// + /// Whether consumer is paused. + /// Pause-until deadline. + /// Pause state response payload. public static JetStreamApiResponse PauseResponse(bool paused, DateTime? pauseUntil) => new() { Success = true, @@ -233,41 +322,109 @@ public sealed class JetStreamApiResponse }; } +/// +/// Stream info payload for JetStream API responses. +/// public sealed class JetStreamStreamInfo { + /// + /// Gets stream configuration. + /// public required StreamConfig Config { get; init; } + + /// + /// Gets stream runtime state. + /// public required ApiStreamState State { get; init; } } +/// +/// Consumer info payload for JetStream API responses. +/// public sealed class JetStreamConsumerInfo { + /// + /// Gets consumer name. + /// public string? Name { get; init; } + + /// + /// Gets parent stream name. + /// public string? StreamName { get; init; } + + /// + /// Gets consumer configuration. + /// public required ConsumerConfig Config { get; init; } } +/// +/// Account-level JetStream usage payload. +/// public sealed class JetStreamAccountInfo { + /// + /// Gets stream count for the account. + /// public int Streams { get; init; } + + /// + /// Gets consumer count for the account. + /// public int Consumers { get; init; } } +/// +/// Stream message payload returned by stream message APIs. +/// public sealed class JetStreamStreamMessage { + /// + /// Gets stream sequence. + /// public ulong Sequence { get; init; } + + /// + /// Gets message subject. + /// public string Subject { get; init; } = string.Empty; + + /// + /// Gets encoded payload. + /// public string Payload { get; init; } = string.Empty; } +/// +/// Direct message payload returned by direct message APIs. +/// public sealed class JetStreamDirectMessage { + /// + /// Gets stream sequence. + /// public ulong Sequence { get; init; } + + /// + /// Gets message subject. + /// public string Subject { get; init; } = string.Empty; + + /// + /// Gets encoded payload. + /// public string Payload { get; init; } = string.Empty; } +/// +/// Snapshot payload returned by snapshot APIs. +/// public sealed class JetStreamSnapshot { + /// + /// Gets snapshot payload bytes encoded for transport. + /// public string Payload { get; init; } = string.Empty; /// Stream name this snapshot was taken from. @@ -280,7 +437,13 @@ public sealed class JetStreamSnapshot public int BlkSize { get; init; } } +/// +/// Pull batch payload returned by pull APIs. +/// public sealed class JetStreamPullBatch { + /// + /// Gets batch messages. + /// public IReadOnlyList Messages { get; init; } = []; } diff --git a/src/NATS.Server/JetStream/Models/StreamConfig.cs b/src/NATS.Server/JetStream/Models/StreamConfig.cs index 7012956..874090a 100644 --- a/src/NATS.Server/JetStream/Models/StreamConfig.cs +++ b/src/NATS.Server/JetStream/Models/StreamConfig.cs @@ -1,129 +1,272 @@ namespace NATS.Server.JetStream.Models; +/// +/// Defines JetStream stream configuration used when creating or updating streams. +/// This mirrors the Go StreamConfig shape for wire compatibility. +/// public sealed class StreamConfig { + /// + /// Gets or sets stream name. + /// public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets optional human-readable stream description. + /// public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets subjects bound to the stream. + /// public List Subjects { get; set; } = []; + + /// + /// Gets or sets maximum message count limit. + /// public int MaxMsgs { get; set; } + + /// + /// Gets or sets maximum byte storage limit. + /// public long MaxBytes { get; set; } + + /// + /// Gets or sets per-subject message cap. + /// public int MaxMsgsPer { get; set; } + + /// + /// Gets or sets max age in milliseconds for in-process calculations. + /// [System.Text.Json.Serialization.JsonIgnore] public int MaxAgeMs { get; set; } /// - /// MaxAge in nanoseconds for JSON wire compatibility with Go server. - /// Go reference: StreamConfig.MaxAge is a time.Duration (nanoseconds in JSON). + /// Gets or sets max age in nanoseconds for JSON wire compatibility with Go. /// public long MaxAge { get => (long)MaxAgeMs * 1_000_000L; set => MaxAgeMs = (int)(value / 1_000_000); } + + /// + /// Gets or sets maximum accepted message size. + /// public int MaxMsgSize { get; set; } + + /// + /// Gets or sets maximum consumer count allowed for the stream. + /// public int MaxConsumers { get; set; } + + /// + /// Gets or sets duplicate window in milliseconds for publish deduplication. + /// public int DuplicateWindowMs { get; set; } + + /// + /// Gets or sets a value indicating whether stream configuration is sealed. + /// public bool Sealed { get; set; } + + /// + /// Gets or sets a value indicating whether explicit delete is denied. + /// public bool DenyDelete { get; set; } + + /// + /// Gets or sets a value indicating whether purge operations are denied. + /// public bool DenyPurge { get; set; } + + /// + /// Gets or sets a value indicating whether direct get APIs are enabled. + /// public bool AllowDirect { get; set; } - // Go: StreamConfig.AllowMsgTTL — per-message TTL header support + + /// + /// Gets or sets a value indicating whether per-message TTL headers are honored. + /// public bool AllowMsgTtl { get; set; } - // Go: StreamConfig.FirstSeq — initial sequence number for the stream + + /// + /// Gets or sets initial sequence number assigned to the stream. + /// public ulong FirstSeq { get; set; } + + /// + /// Gets or sets stream retention policy. + /// public RetentionPolicy Retention { get; set; } = RetentionPolicy.Limits; + + /// + /// Gets or sets discard policy when limits are exceeded. + /// public DiscardPolicy Discard { get; set; } = DiscardPolicy.Old; + + /// + /// Gets or sets storage backend type. + /// public StorageType Storage { get; set; } = StorageType.Memory; + + /// + /// Gets or sets replication factor. + /// public int Replicas { get; set; } = 1; + + /// + /// Gets or sets mirror stream name when this stream is a mirror. + /// public string? Mirror { get; set; } + + /// + /// Gets or sets legacy single-source name. + /// public string? Source { get; set; } + + /// + /// Gets or sets source stream configurations. + /// public List Sources { get; set; } = []; - // Go: StreamConfig.SubjectTransform — transforms inbound message subjects on store. - // Source and Dest follow the same token-wildcard rules as NATS subject transforms. - // Go reference: server/stream.go:352 (SubjectTransform field in StreamConfig) + /// + /// Gets or sets source subject for stream-level subject transforms. + /// public string? SubjectTransformSource { get; set; } + + /// + /// Gets or sets destination subject for stream-level subject transforms. + /// public string? SubjectTransformDest { get; set; } - // Go: StreamConfig.RePublish — re-publish stored messages on a separate subject. - // Source is the filter (empty = match all); Dest is the target subject pattern. - // Go reference: server/stream.go:356 (RePublish field in StreamConfig) + /// + /// Gets or sets source filter subject for republish. + /// public string? RePublishSource { get; set; } + + /// + /// Gets or sets destination subject pattern for republish. + /// public string? RePublishDest { get; set; } - // Go: RePublish.HeadersOnly — republished copy omits message body. + + /// + /// Gets or sets a value indicating whether republish emits headers only. + /// public bool RePublishHeadersOnly { get; set; } - // Go: StreamConfig.SubjectDeleteMarkerTTL — duration to retain delete markers. - // When > 0 and AllowMsgTTL is true, expired messages emit a delete-marker msg. - // Incompatible with Mirror config. - // Go reference: server/stream.go:361 (SubjectDeleteMarkerTTL field) + /// + /// Gets or sets delete-marker TTL in milliseconds. + /// public int SubjectDeleteMarkerTtlMs { get; set; } - // Go: StreamConfig.AllowMsgSchedules — enables scheduled publish headers. - // Incompatible with Mirror and Sources. - // Go reference: server/stream.go:369 (AllowMsgSchedules field) + /// + /// Gets or sets a value indicating whether scheduled publish headers are allowed. + /// public bool AllowMsgSchedules { get; set; } - // Go: StreamConfig.AllowMsgCounter — enables CRDT counter semantics on messages. - // Added in v2.12, requires API level 2. - // Go reference: server/stream.go:365 (AllowMsgCounter field) + /// + /// Gets or sets a value indicating whether CRDT message counters are enabled. + /// public bool AllowMsgCounter { get; set; } - // Go: StreamConfig.AllowAtomicPublish — enables atomic batch publishing. - // Added in v2.12, requires API level 2. - // Go reference: server/stream.go:367 (AllowAtomicPublish field) + /// + /// Gets or sets a value indicating whether atomic batch publish is enabled. + /// public bool AllowAtomicPublish { get; set; } - // Go: StreamConfig.PersistMode — async vs sync storage persistence. - // AsyncPersistMode requires API level 2. - // Go reference: server/stream.go:375 (PersistMode field) + /// + /// Gets or sets persistence mode (sync or async). + /// public PersistMode PersistMode { get; set; } = PersistMode.Sync; - // Go: StreamConfig.Metadata — user-supplied and server-managed key/value metadata. - // The server automatically sets _nats.req.level, _nats.ver, _nats.level. - // Go reference: server/stream.go:380 (Metadata field) + /// + /// Gets or sets stream metadata key/value pairs. + /// public Dictionary? Metadata { get; set; } } /// -/// Persistence mode for the stream. -/// Go reference: server/stream.go — AsyncPersistMode constant. +/// Persistence mode for stream storage writes. /// public enum PersistMode { + /// + /// Persist writes synchronously. + /// Sync = 0, + + /// + /// Persist writes asynchronously. + /// Async = 1, } +/// +/// Storage backend type for JetStream streams. +/// public enum StorageType { + /// + /// In-memory storage. + /// Memory, + + /// + /// File-based storage. + /// File, } +/// +/// Defines a stream source configuration for sourced streams. +/// public sealed class StreamSourceConfig { + /// + /// Gets or sets source stream name. + /// public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets destination subject prefix for source transforms. + /// public string? SubjectTransformPrefix { get; set; } + + /// + /// Gets or sets source account for cross-account sourcing. + /// public string? SourceAccount { get; set; } - // Go: StreamSource.FilterSubject — only forward messages matching this subject filter. + /// + /// Gets or sets filter subject for sourced messages. + /// public string? FilterSubject { get; set; } - // Deduplication window in milliseconds for Nats-Msg-Id header-based dedup. - // Defaults to 0 (disabled). When > 0, duplicate messages with the same Nats-Msg-Id - // within this window are silently dropped. + /// + /// Gets or sets duplicate window in milliseconds for Nats-Msg-Id dedup. + /// public int DuplicateWindowMs { get; set; } - // Go: StreamSource.SubjectTransforms — per-source subject transforms. - // Reference: golang/nats-server/server/stream.go — SubjectTransforms field. + /// + /// Gets or sets per-source subject transform rules. + /// public List SubjectTransforms { get; set; } = []; } -// Go: SubjectTransformConfig — source/destination subject transform pair. -// Reference: golang/nats-server/server/stream.go — SubjectTransformConfig struct. +/// +/// Defines a source/destination subject transform pair. +/// public sealed class SubjectTransformConfig { + /// + /// Gets or sets source subject pattern. + /// public string Source { get; set; } = string.Empty; + + /// + /// Gets or sets destination subject pattern. + /// public string Destination { get; set; } = string.Empty; } diff --git a/src/NATS.Server/JetStream/Storage/FileStore.cs b/src/NATS.Server/JetStream/Storage/FileStore.cs index c1170ea..8031efe 100644 --- a/src/NATS.Server/JetStream/Storage/FileStore.cs +++ b/src/NATS.Server/JetStream/Storage/FileStore.cs @@ -85,8 +85,8 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable // Go: filestore.go:5841 — background flush loop coalesces buffered writes. // Reference: golang/nats-server/server/filestore.go:328-331 (coalesce constants). - private readonly Channel _flushSignal = Channel.CreateBounded(1); - private readonly CancellationTokenSource _flushCts = new(); + private Channel _flushSignal = Channel.CreateBounded(1); + private CancellationTokenSource _flushCts = new(); private Task? _flushTask; private const int CoalesceMinimum = 16 * 1024; // 16KB — Go: filestore.go:328 private const int MaxFlushWaitMs = 8; // 8ms — Go: filestore.go:331 @@ -240,6 +240,11 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable public ValueTask PurgeAsync(CancellationToken ct) { + // Stop the background flush loop before disposing blocks to prevent + // the flush task from accessing a disposed ReaderWriterLockSlim. + // Pattern matches Stop() at line 2309. + StopFlushLoop(); + _meta.Clear(); _generation++; _last = 0; @@ -259,6 +264,12 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable if (File.Exists(manifestPath)) File.Delete(manifestPath); + // Restart the background flush loop with a fresh CTS so new appends + // after purge still get coalesced writes. + _flushCts = new CancellationTokenSource(); + _flushSignal = Channel.CreateBounded(1); + _flushTask = Task.Run(() => FlushLoopAsync(_flushCts.Token)); + return ValueTask.CompletedTask; } diff --git a/src/NATS.Server/JetStream/Storage/IStreamStore.cs b/src/NATS.Server/JetStream/Storage/IStreamStore.cs index 1601897..c4a7962 100644 --- a/src/NATS.Server/JetStream/Storage/IStreamStore.cs +++ b/src/NATS.Server/JetStream/Storage/IStreamStore.cs @@ -8,172 +8,361 @@ namespace NATS.Server.JetStream.Storage; // Go: server/store.go:91 /// /// Abstraction over a single stream's message store. -/// The async methods (AppendAsync, LoadAsync, …) are used by the current -/// high-level JetStream layer. The sync methods (StoreMsg, LoadMsg, State, …) -/// mirror Go's StreamStore interface exactly and will be the primary surface -/// once the block-engine FileStore implementation lands. +/// The async methods are used by the current JetStream layer, while sync methods mirror +/// Go's StreamStore API for parity and replication behaviors. /// public interface IStreamStore { - // ------------------------------------------------------------------------- - // Async helpers — used by the current JetStream layer - // ------------------------------------------------------------------------- - + /// + /// Appends a new message to the stream. + /// + /// Subject to store. + /// Message payload bytes. + /// Cancellation token. + /// The assigned stream sequence. ValueTask AppendAsync(string subject, ReadOnlyMemory payload, CancellationToken ct); + + /// + /// Loads a message by exact stream sequence. + /// + /// Sequence to load. + /// Cancellation token. + /// The stored message, or null when absent. ValueTask LoadAsync(ulong sequence, CancellationToken ct); + + /// + /// Loads the most recent message for a subject. + /// + /// Subject filter. + /// Cancellation token. + /// The last stored message for the subject, or null. ValueTask LoadLastBySubjectAsync(string subject, CancellationToken ct); + + /// + /// Lists all currently stored messages. + /// + /// Cancellation token. + /// All stored messages. ValueTask> ListAsync(CancellationToken ct); + + /// + /// Removes a message by sequence. + /// + /// Sequence to remove. + /// Cancellation token. + /// True when a message was removed. ValueTask RemoveAsync(ulong sequence, CancellationToken ct); + + /// + /// Purges all messages from the stream. + /// + /// Cancellation token. ValueTask PurgeAsync(CancellationToken ct); + + /// + /// Creates a point-in-time snapshot of stream contents. + /// + /// Cancellation token. + /// Serialized snapshot bytes. ValueTask CreateSnapshotAsync(CancellationToken ct); + + /// + /// Restores stream contents from a snapshot. + /// + /// Snapshot payload. + /// Cancellation token. ValueTask RestoreSnapshotAsync(ReadOnlyMemory snapshot, CancellationToken ct); - // Returns Models.StreamState for API-layer JSON serialisation compatibility. - // Existing MemStore/FileStore implementations return this type. + /// + /// Returns API-facing stream state. + /// + /// Cancellation token. + /// Stream state for APIs/monitoring. ValueTask GetStateAsync(CancellationToken ct); - // Cached state properties — avoid GetStateAsync on the publish hot path. - // These are maintained incrementally by FileStore/MemStore and are O(1). + /// + /// Gets the last assigned stream sequence. + /// ulong LastSeq => throw new NotSupportedException("LastSeq not implemented."); + + /// + /// Gets number of stored messages. + /// ulong MessageCount => throw new NotSupportedException("MessageCount not implemented."); + + /// + /// Gets total bytes stored across messages. + /// ulong TotalBytes => throw new NotSupportedException("TotalBytes not implemented."); + + /// + /// Gets the first available stream sequence. + /// ulong FirstSeq => throw new NotSupportedException("FirstSeq not implemented."); - // ------------------------------------------------------------------------- - // Go-parity sync interface — mirrors server/store.go StreamStore - // Default implementations throw NotSupportedException so existing - // MemStore / FileStore implementations continue to compile while the - // block-engine port is in progress. - // ------------------------------------------------------------------------- - - // Go: StreamStore.StoreMsg — append a message; returns (seq, timestamp) + /// + /// Appends a message and returns assigned sequence and timestamp. + /// + /// Message subject. + /// Optional NATS headers. + /// Message payload bytes. + /// Message TTL in nanoseconds, if enabled. + /// Assigned sequence and store timestamp. (ulong Seq, long Ts) StoreMsg(string subject, byte[]? hdr, byte[] msg, long ttl) => throw new NotSupportedException("Block-engine StoreMsg not yet implemented."); - // Go: StreamStore.StoreRawMsg — store a raw message at a specified sequence + /// + /// Stores a raw message at an explicit sequence and timestamp. + /// + /// Message subject. + /// Optional NATS headers. + /// Message payload bytes. + /// Sequence to assign. + /// Timestamp to assign. + /// Message TTL in nanoseconds. + /// Whether to bypass discard-new checks. void StoreRawMsg(string subject, byte[]? hdr, byte[] msg, ulong seq, long ts, long ttl, bool discardNewCheck) => throw new NotSupportedException("Block-engine StoreRawMsg not yet implemented."); - // Go: StreamStore.SkipMsg — reserve a sequence without storing a message + /// + /// Reserves a single sequence without storing payload data. + /// + /// Sequence to reserve. + /// The reserved sequence. ulong SkipMsg(ulong seq) => throw new NotSupportedException("Block-engine SkipMsg not yet implemented."); - // Go: StreamStore.SkipMsgs — reserve a range of sequences + /// + /// Reserves a range of sequences. + /// + /// First sequence to reserve. + /// Number of sequences to reserve. void SkipMsgs(ulong seq, ulong num) => throw new NotSupportedException("Block-engine SkipMsgs not yet implemented."); - // Go: StreamStore.FlushAllPending — flush any buffered writes to backing storage + /// + /// Flushes buffered writes to backing storage. + /// + /// A task that completes when flushing finishes. Task FlushAllPending() => throw new NotSupportedException("Block-engine FlushAllPending not yet implemented."); - // Go: StreamStore.LoadMsg — load message by exact sequence; sm is an optional reusable buffer + /// + /// Loads a message by exact sequence. + /// + /// Sequence to load. + /// Optional reusable message holder. + /// The loaded store message. StoreMsg LoadMsg(ulong seq, StoreMsg? sm) => throw new NotSupportedException("Block-engine LoadMsg not yet implemented."); - // Go: StreamStore.LoadNextMsg — load next message at or after start matching filter; - // returns the message and the number of sequences skipped + /// + /// Loads next matching message at or after a start sequence. + /// + /// Subject filter. + /// Whether filter includes wildcards. + /// Starting sequence. + /// Optional reusable message holder. + /// The next matching message and skipped-sequence count. (StoreMsg Msg, ulong Skip) LoadNextMsg(string filter, bool wc, ulong start, StoreMsg? sm) => throw new NotSupportedException("Block-engine LoadNextMsg not yet implemented."); - // Go: StreamStore.LoadLastMsg — load the most recent message on a given subject + /// + /// Loads the last message stored for a subject. + /// + /// Subject to search. + /// Optional reusable message holder. + /// The last message for the subject. StoreMsg LoadLastMsg(string subject, StoreMsg? sm) => throw new NotSupportedException("Block-engine LoadLastMsg not yet implemented."); - // Go: StreamStore.LoadPrevMsg — load message before start sequence + /// + /// Loads the previous message before a sequence. + /// + /// Sequence boundary. + /// Optional reusable message holder. + /// The previous message. StoreMsg LoadPrevMsg(ulong start, StoreMsg? sm) => throw new NotSupportedException("Block-engine LoadPrevMsg not yet implemented."); - // Go: StreamStore.RemoveMsg — soft-delete a message by sequence; returns true if found + /// + /// Soft-deletes a message by sequence. + /// + /// Sequence to remove. + /// True when message existed. bool RemoveMsg(ulong seq) => throw new NotSupportedException("Block-engine RemoveMsg not yet implemented."); - // Go: StreamStore.EraseMsg — overwrite a message with random bytes before removing it + /// + /// Erases and removes a message by sequence. + /// + /// Sequence to erase. + /// True when message existed. bool EraseMsg(ulong seq) => throw new NotSupportedException("Block-engine EraseMsg not yet implemented."); - // Go: StreamStore.Purge — remove all messages; returns count purged + /// + /// Purges all messages from the store. + /// + /// Number of purged messages. ulong Purge() => throw new NotSupportedException("Block-engine Purge not yet implemented."); - // Go: StreamStore.PurgeEx — purge messages on subject up to seq keeping keep newest + /// + /// Purges messages by subject and sequence boundary. + /// + /// Subject to purge. + /// Upper sequence bound. + /// Number of most recent matches to keep. + /// Number of purged messages. ulong PurgeEx(string subject, ulong seq, ulong keep) => throw new NotSupportedException("Block-engine PurgeEx not yet implemented."); - // Go: StreamStore.Compact — remove all messages with seq < given sequence + /// + /// Compacts store by removing messages below a sequence. + /// + /// Lowest sequence to retain. + /// Number of removed messages. ulong Compact(ulong seq) => throw new NotSupportedException("Block-engine Compact not yet implemented."); - // Go: StreamStore.Truncate — remove all messages with seq > given sequence + /// + /// Truncates store by removing messages above a sequence. + /// + /// Highest sequence to retain. void Truncate(ulong seq) => throw new NotSupportedException("Block-engine Truncate not yet implemented."); - // Go: StreamStore.GetSeqFromTime — return first sequence at or after wall-clock time t + /// + /// Returns first sequence at or after a wall-clock time. + /// + /// Target timestamp. + /// First sequence at or after the timestamp. ulong GetSeqFromTime(DateTime t) => throw new NotSupportedException("Block-engine GetSeqFromTime not yet implemented."); - // Go: StreamStore.FilteredState — compact state for messages matching subject at or after seq + /// + /// Returns compact state for messages matching a subject filter. + /// + /// Starting sequence. + /// Subject filter. + /// Filtered simple state. SimpleState FilteredState(ulong seq, string subject) => throw new NotSupportedException("Block-engine FilteredState not yet implemented."); - // Go: StreamStore.SubjectsState — per-subject SimpleState for all subjects matching filter + /// + /// Returns per-subject state for subjects matching a filter. + /// + /// Subject filter. + /// Per-subject state map. Dictionary SubjectsState(string filterSubject) => throw new NotSupportedException("Block-engine SubjectsState not yet implemented."); - // Go: StreamStore.SubjectsTotals — per-subject message count for subjects matching filter + /// + /// Returns per-subject totals for subjects matching a filter. + /// + /// Subject filter. + /// Per-subject totals map. Dictionary SubjectsTotals(string filterSubject) => throw new NotSupportedException("Block-engine SubjectsTotals not yet implemented."); - // Go: StreamStore.AllLastSeqs — last sequence for every subject in the stream + /// + /// Returns last sequence for every subject in the stream. + /// + /// Array of last sequences. ulong[] AllLastSeqs() => throw new NotSupportedException("Block-engine AllLastSeqs not yet implemented."); - // Go: StreamStore.MultiLastSeqs — last sequences for subjects matching filters, up to maxSeq + /// + /// Returns last sequences for filters up to a maximum sequence. + /// + /// Subject filters. + /// Maximum sequence to consider. + /// Maximum results to return. + /// Array of matching last sequences. ulong[] MultiLastSeqs(string[] filters, ulong maxSeq, int maxAllowed) => throw new NotSupportedException("Block-engine MultiLastSeqs not yet implemented."); - // Go: StreamStore.SubjectForSeq — return the subject stored at the given sequence + /// + /// Returns subject stored at a sequence. + /// + /// Sequence to inspect. + /// Stored subject string. string SubjectForSeq(ulong seq) => throw new NotSupportedException("Block-engine SubjectForSeq not yet implemented."); - // Go: StreamStore.NumPending — count messages pending from sseq on filter subject; - // lastPerSubject restricts to one-per-subject semantics + /// + /// Returns number of pending messages for a filter from a starting sequence. + /// + /// Starting sequence. + /// Subject filter. + /// Whether to enforce one-per-subject semantics. + /// Total pending count and validity horizon sequence. (ulong Total, ulong ValidThrough) NumPending(ulong sseq, string filter, bool lastPerSubject) => throw new NotSupportedException("Block-engine NumPending not yet implemented."); - // Go: StreamStore.State — return full stream state (Go-parity, with deleted sets) + /// + /// Returns full stream state including deleted sets. + /// + /// Full storage stream state. StorageStreamState State() => throw new NotSupportedException("Block-engine State not yet implemented."); - // Go: StreamStore.FastState — populate a pre-allocated StreamState with the minimum - // fields needed for replication without allocating a new struct + /// + /// Populates a pre-allocated stream state object. + /// + /// Target state object to populate. void FastState(ref StorageStreamState state) => throw new NotSupportedException("Block-engine FastState not yet implemented."); - // Go: StreamStore.EncodedStreamState — binary-encode stream state for NRG replication + /// + /// Encodes stream state for replication transfer. + /// + /// Failed sequence watermark. + /// Encoded state bytes. byte[] EncodedStreamState(ulong failed) => throw new NotSupportedException("Block-engine EncodedStreamState not yet implemented."); - // Go: StreamStore.Type — the storage type (File or Memory) + /// + /// Returns backing storage type. + /// + /// Storage type enum value. StorageType Type() => throw new NotSupportedException("Block-engine Type not yet implemented."); - // Go: StreamStore.UpdateConfig — apply a new StreamConfig without restarting the store + /// + /// Applies updated stream configuration without restarting the store. + /// + /// Updated stream configuration. void UpdateConfig(StreamConfig cfg) => throw new NotSupportedException("Block-engine UpdateConfig not yet implemented."); - // Go: StreamStore.Delete — stop and delete all data; inline=true means synchronous deletion + /// + /// Stops the store and deletes persisted data. + /// + /// Whether deletion is performed synchronously. void Delete(bool inline) => throw new NotSupportedException("Block-engine Delete not yet implemented."); - // Go: StreamStore.Stop — flush and stop without deleting data + /// + /// Stops the store without deleting data. + /// void Stop() => throw new NotSupportedException("Block-engine Stop not yet implemented."); - // Go: StreamStore.ConsumerStore — create or open a consumer store for the named consumer + /// + /// Creates or opens a consumer store. + /// + /// Consumer name. + /// Consumer creation time. + /// Consumer configuration. + /// The consumer store instance. IConsumerStore ConsumerStore(string name, DateTime created, ConsumerConfig cfg) => throw new NotSupportedException("Block-engine ConsumerStore not yet implemented."); - // Go: StreamStore.ResetState — reset internal state caches (used after NRG catchup) + /// + /// Resets internal state caches. + /// void ResetState() => throw new NotSupportedException("Block-engine ResetState not yet implemented."); } diff --git a/src/NATS.Server/Monitoring/Connz.cs b/src/NATS.Server/Monitoring/Connz.cs index af9ee67..d50c633 100644 --- a/src/NATS.Server/Monitoring/Connz.cs +++ b/src/NATS.Server/Monitoring/Connz.cs @@ -7,24 +7,45 @@ namespace NATS.Server.Monitoring; /// public sealed class Connz { + /// + /// Gets or sets the server ID associated with this connection snapshot. + /// [JsonPropertyName("server_id")] public string Id { get; set; } = ""; + /// + /// Gets or sets the UTC timestamp when this snapshot was produced. + /// [JsonPropertyName("now")] public DateTime Now { get; set; } + /// + /// Gets or sets the number of connection records in this page. + /// [JsonPropertyName("num_connections")] public int NumConns { get; set; } + /// + /// Gets or sets the total number of connections matching the query. + /// [JsonPropertyName("total")] public int Total { get; set; } + /// + /// Gets or sets the pagination offset applied to the result set. + /// [JsonPropertyName("offset")] public int Offset { get; set; } + /// + /// Gets or sets the pagination limit applied to the result set. + /// [JsonPropertyName("limit")] public int Limit { get; set; } + /// + /// Gets or sets the connection records returned for the current page. + /// [JsonPropertyName("connections")] public ConnInfo[] Conns { get; set; } = []; } @@ -35,114 +56,225 @@ public sealed class Connz /// public sealed class ConnInfo { + /// + /// Gets or sets the unique client connection ID. + /// [JsonPropertyName("cid")] public ulong Cid { get; set; } + /// + /// Gets or sets the connection kind (client, route, gateway, or leaf). + /// [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + /// + /// Gets or sets the protocol type for the connection. + /// [JsonPropertyName("type")] public string Type { get; set; } = ""; + /// + /// Gets or sets the remote peer IP address. + /// [JsonPropertyName("ip")] public string Ip { get; set; } = ""; + /// + /// Gets or sets the remote peer port. + /// [JsonPropertyName("port")] public int Port { get; set; } + /// + /// Gets or sets when the connection was established. + /// [JsonPropertyName("start")] public DateTime Start { get; set; } + /// + /// Gets or sets the timestamp of the last observed protocol activity. + /// [JsonPropertyName("last_activity")] public DateTime LastActivity { get; set; } + /// + /// Gets or sets when the connection was closed, if closed. + /// [JsonPropertyName("stop")] public DateTime? Stop { get; set; } + /// + /// Gets or sets the close reason text for closed connections. + /// [JsonPropertyName("reason")] public string Reason { get; set; } = ""; + /// + /// Gets or sets the measured round-trip time string. + /// [JsonPropertyName("rtt")] public string Rtt { get; set; } = ""; + /// + /// Gets or sets the connection uptime string. + /// [JsonPropertyName("uptime")] public string Uptime { get; set; } = ""; + /// + /// Gets or sets the current idle duration string. + /// [JsonPropertyName("idle")] public string Idle { get; set; } = ""; + /// + /// Gets or sets pending outbound bytes queued for this connection. + /// [JsonPropertyName("pending_bytes")] public int Pending { get; set; } + /// + /// Gets or sets total inbound messages received from this peer. + /// [JsonPropertyName("in_msgs")] public long InMsgs { get; set; } + /// + /// Gets or sets total outbound messages sent to this peer. + /// [JsonPropertyName("out_msgs")] public long OutMsgs { get; set; } + /// + /// Gets or sets total inbound bytes received from this peer. + /// [JsonPropertyName("in_bytes")] public long InBytes { get; set; } + /// + /// Gets or sets total outbound bytes sent to this peer. + /// [JsonPropertyName("out_bytes")] public long OutBytes { get; set; } + /// + /// Gets or sets write stall counter for this connection. + /// [JsonPropertyName("stalls")] public long Stalls { get; set; } + /// + /// Gets or sets number of active subscriptions on this connection. + /// [JsonPropertyName("subscriptions")] public uint NumSubs { get; set; } + /// + /// Gets or sets subscription subjects when subscription listing is requested. + /// [JsonPropertyName("subscriptions_list")] public string[] Subs { get; set; } = []; + /// + /// Gets or sets detailed subscription metadata when detailed listing is requested. + /// [JsonPropertyName("subscriptions_list_detail")] public SubDetail[] SubsDetail { get; set; } = []; + /// + /// Gets or sets the client-provided connection name. + /// [JsonPropertyName("name")] public string Name { get; set; } = ""; + /// + /// Gets or sets the client library language. + /// [JsonPropertyName("lang")] public string Lang { get; set; } = ""; + /// + /// Gets or sets the client library version. + /// [JsonPropertyName("version")] public string Version { get; set; } = ""; + /// + /// Gets or sets the resolved authorized user identity. + /// [JsonPropertyName("authorized_user")] public string AuthorizedUser { get; set; } = ""; + /// + /// Gets or sets the account associated with this connection. + /// [JsonPropertyName("account")] public string Account { get; set; } = ""; + /// + /// Gets or sets the negotiated TLS protocol version. + /// [JsonPropertyName("tls_version")] public string TlsVersion { get; set; } = ""; + /// + /// Gets or sets the negotiated TLS cipher suite. + /// [JsonPropertyName("tls_cipher_suite")] public string TlsCipherSuite { get; set; } = ""; + /// + /// Gets or sets the peer certificate subject string. + /// [JsonPropertyName("tls_peer_cert_subject")] public string TlsPeerCertSubject { get; set; } = ""; + /// + /// Gets or sets peer certificate chain details. + /// [JsonPropertyName("tls_peer_certs")] public TLSPeerCert[] TlsPeerCerts { get; set; } = []; + /// + /// Gets or sets a value indicating whether the connection used TLS-first handshake mode. + /// [JsonPropertyName("tls_first")] public bool TlsFirst { get; set; } + /// + /// Gets or sets the MQTT client identifier when the connection is MQTT. + /// [JsonPropertyName("mqtt_client")] public string MqttClient { get; set; } = ""; + /// + /// Gets or sets the client JWT claim value, when present. + /// [JsonPropertyName("jwt")] public string Jwt { get; set; } = ""; + /// + /// Gets or sets the issuer key for JWT-authenticated clients. + /// [JsonPropertyName("issuer_key")] public string IssuerKey { get; set; } = ""; + /// + /// Gets or sets the connection name tag used in identity tagging. + /// [JsonPropertyName("name_tag")] public string NameTag { get; set; } = ""; + /// + /// Gets or sets connection tags resolved during authentication. + /// [JsonPropertyName("tags")] public string[] Tags { get; set; } = []; + /// + /// Gets or sets proxy metadata when the connection was established via a trusted proxy. + /// [JsonPropertyName("proxy")] public ProxyInfo? Proxy { get; set; } } @@ -153,6 +285,9 @@ public sealed class ConnInfo /// public sealed class ProxyInfo { + /// + /// Gets or sets the trusted proxy key representing the forwarding hop. + /// [JsonPropertyName("key")] public string Key { get; set; } = ""; } @@ -162,12 +297,21 @@ public sealed class ProxyInfo /// public sealed class TLSPeerCert { + /// + /// Gets or sets the subject distinguished name from the peer certificate. + /// [JsonPropertyName("subject")] public string Subject { get; set; } = ""; + /// + /// Gets or sets the SHA-256 digest of the peer public key info. + /// [JsonPropertyName("subject_pk_sha256")] public string SubjectPKISha256 { get; set; } = ""; + /// + /// Gets or sets the SHA-256 digest of the full peer certificate. + /// [JsonPropertyName("cert_sha256")] public string CertSha256 { get; set; } = ""; } @@ -178,24 +322,45 @@ public sealed class TLSPeerCert /// public sealed class SubDetail { + /// + /// Gets or sets the account that owns the subscription. + /// [JsonPropertyName("account")] public string Account { get; set; } = ""; + /// + /// Gets or sets the subscription subject. + /// [JsonPropertyName("subject")] public string Subject { get; set; } = ""; + /// + /// Gets or sets the queue group name, if this is a queue subscription. + /// [JsonPropertyName("qgroup")] public string Queue { get; set; } = ""; + /// + /// Gets or sets the subscription ID on the client connection. + /// [JsonPropertyName("sid")] public string Sid { get; set; } = ""; + /// + /// Gets or sets the number of messages delivered to this subscription. + /// [JsonPropertyName("msgs")] public long Msgs { get; set; } + /// + /// Gets or sets the auto-unsubscribe limit for this subscription. + /// [JsonPropertyName("max")] public long Max { get; set; } + /// + /// Gets or sets the owning client connection ID. + /// [JsonPropertyName("cid")] public ulong Cid { get; set; } } @@ -236,13 +401,22 @@ public sealed record ConnzFilterResult( /// public sealed class ConnzFilterOptions { + /// + /// Gets or sets the account name filter applied to connection records. + /// public string? AccountFilter { get; init; } /// "open", "closed", or "any" (default: "open") public string? StateFilter { get; init; } + /// + /// Gets or sets the pagination offset. + /// public int Offset { get; init; } + /// + /// Gets or sets the pagination limit. + /// public int Limit { get; init; } = 1024; /// @@ -260,6 +434,8 @@ public sealed class ConnzFilterOptions /// /// Parses a raw query string into a ConnzFilterOptions instance. /// + /// The raw request query string including or excluding the leading ?. + /// The parsed and normalized filter options. public static ConnzFilterOptions Parse(string? queryString) { if (string.IsNullOrEmpty(queryString)) @@ -328,6 +504,9 @@ public static class ConnzFilter /// Filters connections to only those whose AccountName matches accountName /// using a case-insensitive ordinal comparison. /// + /// The source connection set. + /// The account name to match. + /// The filtered connection list. public static IReadOnlyList FilterByAccount( IEnumerable connections, string accountName) => @@ -338,6 +517,9 @@ public static class ConnzFilter /// /// Applies all filters specified in options and returns a paginated result. /// + /// The source connection set. + /// The filtering and pagination options. + /// A page of filtered connections with total count metadata. public static ConnzFilterResult ApplyFilters( IEnumerable connections, ConnzFilterOptions options) @@ -411,6 +593,8 @@ public static class ConnzSorter /// Returns ConnzSortOption.ConnectionId for null, empty, or unrecognised values. /// Go reference: server/monitor_sort_opts.go UnmarshalJSON. /// + /// The raw sort token from query string. + /// The corresponding sort option, or when unknown. public static ConnzSortOption Parse(string? sortBy) => sortBy?.ToLowerInvariant() switch { @@ -433,6 +617,10 @@ public static class ConnzSorter /// default to ascending. Setting descending=true reverses the default. /// Go reference: monitor.go Connz() sort switch. /// + /// The connection set to sort. + /// The selected sort field. + /// If , reverses the default direction for the selected field. + /// A sorted connection list. public static IReadOnlyList Sort( IEnumerable connections, ConnzSortOption sortBy, @@ -513,23 +701,77 @@ public static class ConnzSorter /// public enum ClosedReason { + /// + /// Close reason was not captured. + /// Unknown, + /// + /// Client initiated a normal close. + /// ClientClosed, + /// + /// Server is shutting down. + /// ServerShutdown, + /// + /// Client did not complete authentication in time. + /// AuthTimeout, + /// + /// Authentication failed. + /// AuthViolation, + /// + /// Server connection limit was exceeded. + /// MaxConnectionsExceeded, + /// + /// Client was disconnected as a slow consumer. + /// SlowConsumer, + /// + /// Outbound write failed. + /// WriteError, + /// + /// Inbound read failed. + /// ReadError, + /// + /// Protocol parse failed. + /// ParseError, + /// + /// Connection became stale due to missed pings. + /// StaleConnection, + /// + /// Client exceeded maximum payload size. + /// MaxPayloadExceeded, + /// + /// Client exceeded maximum subscriptions. + /// MaxSubscriptionsExceeded, + /// + /// Route was closed due to duplicate peer route. + /// DuplicateRoute, + /// + /// Account credentials have expired. + /// AccountExpired, + /// + /// Credentials were revoked. + /// Revoked, + /// + /// Internal server failure triggered close. + /// ServerError, + /// + /// Operator explicitly kicked the client. + /// KickedByOperator, } @@ -545,6 +787,8 @@ public static class ClosedReasonHelper /// These strings match the Go server's monitor.go ClosedState.String() output /// so that tooling consuming the /connz endpoint sees identical values. /// + /// The close reason enum value. + /// The Go-compatible close reason text. public static string ToReasonString(ClosedReason reason) => reason switch { ClosedReason.ClientClosed => "Client Closed", @@ -571,6 +815,8 @@ public static class ClosedReasonHelper /// Parses a Go-compatible reason string back to a . /// Returns for null, empty, or unrecognised values. /// + /// The reason text from monitoring output. + /// The parsed close reason enum. public static ClosedReason FromReasonString(string? reason) => reason switch { "Client Closed" => ClosedReason.ClientClosed, @@ -597,6 +843,8 @@ public static class ClosedReasonHelper /// Returns true when the close was initiated by the client itself (not the server). /// Go reference: server/client.go closeConnection — client-side disconnect path. /// + /// The close reason to evaluate. + /// when client initiated the close. public static bool IsClientInitiated(ClosedReason reason) => reason == ClosedReason.ClientClosed; @@ -604,6 +852,8 @@ public static class ClosedReasonHelper /// Returns true when the close was caused by an authentication or authorisation failure. /// Go reference: server/auth.go getAuthErrClosedState. /// + /// The close reason to evaluate. + /// when the close reason is auth-related. public static bool IsAuthRelated(ClosedReason reason) => reason is ClosedReason.AuthTimeout or ClosedReason.AuthViolation @@ -614,6 +864,8 @@ public static class ClosedReasonHelper /// Returns true when the close was caused by a resource limit being exceeded. /// Go reference: server/client.go maxPayloadExceeded / maxSubscriptionsExceeded paths. /// + /// The close reason to evaluate. + /// when the close reason is a limit violation. public static bool IsResourceLimit(ClosedReason reason) => reason is ClosedReason.MaxConnectionsExceeded or ClosedReason.MaxPayloadExceeded @@ -631,19 +883,61 @@ public static class ClosedReasonHelper /// public enum SortOpt { + /// + /// Sort by connection ID. + /// ByCid, + /// + /// Sort by connection start time. + /// ByStart, + /// + /// Sort by subscription count. + /// BySubs, + /// + /// Sort by pending bytes. + /// ByPending, + /// + /// Sort by outbound message count. + /// ByMsgsTo, + /// + /// Sort by inbound message count. + /// ByMsgsFrom, + /// + /// Sort by outbound byte count. + /// ByBytesTo, + /// + /// Sort by inbound byte count. + /// ByBytesFrom, + /// + /// Sort by last-activity timestamp. + /// ByLast, + /// + /// Sort by idle duration. + /// ByIdle, + /// + /// Sort by uptime duration. + /// ByUptime, + /// + /// Sort by round-trip time. + /// ByRtt, + /// + /// Sort by close time. + /// ByStop, + /// + /// Sort by close reason. + /// ByReason, } @@ -652,6 +946,8 @@ public static class SortOptExtensions /// /// Go parity for SortOpt.IsValid(). /// + /// The sort option to validate. + /// when the sort option is supported. public static bool IsValid(this SortOpt sort) => sort is SortOpt.ByCid or SortOpt.ByStart @@ -675,8 +971,17 @@ public static class SortOptExtensions /// public enum ConnState { + /// + /// Include only currently open connections. + /// Open, + /// + /// Include only closed connections. + /// Closed, + /// + /// Include open and closed connections. + /// All, } @@ -686,20 +991,44 @@ public enum ConnState /// public sealed class ConnzOptions { + /// + /// Gets or sets the sort option applied to returned connections. + /// public SortOpt Sort { get; set; } = SortOpt.ByCid; + /// + /// Gets or sets a value indicating whether to include subject list details. + /// public bool Subscriptions { get; set; } + /// + /// Gets or sets a value indicating whether to include structured subscription detail objects. + /// public bool SubscriptionsDetail { get; set; } + /// + /// Gets or sets the connection state filter. + /// public ConnState State { get; set; } = ConnState.Open; + /// + /// Gets or sets the authorized user filter. + /// public string User { get; set; } = ""; + /// + /// Gets or sets the account name filter. + /// public string Account { get; set; } = ""; + /// + /// Gets or sets the subject filter used when returning subscription details. + /// public string FilterSubject { get; set; } = ""; + /// + /// Gets or sets the MQTT client identifier filter. + /// public string MqttClient { get; set; } = ""; /// @@ -714,7 +1043,13 @@ public sealed class ConnzOptions /// public bool Auth { get; set; } + /// + /// Gets or sets the pagination offset. + /// public int Offset { get; set; } + /// + /// Gets or sets the pagination limit. + /// public int Limit { get; set; } = 1024; } diff --git a/src/NATS.Server/Monitoring/Varz.cs b/src/NATS.Server/Monitoring/Varz.cs index 0b0d040..81ada6c 100644 --- a/src/NATS.Server/Monitoring/Varz.cs +++ b/src/NATS.Server/Monitoring/Varz.cs @@ -3,465 +3,812 @@ using System.Text.Json.Serialization; namespace NATS.Server.Monitoring; /// -/// Server general information. Corresponds to Go server/monitor.go Varz struct. +/// Snapshot of server status exposed by the /varz monitoring endpoint. +/// Corresponds to Go server/monitor.go Varz. /// public sealed class Varz { - // Identity + /// + /// Gets or sets the unique server identifier used across cluster telemetry. + /// [JsonPropertyName("server_id")] public string Id { get; set; } = ""; + /// + /// Gets or sets the configured server name. + /// [JsonPropertyName("server_name")] public string Name { get; set; } = ""; + /// + /// Gets or sets the running server version. + /// [JsonPropertyName("version")] public string Version { get; set; } = ""; + /// + /// Gets or sets the protocol version advertised to clients. + /// [JsonPropertyName("proto")] public int Proto { get; set; } + /// + /// Gets or sets the build commit hash. + /// [JsonPropertyName("git_commit")] public string GitCommit { get; set; } = ""; + /// + /// Gets or sets the Go runtime version value used for parity with upstream monitoring shape. + /// [JsonPropertyName("go")] public string GoVersion { get; set; } = ""; + /// + /// Gets or sets the primary host name bound for client traffic. + /// [JsonPropertyName("host")] public string Host { get; set; } = ""; + /// + /// Gets or sets the primary client listener port. + /// [JsonPropertyName("port")] public int Port { get; set; } - // Network + /// + /// Gets or sets the resolved server IP address. + /// [JsonPropertyName("ip")] public string Ip { get; set; } = ""; + /// + /// Gets or sets connect URLs advertised to core NATS clients. + /// [JsonPropertyName("connect_urls")] public string[] ConnectUrls { get; set; } = []; + /// + /// Gets or sets websocket connect URLs advertised to websocket clients. + /// [JsonPropertyName("ws_connect_urls")] public string[] WsConnectUrls { get; set; } = []; + /// + /// Gets or sets the monitoring endpoint bind host. + /// [JsonPropertyName("http_host")] public string HttpHost { get; set; } = ""; + /// + /// Gets or sets the monitoring HTTP port. + /// [JsonPropertyName("http_port")] public int HttpPort { get; set; } + /// + /// Gets or sets the monitoring base path prefix. + /// [JsonPropertyName("http_base_path")] public string HttpBasePath { get; set; } = ""; + /// + /// Gets or sets the monitoring HTTPS port. + /// [JsonPropertyName("https_port")] public int HttpsPort { get; set; } - // Security + /// + /// Gets or sets a value indicating whether authentication is required for client connections. + /// [JsonPropertyName("auth_required")] public bool AuthRequired { get; set; } + /// + /// Gets or sets a value indicating whether TLS is required for client connections. + /// [JsonPropertyName("tls_required")] public bool TlsRequired { get; set; } + /// + /// Gets or sets a value indicating whether client TLS certificates are verified. + /// [JsonPropertyName("tls_verify")] public bool TlsVerify { get; set; } + /// + /// Gets or sets a value indicating whether OCSP peer verification is enabled for TLS clients. + /// [JsonPropertyName("tls_ocsp_peer_verify")] public bool TlsOcspPeerVerify { get; set; } + /// + /// Gets or sets the authentication timeout in seconds. + /// [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } + /// + /// Gets or sets the TLS handshake timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } - // Limits + /// + /// Gets or sets the maximum number of concurrently connected clients. + /// [JsonPropertyName("max_connections")] public int MaxConnections { get; set; } + /// + /// Gets or sets the configured maximum subscriptions per client. + /// [JsonPropertyName("max_subscriptions")] public int MaxSubscriptions { get; set; } + /// + /// Gets or sets the largest accepted publish payload size in bytes. + /// [JsonPropertyName("max_payload")] public int MaxPayload { get; set; } + /// + /// Gets or sets the maximum pending outbound bytes per client. + /// [JsonPropertyName("max_pending")] public long MaxPending { get; set; } + /// + /// Gets or sets the maximum protocol control line length in bytes. + /// [JsonPropertyName("max_control_line")] public int MaxControlLine { get; set; } + /// + /// Gets or sets the maximum unanswered server pings before disconnect. + /// [JsonPropertyName("ping_max")] public int MaxPingsOut { get; set; } - // Timing + /// + /// Gets or sets the configured ping interval in nanoseconds. + /// [JsonPropertyName("ping_interval")] public long PingInterval { get; set; } + /// + /// Gets or sets the configured write deadline in nanoseconds. + /// [JsonPropertyName("write_deadline")] public long WriteDeadline { get; set; } + /// + /// Gets or sets when the server process started. + /// [JsonPropertyName("start")] public DateTime Start { get; set; } + /// + /// Gets or sets the snapshot timestamp when the varz payload was generated. + /// [JsonPropertyName("now")] public DateTime Now { get; set; } + /// + /// Gets or sets a human-readable uptime string. + /// [JsonPropertyName("uptime")] public string Uptime { get; set; } = ""; - // Runtime + /// + /// Gets or sets current process memory usage in bytes. + /// [JsonPropertyName("mem")] public long Mem { get; set; } + /// + /// Gets or sets current process CPU utilization. + /// [JsonPropertyName("cpu")] public double Cpu { get; set; } + /// + /// Gets or sets number of CPU cores available to the process. + /// [JsonPropertyName("cores")] public int Cores { get; set; } + /// + /// Gets or sets runtime thread parallelism value. + /// [JsonPropertyName("gomaxprocs")] public int MaxProcs { get; set; } - // Connections + /// + /// Gets or sets current active client connection count. + /// [JsonPropertyName("connections")] public int Connections { get; set; } + /// + /// Gets or sets cumulative accepted client connections since startup. + /// [JsonPropertyName("total_connections")] public ulong TotalConnections { get; set; } + /// + /// Gets or sets current active route connection count. + /// [JsonPropertyName("routes")] public int Routes { get; set; } + /// + /// Gets or sets current active remote gateway connection count. + /// [JsonPropertyName("remotes")] public int Remotes { get; set; } + /// + /// Gets or sets current active leaf node connection count. + /// [JsonPropertyName("leafnodes")] public int Leafnodes { get; set; } - // Messages + /// + /// Gets or sets cumulative inbound messages processed. + /// [JsonPropertyName("in_msgs")] public long InMsgs { get; set; } + /// + /// Gets or sets cumulative outbound messages delivered. + /// [JsonPropertyName("out_msgs")] public long OutMsgs { get; set; } + /// + /// Gets or sets cumulative inbound bytes processed. + /// [JsonPropertyName("in_bytes")] public long InBytes { get; set; } + /// + /// Gets or sets cumulative outbound bytes delivered. + /// [JsonPropertyName("out_bytes")] public long OutBytes { get; set; } - // Health + /// + /// Gets or sets total slow-consumer events detected. + /// [JsonPropertyName("slow_consumers")] public long SlowConsumers { get; set; } + /// + /// Gets or sets slow-consumer counters partitioned by connection type. + /// [JsonPropertyName("slow_consumer_stats")] public SlowConsumersStats SlowConsumerStats { get; set; } = new(); + /// + /// Gets or sets total stale connection events detected. + /// [JsonPropertyName("stale_connections")] public long StaleConnections { get; set; } + /// + /// Gets or sets stale-connection counters partitioned by connection type. + /// [JsonPropertyName("stale_connection_stats")] public StaleConnectionStats StaleConnectionStatsDetail { get; set; } = new(); + /// + /// Gets or sets total active subscription count across accounts. + /// [JsonPropertyName("subscriptions")] public uint Subscriptions { get; set; } - // Config + /// + /// Gets or sets when configuration was last loaded successfully. + /// [JsonPropertyName("config_load_time")] public DateTime ConfigLoadTime { get; set; } + /// + /// Gets or sets configured server tags. + /// [JsonPropertyName("tags")] public string[] Tags { get; set; } = []; + /// + /// Gets or sets the configured system account identifier. + /// [JsonPropertyName("system_account")] public string SystemAccount { get; set; } = ""; + /// + /// Gets or sets failed pinned-account resolution count. + /// [JsonPropertyName("pinned_account_fails")] public ulong PinnedAccountFail { get; set; } + /// + /// Gets or sets TLS certificate expiration timestamp for monitoring. + /// [JsonPropertyName("tls_cert_not_after")] public DateTime TlsCertNotAfter { get; set; } - // HTTP + /// + /// Gets or sets per-endpoint HTTP request counters. + /// [JsonPropertyName("http_req_stats")] public Dictionary HttpReqStats { get; set; } = new(); - // Subsystems + /// + /// Gets or sets cluster listener settings exposed in monitoring. + /// [JsonPropertyName("cluster")] public ClusterOptsVarz Cluster { get; set; } = new(); + /// + /// Gets or sets gateway settings exposed in monitoring. + /// [JsonPropertyName("gateway")] public GatewayOptsVarz Gateway { get; set; } = new(); + /// + /// Gets or sets leaf-node settings exposed in monitoring. + /// [JsonPropertyName("leaf")] public LeafNodeOptsVarz Leaf { get; set; } = new(); + /// + /// Gets or sets MQTT settings exposed in monitoring. + /// [JsonPropertyName("mqtt")] public MqttOptsVarz Mqtt { get; set; } = new(); + /// + /// Gets or sets websocket settings exposed in monitoring. + /// [JsonPropertyName("websocket")] public WebsocketOptsVarz Websocket { get; set; } = new(); + /// + /// Gets or sets JetStream monitoring data. + /// [JsonPropertyName("jetstream")] public JetStreamVarz JetStream { get; set; } = new(); } /// -/// Statistics about slow consumers by connection type. -/// Corresponds to Go server/monitor.go SlowConsumersStats struct. +/// Slow-consumer counters by connection class. /// public sealed class SlowConsumersStats { + /// + /// Gets or sets slow-consumer events attributed to client connections. + /// [JsonPropertyName("clients")] public ulong Clients { get; set; } + /// + /// Gets or sets slow-consumer events attributed to route links. + /// [JsonPropertyName("routes")] public ulong Routes { get; set; } + /// + /// Gets or sets slow-consumer events attributed to gateway links. + /// [JsonPropertyName("gateways")] public ulong Gateways { get; set; } + /// + /// Gets or sets slow-consumer events attributed to leaf links. + /// [JsonPropertyName("leafs")] public ulong Leafs { get; set; } } /// -/// Statistics about stale connections by connection type. -/// Corresponds to Go server/monitor.go StaleConnectionStats struct. +/// Stale-connection counters by connection class. /// public sealed class StaleConnectionStats { + /// + /// Gets or sets stale-connection events attributed to client connections. + /// [JsonPropertyName("clients")] public ulong Clients { get; set; } + /// + /// Gets or sets stale-connection events attributed to route links. + /// [JsonPropertyName("routes")] public ulong Routes { get; set; } + /// + /// Gets or sets stale-connection events attributed to gateway links. + /// [JsonPropertyName("gateways")] public ulong Gateways { get; set; } + /// + /// Gets or sets stale-connection events attributed to leaf links. + /// [JsonPropertyName("leafs")] public ulong Leafs { get; set; } } /// -/// Cluster configuration monitoring information. -/// Corresponds to Go server/monitor.go ClusterOptsVarz struct. +/// Cluster listener settings as exposed by monitoring. /// public sealed class ClusterOptsVarz { + /// + /// Gets or sets the configured cluster name. + /// [JsonPropertyName("name")] public string Name { get; set; } = ""; + /// + /// Gets or sets the bind address for route listeners. + /// [JsonPropertyName("addr")] public string Host { get; set; } = ""; + /// + /// Gets or sets the route listener port. + /// [JsonPropertyName("cluster_port")] public int Port { get; set; } + /// + /// Gets or sets route authentication timeout in seconds. + /// [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } + /// + /// Gets or sets route TLS timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } + /// + /// Gets or sets a value indicating whether route TLS is required. + /// [JsonPropertyName("tls_required")] public bool TlsRequired { get; set; } + /// + /// Gets or sets a value indicating whether route peer certificates are verified. + /// [JsonPropertyName("tls_verify")] public bool TlsVerify { get; set; } + /// + /// Gets or sets configured route pool size. + /// [JsonPropertyName("pool_size")] public int PoolSize { get; set; } + /// + /// Gets or sets configured route URLs. + /// [JsonPropertyName("urls")] public string[] Urls { get; set; } = []; } /// -/// Gateway configuration monitoring information. -/// Corresponds to Go server/monitor.go GatewayOptsVarz struct. +/// Gateway listener settings as exposed by monitoring. /// public sealed class GatewayOptsVarz { + /// + /// Gets or sets the configured gateway name. + /// [JsonPropertyName("name")] public string Name { get; set; } = ""; + /// + /// Gets or sets gateway bind host. + /// [JsonPropertyName("host")] public string Host { get; set; } = ""; + /// + /// Gets or sets gateway listener port. + /// [JsonPropertyName("port")] public int Port { get; set; } + /// + /// Gets or sets gateway auth timeout in seconds. + /// [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } + /// + /// Gets or sets gateway TLS timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } + /// + /// Gets or sets a value indicating whether gateway TLS is required. + /// [JsonPropertyName("tls_required")] public bool TlsRequired { get; set; } + /// + /// Gets or sets a value indicating whether gateway peer certificates are verified. + /// [JsonPropertyName("tls_verify")] public bool TlsVerify { get; set; } + /// + /// Gets or sets advertised gateway address used for cross-cluster dialing. + /// [JsonPropertyName("advertise")] public string Advertise { get; set; } = ""; + /// + /// Gets or sets the number of dial retries for outbound gateways. + /// [JsonPropertyName("connect_retries")] public int ConnectRetries { get; set; } + /// + /// Gets or sets a value indicating whether unknown gateway clusters are rejected. + /// [JsonPropertyName("reject_unknown")] public bool RejectUnknown { get; set; } } /// -/// Leaf node configuration monitoring information. -/// Corresponds to Go server/monitor.go LeafNodeOptsVarz struct. +/// Leaf-node listener settings as exposed by monitoring. /// public sealed class LeafNodeOptsVarz { + /// + /// Gets or sets leaf-node bind host. + /// [JsonPropertyName("host")] public string Host { get; set; } = ""; + /// + /// Gets or sets leaf-node listener port. + /// [JsonPropertyName("port")] public int Port { get; set; } + /// + /// Gets or sets leaf-node auth timeout in seconds. + /// [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } + /// + /// Gets or sets leaf-node TLS timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } + /// + /// Gets or sets a value indicating whether leaf-node TLS is required. + /// [JsonPropertyName("tls_required")] public bool TlsRequired { get; set; } + /// + /// Gets or sets a value indicating whether leaf-node peer certificates are verified. + /// [JsonPropertyName("tls_verify")] public bool TlsVerify { get; set; } + /// + /// Gets or sets a value indicating whether leaf-node OCSP peer verification is enabled. + /// [JsonPropertyName("tls_ocsp_peer_verify")] public bool TlsOcspPeerVerify { get; set; } } /// -/// MQTT configuration monitoring information. -/// Corresponds to Go server/monitor.go MQTTOptsVarz struct. +/// MQTT listener settings as exposed by monitoring. /// public sealed class MqttOptsVarz { + /// + /// Gets or sets MQTT bind host. + /// [JsonPropertyName("host")] public string Host { get; set; } = ""; + /// + /// Gets or sets MQTT listener port. + /// [JsonPropertyName("port")] public int Port { get; set; } + /// + /// Gets or sets fallback no-auth user for MQTT clients. + /// [JsonPropertyName("no_auth_user")] public string NoAuthUser { get; set; } = ""; + /// + /// Gets or sets MQTT auth timeout in seconds. + /// [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } + /// + /// Gets or sets a value indicating whether TLS certificate subject mapping is enabled. + /// [JsonPropertyName("tls_map")] public bool TlsMap { get; set; } + /// + /// Gets or sets MQTT TLS timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } + /// + /// Gets or sets pinned TLS certificate fingerprints for MQTT peers. + /// [JsonPropertyName("tls_pinned_certs")] public string[] TlsPinnedCerts { get; set; } = []; + /// + /// Gets or sets JetStream domain exposed to MQTT sessions. + /// [JsonPropertyName("js_domain")] public string JsDomain { get; set; } = ""; + /// + /// Gets or sets MQTT ack wait time in nanoseconds. + /// [JsonPropertyName("ack_wait")] public long AckWait { get; set; } + /// + /// Gets or sets maximum in-flight MQTT acknowledgements. + /// [JsonPropertyName("max_ack_pending")] public ushort MaxAckPending { get; set; } } /// -/// Websocket configuration monitoring information. -/// Corresponds to Go server/monitor.go WebsocketOptsVarz struct. +/// Websocket listener settings as exposed by monitoring. /// public sealed class WebsocketOptsVarz { + /// + /// Gets or sets websocket bind host. + /// [JsonPropertyName("host")] public string Host { get; set; } = ""; + /// + /// Gets or sets websocket listener port. + /// [JsonPropertyName("port")] public int Port { get; set; } + /// + /// Gets or sets websocket TLS timeout in seconds. + /// [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } } /// -/// JetStream runtime information. -/// Corresponds to Go server/monitor.go JetStreamVarz struct. +/// JetStream monitoring payload. /// public sealed class JetStreamVarz { + /// + /// Gets or sets JetStream static configuration limits. + /// [JsonPropertyName("config")] public JetStreamConfig Config { get; set; } = new(); + /// + /// Gets or sets JetStream runtime usage and API statistics. + /// [JsonPropertyName("stats")] public JetStreamStats Stats { get; set; } = new(); } /// -/// JetStream configuration. -/// Corresponds to Go server/jetstream.go JetStreamConfig struct. +/// JetStream configuration limits as shown by monitoring. /// public sealed class JetStreamConfig { + /// + /// Gets or sets the maximum memory budget for JetStream. + /// [JsonPropertyName("max_memory")] public long MaxMemory { get; set; } + /// + /// Gets or sets the maximum storage budget for JetStream. + /// [JsonPropertyName("max_storage")] public long MaxStorage { get; set; } + /// + /// Gets or sets the filesystem directory used for JetStream persistence. + /// [JsonPropertyName("store_dir")] public string StoreDir { get; set; } = ""; } /// -/// JetStream statistics. -/// Corresponds to Go server/jetstream.go JetStreamStats struct. +/// JetStream runtime counters as shown by monitoring. /// public sealed class JetStreamStats { + /// + /// Gets or sets currently used JetStream memory in bytes. + /// [JsonPropertyName("memory")] public ulong Memory { get; set; } + /// + /// Gets or sets currently used JetStream storage in bytes. + /// [JsonPropertyName("storage")] public ulong Storage { get; set; } + /// + /// Gets or sets the number of accounts with JetStream enabled. + /// [JsonPropertyName("accounts")] public int Accounts { get; set; } + /// + /// Gets or sets the number of high-availability assets managed by JetStream. + /// [JsonPropertyName("ha_assets")] public int HaAssets { get; set; } + /// + /// Gets or sets the number of active streams. + /// [JsonPropertyName("streams")] public int Streams { get; set; } + /// + /// Gets or sets the number of active consumers. + /// [JsonPropertyName("consumers")] public int Consumers { get; set; } + /// + /// Gets or sets JetStream API call counters. + /// [JsonPropertyName("api")] public JetStreamApiStats Api { get; set; } = new(); } /// -/// JetStream API statistics. -/// Corresponds to Go server/jetstream.go JetStreamAPIStats struct. +/// JetStream API request error/success totals. /// public sealed class JetStreamApiStats { + /// + /// Gets or sets total JetStream API calls served. + /// [JsonPropertyName("total")] public ulong Total { get; set; } + /// + /// Gets or sets total JetStream API calls that returned errors. + /// [JsonPropertyName("errors")] public ulong Errors { get; set; } } diff --git a/src/NATS.Server/Mqtt/MqttListener.cs b/src/NATS.Server/Mqtt/MqttListener.cs index 486d0ae..8421a09 100644 --- a/src/NATS.Server/Mqtt/MqttListener.cs +++ b/src/NATS.Server/Mqtt/MqttListener.cs @@ -39,11 +39,18 @@ public sealed class MqttListener : IAsyncDisposable /// internal bool UseBinaryProtocol { get; set; } = true; + /// + /// Gets the bound MQTT listener port. + /// public int Port => _port; /// /// Simple constructor for tests using static username/password auth (no TLS). /// + /// Bind host. + /// Bind port. + /// Optional required username. + /// Optional required password. public MqttListener( string host, int port, @@ -59,6 +66,13 @@ public sealed class MqttListener : IAsyncDisposable /// /// Full constructor for production use with AuthService, TLS, and optional JetStream support. /// + /// Bind host. + /// Bind port. + /// Authentication service. + /// MQTT options. + /// Optional JetStream stream initializer. + /// Optional MQTT consumer manager. + /// Optional cross-protocol message router. public MqttListener( string host, int port, @@ -113,12 +127,24 @@ public sealed class MqttListener : IAsyncDisposable /// internal Func? ResolveAccount { get; set; } + /// + /// Registers an MQTT-to-NATS adapter for cross-protocol routing. + /// + /// Adapter instance to register. internal void RegisterMqttAdapter(MqttNatsClientAdapter adapter) => _mqttAdapters[adapter.Id] = adapter; + /// + /// Unregisters an MQTT-to-NATS adapter. + /// + /// Adapter instance to unregister. internal void UnregisterMqttAdapter(MqttNatsClientAdapter adapter) => _mqttAdapters.TryRemove(adapter.Id, out _); + /// + /// Returns currently registered MQTT-to-NATS adapters. + /// + /// Adapter collection snapshot. internal IEnumerable GetMqttAdapters() => _mqttAdapters.Values; @@ -126,6 +152,9 @@ public sealed class MqttListener : IAsyncDisposable /// Looks up a specific pending publish by client ID and packet ID. /// Used by QoS 2 PUBREL to retrieve the stored message for delivery. /// + /// MQTT client identifier. + /// MQTT packet identifier. + /// Pending publish entry, or null when not found. internal MqttPendingPublish? GetPendingPublish(string clientId, int packetId) { if (string.IsNullOrWhiteSpace(clientId) || packetId <= 0) @@ -138,6 +167,11 @@ public sealed class MqttListener : IAsyncDisposable return null; } + /// + /// Starts the MQTT listener and accept loop. + /// + /// Cancellation token for startup and listener lifecycle. + /// A completed task when startup finishes. public Task StartAsync(CancellationToken ct) { var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, _cts.Token); @@ -151,18 +185,36 @@ public sealed class MqttListener : IAsyncDisposable return Task.CompletedTask; } + /// + /// Registers a topic subscription for a connection. + /// + /// MQTT connection. + /// Topic filter. internal void RegisterSubscription(MqttConnection connection, string topic) { var set = _subscriptions.GetOrAdd(topic, static _ => new ConcurrentDictionary()); set[connection] = 0; } + /// + /// Unregisters a topic subscription for a connection. + /// + /// MQTT connection. + /// Topic filter. internal void UnregisterSubscription(MqttConnection connection, string topic) { if (_subscriptions.TryGetValue(topic, out var set)) set.TryRemove(connection, out _); } + /// + /// Publishes a message to matching local MQTT subscribers. + /// + /// Publish topic. + /// Payload string. + /// Sending connection. + /// Cancellation token. + /// A task representing asynchronous fan-out delivery. internal async Task PublishAsync(string topic, string payload, MqttConnection sender, CancellationToken ct) { if (!_subscriptions.TryGetValue(topic, out var subscribers)) @@ -177,6 +229,12 @@ public sealed class MqttListener : IAsyncDisposable } } + /// + /// Opens or resets session state for a client. + /// + /// MQTT client identifier. + /// Whether to clear any existing session. + /// Pending publishes to resume for persistent sessions. internal IReadOnlyList OpenSession(string clientId, bool cleanSession) { if (string.IsNullOrWhiteSpace(clientId)) @@ -194,6 +252,13 @@ public sealed class MqttListener : IAsyncDisposable .ToArray(); } + /// + /// Records a pending publish for QoS acknowledgement tracking. + /// + /// MQTT client identifier. + /// MQTT packet identifier. + /// Topic for pending publish. + /// Payload for pending publish. internal void RecordPendingPublish(string clientId, int packetId, string topic, string payload) { if (string.IsNullOrWhiteSpace(clientId) || packetId <= 0) @@ -203,6 +268,11 @@ public sealed class MqttListener : IAsyncDisposable session.Pending[packetId] = new MqttPendingPublish(packetId, topic, payload); } + /// + /// Acknowledges and removes a tracked pending publish. + /// + /// MQTT client identifier. + /// MQTT packet identifier. internal void AckPendingPublish(string clientId, int packetId) { if (string.IsNullOrWhiteSpace(clientId) || packetId <= 0) @@ -216,6 +286,10 @@ public sealed class MqttListener : IAsyncDisposable /// Authenticates MQTT CONNECT credentials. Uses AuthService when available, /// falls back to static username/password validation. /// + /// Username from CONNECT packet. + /// Password from CONNECT packet. + /// Optional TLS client certificate. + /// Authentication result when successful; otherwise null. internal AuthResult? AuthenticateMqtt(string? username, string? password, X509Certificate2? clientCert = null) { if (_authService != null) @@ -249,11 +323,19 @@ public sealed class MqttListener : IAsyncDisposable /// /// Backward-compatible simple auth check for text-protocol mode. /// + /// Username from CONNECT packet. + /// Password from CONNECT packet. + /// True when credentials are valid. internal bool TryAuthenticate(string? username, string? password) { return AuthenticateMqtt(username, password) != null; } + /// + /// Resolves effective keepalive timeout from MQTT keepalive seconds. + /// + /// Keepalive interval from CONNECT packet. + /// Effective timeout duration. internal TimeSpan ResolveKeepAliveTimeout(int keepAliveSeconds) { if (keepAliveSeconds <= 0) @@ -266,6 +348,8 @@ public sealed class MqttListener : IAsyncDisposable /// Disconnects an existing connection with the same client-id (takeover). /// Go reference: mqtt.go mqttHandleConnect ~line 850 duplicate client handling. /// + /// MQTT client identifier. + /// New connection replacing any existing connection. internal void TakeoverExistingConnection(string clientId, MqttConnection newConnection) { if (_clientIdMap.TryGetValue(clientId, out var existing) && existing != newConnection) @@ -280,6 +364,8 @@ public sealed class MqttListener : IAsyncDisposable /// /// Stores or deletes a retained message. Null payload = tombstone (delete). /// + /// Retained message topic. + /// Retained payload, or null to delete retained state. internal void SetRetainedMessage(string topic, string? payload) { if (payload == null) @@ -291,12 +377,18 @@ public sealed class MqttListener : IAsyncDisposable /// /// Gets the retained message for a topic, or null if none. /// + /// Topic to query. + /// Retained payload, or null when none exists. internal string? GetRetainedMessage(string topic) { _retainedMessages.TryGetValue(topic, out var payload); return payload; } + /// + /// Unregisters a connection and removes all its tracked subscriptions. + /// + /// Connection to unregister. internal void Unregister(MqttConnection connection) { _connections.TryRemove(connection, out _); @@ -311,6 +403,10 @@ public sealed class MqttListener : IAsyncDisposable } } + /// + /// Stops listener, closes active connections, and clears in-memory state. + /// + /// A task that completes when disposal finishes. public async ValueTask DisposeAsync() { await _cts.CancelAsync(); @@ -431,6 +527,9 @@ public sealed class MqttListener : IAsyncDisposable private sealed class MqttSessionState { + /// + /// Gets pending publishes keyed by MQTT packet identifier. + /// public ConcurrentDictionary Pending { get; } = new(); } } diff --git a/src/NATS.Server/Mqtt/MqttSessionStore.cs b/src/NATS.Server/Mqtt/MqttSessionStore.cs index 84004a7..ce35e96 100644 --- a/src/NATS.Server/Mqtt/MqttSessionStore.cs +++ b/src/NATS.Server/Mqtt/MqttSessionStore.cs @@ -34,10 +34,25 @@ public sealed class FlapperState /// public sealed class WillMessage { + /// + /// Gets MQTT topic used to publish the will. + /// public string Topic { get; init; } = string.Empty; + /// + /// Gets raw will payload bytes. + /// public byte[] Payload { get; init; } = []; + /// + /// Gets will publish QoS level. + /// public byte QoS { get; init; } + /// + /// Gets a value indicating whether will publish uses retained semantics. + /// public bool Retain { get; init; } + /// + /// Gets configured will delay interval in seconds. + /// public int DelayIntervalSeconds { get; init; } } @@ -47,15 +62,45 @@ public sealed class WillMessage /// public sealed record MqttSessionData { + /// + /// Gets MQTT client identifier. + /// public required string ClientId { get; init; } + /// + /// Gets active subscriptions keyed by topic with QoS values. + /// public Dictionary Subscriptions { get; init; } = []; + /// + /// Gets queued pending publishes for in-flight session state. + /// public List PendingPublishes { get; init; } = []; + /// + /// Gets will topic, when a will is configured. + /// public string? WillTopic { get; init; } + /// + /// Gets will payload bytes, when a will is configured. + /// public byte[]? WillPayload { get; init; } + /// + /// Gets will QoS level. + /// public int WillQoS { get; init; } + /// + /// Gets a value indicating whether will publish is retained. + /// public bool WillRetain { get; init; } + /// + /// Gets a value indicating whether session started as clean session. + /// public bool CleanSession { get; init; } + /// + /// Gets UTC timestamp when session was created/connected. + /// public DateTime ConnectedAtUtc { get; init; } = DateTime.UtcNow; + /// + /// Gets or sets UTC timestamp of last session activity. + /// public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow; } @@ -131,6 +176,8 @@ public sealed class MqttSessionStore /// Called when a CONNECT packet with will flag is received. /// Go reference: server/mqtt.go mqttSession will field ~line 270. /// + /// MQTT client identifier. + /// Will message definition. public void SetWill(string clientId, WillMessage will) { ArgumentNullException.ThrowIfNull(will); @@ -142,6 +189,7 @@ public sealed class MqttSessionStore /// Called on a clean DISCONNECT (no will should be sent). /// Go reference: server/mqtt.go mqttDeliverWill — will is cleared on graceful disconnect. /// + /// MQTT client identifier. public void ClearWill(string clientId) { _wills.TryRemove(clientId, out _); @@ -151,6 +199,8 @@ public sealed class MqttSessionStore /// /// Returns the current will message for the given client, or null if none. /// + /// MQTT client identifier. + /// The will message, or null when not configured. public WillMessage? GetWill(string clientId) => _wills.TryGetValue(clientId, out var will) ? will : null; @@ -161,6 +211,8 @@ public sealed class MqttSessionStore /// Returns true if a will was found, false if none was registered. /// Go reference: server/mqtt.go mqttDeliverWill ~line 490. /// + /// MQTT client identifier. + /// True when a will was found and handled. public bool PublishWillMessage(string clientId) { if (!_wills.TryRemove(clientId, out var will)) @@ -182,6 +234,8 @@ public sealed class MqttSessionStore /// Returns the delayed will entry for the given client if one exists, /// or null if the client has no pending delayed will. /// + /// MQTT client identifier. + /// Delayed will entry with schedule time, or null. public (WillMessage Will, DateTime ScheduledAt)? GetDelayedWill(string clientId) => _delayedWills.TryGetValue(clientId, out var entry) ? entry : null; @@ -189,6 +243,7 @@ public sealed class MqttSessionStore /// Saves (or overwrites) session data for the given client. /// Go reference: server/mqtt.go mqttStoreSession. /// + /// Session payload to persist. public void SaveSession(MqttSessionData session) { ArgumentNullException.ThrowIfNull(session); @@ -199,6 +254,8 @@ public sealed class MqttSessionStore /// Loads session data for the given client, or null if not found. /// Go reference: server/mqtt.go mqttLoadSession. /// + /// MQTT client identifier. + /// Session payload, or null when absent. public MqttSessionData? LoadSession(string clientId) => _sessions.TryGetValue(clientId, out var session) ? session : null; @@ -206,6 +263,7 @@ public sealed class MqttSessionStore /// Deletes the session for the given client. No-op if not found. /// Go reference: server/mqtt.go mqttDeleteSession. /// + /// MQTT client identifier. public void DeleteSession(string clientId) => _sessions.TryRemove(clientId, out _); @@ -220,6 +278,8 @@ public sealed class MqttSessionStore /// Delegates to when is true. /// Go reference: server/mqtt.go mqttCheckFlapper ~line 300. /// + /// MQTT client identifier. + /// Whether this event is a connect event. public void TrackConnectDisconnect(string clientId, bool connected) { if (connected) @@ -284,6 +344,8 @@ public sealed class MqttSessionStore /// Returns true if the client is currently in a backoff period (is a flapper). /// Go reference: server/mqtt.go mqttCheckFlapper ~line 320. /// + /// MQTT client identifier. + /// True when backoff is currently active. public bool IsFlapper(string clientId) { if (!_flapperStates.TryGetValue(clientId, out var state)) @@ -300,6 +362,8 @@ public sealed class MqttSessionStore /// Returns the remaining backoff in milliseconds, or 0 if the client is not flapping. /// Go reference: server/mqtt.go mqttCheckFlapper ~line 325. /// + /// MQTT client identifier. + /// Remaining backoff duration in milliseconds. public long GetBackoffMs(string clientId) { if (!_flapperStates.TryGetValue(clientId, out var state)) @@ -319,6 +383,7 @@ public sealed class MqttSessionStore /// Removes all flapper tracking state for the given client. /// Called when stability is restored or the client is cleanly disconnected. /// + /// MQTT client identifier. public void ClearFlapperState(string clientId) => _flapperStates.TryRemove(clientId, out _); @@ -347,6 +412,8 @@ public sealed class MqttSessionStore /// Returns the backoff delay if the client is flapping, otherwise . /// Go reference: server/mqtt.go mqttCheckFlapper ~line 320. /// + /// MQTT client identifier. + /// Backoff delay when client is flapping, otherwise . public TimeSpan ShouldApplyBackoff(string clientId) { if (!_connectHistory.TryGetValue(clientId, out var history)) @@ -367,6 +434,9 @@ public sealed class MqttSessionStore /// If cleanSession is true, deletes existing session data. /// Go reference: server/mqtt.go mqttInitSessionStore. /// + /// MQTT client identifier. + /// Whether to start with a clean session state. + /// Cancellation token. public async Task ConnectAsync(string clientId, bool cleanSession, CancellationToken ct = default) { if (cleanSession) @@ -404,6 +474,9 @@ public sealed class MqttSessionStore /// /// Adds a subscription to the client's session. /// + /// MQTT client identifier. + /// Subscribed topic filter. + /// Granted QoS level. public void AddSubscription(string clientId, string topic, int qos) { var session = LoadSession(clientId); @@ -420,6 +493,8 @@ public sealed class MqttSessionStore /// Uses the $MQTT_sess stream with MaxMsgsPer=1 for idempotent per-subject writes. /// Go reference: server/mqtt.go mqttStoreSession. /// + /// MQTT client identifier. + /// Cancellation token. public async Task SaveSessionAsync(string clientId, CancellationToken ct = default) { var session = LoadSession(clientId); @@ -433,6 +508,8 @@ public sealed class MqttSessionStore /// /// Returns subscriptions for the given client, or an empty dictionary. /// + /// MQTT client identifier. + /// Subscription map for the client. public IReadOnlyDictionary GetSubscriptions(string clientId) { var session = LoadSession(clientId); diff --git a/src/NATS.Server/NatsOptions.cs b/src/NATS.Server/NatsOptions.cs index 4ca2ebd..6aedf8a 100644 --- a/src/NATS.Server/NatsOptions.cs +++ b/src/NATS.Server/NatsOptions.cs @@ -9,150 +9,429 @@ using NATS.Server.Tls; namespace NATS.Server; +/// +/// Represents the complete runtime configuration for a NATS server instance, +/// including client connectivity, auth, clustering, transport, and observability settings. +/// public sealed class NatsOptions { private static bool _allowUnknownTopLevelFields; private string _configDigest = string.Empty; + /// + /// Gets or sets the listener host for client connections. + /// public string Host { get; set; } = NatsProtocol.DefaultHost; + + /// + /// Gets or sets the TCP port for client connections. + /// public int Port { get; set; } = NatsProtocol.DefaultPort; + + /// + /// Gets or sets a logical server name advertised to clients and peers. + /// public string? ServerName { get; set; } + + /// + /// Gets or sets the maximum accepted payload size in bytes for client publishes. + /// public int MaxPayload { get; set; } = 1024 * 1024; + + /// + /// Gets or sets the maximum protocol control line length in bytes. + /// public int MaxControlLine { get; set; } = 4096; + + /// + /// Gets or sets the maximum number of concurrent client connections. + /// public int MaxConnections { get; set; } = NatsProtocol.DefaultMaxConnections; + + /// + /// Gets or sets the maximum buffered outbound data per client before the connection is considered slow. + /// public long MaxPending { get; set; } = 64 * 1024 * 1024; // 64MB, matching Go MAX_PENDING_SIZE + + /// + /// Gets or sets the write deadline used for flushing outbound protocol frames. + /// public TimeSpan WriteDeadline { get; set; } = NatsProtocol.DefaultFlushDeadline; + + /// + /// Gets or sets the server heartbeat interval used to detect stale clients. + /// public TimeSpan PingInterval { get; set; } = NatsProtocol.DefaultPingInterval; + + /// + /// Gets or sets the maximum number of unanswered pings before disconnect. + /// public int MaxPingsOut { get; set; } = NatsProtocol.DefaultPingMaxOut; - // Go: opts.go — DisableShortFirstPing. When true, the first PING timer tick - // is not suppressed by the FirstPongSent / 2-second grace period. - // Useful in tests to ensure deterministic ping behavior. + /// + /// Gets or sets a value indicating whether to disable the short-first-ping grace behavior. + /// public bool DisableShortFirstPing { get; set; } - // Subscription limits - public int MaxSubs { get; set; } // 0 = unlimited (per-connection) - public int MaxSubTokens { get; set; } // 0 = unlimited + /// + /// Gets or sets the maximum subscriptions allowed per connection. A value of 0 means unlimited. + /// + public int MaxSubs { get; set; } - // Server tags (exposed via /varz) + /// + /// Gets or sets the maximum number of subject tokens allowed in a subscription. A value of 0 means unlimited. + /// + public int MaxSubTokens { get; set; } + + /// + /// Gets or sets user-defined server tags exposed via monitoring endpoints. + /// public Dictionary? Tags { get; set; } - // Account configuration + /// + /// Gets or sets account definitions used to isolate tenants and permissions. + /// public Dictionary? Accounts { get; set; } - // Simple auth (single user) + /// + /// Gets or sets the global username for single-user authentication mode. + /// public string? Username { get; set; } + + /// + /// Gets or sets the global password for single-user authentication mode. + /// public string? Password { get; set; } + + /// + /// Gets or sets the global token for token-based authentication mode. + /// public string? Authorization { get; set; } - // Multiple users/nkeys + /// + /// Gets or sets the configured list of explicit users for multi-user auth. + /// public IReadOnlyList? Users { get; set; } + + /// + /// Gets or sets the configured list of NKey users for signature-based auth. + /// public IReadOnlyList? NKeys { get; set; } - // Default/fallback + /// + /// Gets or sets the fallback account user identity when no credentials are provided. + /// public string? NoAuthUser { get; set; } - // Auth extensions + /// + /// Gets or sets external authorization callout settings. + /// public Auth.ExternalAuthOptions? ExternalAuth { get; set; } + + /// + /// Gets or sets proxy-based authentication settings. + /// public Auth.ProxyAuthOptions? ProxyAuth { get; set; } - // Auth timing + /// + /// Gets or sets the timeout for completing client authentication. + /// public TimeSpan AuthTimeout { get; set; } = NatsProtocol.AuthTimeout; - // Monitoring (0 = disabled; standard port is 8222) + /// + /// Gets or sets the HTTP monitoring port. A value of 0 disables monitoring. + /// public int MonitorPort { get; set; } + + /// + /// Gets or sets the bind host for monitoring endpoints. + /// public string MonitorHost { get; set; } = "0.0.0.0"; + + /// + /// Gets or sets an optional URL base path prefix for monitoring endpoints. + /// public string? MonitorBasePath { get; set; } - // 0 = disabled + + /// + /// Gets or sets the HTTPS monitoring port. A value of 0 disables HTTPS monitoring. + /// public int MonitorHttpsPort { get; set; } - // Lifecycle / lame-duck mode + /// + /// Gets or sets the duration of lame duck mode before final shutdown. + /// public TimeSpan LameDuckDuration { get; set; } = NatsProtocol.DefaultLameDuckDuration; + + /// + /// Gets or sets the grace period before starting client eviction during lame duck mode. + /// public TimeSpan LameDuckGracePeriod { get; set; } = NatsProtocol.DefaultLameDuckGracePeriod; - // File paths + /// + /// Gets or sets the optional PID file path written at startup. + /// public string? PidFile { get; set; } + + /// + /// Gets or sets the directory where dynamic listener port files are written. + /// public string? PortsFileDir { get; set; } + + /// + /// Gets or sets the primary configuration file path. + /// public string? ConfigFile { get; set; } - // Logging + /// + /// Gets or sets the output file path for server logs. + /// public string? LogFile { get; set; } + + /// + /// Gets or sets the maximum log file size before rotation. + /// public long LogSizeLimit { get; set; } + + /// + /// Gets or sets the number of rotated log files to retain. + /// public int LogMaxFiles { get; set; } + + /// + /// Gets or sets a value indicating whether debug-level logging is enabled. + /// public bool Debug { get; set; } + + /// + /// Gets or sets a value indicating whether protocol trace logging is enabled. + /// public bool Trace { get; set; } + + /// + /// Gets or sets a value indicating whether timestamps are included in log entries. + /// public bool Logtime { get; set; } = true; + + /// + /// Gets or sets a value indicating whether log timestamps use UTC instead of local time. + /// public bool LogtimeUTC { get; set; } + + /// + /// Gets or sets a value indicating whether logs are emitted to syslog. + /// public bool Syslog { get; set; } + + /// + /// Gets or sets the remote syslog endpoint, when remote syslog forwarding is enabled. + /// public string? RemoteSyslog { get; set; } - // Profiling (0 = disabled) + /// + /// Gets or sets the profiling HTTP port. A value of 0 disables profiling endpoints. + /// public int ProfPort { get; set; } - // Extended options for Go parity + /// + /// Gets or sets the client advertise address provided to peers and clients. + /// public string? ClientAdvertise { get; set; } + + /// + /// Gets or sets a value indicating whether verbose protocol tracing is enabled. + /// public bool TraceVerbose { get; set; } + + /// + /// Gets or sets the maximum payload length included in trace log messages. + /// public int MaxTracedMsgLen { get; set; } + + /// + /// Gets or sets a value indicating whether sublist result caching is disabled. + /// public bool DisableSublistCache { get; set; } + + /// + /// Gets or sets how many connection errors are logged before log suppression starts. + /// public int ConnectErrorReports { get; set; } = NatsProtocol.DefaultConnectErrorReports; + + /// + /// Gets or sets how many reconnect errors are logged before log suppression starts. + /// public int ReconnectErrorReports { get; set; } = NatsProtocol.DefaultReconnectErrorReports; + + /// + /// Gets or sets a value indicating whether protocol headers are disabled. + /// public bool NoHeaderSupport { get; set; } + + /// + /// Gets or sets the number of closed-client records retained for monitoring. + /// public int MaxClosedClients { get; set; } = NatsProtocol.DefaultMaxClosedClients; + + /// + /// Gets or sets a value indicating whether system account setup is disabled. + /// public bool NoSystemAccount { get; set; } + + /// + /// Gets or sets the configured system account name used for server-level subjects. + /// public string? SystemAccount { get; set; } - // Tracks which fields were set via CLI flags (for reload precedence) + /// + /// Gets the set of fields explicitly provided on the command line to preserve override precedence. + /// public HashSet InCmdLine { get; } = []; - // TLS + /// + /// Gets or sets the server TLS certificate file path. + /// public string? TlsCert { get; set; } + + /// + /// Gets or sets the server TLS private key file path. + /// public string? TlsKey { get; set; } + + /// + /// Gets or sets the trusted CA certificate file used to validate peers. + /// public string? TlsCaCert { get; set; } + + /// + /// Gets or sets a value indicating whether client certificates are required and verified. + /// public bool TlsVerify { get; set; } + + /// + /// Gets or sets a value indicating whether certificate subject mapping to users is enabled. + /// public bool TlsMap { get; set; } + + /// + /// Gets or sets the timeout for TLS handshake and verification. + /// public TimeSpan TlsTimeout { get; set; } = NatsProtocol.TlsTimeout; + + /// + /// Gets or sets a value indicating whether clients must start with TLS handshake bytes. + /// public bool TlsHandshakeFirst { get; set; } + + /// + /// Gets or sets the fallback delay before trying plaintext parsing when handshake-first is enabled. + /// public TimeSpan TlsHandshakeFirstFallback { get; set; } = NatsProtocol.DefaultTlsHandshakeFirstFallbackDelay; + + /// + /// Gets or sets a value indicating whether non-TLS client connections are allowed. + /// public bool AllowNonTls { get; set; } + + /// + /// Gets or sets an optional TLS handshake rate limit. + /// public long TlsRateLimit { get; set; } + + /// + /// Gets or sets the pinned server certificate fingerprints accepted for peers. + /// public HashSet? TlsPinnedCerts { get; set; } + + /// + /// Gets or sets the minimum TLS protocol version allowed for client connections. + /// public SslProtocols TlsMinVersion { get; set; } = SslProtocols.Tls12; - // OCSP stapling and peer verification + /// + /// Gets or sets OCSP stapling behavior for presented server certificates. + /// public OcspConfig? OcspConfig { get; set; } + + /// + /// Gets or sets a value indicating whether OCSP status on peer certificates is enforced. + /// public bool OcspPeerVerify { get; set; } - // JWT / Operator mode + /// + /// Gets or sets the trusted operator/public keys used to validate account JWTs. + /// public string[]? TrustedKeys { get; set; } + + /// + /// Gets or sets the account resolver used to fetch and cache account JWT metadata. + /// public Auth.Jwt.IAccountResolver? AccountResolver { get; set; } - // Per-subsystem log level overrides (namespace -> level) + /// + /// Gets or sets per-subsystem log level overrides. + /// public Dictionary? LogOverrides { get; set; } - // Subject mapping / transforms (source pattern -> destination template) + /// + /// Gets or sets configured subject mapping transforms. + /// public Dictionary? SubjectMappings { get; set; } - // MQTT configuration (parsed from config, no listener yet) + /// + /// Gets or sets MQTT bridge options. + /// public MqttOptions? Mqtt { get; set; } - // Cluster and JetStream settings + /// + /// Gets or sets cluster route listener and dial settings. + /// public ClusterOptions? Cluster { get; set; } + + /// + /// Gets or sets gateway federation settings. + /// public GatewayOptions? Gateway { get; set; } + + /// + /// Gets or sets leaf node listener and remote settings. + /// public LeafNodeOptions? LeafNode { get; set; } + + /// + /// Gets or sets JetStream persistence and API options. + /// public JetStreamOptions? JetStream { get; set; } + /// + /// Gets a value indicating whether TLS server certificate and key paths are both configured. + /// public bool HasTls => TlsCert != null && TlsKey != null; - // WebSocket + /// + /// Gets or sets websocket listener and authentication options. + /// public WebSocketOptions WebSocket { get; set; } = new(); + /// + /// Enables or disables tolerance for unknown top-level configuration fields. + /// + /// If , unknown fields are accepted without scan failure. public static void NoErrOnUnknownFields(bool noError) { _allowUnknownTopLevelFields = noError; } + /// + /// Gets a value indicating whether unknown top-level config keys are currently accepted. + /// internal static bool AllowUnknownTopLevelFields => _allowUnknownTopLevelFields; + /// + /// Parses a comma-delimited route URL string into absolute route URIs. + /// + /// The route list from CLI or config. + /// A list of valid absolute route endpoints. public static List RoutesFromStr(string routesStr) { if (string.IsNullOrWhiteSpace(routesStr)) @@ -168,6 +447,10 @@ public sealed class NatsOptions return routes; } + /// + /// Creates a deep copy of the options object for reload and runtime mutation workflows. + /// + /// A cloned options instance preserving effective values and command-line precedence flags. public NatsOptions Clone() { try @@ -198,6 +481,10 @@ public sealed class NatsOptions } } + /// + /// Applies configuration parsed from raw config text to this options instance. + /// + /// Configuration file contents in NATS config format. public void ProcessConfigString(string data) { var parsed = ConfigProcessor.ProcessConfig(data); @@ -205,6 +492,10 @@ public sealed class NatsOptions _configDigest = ComputeDigest(data); } + /// + /// Returns the SHA-256 digest of the last processed config text. + /// + /// A lowercase hex digest for reload/change detection. public string ConfigDigest() => _configDigest; private static void CopyFrom(NatsOptions destination, NatsOptions source) @@ -224,89 +515,310 @@ public sealed class NatsOptions } } +/// +/// Defines operational limits for JetStream API request handling. +/// public sealed class JSLimitOpts { + /// + /// Gets or sets the maximum number of messages requested in a single batch pull. + /// public int MaxRequestBatch { get; set; } + + /// + /// Gets or sets the maximum number of pending acknowledgements per consumer. + /// public int MaxAckPending { get; set; } + + /// + /// Gets or sets the maximum number of high-availability JetStream assets. + /// public int MaxHAAssets { get; set; } + + /// + /// Gets or sets the duplicate window used for message de-duplication. + /// public TimeSpan Duplicates { get; set; } + + /// + /// Gets or sets the per-stream cap for in-flight batched pull requests. + /// public int MaxBatchInflightPerStream { get; set; } + + /// + /// Gets or sets the global cap for in-flight batched pull requests. + /// public int MaxBatchInflightTotal { get; set; } + + /// + /// Gets or sets the maximum payload size permitted for batch responses. + /// public int MaxBatchSize { get; set; } + + /// + /// Gets or sets the maximum wait time for a pull batch request. + /// public TimeSpan MaxBatchTimeout { get; set; } } +/// +/// Configures operator-issued external auth callout claims and delegation. +/// public sealed class AuthCallout { + /// + /// Gets or sets the issuer public key for validating callout-issued JWTs. + /// public string Issuer { get; set; } = string.Empty; + + /// + /// Gets or sets the account used for auth callout request subjects. + /// public string Account { get; set; } = string.Empty; + + /// + /// Gets or sets the users permitted to receive callout requests. + /// public List AuthUsers { get; set; } = []; + + /// + /// Gets or sets the XKey used for encrypting auth callout payloads. + /// public string XKey { get; set; } = string.Empty; + + /// + /// Gets or sets the accounts that can be authenticated via this callout. + /// public List AllowedAccounts { get; set; } = []; } +/// +/// Contains trusted proxy connection settings for delegated client identity. +/// public sealed class ProxiesConfig { + /// + /// Gets or sets the list of trusted proxy identities. + /// public List Trusted { get; set; } = []; } +/// +/// Describes a trusted proxy identity key. +/// public sealed class ProxyConfig { + /// + /// Gets or sets the trusted proxy public key. + /// public string Key { get; set; } = string.Empty; } +/// +/// Captures dynamically assigned listener ports for process integration and discovery. +/// public sealed class Ports { + /// + /// Gets or sets exposed client listener endpoints. + /// public List Nats { get; set; } = []; + + /// + /// Gets or sets exposed monitoring listener endpoints. + /// public List Monitoring { get; set; } = []; + + /// + /// Gets or sets exposed route listener endpoints. + /// public List Cluster { get; set; } = []; + + /// + /// Gets or sets exposed profiler listener endpoints. + /// public List Profile { get; set; } = []; + + /// + /// Gets or sets exposed websocket listener endpoints. + /// public List WebSocket { get; set; } = []; + + /// + /// Gets or sets exposed leaf node listener endpoints. + /// public List LeafNodes { get; set; } = []; } +/// +/// Enumerates supported wire-compression mode names shared with config parsing. +/// public static class CompressionModes { + /// + /// Disables transport compression. + /// public const string Off = "off"; + + /// + /// Accepts compression when the peer requests it. + /// public const string Accept = "accept"; + + /// + /// Uses fast S2 compression. + /// public const string S2Fast = "s2_fast"; + + /// + /// Uses balanced S2 compression. + /// public const string S2Better = "s2_better"; + + /// + /// Uses highest-ratio S2 compression. + /// public const string S2Best = "s2_best"; + + /// + /// Uses S2 framing without payload compression. + /// public const string S2Uncompressed = "s2_uncompressed"; + + /// + /// Chooses an S2 mode automatically from latency thresholds. + /// public const string S2Auto = "s2_auto"; } +/// +/// Configures connection compression policy. +/// public sealed class CompressionOpts { + /// + /// Gets or sets the selected compression mode. + /// public string Mode { get; set; } = CompressionModes.Off; + + /// + /// Gets or sets RTT thresholds used by automatic compression mode selection. + /// public List RTTThresholds { get; set; } = [10, 50, 100, 250]; } +/// +/// Represents websocket listener and auth configuration for browser and websocket clients. +/// public sealed class WebSocketOptions { + /// + /// Gets or sets the websocket bind host. + /// public string Host { get; set; } = "0.0.0.0"; + + /// + /// Gets or sets the websocket bind port. + /// public int Port { get; set; } = -1; + + /// + /// Gets or sets the advertised websocket URL authority. + /// public string? Advertise { get; set; } + + /// + /// Gets or sets the fallback no-auth user for websocket clients. + /// public string? NoAuthUser { get; set; } + + /// + /// Gets or sets the cookie name used to read JWT credentials. + /// public string? JwtCookie { get; set; } + + /// + /// Gets or sets the cookie name used to read username credentials. + /// public string? UsernameCookie { get; set; } + + /// + /// Gets or sets the cookie name used to read password credentials. + /// public string? PasswordCookie { get; set; } + + /// + /// Gets or sets the cookie name used to read token credentials. + /// public string? TokenCookie { get; set; } + + /// + /// Gets or sets a static websocket username override. + /// public string? Username { get; set; } + + /// + /// Gets or sets a static websocket password override. + /// public string? Password { get; set; } + + /// + /// Gets or sets a static websocket token override. + /// public string? Token { get; set; } + + /// + /// Gets or sets the websocket authentication timeout. + /// public TimeSpan AuthTimeout { get; set; } = TimeSpan.FromSeconds(2); + + /// + /// Gets or sets a value indicating whether websocket TLS is disabled. + /// public bool NoTls { get; set; } + + /// + /// Gets or sets the websocket TLS certificate path. + /// public string? TlsCert { get; set; } + + /// + /// Gets or sets the websocket TLS key path. + /// public string? TlsKey { get; set; } + + /// + /// Gets or sets a value indicating whether same-origin checks are enforced. + /// public bool SameOrigin { get; set; } + + /// + /// Gets or sets explicit allowed origins for cross-origin websocket upgrades. + /// public List? AllowedOrigins { get; set; } + + /// + /// Gets or sets a value indicating whether per-message websocket compression is enabled. + /// public bool Compression { get; set; } + + /// + /// Gets or sets the websocket handshake timeout. + /// public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.FromSeconds(2); + + /// + /// Gets or sets an optional server ping interval for websocket connections. + /// public TimeSpan? PingInterval { get; set; } + + /// + /// Gets or sets additional HTTP headers included on websocket upgrade responses. + /// public Dictionary? Headers { get; set; } - // Go websocket.go srvWebsocket.authOverride parity bit: - // true when websocket auth options override top-level auth config. + /// + /// Gets a value indicating whether websocket auth settings override top-level auth configuration. + /// public bool AuthOverride { get; internal set; } } diff --git a/tests.md b/tests.md index 50f8216..8608c26 100644 --- a/tests.md +++ b/tests.md @@ -4,6 +4,16 @@ No known failing tests. +## Flaky Tests + +No known flaky tests. + +### Medium — May fail on very slow systems or under extreme CI load + +| Test | File | Root Cause | +|------|------|------------| +| Route/Gateway/LeafNode propagation tests (20+ tests) | `RouteGoParityTests.cs`, `GatewayGoParityTests.cs`, `LeafNodeGoParityTests.cs` etc. | `await Task.Delay(50)` polling loops waiting for subscription/route propagation. Timeout can be exceeded under extreme load. | + ## Skipped Tests ### NATS.E2E.Tests (3 skipped) diff --git a/tests/NATS.E2E.Tests/AccountIsolationTests.cs b/tests/NATS.E2E.Tests/AccountIsolationTests.cs index 077168e..9768038 100644 --- a/tests/NATS.E2E.Tests/AccountIsolationTests.cs +++ b/tests/NATS.E2E.Tests/AccountIsolationTests.cs @@ -42,7 +42,7 @@ public class AccountIsolationTests(AccountServerFixture fixture) using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask(); - var completed = await Task.WhenAny(readTask, Task.Delay(1000)); + var completed = await Task.WhenAny(readTask, Task.Delay(3000)); completed.ShouldNotBe(readTask); } @@ -75,7 +75,7 @@ public class AccountIsolationTests(AccountServerFixture fixture) using var ctsBNoMsg = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var readBTask = subscriptionB.Msgs.ReadAsync(ctsBNoMsg.Token).AsTask(); - var completedB = await Task.WhenAny(readBTask, Task.Delay(1000)); + var completedB = await Task.WhenAny(readBTask, Task.Delay(3000)); completedB.ShouldNotBe(readBTask); // Cancel the abandoned read so it doesn't consume the next message await ctsBNoMsg.CancelAsync(); @@ -90,7 +90,7 @@ public class AccountIsolationTests(AccountServerFixture fixture) using var ctsANoMsg = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var readATask2 = subscriptionA.Msgs.ReadAsync(ctsANoMsg.Token).AsTask(); - var completedA2 = await Task.WhenAny(readATask2, Task.Delay(1000)); + var completedA2 = await Task.WhenAny(readATask2, Task.Delay(3000)); completedA2.ShouldNotBe(readATask2); await ctsANoMsg.CancelAsync(); try { await readATask2; } catch (OperationCanceledException) { } diff --git a/tests/NATS.E2E.Tests/CoreMessagingTests.cs b/tests/NATS.E2E.Tests/CoreMessagingTests.cs index fe16221..62bc948 100644 --- a/tests/NATS.E2E.Tests/CoreMessagingTests.cs +++ b/tests/NATS.E2E.Tests/CoreMessagingTests.cs @@ -62,7 +62,7 @@ public class CoreMessagingTests(NatsServerFixture fixture) using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask(); - var winner = await Task.WhenAny(readTask, Task.Delay(1000)); + var winner = await Task.WhenAny(readTask, Task.Delay(3000)); winner.ShouldNotBe(readTask); } @@ -327,7 +327,7 @@ public class CoreMessagingTests(NatsServerFixture fixture) using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask(); - var winner = await Task.WhenAny(readTask, Task.Delay(1000)); + var winner = await Task.WhenAny(readTask, Task.Delay(3000)); winner.ShouldNotBe(readTask); } diff --git a/tests/NATS.Server.Core.Tests/ConcurrencyStressTests.cs b/tests/NATS.Server.Core.Tests/ConcurrencyStressTests.cs index 55cc474..a5ac90d 100644 --- a/tests/NATS.Server.Core.Tests/ConcurrencyStressTests.cs +++ b/tests/NATS.Server.Core.Tests/ConcurrencyStressTests.cs @@ -491,7 +491,7 @@ public class ConcurrencyStressTests for (var i = 0; i < 10; i++) { streamManager.Purge("PURGECONC"); - Thread.Sleep(1); + Thread.Sleep(5); } } catch (Exception ex) { errors.Add(ex); } diff --git a/tests/NATS.Server.Core.Tests/ResponseTrackerTests.cs b/tests/NATS.Server.Core.Tests/ResponseTrackerTests.cs index 1eccff5..8a7dc7b 100644 --- a/tests/NATS.Server.Core.Tests/ResponseTrackerTests.cs +++ b/tests/NATS.Server.Core.Tests/ResponseTrackerTests.cs @@ -32,19 +32,19 @@ public class ResponseTrackerTests [Fact] public void Enforces_expiry() { - var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(1)); + var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(50)); tracker.RegisterReply("_INBOX.abc"); - Thread.Sleep(50); + Thread.Sleep(200); tracker.IsReplyAllowed("_INBOX.abc").ShouldBeFalse(); } [Fact] public void Prune_removes_expired() { - var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(1)); + var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(50)); tracker.RegisterReply("_INBOX.a"); tracker.RegisterReply("_INBOX.b"); - Thread.Sleep(50); + Thread.Sleep(200); tracker.Prune(); tracker.Count.ShouldBe(0); } diff --git a/tests/NATS.Server.Core.Tests/Stress/ClusterStressTests.cs b/tests/NATS.Server.Core.Tests/Stress/ClusterStressTests.cs index 9847e6c..f09503d 100644 --- a/tests/NATS.Server.Core.Tests/Stress/ClusterStressTests.cs +++ b/tests/NATS.Server.Core.Tests/Stress/ClusterStressTests.cs @@ -296,7 +296,7 @@ public class ClusterStressTests for (var i = 0; i < 5; i++) { meta.StepDown(); - Thread.Sleep(2); + Thread.Sleep(10); } } catch (Exception ex) { errors.Add(ex); } diff --git a/tests/NATS.Server.Gateways.Tests/Gateways/GatewayGoParityTests.cs b/tests/NATS.Server.Gateways.Tests/Gateways/GatewayGoParityTests.cs index 3b805bb..58c41d5 100644 --- a/tests/NATS.Server.Gateways.Tests/Gateways/GatewayGoParityTests.cs +++ b/tests/NATS.Server.Gateways.Tests/Gateways/GatewayGoParityTests.cs @@ -186,11 +186,11 @@ public class GatewayGoParityTests var sw = System.Diagnostics.Stopwatch.StartNew(); var disposeTask = manager.DisposeAsync().AsTask(); - var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(5))); + var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(10))); sw.Stop(); - completed.ShouldBe(disposeTask, "DisposeAsync should complete within 5 seconds"); - sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(4)); + completed.ShouldBe(disposeTask, "DisposeAsync should complete within 10 seconds"); + sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(8)); } // ── TestGatewayAuth (stub — auth not yet wired to gateway handshake) ── diff --git a/tests/NATS.Server.Gateways.Tests/Gateways/ReplyMapCacheTests.cs b/tests/NATS.Server.Gateways.Tests/Gateways/ReplyMapCacheTests.cs index ae6ddf9..97686cb 100644 --- a/tests/NATS.Server.Gateways.Tests/Gateways/ReplyMapCacheTests.cs +++ b/tests/NATS.Server.Gateways.Tests/Gateways/ReplyMapCacheTests.cs @@ -53,13 +53,12 @@ public class ReplyMapCacheTests // Go: gateway.go — entries expire after the configured TTL window [Fact] - [SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")] public void TTL_expiration() { - var cache = new ReplyMapCache(capacity: 16, ttlMs: 1); + var cache = new ReplyMapCache(capacity: 16, ttlMs: 50); cache.Set("_INBOX.ttl", "_GR_.c1.1._INBOX.ttl"); - Thread.Sleep(5); // Wait longer than the 1ms TTL + Thread.Sleep(200); // Wait longer than the 50ms TTL var found = cache.TryGet("_INBOX.ttl", out var value); @@ -126,14 +125,13 @@ public class ReplyMapCacheTests // Go: gateway.go — PurgeExpired removes only expired entries [Fact] - [SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")] public void PurgeExpired_removes_old_entries() { - var cache = new ReplyMapCache(capacity: 16, ttlMs: 1); + var cache = new ReplyMapCache(capacity: 16, ttlMs: 50); cache.Set("old1", "v1"); cache.Set("old2", "v2"); - Thread.Sleep(5); // Ensure both entries are past the 1ms TTL + Thread.Sleep(200); // Ensure both entries are past the 50ms TTL var purged = cache.PurgeExpired(); diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreGoParityTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreGoParityTests.cs index ec77810..714ea75 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreGoParityTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreGoParityTests.cs @@ -1160,11 +1160,11 @@ public sealed class FileStoreGoParityTests : IDisposable store.StoreMsg("foo", null, "1"u8.ToArray(), 0); // seq 1 // A small sleep so timestamps are distinct. - System.Threading.Thread.Sleep(10); + System.Threading.Thread.Sleep(50); var t2 = DateTime.UtcNow; store.StoreMsg("foo", null, "2"u8.ToArray(), 0); // seq 2 - System.Threading.Thread.Sleep(10); + System.Threading.Thread.Sleep(50); var t3 = DateTime.UtcNow; store.StoreMsg("foo", null, "3"u8.ToArray(), 0); // seq 3 diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreTombstoneTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreTombstoneTests.cs index 19c8e1b..f55bfd7 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreTombstoneTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/FileStoreTombstoneTests.cs @@ -25,6 +25,7 @@ using System.Text; using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Storage; +using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Storage; @@ -335,7 +336,7 @@ public sealed class FileStoreTombstoneTests : IDisposable // After restart the message should still be present (not yet expired), // and after waiting 2 seconds it should expire. [Fact] - public void MessageTTL_RecoverSingleMessageWithoutStreamState() + public async Task MessageTTL_RecoverSingleMessageWithoutStreamState() { var dir = UniqueDir("ttl-recover"); var opts = new FileStoreOptions { Directory = dir, MaxAgeMs = 1000 }; @@ -358,8 +359,8 @@ public sealed class FileStoreTombstoneTests : IDisposable ss.LastSeq.ShouldBe(1UL); ss.Msgs.ShouldBe(1UL); - // Wait for TTL to expire. - Thread.Sleep(2000); + // Wait for TTL to expire (1s TTL + generous margin). + await Task.Delay(2_500); // Force expiry by storing a new message (expiry check runs before store). store.StoreMsg("test", null, [], 0); @@ -373,7 +374,7 @@ public sealed class FileStoreTombstoneTests : IDisposable // After TTL expiry and restart (without stream state file), // a tombstone should allow proper recovery of the stream state. [Fact] - public void MessageTTL_WriteTombstoneAllowsRecovery() + public async Task MessageTTL_WriteTombstoneAllowsRecovery() { var dir = UniqueDir("ttl-tombstone"); var opts = new FileStoreOptions { Directory = dir, MaxAgeMs = 1000 }; @@ -388,8 +389,8 @@ public sealed class FileStoreTombstoneTests : IDisposable ss.FirstSeq.ShouldBe(1UL); ss.LastSeq.ShouldBe(2UL); - // Wait for seq=1 to expire. - Thread.Sleep(1500); + // Wait for seq=1 to expire (1s TTL + generous margin). + await Task.Delay(2_500); // Force expiry. store.StoreMsg("test", null, [], 0); @@ -629,7 +630,6 @@ public sealed class FileStoreTombstoneTests : IDisposable cs1.UpdateDelivered(5, 2, 1, ts); cs1.Stop(); - Thread.Sleep(20); // wait for flush // Reopen — should recover redelivered. var cs2 = store.ConsumerStore("o22", DateTime.UtcNow, cfg); @@ -641,7 +641,6 @@ public sealed class FileStoreTombstoneTests : IDisposable cs2.UpdateDelivered(7, 3, 1, ts); cs2.Stop(); - Thread.Sleep(20); // Reopen again. var cs3 = store.ConsumerStore("o22", DateTime.UtcNow, cfg); @@ -654,7 +653,6 @@ public sealed class FileStoreTombstoneTests : IDisposable cs3.UpdateAcks(6, 2); cs3.Stop(); - Thread.Sleep(20); // Reopen and ack 4. var cs4 = store.ConsumerStore("o22", DateTime.UtcNow, cfg); diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/MemStoreGoParityTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/MemStoreGoParityTests.cs index ab9ba2a..5d3c32b 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/MemStoreGoParityTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/MemStoreGoParityTests.cs @@ -29,6 +29,7 @@ using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Storage; +using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Storage; @@ -491,7 +492,7 @@ public sealed class MemStoreGoParityTests s.StoreMsg("A", null, "OK"u8.ToArray(), 0); if (i == total / 2) { - Thread.Sleep(100); + Thread.Sleep(250); midTime = DateTime.UtcNow; } } @@ -593,7 +594,7 @@ public sealed class MemStoreGoParityTests // Go: TestMemStoreMessageTTL server/memstore_test.go:1202 [Fact] - public void MessageTTL_ExpiresAfterDelay() + public async Task MessageTTL_ExpiresAfterDelay() { var cfg = new StreamConfig { @@ -616,8 +617,13 @@ public sealed class MemStoreGoParityTests ss.LastSeq.ShouldBe(10UL); ss.Msgs.ShouldBe(10UL); - // Wait for TTL to expire (> 1 sec + check interval of 1 sec) - Thread.Sleep(2_500); + // Wait for TTL to expire + await PollHelper.WaitOrThrowAsync(() => + { + var ss2 = new StreamState(); + s.FastState(ref ss2); + return ss2.Msgs == 0; + }, "TTL expiry", timeoutMs: 10_000, intervalMs: 100); s.FastState(ref ss); ss.FirstSeq.ShouldBe(11UL); diff --git a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs index 701bff9..997d820 100644 --- a/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs +++ b/tests/NATS.Server.JetStream.Tests/JetStream/Storage/StoreInterfaceTests.cs @@ -14,6 +14,7 @@ using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Storage; +using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Storage; @@ -286,8 +287,7 @@ public sealed class StoreInterfaceTests // Go: TestStoreUpdateConfigTTLState server/store_test.go:574 [Fact] - [SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; Thread.Sleep waits for message TTL to expire or survive")] - public void UpdateConfigTTLState_MessageSurvivesWhenTtlDisabled() + public async Task UpdateConfigTTLState_MessageSurvivesWhenTtlDisabled() { var cfg = new StreamConfig { @@ -301,7 +301,7 @@ public sealed class StoreInterfaceTests // TTLs disabled — message with ttl=1s should survive even after 2s. var (seq, _) = s.StoreMsg("foo", null, [], 1); - Thread.Sleep(2_000); + await Task.Delay(2_500); // Should not throw — message should still be present. var loaded = s.LoadMsg(seq, null); loaded.Sequence.ShouldBe(seq); @@ -312,9 +312,11 @@ public sealed class StoreInterfaceTests // TTLs enabled — message with ttl=1s should expire. var (seq2, _) = s.StoreMsg("foo", null, [], 1); - Thread.Sleep(2_500); - // Should throw — message should have expired. - Should.Throw(() => s.LoadMsg(seq2, null)); + await PollHelper.WaitOrThrowAsync(() => + { + try { s.LoadMsg(seq2, null); return false; } + catch (KeyNotFoundException) { return true; } + }, "TTL expiry", timeoutMs: 10_000, intervalMs: 100); // Now disable TTLs again. cfg.AllowMsgTtl = false; @@ -322,7 +324,7 @@ public sealed class StoreInterfaceTests // TTLs disabled — message with ttl=1s should survive. var (seq3, _) = s.StoreMsg("foo", null, [], 1); - Thread.Sleep(2_000); + await Task.Delay(2_500); // Should not throw — TTL wheel is gone so message stays. var loaded3 = s.LoadMsg(seq3, null); loaded3.Sequence.ShouldBe(seq3); diff --git a/tests/NATS.Server.Raft.Tests/Raft/RaftElectionTimerTests.cs b/tests/NATS.Server.Raft.Tests/Raft/RaftElectionTimerTests.cs index c3d7b1f..4df49ce 100644 --- a/tests/NATS.Server.Raft.Tests/Raft/RaftElectionTimerTests.cs +++ b/tests/NATS.Server.Raft.Tests/Raft/RaftElectionTimerTests.cs @@ -1,5 +1,6 @@ using NATS.Server; using NATS.Server.Raft; +using NATS.Server.TestUtilities; namespace NATS.Server.Raft.Tests.Raft; @@ -226,7 +227,6 @@ public class RaftElectionTimerTests : IDisposable } [Fact] - [SlopwatchSuppress("SW004", "Testing timer fires after heartbeats stop requires real delays for heartbeat simulation and timeout expiry")] public async Task Timer_fires_after_heartbeats_stop() { var nodes = CreateTrackedCluster(3); @@ -246,7 +246,7 @@ public class RaftElectionTimerTests : IDisposable node.Role.ShouldBe(RaftRole.Follower); // Stop sending heartbeats and wait for timer to fire - await Task.Delay(200); + await PollHelper.WaitOrThrowAsync(() => node.Role == RaftRole.Candidate, "election timeout", timeoutMs: 5000); // Should have started an election node.Role.ShouldBe(RaftRole.Candidate);