Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
// Go reference: server/events.go:2081-2090 — compressionType, snappyCompression,
|
||||
// and events.go:578-598 — internalSendLoop compression via s2.WriterSnappyCompat().
|
||||
|
||||
using System.IO.Compression;
|
||||
using IronSnappy;
|
||||
|
||||
namespace NATS.Server.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Compression encodings supported for event API responses.
|
||||
/// Go reference: events.go compressionType constants.
|
||||
/// </summary>
|
||||
public enum EventCompressionType : sbyte
|
||||
{
|
||||
None = 0,
|
||||
Gzip = 1,
|
||||
Snappy = 2,
|
||||
Unsupported = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides S2 (Snappy-compatible) compression for system event payloads.
|
||||
/// Maps to Go's compressionType / snappyCompression handling in events.go:2082-2098
|
||||
@@ -12,6 +25,9 @@ namespace NATS.Server.Events;
|
||||
/// </summary>
|
||||
public static class EventCompressor
|
||||
{
|
||||
public const string AcceptEncodingHeader = "Accept-Encoding";
|
||||
public const string ContentEncodingHeader = "Content-Encoding";
|
||||
|
||||
// Default threshold: only compress payloads larger than this many bytes.
|
||||
// Compressing tiny payloads wastes CPU and may produce larger output.
|
||||
private const int DefaultThresholdBytes = 256;
|
||||
@@ -56,11 +72,23 @@ public static class EventCompressor
|
||||
/// <param name="payload">Raw bytes to compress.</param>
|
||||
/// <returns>Compressed bytes. Returns an empty array for empty input.</returns>
|
||||
public static byte[] Compress(ReadOnlySpan<byte> payload)
|
||||
=> Compress(payload, EventCompressionType.Snappy);
|
||||
|
||||
/// <summary>
|
||||
/// Compresses <paramref name="payload"/> using the requested <paramref name="compression"/>.
|
||||
/// </summary>
|
||||
public static byte[] Compress(ReadOnlySpan<byte> payload, EventCompressionType compression)
|
||||
{
|
||||
if (payload.IsEmpty)
|
||||
return [];
|
||||
|
||||
return Snappy.Encode(payload);
|
||||
return compression switch
|
||||
{
|
||||
EventCompressionType.None => payload.ToArray(),
|
||||
EventCompressionType.Gzip => CompressGzip(payload),
|
||||
EventCompressionType.Snappy => Snappy.Encode(payload),
|
||||
_ => throw new InvalidOperationException($"Unsupported compression type: {compression}."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,11 +99,23 @@ public static class EventCompressor
|
||||
/// <returns>Decompressed bytes. Returns an empty array for empty input.</returns>
|
||||
/// <exception cref="Exception">Propagated from IronSnappy if data is corrupt.</exception>
|
||||
public static byte[] Decompress(ReadOnlySpan<byte> compressed)
|
||||
=> Decompress(compressed, EventCompressionType.Snappy);
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses <paramref name="compressed"/> using the selected <paramref name="compression"/>.
|
||||
/// </summary>
|
||||
public static byte[] Decompress(ReadOnlySpan<byte> compressed, EventCompressionType compression)
|
||||
{
|
||||
if (compressed.IsEmpty)
|
||||
return [];
|
||||
|
||||
return Snappy.Decode(compressed);
|
||||
return compression switch
|
||||
{
|
||||
EventCompressionType.None => compressed.ToArray(),
|
||||
EventCompressionType.Gzip => DecompressGzip(compressed),
|
||||
EventCompressionType.Snappy => Snappy.Decode(compressed),
|
||||
_ => throw new InvalidOperationException($"Unsupported compression type: {compression}."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,6 +145,15 @@ public static class EventCompressor
|
||||
public static (byte[] Data, bool Compressed) CompressIfBeneficial(
|
||||
ReadOnlySpan<byte> payload,
|
||||
int thresholdBytes = DefaultThresholdBytes)
|
||||
=> CompressIfBeneficial(payload, EventCompressionType.Snappy, thresholdBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Compresses using <paramref name="compression"/> when payload size exceeds threshold.
|
||||
/// </summary>
|
||||
public static (byte[] Data, bool Compressed) CompressIfBeneficial(
|
||||
ReadOnlySpan<byte> payload,
|
||||
EventCompressionType compression,
|
||||
int thresholdBytes = DefaultThresholdBytes)
|
||||
{
|
||||
if (!ShouldCompress(payload.Length, thresholdBytes))
|
||||
{
|
||||
@@ -112,7 +161,7 @@ public static class EventCompressor
|
||||
return (payload.ToArray(), false);
|
||||
}
|
||||
|
||||
var compressed = Compress(payload);
|
||||
var compressed = Compress(payload, compression);
|
||||
Interlocked.Increment(ref _totalCompressed);
|
||||
var saved = payload.Length - compressed.Length;
|
||||
if (saved > 0)
|
||||
@@ -135,4 +184,45 @@ public static class EventCompressor
|
||||
|
||||
return (double)compressedSize / originalSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an HTTP Accept-Encoding value into a supported compression type.
|
||||
/// Go reference: events.go getAcceptEncoding().
|
||||
/// </summary>
|
||||
public static EventCompressionType GetAcceptEncoding(string? acceptEncoding)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(acceptEncoding))
|
||||
return EventCompressionType.None;
|
||||
|
||||
var value = acceptEncoding.ToLowerInvariant();
|
||||
if (value.Contains("snappy", StringComparison.Ordinal)
|
||||
|| value.Contains("s2", StringComparison.Ordinal))
|
||||
{
|
||||
return EventCompressionType.Snappy;
|
||||
}
|
||||
|
||||
if (value.Contains("gzip", StringComparison.Ordinal))
|
||||
return EventCompressionType.Gzip;
|
||||
|
||||
return EventCompressionType.Unsupported;
|
||||
}
|
||||
|
||||
private static byte[] CompressGzip(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true))
|
||||
{
|
||||
gzip.Write(payload);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] DecompressGzip(ReadOnlySpan<byte> compressed)
|
||||
{
|
||||
using var input = new MemoryStream(compressed.ToArray());
|
||||
using var gzip = new GZipStream(input, CompressionMode.Decompress);
|
||||
using var output = new MemoryStream();
|
||||
gzip.CopyTo(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ public static class EventSubjects
|
||||
// Remote server and leaf node events
|
||||
public const string RemoteServerShutdown = "$SYS.SERVER.{0}.REMOTE.SHUTDOWN";
|
||||
public const string RemoteServerUpdate = "$SYS.SERVER.{0}.REMOTE.UPDATE";
|
||||
public const string LeafNodeConnected = "$SYS.SERVER.{0}.LEAFNODE.CONNECT";
|
||||
public const string LeafNodeConnected = "$SYS.ACCOUNT.{0}.LEAFNODE.CONNECT";
|
||||
public const string LeafNodeDisconnected = "$SYS.SERVER.{0}.LEAFNODE.DISCONNECT";
|
||||
public const string RemoteLatency = "$SYS.SERVER.{0}.ACC.{1}.LATENCY.M2";
|
||||
|
||||
// Request-reply subjects (server-specific)
|
||||
public const string ServerReq = "$SYS.REQ.SERVER.{0}.{1}";
|
||||
@@ -36,13 +37,22 @@ public static class EventSubjects
|
||||
|
||||
// Account-scoped request subjects
|
||||
public const string AccountReq = "$SYS.REQ.ACCOUNT.{0}.{1}";
|
||||
public const string UserDirectInfo = "$SYS.REQ.USER.INFO";
|
||||
public const string UserDirectReq = "$SYS.REQ.USER.{0}.INFO";
|
||||
public const string AccountNumSubsReq = "$SYS.REQ.ACCOUNT.NSUBS";
|
||||
public const string AccountSubs = "$SYS._INBOX_.{0}.NSUBS";
|
||||
public const string ClientKickReq = "$SYS.REQ.SERVER.{0}.KICK";
|
||||
public const string ClientLdmReq = "$SYS.REQ.SERVER.{0}.LDM";
|
||||
public const string ServerStatsPingReq = "$SYS.REQ.SERVER.PING.STATSZ";
|
||||
public const string ServerReloadReq = "$SYS.REQ.SERVER.{0}.RELOAD";
|
||||
|
||||
// Inbox for responses
|
||||
public const string InboxResponse = "$SYS._INBOX_.{0}";
|
||||
|
||||
// OCSP advisory events
|
||||
// Go reference: ocsp.go — OCSP peer reject and chain validation subjects.
|
||||
public const string OcspPeerReject = "$SYS.SERVER.{0}.OCSP.PEER.REJECT";
|
||||
public const string OcspPeerReject = "$SYS.SERVER.{0}.OCSP.PEER.CONN.REJECT";
|
||||
public const string OcspPeerChainlinkInvalid = "$SYS.SERVER.{0}.OCSP.PEER.LINK.INVALID";
|
||||
public const string OcspChainValidation = "$SYS.SERVER.{0}.OCSP.CHAIN.VALIDATION";
|
||||
|
||||
// JetStream advisory events
|
||||
|
||||
@@ -2,6 +2,35 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace NATS.Server.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Server capability flags.
|
||||
/// Go reference: events.go ServerCapability constants.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ServerCapability : ulong
|
||||
{
|
||||
None = 0,
|
||||
JetStreamEnabled = 1UL << 0,
|
||||
BinaryStreamSnapshot = 1UL << 1,
|
||||
AccountNRG = 1UL << 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server identity response model used by IDZ requests.
|
||||
/// Go reference: events.go ServerID.
|
||||
/// </summary>
|
||||
public sealed class ServerID
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("host")]
|
||||
public string Host { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server identity block embedded in all system events.
|
||||
/// Go reference: events.go:249-265 ServerInfo struct.
|
||||
@@ -53,6 +82,27 @@ public sealed class EventServerInfo
|
||||
[JsonPropertyName("time")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
public void SetJetStreamEnabled()
|
||||
{
|
||||
JetStream = true;
|
||||
Flags |= (ulong)ServerCapability.JetStreamEnabled;
|
||||
}
|
||||
|
||||
public bool JetStreamEnabled() =>
|
||||
(Flags & (ulong)ServerCapability.JetStreamEnabled) != 0;
|
||||
|
||||
public void SetBinaryStreamSnapshot() =>
|
||||
Flags |= (ulong)ServerCapability.BinaryStreamSnapshot;
|
||||
|
||||
public bool BinaryStreamSnapshot() =>
|
||||
(Flags & (ulong)ServerCapability.BinaryStreamSnapshot) != 0;
|
||||
|
||||
public void SetAccountNRG() =>
|
||||
Flags |= (ulong)ServerCapability.AccountNRG;
|
||||
|
||||
public bool AccountNRG() =>
|
||||
(Flags & (ulong)ServerCapability.AccountNRG) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -536,10 +586,66 @@ public sealed class OcspPeerRejectEventMsg
|
||||
[JsonPropertyName("server")]
|
||||
public EventServerInfo Server { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("peer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EventCertInfo? Peer { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Certificate identity block used by OCSP peer advisories.
|
||||
/// Go reference: certidp.CertInfo payload embedded in events.go OCSP messages.
|
||||
/// </summary>
|
||||
public sealed class EventCertInfo
|
||||
{
|
||||
[JsonPropertyName("subject")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("issuer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("fingerprint")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Fingerprint { get; set; }
|
||||
|
||||
[JsonPropertyName("raw")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Raw { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCSP chain-link invalid advisory.
|
||||
/// Go reference: events.go OCSPPeerChainlinkInvalidEventMsg.
|
||||
/// </summary>
|
||||
public sealed class OcspPeerChainlinkInvalidEventMsg
|
||||
{
|
||||
public const string EventType = "io.nats.server.advisory.v1.ocsp_peer_link_invalid";
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = EventType;
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[JsonPropertyName("server")]
|
||||
public EventServerInfo Server { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("link")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EventCertInfo? Link { get; set; }
|
||||
|
||||
[JsonPropertyName("peer")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EventCertInfo? Peer { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCSP chain validation advisory, published when a certificate's OCSP status
|
||||
/// is checked during TLS handshake.
|
||||
@@ -803,6 +909,149 @@ public sealed class AccNumConnsReq
|
||||
public string Account { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Account subscription-count request.
|
||||
/// Go reference: events.go accNumSubsReq.
|
||||
/// </summary>
|
||||
public sealed class AccNumSubsReq
|
||||
{
|
||||
[JsonPropertyName("server")]
|
||||
public EventServerInfo Server { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("acc")]
|
||||
public string Account { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared request filter options for system request subjects.
|
||||
/// Go reference: events.go EventFilterOptions.
|
||||
/// </summary>
|
||||
public class EventFilterOptions
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("cluster")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Cluster { get; set; }
|
||||
|
||||
[JsonPropertyName("host")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Host { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string[]? Tags { get; set; }
|
||||
|
||||
[JsonPropertyName("domain")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Domain { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StatszEventOptions : EventFilterOptions;
|
||||
public sealed class AccInfoEventOptions : EventFilterOptions;
|
||||
public sealed class ConnzEventOptions : EventFilterOptions;
|
||||
public sealed class RoutezEventOptions : EventFilterOptions;
|
||||
public sealed class SubszEventOptions : EventFilterOptions;
|
||||
public sealed class VarzEventOptions : EventFilterOptions;
|
||||
public sealed class GatewayzEventOptions : EventFilterOptions;
|
||||
public sealed class LeafzEventOptions : EventFilterOptions;
|
||||
public sealed class AccountzEventOptions : EventFilterOptions;
|
||||
public sealed class AccountStatzEventOptions : EventFilterOptions;
|
||||
public sealed class JszEventOptions : EventFilterOptions;
|
||||
public sealed class HealthzEventOptions : EventFilterOptions;
|
||||
public sealed class ProfilezEventOptions : EventFilterOptions;
|
||||
public sealed class ExpvarzEventOptions : EventFilterOptions;
|
||||
public sealed class IpqueueszEventOptions : EventFilterOptions;
|
||||
public sealed class RaftzEventOptions : EventFilterOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Generic server API error payload.
|
||||
/// Go reference: events.go server API response errors.
|
||||
/// </summary>
|
||||
public sealed class ServerAPIError
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic server request/response envelope for $SYS.REQ services.
|
||||
/// Go reference: events.go ServerAPIResponse.
|
||||
/// </summary>
|
||||
public class ServerAPIResponse
|
||||
{
|
||||
[JsonPropertyName("server")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EventServerInfo? Server { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public object? Data { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ServerAPIError? Error { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ServerAPIConnzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIRoutezResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIGatewayzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIJszResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIHealthzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIVarzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPISubszResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPILeafzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIAccountzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIExpvarzResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIpqueueszResponse : ServerAPIResponse;
|
||||
public sealed class ServerAPIRaftzResponse : ServerAPIResponse;
|
||||
|
||||
/// <summary>
|
||||
/// Kick client request payload.
|
||||
/// Go reference: events.go KickClientReq.
|
||||
/// </summary>
|
||||
public sealed class KickClientReq
|
||||
{
|
||||
[JsonPropertyName("cid")]
|
||||
public ulong ClientId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lame duck mode client request payload.
|
||||
/// Go reference: events.go LDMClientReq.
|
||||
/// </summary>
|
||||
public sealed class LDMClientReq
|
||||
{
|
||||
[JsonPropertyName("cid")]
|
||||
public ulong ClientId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User info payload for direct user info requests.
|
||||
/// Go reference: events.go UserInfo.
|
||||
/// </summary>
|
||||
public sealed class UserInfo
|
||||
{
|
||||
[JsonPropertyName("user")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? User { get; set; }
|
||||
|
||||
[JsonPropertyName("acc")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Account { get; set; }
|
||||
|
||||
[JsonPropertyName("permissions")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Permissions { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory helpers that construct fully-populated system event messages,
|
||||
/// mirroring Go's inline struct initialization patterns in events.go.
|
||||
|
||||
@@ -61,7 +61,14 @@ public sealed record ConnectEventDetail(
|
||||
string RemoteAddress,
|
||||
string? AccountName,
|
||||
string? UserName,
|
||||
DateTime ConnectedAt);
|
||||
DateTime ConnectedAt,
|
||||
string? Jwt = null,
|
||||
string? IssuerKey = null,
|
||||
string[]? Tags = null,
|
||||
string? NameTag = null,
|
||||
string? Kind = null,
|
||||
string? ClientType = null,
|
||||
string? MqttClientId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Detail payload for a client disconnect advisory.
|
||||
@@ -73,7 +80,15 @@ public sealed record DisconnectEventDetail(
|
||||
string RemoteAddress,
|
||||
string? AccountName,
|
||||
string Reason,
|
||||
DateTime DisconnectedAt);
|
||||
DateTime DisconnectedAt,
|
||||
long RttNanos = 0,
|
||||
string? Jwt = null,
|
||||
string? IssuerKey = null,
|
||||
string[]? Tags = null,
|
||||
string? NameTag = null,
|
||||
string? Kind = null,
|
||||
string? ClientType = null,
|
||||
string? MqttClientId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Manages the server's internal event system with Channel-based send/receive loops.
|
||||
@@ -114,14 +129,29 @@ public sealed class InternalEventSystem : IAsyncDisposable
|
||||
SystemAccount = systemAccount;
|
||||
SystemClient = systemClient;
|
||||
|
||||
// Hash server name for inbox routing (matches Go's shash)
|
||||
ServerHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(serverName)))[..8].ToLowerInvariant();
|
||||
// Hash server name for inbox routing (matches Go's shash).
|
||||
ServerHash = GetHash(serverName, GetHashSize());
|
||||
|
||||
_sendQueue = Channel.CreateUnbounded<PublishMessage>(new UnboundedChannelOptions { SingleReader = true });
|
||||
_receiveQueue = Channel.CreateUnbounded<InternalSystemMessage>(new UnboundedChannelOptions { SingleReader = true });
|
||||
_receiveQueuePings = Channel.CreateUnbounded<InternalSystemMessage>(new UnboundedChannelOptions { SingleReader = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to Go getHashSize(): default short hash width used in eventing subjects.
|
||||
/// </summary>
|
||||
public static int GetHashSize() => 8;
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to Go getHash() / getHashSize() helpers for server hash identifiers.
|
||||
/// </summary>
|
||||
public static string GetHash(string value, int size)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
|
||||
var full = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
|
||||
return size >= full.Length ? full : full[..size];
|
||||
}
|
||||
|
||||
public void Start(NatsServer server)
|
||||
{
|
||||
_server = server;
|
||||
@@ -316,6 +346,13 @@ public sealed class InternalEventSystem : IAsyncDisposable
|
||||
Account = detail.AccountName,
|
||||
User = detail.UserName,
|
||||
Start = detail.ConnectedAt,
|
||||
Jwt = detail.Jwt,
|
||||
IssuerKey = detail.IssuerKey,
|
||||
Tags = detail.Tags,
|
||||
NameTag = detail.NameTag,
|
||||
Kind = detail.Kind,
|
||||
ClientType = detail.ClientType,
|
||||
MqttClient = detail.MqttClientId,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -341,6 +378,14 @@ public sealed class InternalEventSystem : IAsyncDisposable
|
||||
Host = detail.RemoteAddress,
|
||||
Account = detail.AccountName,
|
||||
Stop = detail.DisconnectedAt,
|
||||
RttNanos = detail.RttNanos,
|
||||
Jwt = detail.Jwt,
|
||||
IssuerKey = detail.IssuerKey,
|
||||
Tags = detail.Tags,
|
||||
NameTag = detail.NameTag,
|
||||
Kind = detail.Kind,
|
||||
ClientType = detail.ClientType,
|
||||
MqttClient = detail.MqttClientId,
|
||||
},
|
||||
Reason = detail.Reason,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user