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);