From 8050ee18974b068bd63e825066055c1fb4a3d39c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 09:15:20 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20session=2001=20=E2=80=94=20Found?= =?UTF-8?q?ation=20Types=20(const,=20errors,=20proto,=20ring,=20rate=5Fcou?= =?UTF-8?q?nter,=20sdm)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports server/const.go, errors.go, proto.go, ring.go, rate_counter.go, sdm.go. - ServerConstants: all protocol constants and version info from const.go - ServerErrors: ~60 sentinel exceptions plus errCtx/configErr/processConfigErr types - ProtoWire: protobuf varint encode/decode helpers (proto.go) - RateCounter: sliding-window rate limiter (rate_counter.go) - ClosedRingBuffer: fixed-size ring buffer for /connz (ring.go) - StreamDeletionMeta: SDM tracking for JetStream cluster consensus (sdm.go) - 5 unit tests passing (errors, ring buffer, rate counter) - errors_gen.go (code generator tool) and nkey.go Server methods marked n_a --- .../Internal/ClosedRingBuffer.cs | 118 +++++ .../DataStructures/StreamDeletionMeta.cs | 121 +++++ .../Internal/ProtoWire.cs | 284 +++++++++++ .../Internal/RateCounter.cs | 81 ++++ .../ZB.MOM.NatsNet.Server/ServerConstants.cs | 228 +++++++++ .../src/ZB.MOM.NatsNet.Server/ServerErrors.cs | 446 ++++++++++++++++++ .../Foundation/ServerErrorsTests.cs | 87 ++++ .../Internal/ClosedRingBufferTests.cs | 79 ++++ .../Internal/RateCounterTests.cs | 47 ++ porting.db | Bin 2453504 -> 2461696 bytes reports/current.md | 19 +- reports/report_66628bc.md | 37 ++ 12 files changed, 1538 insertions(+), 9 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs create mode 100644 reports/report_66628bc.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs new file mode 100644 index 0000000..09109be --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs @@ -0,0 +1,118 @@ +// Copyright 2018-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/ring.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server.Internal; + +// ------------------------------------------------------------------------- +// Placeholder types — will be populated in session 08 (Client) and +// session 12 (Monitor/Events). +// ------------------------------------------------------------------------- + +/// +/// Subset of client connection info exposed on the monitoring endpoint (/connz). +/// Full implementation is in session 12 (monitor.go). +/// +public class ConnInfo { } + +/// +/// Subscription detail for the monitoring endpoint. +/// Full implementation is in session 12 (monitor.go). +/// +public class SubDetail { } + +// ------------------------------------------------------------------------- +// ClosedClient / ClosedRingBuffer (ring.go) +// ------------------------------------------------------------------------- + +/// +/// Wraps connection info with optional items for the /connz endpoint. +/// Mirrors closedClient in server/ring.go. +/// +public sealed class ClosedClient +{ + public ConnInfo Info { get; init; } = new(); + public IReadOnlyList Subs { get; init; } = []; + public string User { get; init; } = string.Empty; + public string Account { get; init; } = string.Empty; +} + +/// +/// Fixed-size ring buffer that retains the most recent closed connections, +/// evicting the oldest when full. +/// Mirrors closedRingBuffer in server/ring.go. +/// +public sealed class ClosedRingBuffer +{ + private ulong _total; + private readonly ClosedClient?[] _conns; + + /// Creates a ring buffer that holds at most entries. + public ClosedRingBuffer(int max) + { + _conns = new ClosedClient?[max]; + } + + /// + /// Appends a closed connection, evicting the oldest if the buffer is full. + /// Mirrors closedRingBuffer.append. + /// + public void Append(ClosedClient cc) + { + _conns[Next()] = cc; + _total++; + } + + // Index of the slot to write next — wraps around. + private int Next() => (int)(_total % (ulong)_conns.Length); + + /// + /// Returns the number of entries currently stored (≤ capacity). + /// Mirrors closedRingBuffer.len. + /// + public int Len() => + _total > (ulong)_conns.Length ? _conns.Length : (int)_total; + + /// + /// Returns the total number of connections ever appended (not capped). + /// Mirrors closedRingBuffer.totalConns. + /// + public ulong TotalConns() => _total; + + /// + /// Returns a chronologically ordered copy of the stored closed connections. + /// The caller may freely modify the returned array. + /// Mirrors closedRingBuffer.closedClients. + /// + public ClosedClient?[] ClosedClients() + { + var len = Len(); + var dup = new ClosedClient?[len]; + var head = Next(); + + if (_total <= (ulong)_conns.Length || head == 0) + { + Array.Copy(_conns, dup, len); + } + else + { + var fp = _conns.AsSpan(head); // oldest … end of array + var sp = _conns.AsSpan(0, head); // wrap-around … newest + fp.CopyTo(dup.AsSpan()); + sp.CopyTo(dup.AsSpan(fp.Length)); + } + + return dup; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs new file mode 100644 index 0000000..4e7af96 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs @@ -0,0 +1,121 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/sdm.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server.Internal.DataStructures; + +/// +/// Per-sequence data tracked by . +/// Mirrors SDMBySeq in server/sdm.go. +/// +public readonly struct SdmBySeq +{ + /// Whether this sequence was the last message for its subject. + public bool Last { get; init; } + + /// Timestamp (nanoseconds UTC) when the removal/SDM was last proposed. + public long Ts { get; init; } +} + +/// +/// Tracks pending subject delete markers (SDMs) and message removals for a stream. +/// Used by JetStream cluster consensus to avoid redundant proposals. +/// Mirrors SDMMeta in server/sdm.go. +/// +public sealed class StreamDeletionMeta +{ + // Per-subject pending-count totals. + private readonly Dictionary _totals = new(1); + + // Per-sequence data keyed by sequence number. + private readonly Dictionary _pending = new(1); + + // ------------------------------------------------------------------------- + // Header constants (forward-declared; populated in session 19 — JetStream). + // isSubjectDeleteMarker checks these header keys. + // ------------------------------------------------------------------------- + + // Mirrors JSMarkerReason header key (defined in jetstream.go). + internal const string HeaderJsMarkerReason = "Nats-Marker-Reason"; + + // Mirrors KVOperation header key (defined in jetstream.go). + internal const string HeaderKvOperation = "KV-Operation"; + + // Mirrors KVOperationValuePurge (defined in jetstream.go). + internal const string KvOperationValuePurge = "PURGE"; + + /// + /// Returns true when the given header block contains a subject delete marker + /// (either a JetStream marker or a KV purge operation). + /// Mirrors isSubjectDeleteMarker in server/sdm.go. + /// + public static bool IsSubjectDeleteMarker(ReadOnlySpan hdr) + { + // Simplified header scan: checks whether JSMarkerReason key is present + // or whether KV-Operation equals "PURGE". + // Full implementation depends on SliceHeader from session 08 (client.go). + // Until then this provides the correct contract. + var text = System.Text.Encoding.UTF8.GetString(hdr); + if (text.Contains(HeaderJsMarkerReason)) + return true; + if (text.Contains($"{HeaderKvOperation}: {KvOperationValuePurge}")) + return true; + return false; + } + + /// + /// Clears all tracked data. + /// Mirrors SDMMeta.empty. + /// + public void Empty() + { + _totals.Clear(); + _pending.Clear(); + } + + /// + /// Tracks as pending and returns whether it was + /// the last message for its subject. If the sequence is already tracked + /// the existing Last value is returned without modification. + /// Mirrors SDMMeta.trackPending. + /// + public bool TrackPending(ulong seq, string subj, bool last) + { + if (_pending.TryGetValue(seq, out var p)) + return p.Last; + + _pending[seq] = new SdmBySeq { Last = last, Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L }; + _totals[subj] = _totals.TryGetValue(subj, out var cnt) ? cnt + 1 : 1; + return last; + } + + /// + /// Removes and decrements the pending count for + /// , deleting the subject entry when it reaches zero. + /// Mirrors SDMMeta.removeSeqAndSubject. + /// + public void RemoveSeqAndSubject(ulong seq, string subj) + { + if (!_pending.Remove(seq)) + return; + + if (_totals.TryGetValue(subj, out var msgs)) + { + if (msgs <= 1) + _totals.Remove(subj); + else + _totals[subj] = msgs - 1; + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs new file mode 100644 index 0000000..22eaff2 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs @@ -0,0 +1,284 @@ +// Copyright 2024 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/proto.go in the NATS server Go source. +// Inspired by https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protowire/wire.go + +namespace ZB.MOM.NatsNet.Server.Internal; + +/// +/// Low-level protobuf wire format helpers used internally for JetStream API encoding. +/// Mirrors server/proto.go. +/// +public static class ProtoWire +{ + private static readonly Exception ErrInsufficient = + new InvalidOperationException("insufficient data to read a value"); + + private static readonly Exception ErrOverflow = + new InvalidOperationException("too much data for a value"); + + private static readonly Exception ErrInvalidFieldNumber = + new InvalidOperationException("invalid field number"); + + // ------------------------------------------------------------------------- + // Field scanning + // ------------------------------------------------------------------------- + + /// + /// Reads a complete protobuf field (tag + value) from + /// and returns the field number, wire type, and total byte size consumed. + /// Mirrors protoScanField. + /// + public static (int num, int typ, int size, Exception? err) ScanField(ReadOnlySpan b) + { + var (num, typ, sizeTag, err) = ScanTag(b); + if (err != null) return (0, 0, 0, err); + + var (sizeValue, err2) = ScanFieldValue(typ, b[sizeTag..]); + if (err2 != null) return (0, 0, 0, err2); + + return (num, typ, sizeTag + sizeValue, null); + } + + /// + /// Reads a protobuf tag varint and returns field number, wire type, and bytes consumed. + /// Mirrors protoScanTag. + /// + public static (int num, int typ, int size, Exception? err) ScanTag(ReadOnlySpan b) + { + var (tagint, size, err) = ScanVarint(b); + if (err != null) return (0, 0, 0, err); + + if ((tagint >> 3) > int.MaxValue) + return (0, 0, 0, ErrInvalidFieldNumber); + + var num = (int)(tagint >> 3); + if (num < 1) + return (0, 0, 0, ErrInvalidFieldNumber); + + var typ = (int)(tagint & 7); + return (num, typ, size, null); + } + + /// + /// Returns the byte count consumed by a field value with the given wire type. + /// Mirrors protoScanFieldValue. + /// + public static (int size, Exception? err) ScanFieldValue(int typ, ReadOnlySpan b) + { + switch (typ) + { + case 0: // varint + { + var (_, size, err) = ScanVarint(b); + return (size, err); + } + case 5: // fixed32 + return (4, null); + case 1: // fixed64 + return (8, null); + case 2: // length-delimited + { + var (size, err) = ScanBytes(b); + return (size, err); + } + default: + return (0, new InvalidOperationException($"unsupported type: {typ}")); + } + } + + // ------------------------------------------------------------------------- + // Varint decode + // ------------------------------------------------------------------------- + + /// + /// Decodes a protobuf varint from . + /// Returns (value, bytes_consumed, error). + /// Mirrors protoScanVarint. + /// + public static (ulong v, int size, Exception? err) ScanVarint(ReadOnlySpan b) + { + if (b.Length < 1) return (0, 0, ErrInsufficient); + ulong v = b[0]; + if (v < 0x80) return (v, 1, null); + v -= 0x80; + + if (b.Length < 2) return (0, 0, ErrInsufficient); + ulong y = b[1]; + v += y << 7; + if (y < 0x80) return (v, 2, null); + v -= 0x80UL << 7; + + if (b.Length < 3) return (0, 0, ErrInsufficient); + y = b[2]; + v += y << 14; + if (y < 0x80) return (v, 3, null); + v -= 0x80UL << 14; + + if (b.Length < 4) return (0, 0, ErrInsufficient); + y = b[3]; + v += y << 21; + if (y < 0x80) return (v, 4, null); + v -= 0x80UL << 21; + + if (b.Length < 5) return (0, 0, ErrInsufficient); + y = b[4]; + v += y << 28; + if (y < 0x80) return (v, 5, null); + v -= 0x80UL << 28; + + if (b.Length < 6) return (0, 0, ErrInsufficient); + y = b[5]; + v += y << 35; + if (y < 0x80) return (v, 6, null); + v -= 0x80UL << 35; + + if (b.Length < 7) return (0, 0, ErrInsufficient); + y = b[6]; + v += y << 42; + if (y < 0x80) return (v, 7, null); + v -= 0x80UL << 42; + + if (b.Length < 8) return (0, 0, ErrInsufficient); + y = b[7]; + v += y << 49; + if (y < 0x80) return (v, 8, null); + v -= 0x80UL << 49; + + if (b.Length < 9) return (0, 0, ErrInsufficient); + y = b[8]; + v += y << 56; + if (y < 0x80) return (v, 9, null); + v -= 0x80UL << 56; + + if (b.Length < 10) return (0, 0, ErrInsufficient); + y = b[9]; + v += y << 63; + if (y < 2) return (v, 10, null); + + return (0, 0, ErrOverflow); + } + + // ------------------------------------------------------------------------- + // Length-delimited decode + // ------------------------------------------------------------------------- + + /// + /// Returns the total byte count consumed by a length-delimited field + /// (length varint + content). + /// Mirrors protoScanBytes. + /// + public static (int size, Exception? err) ScanBytes(ReadOnlySpan b) + { + var (l, lenSize, err) = ScanVarint(b); + if (err != null) return (0, err); + + if (l > (ulong)(b.Length - lenSize)) + return (0, ErrInsufficient); + + return (lenSize + (int)l, null); + } + + // ------------------------------------------------------------------------- + // Varint encode + // ------------------------------------------------------------------------- + + /// + /// Encodes a as a protobuf varint. + /// Mirrors protoEncodeVarint. + /// + public static byte[] EncodeVarint(ulong v) + { + if (v < 1UL << 7) + return [(byte)v]; + + if (v < 1UL << 14) + return [(byte)((v >> 0) & 0x7F | 0x80), (byte)(v >> 7)]; + + if (v < 1UL << 21) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)(v >> 14)]; + + if (v < 1UL << 28) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)(v >> 21)]; + + if (v < 1UL << 35) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)(v >> 28)]; + + if (v < 1UL << 42) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)((v >> 28) & 0x7F | 0x80), + (byte)(v >> 35)]; + + if (v < 1UL << 49) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)((v >> 28) & 0x7F | 0x80), + (byte)((v >> 35) & 0x7F | 0x80), + (byte)(v >> 42)]; + + if (v < 1UL << 56) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)((v >> 28) & 0x7F | 0x80), + (byte)((v >> 35) & 0x7F | 0x80), + (byte)((v >> 42) & 0x7F | 0x80), + (byte)(v >> 49)]; + + if (v < 1UL << 63) + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)((v >> 28) & 0x7F | 0x80), + (byte)((v >> 35) & 0x7F | 0x80), + (byte)((v >> 42) & 0x7F | 0x80), + (byte)((v >> 49) & 0x7F | 0x80), + (byte)(v >> 56)]; + + return [ + (byte)((v >> 0) & 0x7F | 0x80), + (byte)((v >> 7) & 0x7F | 0x80), + (byte)((v >> 14) & 0x7F | 0x80), + (byte)((v >> 21) & 0x7F | 0x80), + (byte)((v >> 28) & 0x7F | 0x80), + (byte)((v >> 35) & 0x7F | 0x80), + (byte)((v >> 42) & 0x7F | 0x80), + (byte)((v >> 49) & 0x7F | 0x80), + (byte)((v >> 56) & 0x7F | 0x80), + 1]; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs new file mode 100644 index 0000000..e1eec15 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs @@ -0,0 +1,81 @@ +// Copyright 2021-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/rate_counter.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server.Internal; + +/// +/// A sliding-window rate limiter that allows at most limit events +/// per window. +/// Mirrors rateCounter in server/rate_counter.go. +/// +public sealed class RateCounter +{ + private readonly long _limit; + private long _count; + private ulong _blocked; + private DateTime _end; + + // Exposed for tests (mirrors direct field access in rate_counter_test.go). + public TimeSpan Interval; + + private readonly object _lock = new(); + + public RateCounter(long limit) + { + _limit = limit; + Interval = TimeSpan.FromSeconds(1); + } + + /// + /// Returns true if the event is within the rate limit for the current window. + /// Mirrors rateCounter.allow. + /// + public bool Allow() + { + var now = DateTime.UtcNow; + + lock (_lock) + { + if (now > _end) + { + _count = 0; + _end = now + Interval; + } + else + { + _count++; + } + + var allow = _count < _limit; + if (!allow) + _blocked++; + return allow; + } + } + + /// + /// Returns and resets the count of blocked events since the last call. + /// Mirrors rateCounter.countBlocked. + /// + public ulong CountBlocked() + { + lock (_lock) + { + var blocked = _blocked; + _blocked = 0; + return blocked; + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs new file mode 100644 index 0000000..40e8a45 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs @@ -0,0 +1,228 @@ +// Copyright 2012-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/const.go in the NATS server Go source. + +using System.Reflection; + +namespace ZB.MOM.NatsNet.Server; + +/// +/// Server-wide constants and version information. +/// Mirrors server/const.go. +/// +public static class ServerConstants +{ + // Server version — mirrors VERSION in const.go. + public const string Version = "2.14.0-dev"; + + // Protocol version — mirrors PROTO in const.go. + public const int Proto = 1; + + // Default port for client connections — mirrors DEFAULT_PORT. + public const int DefaultPort = 4222; + + // Sentinel port value that triggers a random port selection — mirrors RANDOM_PORT. + public const int RandomPort = -1; + + // Default bind address — mirrors DEFAULT_HOST. + public const string DefaultHost = "0.0.0.0"; + + // Maximum allowed control line size (4 KB) — mirrors MAX_CONTROL_LINE_SIZE. + public const int MaxControlLineSize = 4096; + + // Maximum allowed payload size (1 MB) — mirrors MAX_PAYLOAD_SIZE. + public const int MaxPayloadSize = 1024 * 1024; + + // Payload size above which the server warns — mirrors MAX_PAYLOAD_MAX_SIZE. + public const int MaxPayloadMaxSize = 8 * 1024 * 1024; + + // Maximum outbound pending bytes per client (64 MB) — mirrors MAX_PENDING_SIZE. + public const int MaxPendingSize = 64 * 1024 * 1024; + + // Default maximum connections allowed (64 K) — mirrors DEFAULT_MAX_CONNECTIONS. + public const int DefaultMaxConnections = 64 * 1024; + + // TLS handshake timeout — mirrors TLS_TIMEOUT. + public static readonly TimeSpan TlsTimeout = TimeSpan.FromSeconds(2); + + // Fallback delay before sending INFO when using TLSHandshakeFirst + // — mirrors DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY. + public static readonly TimeSpan DefaultTlsHandshakeFirstFallbackDelay = TimeSpan.FromMilliseconds(50); + + // Auth timeout — mirrors AUTH_TIMEOUT. + public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2); + + // How often pings are sent — mirrors DEFAULT_PING_INTERVAL. + public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2); + + // Maximum pings outstanding before disconnect — mirrors DEFAULT_PING_MAX_OUT. + public const int DefaultPingMaxOut = 2; + + // CR LF end-of-line — mirrors CR_LF. + public const string CrLf = "\r\n"; + + // Length of CR_LF — mirrors LEN_CR_LF. + public const int LenCrLf = 2; + + // Write/flush deadline — mirrors DEFAULT_FLUSH_DEADLINE. + public static readonly TimeSpan DefaultFlushDeadline = TimeSpan.FromSeconds(10); + + // Default monitoring port — mirrors DEFAULT_HTTP_PORT. + public const int DefaultHttpPort = 8222; + + // Default monitoring base path — mirrors DEFAULT_HTTP_BASE_PATH. + public const string DefaultHttpBasePath = "/"; + + // Minimum sleep on temporary accept errors — mirrors ACCEPT_MIN_SLEEP. + public static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10); + + // Maximum sleep on temporary accept errors — mirrors ACCEPT_MAX_SLEEP. + public static readonly TimeSpan AcceptMaxSleep = TimeSpan.FromSeconds(1); + + // Route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT. + public static readonly TimeSpan DefaultRouteConnect = TimeSpan.FromSeconds(1); + + // Maximum route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT_MAX. + public static readonly TimeSpan DefaultRouteConnectMax = TimeSpan.FromSeconds(30); + + // Route reconnect delay — mirrors DEFAULT_ROUTE_RECONNECT. + public static readonly TimeSpan DefaultRouteReconnect = TimeSpan.FromSeconds(1); + + // Route dial timeout — mirrors DEFAULT_ROUTE_DIAL. + public static readonly TimeSpan DefaultRouteDial = TimeSpan.FromSeconds(1); + + // Default route pool size — mirrors DEFAULT_ROUTE_POOL_SIZE. + public const int DefaultRoutePoolSize = 3; + + // LeafNode reconnect interval — mirrors DEFAULT_LEAF_NODE_RECONNECT. + public static readonly TimeSpan DefaultLeafNodeReconnect = TimeSpan.FromSeconds(1); + + // TLS timeout for leaf nodes — mirrors DEFAULT_LEAF_TLS_TIMEOUT. + public static readonly TimeSpan DefaultLeafTlsTimeout = TimeSpan.FromSeconds(2); + + // Proto snippet size for parse error display — mirrors PROTO_SNIPPET_SIZE. + public const int ProtoSnippetSize = 32; + + // Max control line snippet size for error display — mirrors MAX_CONTROL_LINE_SNIPPET_SIZE. + public const int MaxControlLineSnippetSize = 128; + + // Maximum MSG proto argument count — mirrors MAX_MSG_ARGS. + public const int MaxMsgArgs = 4; + + // Maximum RMSG proto argument count — mirrors MAX_RMSG_ARGS. + public const int MaxRMsgArgs = 6; + + // Maximum HMSG proto argument count — mirrors MAX_HMSG_ARGS. + public const int MaxHMsgArgs = 7; + + // Maximum PUB proto argument count — mirrors MAX_PUB_ARGS. + public const int MaxPubArgs = 3; + + // Maximum HPUB proto argument count — mirrors MAX_HPUB_ARGS. + public const int MaxHPubArgs = 4; + + // Maximum RS+/LS+ proto argument count — mirrors MAX_RSUB_ARGS. + public const int MaxRSubArgs = 6; + + // Maximum closed connections retained — mirrors DEFAULT_MAX_CLOSED_CLIENTS. + public const int DefaultMaxClosedClients = 10000; + + // Lame duck spread duration — mirrors DEFAULT_LAME_DUCK_DURATION. + public static readonly TimeSpan DefaultLameDuckDuration = TimeSpan.FromMinutes(2); + + // Lame duck grace period — mirrors DEFAULT_LAME_DUCK_GRACE_PERIOD. + public static readonly TimeSpan DefaultLameDuckGracePeriod = TimeSpan.FromSeconds(10); + + // Leaf node INFO wait — mirrors DEFAULT_LEAFNODE_INFO_WAIT. + public static readonly TimeSpan DefaultLeafNodeInfoWait = TimeSpan.FromSeconds(1); + + // Default leaf node port — mirrors DEFAULT_LEAFNODE_PORT. + public const int DefaultLeafNodePort = 7422; + + // Connect error report threshold — mirrors DEFAULT_CONNECT_ERROR_REPORTS. + public const int DefaultConnectErrorReports = 3600; + + // Reconnect error report threshold — mirrors DEFAULT_RECONNECT_ERROR_REPORTS. + public const int DefaultReconnectErrorReports = 1; + + // RTT measurement interval — mirrors DEFAULT_RTT_MEASUREMENT_INTERVAL. + public static readonly TimeSpan DefaultRttMeasurementInterval = TimeSpan.FromHours(1); + + // Default allowed response max messages — mirrors DEFAULT_ALLOW_RESPONSE_MAX_MSGS. + public const int DefaultAllowResponseMaxMsgs = 1; + + // Default allowed response expiration — mirrors DEFAULT_ALLOW_RESPONSE_EXPIRATION. + public static readonly TimeSpan DefaultAllowResponseExpiration = TimeSpan.FromMinutes(2); + + // Default service export response threshold — mirrors DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD. + public static readonly TimeSpan DefaultServiceExportResponseThreshold = TimeSpan.FromMinutes(2); + + // Default service latency sampling rate — mirrors DEFAULT_SERVICE_LATENCY_SAMPLING. + public const int DefaultServiceLatencySampling = 100; + + // Default system account name — mirrors DEFAULT_SYSTEM_ACCOUNT. + public const string DefaultSystemAccount = "$SYS"; + + // Default global account name — mirrors DEFAULT_GLOBAL_ACCOUNT. + public const string DefaultGlobalAccount = "$G"; + + // Default account fetch timeout — mirrors DEFAULT_ACCOUNT_FETCH_TIMEOUT. + public static readonly TimeSpan DefaultAccountFetchTimeout = TimeSpan.FromMilliseconds(1900); + + // VCS commit hash embedded at build time, shortened to 7 chars — mirrors gitCommit. + // Populated from AssemblyInformationalVersion metadata if available. + public static readonly string GitCommit; + + static ServerConstants() + { + // Mirror const.go init(): extract VCS revision from build info. + // In .NET we read the AssemblyInformationalVersion attribute which + // is typically set to the semantic version + commit hash by dotnet publish. + var infoVersion = typeof(ServerConstants).Assembly + .GetCustomAttribute() + ?.InformationalVersion; + + if (infoVersion != null) + { + // Convention: "1.2.3+abcdefg" or "1.2.3-dev+abcdefg" + var plusIdx = infoVersion.IndexOf('+'); + if (plusIdx >= 0) + { + var rev = infoVersion[(plusIdx + 1)..]; + GitCommit = FormatRevision(rev); + return; + } + } + + GitCommit = string.Empty; + } + + /// + /// Truncates a VCS revision string to 7 characters for display. + /// Mirrors formatRevision in const.go. + /// + public static string FormatRevision(string revision) => + revision.Length >= 7 ? revision[..7] : revision; +} + +/// +/// Server control commands — mirrors the Command type in const.go. +/// +public enum ServerCommand +{ + Stop, + Quit, + Reopen, + Reload, +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs new file mode 100644 index 0000000..2558140 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs @@ -0,0 +1,446 @@ +// Copyright 2012-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/errors.go in the NATS server Go source. +// Note: errorsUnwrap() and ErrorIs() are Go 1.12 compat shims — .NET has +// Exception.InnerException and errors.Is() equivalents built in; +// both are represented here for completeness but map to built-in .NET patterns. + +namespace ZB.MOM.NatsNet.Server; + +// ------------------------------------------------------------------------- +// Sentinel error values (mirrors package-level var block in errors.go) +// ------------------------------------------------------------------------- + +/// +/// Standard NATS server error sentinels. +/// Mirrors the package-level var error block in server/errors.go. +/// +public static class ServerErrors +{ + public static readonly Exception ErrConnectionClosed = + new InvalidOperationException("connection closed"); + + public static readonly Exception ErrAuthentication = + new InvalidOperationException("authentication error"); + + public static readonly Exception ErrAuthTimeout = + new InvalidOperationException("authentication timeout"); + + public static readonly Exception ErrAuthExpired = + new InvalidOperationException("authentication expired"); + + public static readonly Exception ErrAuthProxyNotTrusted = + new InvalidOperationException("proxy is not trusted"); + + public static readonly Exception ErrAuthProxyRequired = + new InvalidOperationException("proxy connection required"); + + public static readonly Exception ErrMaxPayload = + new InvalidOperationException("maximum payload exceeded"); + + public static readonly Exception ErrMaxControlLine = + new InvalidOperationException("maximum control line exceeded"); + + public static readonly Exception ErrReservedPublishSubject = + new InvalidOperationException("reserved internal subject"); + + public static readonly Exception ErrBadPublishSubject = + new InvalidOperationException("invalid publish subject"); + + public static readonly Exception ErrBadSubject = + new InvalidOperationException("invalid subject"); + + public static readonly Exception ErrBadQualifier = + new InvalidOperationException("bad qualifier"); + + public static readonly Exception ErrBadClientProtocol = + new InvalidOperationException("invalid client protocol"); + + public static readonly Exception ErrTooManyConnections = + new InvalidOperationException("maximum connections exceeded"); + + public static readonly Exception ErrTooManyAccountConnections = + new InvalidOperationException("maximum account active connections exceeded"); + + public static readonly Exception ErrLeafNodeLoop = + new InvalidOperationException("leafnode loop detected"); + + public static readonly Exception ErrTooManySubs = + new InvalidOperationException("maximum subscriptions exceeded"); + + public static readonly Exception ErrTooManySubTokens = + new InvalidOperationException("subject has exceeded number of tokens limit"); + + public static readonly Exception ErrClientConnectedToRoutePort = + new InvalidOperationException("attempted to connect to route port"); + + public static readonly Exception ErrClientConnectedToLeafNodePort = + new InvalidOperationException("attempted to connect to leaf node port"); + + public static readonly Exception ErrLeafNodeHasSameClusterName = + new InvalidOperationException("remote leafnode has same cluster name"); + + public static readonly Exception ErrLeafNodeDisabled = + new InvalidOperationException("leafnodes disabled"); + + public static readonly Exception ErrConnectedToWrongPort = + new InvalidOperationException("attempted to connect to wrong port"); + + public static readonly Exception ErrAccountExists = + new InvalidOperationException("account exists"); + + public static readonly Exception ErrBadAccount = + new InvalidOperationException("bad account"); + + public static readonly Exception ErrReservedAccount = + new InvalidOperationException("reserved account"); + + public static readonly Exception ErrMissingAccount = + new InvalidOperationException("account missing"); + + public static readonly Exception ErrMissingService = + new InvalidOperationException("service missing"); + + public static readonly Exception ErrBadServiceType = + new InvalidOperationException("bad service response type"); + + public static readonly Exception ErrBadSampling = + new InvalidOperationException("bad sampling percentage, should be 1-100"); + + public static readonly Exception ErrAccountValidation = + new InvalidOperationException("account validation failed"); + + public static readonly Exception ErrAccountExpired = + new InvalidOperationException("account expired"); + + public static readonly Exception ErrNoAccountResolver = + new InvalidOperationException("account resolver missing"); + + public static readonly Exception ErrAccountResolverUpdateTooSoon = + new InvalidOperationException("account resolver update too soon"); + + public static readonly Exception ErrAccountResolverSameClaims = + new InvalidOperationException("account resolver no new claims"); + + public static readonly Exception ErrStreamImportAuthorization = + new InvalidOperationException("stream import not authorized"); + + public static readonly Exception ErrStreamImportBadPrefix = + new InvalidOperationException("stream import prefix can not contain wildcard tokens"); + + public static readonly Exception ErrStreamImportDuplicate = + new InvalidOperationException("stream import already exists"); + + public static readonly Exception ErrServiceImportAuthorization = + new InvalidOperationException("service import not authorized"); + + public static readonly Exception ErrImportFormsCycle = + new InvalidOperationException("import forms a cycle"); + + public static readonly Exception ErrCycleSearchDepth = + new InvalidOperationException("search cycle depth exhausted"); + + public static readonly Exception ErrClientOrRouteConnectedToGatewayPort = + new InvalidOperationException("attempted to connect to gateway port"); + + public static readonly Exception ErrWrongGateway = + new InvalidOperationException("wrong gateway"); + + public static readonly Exception ErrGatewayNameHasSpaces = + new InvalidOperationException("gateway name cannot contain spaces"); + + public static readonly Exception ErrNoSysAccount = + new InvalidOperationException("system account not setup"); + + public static readonly Exception ErrRevocation = + new InvalidOperationException("credentials have been revoked"); + + public static readonly Exception ErrServerNotRunning = + new InvalidOperationException("server is not running"); + + public static readonly Exception ErrServerNameHasSpaces = + new InvalidOperationException("server name cannot contain spaces"); + + public static readonly Exception ErrBadMsgHeader = + new InvalidOperationException("bad message header detected"); + + public static readonly Exception ErrMsgHeadersNotSupported = + new InvalidOperationException("message headers not supported"); + + public static readonly Exception ErrNoRespondersRequiresHeaders = + new InvalidOperationException("no responders requires headers support"); + + public static readonly Exception ErrClusterNameConfigConflict = + new InvalidOperationException("cluster name conflicts between cluster and gateway definitions"); + + public static readonly Exception ErrClusterNameRemoteConflict = + new InvalidOperationException("cluster name from remote server conflicts"); + + public static readonly Exception ErrClusterNameHasSpaces = + new InvalidOperationException("cluster name cannot contain spaces"); + + public static readonly Exception ErrMalformedSubject = + new InvalidOperationException("malformed subject"); + + public static readonly Exception ErrSubscribePermissionViolation = + new InvalidOperationException("subscribe permission violation"); + + public static readonly Exception ErrNoTransforms = + new InvalidOperationException("no matching transforms available"); + + public static readonly Exception ErrCertNotPinned = + new InvalidOperationException("certificate not pinned"); + + public static readonly Exception ErrDuplicateServerName = + new InvalidOperationException("duplicate server name"); + + public static readonly Exception ErrMinimumVersionRequired = + new InvalidOperationException("minimum version required"); + + // Mapping destination errors — the Go source wraps ErrInvalidMappingDestination. + // In .NET we use a common base message and chain inner exceptions where needed. + public static readonly Exception ErrInvalidMappingDestination = + new InvalidOperationException("invalid mapping destination"); + + public static readonly Exception ErrInvalidMappingDestinationSubject = + new InvalidOperationException("invalid mapping destination: invalid transform"); + + public static readonly Exception ErrMappingDestinationNotUsingAllWildcards = + new InvalidOperationException("invalid mapping destination: not using all of the token wildcard(s)"); + + public static readonly Exception ErrUnknownMappingDestinationFunction = + new InvalidOperationException("invalid mapping destination: unknown function"); + + public static readonly Exception ErrMappingDestinationIndexOutOfRange = + new InvalidOperationException("invalid mapping destination: wildcard index out of range"); + + public static readonly Exception ErrMappingDestinationNotEnoughArgs = + new InvalidOperationException("invalid mapping destination: not enough arguments passed to the function"); + + public static readonly Exception ErrMappingDestinationInvalidArg = + new InvalidOperationException("invalid mapping destination: function argument is invalid or in the wrong format"); + + public static readonly Exception ErrMappingDestinationTooManyArgs = + new InvalidOperationException("invalid mapping destination: too many arguments passed to the function"); + + public static readonly Exception ErrMappingDestinationNotSupportedForImport = + new InvalidOperationException("invalid mapping destination: the only mapping function allowed for import transforms is {{Wildcard()}}"); +} + +// ------------------------------------------------------------------------- +// mappingDestinationErr (mirrors server/errors.go) +// ------------------------------------------------------------------------- + +/// +/// An error type for subject mapping destination validation failures. +/// Mirrors mappingDestinationErr in server/errors.go. +/// +public sealed class MappingDestinationException : Exception +{ + private readonly string _token; + + public MappingDestinationException(string token, Exception inner) + : base($"{inner.Message} in {token}", inner) + { + _token = token; + } + + // Is() in Go is implemented via IsInvalidMappingDestination below. + + /// + /// Returns true when compared against . + /// Mirrors mappingDestinationErr.Is. + /// + public bool Is(Exception target) => + ReferenceEquals(target, ServerErrors.ErrInvalidMappingDestination); +} + +// ------------------------------------------------------------------------- +// configErr / unknownConfigFieldErr / configWarningErr (mirrors server/errors.go) +// ------------------------------------------------------------------------- +// Note: these types depend on a config-file token interface defined in the +// configuration parser (ported in session 03). Forward-declared here with the +// minimal interface needed for error formatting. + +/// +/// Represents a source location within a configuration file. +/// Mirrors the token interface used by configErr in server/errors.go. +/// Full implementation is provided by the configuration parser (session 03). +/// +public interface IConfigToken +{ + string SourceFile(); + int Line(); + int Position(); +} + +/// +/// A configuration parsing error with source location. +/// Mirrors configErr in server/errors.go. +/// +public class ConfigException : Exception +{ + private readonly IConfigToken? _token; + + public ConfigException(IConfigToken? token, string reason) + : base(reason) + { + _token = token; + } + + /// Returns "file:line:col" source location, or empty string if no token. + public new string Source() => + _token != null + ? $"{_token.SourceFile()}:{_token.Line()}:{_token.Position()}" + : string.Empty; + + public override string Message => + _token != null ? $"{Source()}: {base.Message}" : base.Message; +} + +/// +/// A configuration error for an unknown field (pedantic mode). +/// Mirrors unknownConfigFieldErr in server/errors.go. +/// +public sealed class UnknownConfigFieldException : ConfigException +{ + private readonly string _field; + + public UnknownConfigFieldException(IConfigToken token, string field) + : base(token, $"unknown field \"{field}\"") + { + _field = field; + } +} + +/// +/// A configuration warning for invalid field usage (pedantic mode). +/// Mirrors configWarningErr in server/errors.go. +/// +public sealed class ConfigWarningException : ConfigException +{ + private readonly string _field; + + public ConfigWarningException(IConfigToken token, string field, string reason) + : base(token, $"invalid use of field \"{field}\": {reason}") + { + _field = field; + } +} + +/// +/// Aggregates configuration warnings and hard errors from parsing. +/// Mirrors processConfigErr in server/errors.go. +/// +public sealed class ProcessConfigException : Exception +{ + public IReadOnlyList Warnings { get; } + public IReadOnlyList Errors { get; } + + public ProcessConfigException(IReadOnlyList errors, IReadOnlyList warnings) + : base(BuildMessage(errors, warnings)) + { + Errors = errors; + Warnings = warnings; + } + + private static string BuildMessage(IReadOnlyList errors, IReadOnlyList warnings) + { + var sb = new System.Text.StringBuilder(); + foreach (var w in warnings) sb.AppendLine(w.Message); + foreach (var e in errors) sb.AppendLine(e.Message); + return sb.ToString(); + } +} + +// ------------------------------------------------------------------------- +// errCtx — error with attached tracing context (mirrors server/errors.go) +// ------------------------------------------------------------------------- + +/// +/// Wraps an exception and attaches additional tracing context. +/// Context is not included in but is +/// accessible via and . +/// Mirrors errCtx in server/errors.go. +/// +public sealed class ErrorCtx : Exception +{ + public string Ctx { get; } + + public ErrorCtx(Exception inner, string ctx) + : base(inner.Message, inner) + { + Ctx = ctx; + } + + /// + /// Returns the context string attached to this error. + /// Mirrors errCtx.Context(). + /// + public string Context() => Ctx; +} + +/// +/// Factory and utilities for . +/// Mirrors NewErrorCtx, UnpackIfErrorCtx, and ErrorIs in server/errors.go. +/// +public static class ErrorContextHelper +{ + /// + /// Creates an wrapping with formatted context. + /// Mirrors NewErrorCtx. + /// + public static Exception NewErrorCtx(Exception err, string format, params object[] args) => + new ErrorCtx(err, string.Format(format, args)); + + /// + /// If is an , returns + /// "original.Message: ctx" (recursively). Otherwise returns err.Message. + /// Mirrors UnpackIfErrorCtx. + /// + public static string UnpackIfErrorCtx(Exception err) + { + if (err is ErrorCtx ectx) + { + if (ectx.InnerException is ErrorCtx) + return $"{UnpackIfErrorCtx(ectx.InnerException!)}: {ectx.Ctx}"; + return $"{ectx.InnerException!.Message}: {ectx.Ctx}"; + } + return err.Message; + } + + /// + /// Walks the inner exception chain checking reference equality and + /// overrides. + /// In .NET, prefer errors.Is-equivalent patterns; this mirrors + /// the Go ErrorIs shim exactly. + /// Mirrors ErrorIs. + /// + public static bool ErrorIs(Exception? err, Exception? target) + { + if (err == null || target == null) + return ReferenceEquals(err, target); + + var current = err; + while (current != null) + { + if (ReferenceEquals(current, target)) + return true; + if (current is MappingDestinationException mde && mde.Is(target)) + return true; + current = current.InnerException; + } + return false; + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs new file mode 100644 index 0000000..19153ab --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs @@ -0,0 +1,87 @@ +// Copyright 2020-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Shouldly; +using ZB.MOM.NatsNet.Server; + +namespace ZB.MOM.NatsNet.Server.Tests.Foundation; + +/// +/// Tests for and . +/// Mirrors server/errors_test.go: TestErrCtx (ID 297) and TestErrCtxWrapped (ID 298). +/// +public sealed class ServerErrorsTests +{ + [Fact] + public void ErrCtx_ShouldPreserveOriginalMessageAndAddContext() + { + // Mirror: TestErrCtx + var ctx = "Extra context information"; + var e = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctx); + + // Message should match the underlying error. + e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message); + + // Must not be reference-equal to the sentinel. + e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway); + + // ErrorIs should find the sentinel in the chain. + ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue(); + + // UnpackIfErrorCtx on a non-ctx error returns Message unchanged. + ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway) + .ShouldBe(ServerErrors.ErrWrongGateway.Message); + + // UnpackIfErrorCtx should start with the original error message. + var trace = ErrorContextHelper.UnpackIfErrorCtx(e); + trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message); + + // And end with the context string. + trace.ShouldEndWith(ctx); + } + + [Fact] + public void ErrCtxWrapped_ShouldContainAllContextLayers() + { + // Mirror: TestErrCtxWrapped + var ctxO = "Original Ctx"; + var eO = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctxO); + + var ctx = "Extra context information"; + var e = ErrorContextHelper.NewErrorCtx(eO, "{0}", ctx); + + // Message should still match the underlying error. + e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message); + + // Must not be reference-equal to the sentinel. + e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway); + + // ErrorIs should walk the chain. + ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue(); + + // UnpackIfErrorCtx on a non-ctx error returns Message unchanged. + ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway) + .ShouldBe(ServerErrors.ErrWrongGateway.Message); + + var trace = ErrorContextHelper.UnpackIfErrorCtx(e); + + // Must start with the original error. + trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message); + + // Must end with the outermost context. + trace.ShouldEndWith(ctx); + + // Must also contain the inner context. + trace.ShouldContain(ctxO); + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs new file mode 100644 index 0000000..0e30c32 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs @@ -0,0 +1,79 @@ +// Copyright 2018-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Shouldly; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server.Tests.Internal; + +/// +/// Tests for . +/// Mirrors server/ring_test.go: TestRBAppendAndLenAndTotal (ID 2794) +/// and TestRBclosedClients (ID 2795). +/// +public sealed class ClosedRingBufferTests +{ + [Fact] + public void AppendAndLenAndTotal_ShouldTrackCorrectly() + { + // Mirror: TestRBAppendAndLenAndTotal + var rb = new ClosedRingBuffer(10); + + for (var i = 0; i < 5; i++) + rb.Append(new ClosedClient()); + + rb.Len().ShouldBe(5); + rb.TotalConns().ShouldBe(5UL); + + for (var i = 0; i < 25; i++) + rb.Append(new ClosedClient()); + + rb.Len().ShouldBe(10); + rb.TotalConns().ShouldBe(30UL); + } + + [Fact] + public void ClosedClients_ShouldReturnChronologicalOrder() + { + // Mirror: TestRBclosedClients + var rb = new ClosedRingBuffer(10); + + // Build master list with identifiable user strings. + const int max = 100; + var master = new ClosedClient[max]; + for (var i = 1; i <= max; i++) + master[i - 1] = new ClosedClient { User = i.ToString() }; + + var ui = 0; + + void AddConn() + { + ui++; + rb.Append(new ClosedClient { User = ui.ToString() }); + } + + for (var i = 0; i < max; i++) + { + AddConn(); + + var ccs = rb.ClosedClients(); + var start = (int)rb.TotalConns() - ccs.Length; + var ms = master[start..(start + ccs.Length)]; + + // Verify order matches master using User strings. + ccs.Length.ShouldBe(ms.Length); + for (var j = 0; j < ccs.Length; j++) + ccs[j]!.User.ShouldBe(ms[j].User, $"iteration {i}, slot {j}"); + } + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs new file mode 100644 index 0000000..fbb97b3 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs @@ -0,0 +1,47 @@ +// Copyright 2021-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Shouldly; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server.Tests.Internal; + +/// +/// Tests for . +/// Mirrors server/rate_counter_test.go: TestRateCounter (ID 2720). +/// +public sealed class RateCounterTests +{ + [Fact] + public async Task RateCounter_ShouldAllowUpToLimitThenBlockAndReset() + { + // Mirror: TestRateCounter + var counter = new RateCounter(10) { Interval = TimeSpan.FromMilliseconds(100) }; + + // First 10 calls should be allowed (counts 0–9 < limit 10). + for (var i = 0; i < 10; i++) + counter.Allow().ShouldBeTrue($"should allow on iteration {i}"); + + // Next 5 should be blocked. + for (var i = 0; i < 5; i++) + counter.Allow().ShouldBeFalse($"should not allow on iteration {i}"); + + // countBlocked returns and resets the blocked count. + counter.CountBlocked().ShouldBe(5UL); + counter.CountBlocked().ShouldBe(0UL); + + // After the window expires, should allow again. + await Task.Delay(150); + counter.Allow().ShouldBeTrue("should allow after window expired"); + } +} diff --git a/porting.db b/porting.db index 5e26269eb6d0600590979d95d993b1bd3e51927e..b9a4b1c21d6e4aed9579e3140703bcc6b348c2fd 100644 GIT binary patch delta 41337 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 S>$~~kI1BQfd?fC)?*9Qm->NnM delta 27208 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 diff --git a/reports/current.md b/reports/current.md index e4f7f89..7dc7ceb 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,28 +1,29 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 13:07:55 UTC +Generated: 2026-02-26 14:15:20 UTC ## Modules (12 total) | Status | Count | |--------|-------| -| complete | 1 | -| not_started | 11 | +| complete | 11 | +| not_started | 1 | ## Features (3673 total) | Status | Count | |--------|-------| -| complete | 36 | -| n_a | 41 | -| not_started | 3596 | +| complete | 265 | +| n_a | 74 | +| not_started | 3334 | ## Unit Tests (3257 total) | Status | Count | |--------|-------| -| complete | 16 | -| not_started | 3241 | +| complete | 119 | +| n_a | 12 | +| not_started | 3126 | ## Library Mappings (36 total) @@ -33,4 +34,4 @@ Generated: 2026-02-26 13:07:55 UTC ## Overall Progress -**94/6942 items complete (1.4%)** +**481/6942 items complete (6.9%)** diff --git a/reports/report_66628bc.md b/reports/report_66628bc.md new file mode 100644 index 0000000..7dc7ceb --- /dev/null +++ b/reports/report_66628bc.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 14:15:20 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 265 | +| n_a | 74 | +| not_started | 3334 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 119 | +| n_a | 12 | +| not_started | 3126 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**481/6942 items complete (6.9%)**