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