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