feat: port sessions 14-16 — Routes, Leaf Nodes & Gateways
Session 14 (57 features, IDs 2895-2951): - RouteTypes: RouteType enum, Route, RouteInfo, ConnectInfo, ASubs, GossipMode Session 15 (71 features, IDs 1979-2049): - LeafNodeTypes: Leaf, LeafNodeCfg (replaces stub), LeafConnectInfo Session 16 (91 features, IDs 1263-1353): - GatewayTypes: GatewayInterestMode, SrvGateway (replaces stub), GatewayCfg, Gateway, OutSide, InSide, SitAlly, GwReplyMap, GwReplyMapping
This commit is contained in:
384
dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs
Normal file
384
dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
// 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/gateway.go in the NATS server Go source.
|
||||
|
||||
using System.Threading;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// ============================================================================
|
||||
// Session 16: Gateways
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Represents the interest mode for a given account on a gateway connection.
|
||||
/// Mirrors Go <c>GatewayInterestMode</c> byte iota in gateway.go.
|
||||
/// Do not change values — they are part of the wire-level gossip protocol.
|
||||
/// </summary>
|
||||
public enum GatewayInterestMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Default mode: the cluster sends to a gateway unless told there is no
|
||||
/// interest (applies to plain subscribers only).
|
||||
/// </summary>
|
||||
Optimistic = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Transitioning: the gateway has been sending too many no-interest signals
|
||||
/// and is switching to <see cref="InterestOnly"/> mode for this account.
|
||||
/// </summary>
|
||||
Transitioning = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Interest-only mode: the cluster has sent all its subscription interest;
|
||||
/// the gateway only forwards messages when explicit interest is known.
|
||||
/// </summary>
|
||||
InterestOnly = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Internal sentinel used after a cache flush; not part of the public wire enum.
|
||||
/// </summary>
|
||||
CacheFlushed = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-level gateway state kept on the <see cref="NatsServer"/> instance.
|
||||
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
|
||||
/// Mirrors Go <c>srvGateway</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class SrvGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of queue subs across all remote gateways.
|
||||
/// Accessed via <c>Interlocked</c> — must be 64-bit aligned.
|
||||
/// </summary>
|
||||
public long TotalQSubs;
|
||||
|
||||
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
/// <summary>
|
||||
/// True if both a gateway name and port are configured (immutable after init).
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>Name of this server's gateway cluster.</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Outbound gateway connections keyed by remote gateway name.</summary>
|
||||
public Dictionary<string, ClientConnection> Out { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Outbound gateway connections in RTT order, used for message routing.
|
||||
/// </summary>
|
||||
public List<ClientConnection> Outo { get; set; } = [];
|
||||
|
||||
/// <summary>Inbound gateway connections keyed by connection ID.</summary>
|
||||
public Dictionary<ulong, ClientConnection> In { get; set; } = new();
|
||||
|
||||
/// <summary>Per-remote-gateway configuration, keyed by gateway name.</summary>
|
||||
public Dictionary<string, GatewayCfg> Remotes { get; set; } = new();
|
||||
|
||||
/// <summary>Reference-counted set of all gateway URLs in the cluster.</summary>
|
||||
public RefCountedUrlSet Urls { get; set; } = new();
|
||||
|
||||
/// <summary>This server's own gateway URL (after random-port resolution).</summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gateway INFO protocol object.</summary>
|
||||
public ServerInfo? Info { get; set; }
|
||||
|
||||
/// <summary>Pre-marshalled INFO JSON bytes.</summary>
|
||||
public byte[]? InfoJson { get; set; }
|
||||
|
||||
/// <summary>When true, reject connections from gateways not in <see cref="Remotes"/>.</summary>
|
||||
public bool RejectUnknown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reply prefix bytes: <c>"$GNR.<reserved>.<clusterHash>.<serverHash>."</c>
|
||||
/// </summary>
|
||||
public byte[] ReplyPfx { get; set; } = [];
|
||||
|
||||
// Backward-compatibility reply prefix and hash (old "$GR." scheme)
|
||||
public byte[] OldReplyPfx { get; set; } = [];
|
||||
public byte[] OldHash { get; set; } = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// pasi — per-account subject interest tally (protected by its own mutex)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Per-account subject-interest tally.
|
||||
/// Outer key = account name; inner key = subject (or "subject queue" pair);
|
||||
/// value = tally struct.
|
||||
/// Mirrors Go's anonymous <c>pasi</c> embedded struct in <c>srvGateway</c>.
|
||||
/// </summary>
|
||||
private readonly Lock _pasiLock = new();
|
||||
public Dictionary<string, Dictionary<string, SitAlly>> Pasi { get; set; } = new();
|
||||
|
||||
public Lock PasiLock => _pasiLock;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Recent subscription tracking (thread-safe map)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Recent subscriptions for a given account (subject → expiry ticks).
|
||||
/// Mirrors Go's <c>rsubs sync.Map</c>.
|
||||
/// </summary>
|
||||
public System.Collections.Concurrent.ConcurrentDictionary<string, long> RSubs { get; set; } = new();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Other server-level gateway fields
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>DNS resolver used before dialling gateway connections.</summary>
|
||||
public INetResolver? Resolver { get; set; }
|
||||
|
||||
/// <summary>Max buffer size for sending queue-sub protocol (used in tests).</summary>
|
||||
public int SqbSz { get; set; }
|
||||
|
||||
/// <summary>How long to look for a subscription match for a reply message.</summary>
|
||||
public TimeSpan RecSubExp { get; set; }
|
||||
|
||||
/// <summary>Server ID hash (6 bytes) for routing mapped replies.</summary>
|
||||
public byte[] SIdHash { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Map from a route server's hashed ID (6 bytes) to the route client.
|
||||
/// Mirrors Go's <c>routesIDByHash sync.Map</c>.
|
||||
/// </summary>
|
||||
public System.Collections.Concurrent.ConcurrentDictionary<string, ClientConnection> RoutesIdByHash { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gateway URLs from this server's own entry in the Gateways config block,
|
||||
/// used for monitoring reports.
|
||||
/// </summary>
|
||||
public List<string> OwnCfgUrls { get; set; } = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lock helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void AcquireReadLock() => _lock.EnterReadLock();
|
||||
public void ReleaseReadLock() => _lock.ExitReadLock();
|
||||
public void AcquireWriteLock() => _lock.EnterWriteLock();
|
||||
public void ReleaseWriteLock() => _lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject-interest tally entry. Indicates whether the key in the map is a
|
||||
/// queue subscription and how many matching subscriptions exist.
|
||||
/// Mirrors Go <c>sitally</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class SitAlly
|
||||
{
|
||||
/// <summary>Number of subscriptions directly matching the subject/queue key.</summary>
|
||||
public int N { get; set; }
|
||||
|
||||
/// <summary>True if this entry represents a queue subscription.</summary>
|
||||
public bool Q { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime configuration for a single remote gateway.
|
||||
/// Wraps <see cref="RemoteGatewayOpts"/> with connection-attempt state and a lock.
|
||||
/// Mirrors Go <c>gatewayCfg</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class GatewayCfg
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
/// <summary>The raw remote-gateway options this cfg was built from.</summary>
|
||||
public RemoteGatewayOpts? RemoteOpts { get; set; }
|
||||
|
||||
/// <summary>6-byte cluster hash used for reply routing.</summary>
|
||||
public byte[] Hash { get; set; } = [];
|
||||
|
||||
/// <summary>4-byte old-style hash for backward compatibility.</summary>
|
||||
public byte[] OldHash { get; set; } = [];
|
||||
|
||||
/// <summary>Map of URL string → parsed URL for this remote gateway.</summary>
|
||||
public Dictionary<string, Uri> Urls { get; set; } = new();
|
||||
|
||||
/// <summary>Number of connection attempts made so far.</summary>
|
||||
public int ConnAttempts { get; set; }
|
||||
|
||||
/// <summary>TLS server name override for SNI.</summary>
|
||||
public string TlsName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>True if this remote was discovered via gossip (not configured).</summary>
|
||||
public bool Implicit { get; set; }
|
||||
|
||||
/// <summary>When true, monitoring should refresh the URL list on next varz inspection.</summary>
|
||||
public bool VarzUpdateUrls { get; set; }
|
||||
|
||||
// Forwarded properties from RemoteGatewayOpts
|
||||
public string Name { get => RemoteOpts?.Name ?? string.Empty; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lock helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void AcquireReadLock() => _lock.EnterReadLock();
|
||||
public void ReleaseReadLock() => _lock.ExitReadLock();
|
||||
public void AcquireWriteLock() => _lock.EnterWriteLock();
|
||||
public void ReleaseWriteLock() => _lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-connection gateway state embedded in <see cref="ClientConnection"/>
|
||||
/// when the connection kind is <c>Gateway</c>.
|
||||
/// Mirrors Go <c>gateway</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class Gateway
|
||||
{
|
||||
/// <summary>Name of the remote gateway cluster.</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Configuration block for the remote gateway.</summary>
|
||||
public GatewayCfg? Cfg { get; set; }
|
||||
|
||||
/// <summary>URL used for CONNECT after receiving the remote INFO (outbound only).</summary>
|
||||
public Uri? ConnectUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-account subject interest (outbound connection).
|
||||
/// Maps account name → <see cref="OutSide"/> for that account.
|
||||
/// Uses a thread-safe map because it is read from multiple goroutines.
|
||||
/// </summary>
|
||||
public System.Collections.Concurrent.ConcurrentDictionary<string, OutSide>? OutSim { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-account no-interest subjects or interest-only mode (inbound connection).
|
||||
/// </summary>
|
||||
public Dictionary<string, InSide>? InSim { get; set; }
|
||||
|
||||
/// <summary>True if this is an outbound gateway connection.</summary>
|
||||
public bool Outbound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set in the read loop without locking to record that the inbound side
|
||||
/// sent its CONNECT protocol.
|
||||
/// </summary>
|
||||
public bool Connected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the remote server only understands the old <c>$GR.</c> prefix,
|
||||
/// not the newer <c>$GNR.</c> scheme.
|
||||
/// </summary>
|
||||
public bool UseOldPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true the inbound side switches accounts to interest-only mode
|
||||
/// immediately, so the outbound side can disregard optimistic mode.
|
||||
/// </summary>
|
||||
public bool InterestOnlyMode { get; set; }
|
||||
|
||||
/// <summary>Name of the remote server on this gateway connection.</summary>
|
||||
public string RemoteName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outbound subject-interest entry for a single account on a gateway connection.
|
||||
/// Mirrors Go <c>outsie</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class OutSide
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
/// <summary>Current interest mode for this account on the outbound gateway.</summary>
|
||||
public GatewayInterestMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of subjects for which the remote has signalled no-interest.
|
||||
/// Null when the remote has sent all its subscriptions (interest-only mode).
|
||||
/// </summary>
|
||||
public HashSet<string>? Ni { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscription index: contains queue subs in optimistic mode,
|
||||
/// or all subs when <see cref="Mode"/> has been switched.
|
||||
/// </summary>
|
||||
public SubscriptionIndex? Sl { get; set; }
|
||||
|
||||
/// <summary>Number of queue subscriptions tracked in <see cref="Sl"/>.</summary>
|
||||
public int Qsubs { get; set; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lock helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void AcquireReadLock() => _lock.EnterReadLock();
|
||||
public void ReleaseReadLock() => _lock.ExitReadLock();
|
||||
public void AcquireWriteLock() => _lock.EnterWriteLock();
|
||||
public void ReleaseWriteLock() => _lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inbound subject-interest entry for a single account on a gateway connection.
|
||||
/// Tracks subjects for which an RS- was sent to the remote, and the current mode.
|
||||
/// Mirrors Go <c>insie</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class InSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Subjects for which RS- was sent to the remote (null when in interest-only mode).
|
||||
/// </summary>
|
||||
public HashSet<string>? Ni { get; set; }
|
||||
|
||||
/// <summary>Current interest mode for this account on the inbound gateway.</summary>
|
||||
public GatewayInterestMode Mode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single gateway reply-mapping entry: the mapped subject and its expiry.
|
||||
/// Mirrors Go <c>gwReplyMap</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class GwReplyMap
|
||||
{
|
||||
/// <summary>The mapped (routed) subject string.</summary>
|
||||
public string Ms { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Expiry expressed as <see cref="DateTime.Ticks"/> (UTC).</summary>
|
||||
public long Exp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gateway reply routing table and a fast-path check flag.
|
||||
/// Mirrors Go <c>gwReplyMapping</c> struct in gateway.go.
|
||||
/// </summary>
|
||||
internal sealed class GwReplyMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-zero when the mapping table should be consulted while processing
|
||||
/// inbound messages. Accessed via <c>Interlocked</c> — must be 32-bit aligned.
|
||||
/// </summary>
|
||||
public int Check;
|
||||
|
||||
/// <summary>Active reply-subject → GwReplyMap entries.</summary>
|
||||
public Dictionary<string, GwReplyMap> Mapping { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the routed subject for <paramref name="subject"/> if a mapping
|
||||
/// exists, otherwise returns the original subject and <c>false</c>.
|
||||
/// Caller must hold any required lock before invoking.
|
||||
/// </summary>
|
||||
public (byte[] Subject, bool Found) Get(byte[] subject)
|
||||
{
|
||||
// TODO: session 16 — implement mapping lookup
|
||||
return (subject, false);
|
||||
}
|
||||
}
|
||||
202
dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs
Normal file
202
dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2019-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/leafnode.go in the NATS server Go source.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// ============================================================================
|
||||
// Session 15: Leaf Nodes
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Per-connection leaf-node state embedded in <see cref="ClientConnection"/>
|
||||
/// when the connection kind is <c>Leaf</c>.
|
||||
/// Mirrors Go <c>leaf</c> struct in leafnode.go.
|
||||
/// </summary>
|
||||
internal sealed class Leaf
|
||||
{
|
||||
/// <summary>
|
||||
/// Config for solicited (outbound) leaf connections; null for accepted connections.
|
||||
/// </summary>
|
||||
public LeafNodeCfg? Remote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when we are the spoke side of a hub/spoke leaf pair.
|
||||
/// </summary>
|
||||
public bool IsSpoke { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cluster name of the remote server when we are a hub and the spoke is
|
||||
/// part of a cluster.
|
||||
/// </summary>
|
||||
public string RemoteCluster { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Remote server name or ID.</summary>
|
||||
public string RemoteServer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Domain name of the remote server.</summary>
|
||||
public string RemoteDomain { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Account name of the remote server.</summary>
|
||||
public string RemoteAccName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When true, suppresses propagation of east-west interest from other leaf nodes.
|
||||
/// </summary>
|
||||
public bool Isolated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject-interest suppression map shared with the remote side.
|
||||
/// Key = subject, Value = interest count (positive = subscribe, negative = unsubscribe delta).
|
||||
/// </summary>
|
||||
public Dictionary<string, int> Smap { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Short-lived set of subscriptions added during <c>initLeafNodeSmapAndSendSubs</c>
|
||||
/// to detect and avoid double-counting races.
|
||||
/// </summary>
|
||||
public HashSet<Subscription>? Tsub { get; set; }
|
||||
|
||||
/// <summary>Timer that clears <see cref="Tsub"/> after the initialization window.</summary>
|
||||
public Timer? Tsubt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selected compression mode, which may differ from the server-configured mode.
|
||||
/// </summary>
|
||||
public string Compression { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gateway-mapped reply subscription used for GW reply routing via leaf nodes.
|
||||
/// </summary>
|
||||
public Subscription? GwSub { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime configuration for a remote (solicited) leaf-node connection.
|
||||
/// Wraps <see cref="RemoteLeafOpts"/> with connection-attempt state and a
|
||||
/// reader-writer lock for concurrent access.
|
||||
/// Mirrors Go <c>leafNodeCfg</c> struct in leafnode.go.
|
||||
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
|
||||
/// </summary>
|
||||
public sealed class LeafNodeCfg
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Embedded RemoteLeafOpts fields
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>The raw remote options this cfg was constructed from.</summary>
|
||||
public RemoteLeafOpts? RemoteOpts { get; set; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Runtime connection-attempt fields
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Resolved URLs to attempt connections to.</summary>
|
||||
public List<Uri> Urls { get; set; } = [];
|
||||
|
||||
/// <summary>Currently selected URL from <see cref="Urls"/>.</summary>
|
||||
public Uri? CurUrl { get; set; }
|
||||
|
||||
/// <summary>TLS server name override for SNI.</summary>
|
||||
public string TlsName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Username for authentication (resolved from credentials or options).</summary>
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Password for authentication (resolved from credentials or options).</summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Publish/subscribe permission overrides for this connection.</summary>
|
||||
public Permissions? Perms { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delay before the next connection attempt (e.g. during loop-detection back-off).
|
||||
/// </summary>
|
||||
public TimeSpan ConnDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timer used to trigger JetStream account migration for this leaf.
|
||||
/// </summary>
|
||||
public Timer? JsMigrateTimer { get; set; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Forwarded properties from RemoteLeafOpts
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public string LocalAccount { get => RemoteOpts?.LocalAccount ?? string.Empty; }
|
||||
public bool NoRandomize { get => RemoteOpts?.NoRandomize ?? false; }
|
||||
public string Credentials { get => RemoteOpts?.Credentials ?? string.Empty; }
|
||||
public bool Disabled { get => RemoteOpts?.Disabled ?? false; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lock helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void AcquireReadLock() => _lock.EnterReadLock();
|
||||
public void ReleaseReadLock() => _lock.ExitReadLock();
|
||||
public void AcquireWriteLock() => _lock.EnterWriteLock();
|
||||
public void ReleaseWriteLock() => _lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CONNECT protocol payload sent by a leaf-node connection.
|
||||
/// Fields map 1-to-1 with the JSON tags in Go's <c>leafConnectInfo</c>.
|
||||
/// Mirrors Go <c>leafConnectInfo</c> struct in leafnode.go.
|
||||
/// </summary>
|
||||
internal sealed class LeafConnectInfo
|
||||
{
|
||||
[JsonPropertyName("version")] public string Version { get; set; } = string.Empty;
|
||||
[JsonPropertyName("nkey")] public string Nkey { get; set; } = string.Empty;
|
||||
[JsonPropertyName("jwt")] public string Jwt { get; set; } = string.Empty;
|
||||
[JsonPropertyName("sig")] public string Sig { get; set; } = string.Empty;
|
||||
[JsonPropertyName("user")] public string User { get; set; } = string.Empty;
|
||||
[JsonPropertyName("pass")] public string Pass { get; set; } = string.Empty;
|
||||
[JsonPropertyName("auth_token")] public string Token { get; set; } = string.Empty;
|
||||
[JsonPropertyName("server_id")] public string Id { get; set; } = string.Empty;
|
||||
[JsonPropertyName("domain")] public string Domain { get; set; } = string.Empty;
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
|
||||
[JsonPropertyName("is_hub")] public bool Hub { get; set; }
|
||||
[JsonPropertyName("cluster")] public string Cluster { get; set; } = string.Empty;
|
||||
[JsonPropertyName("headers")] public bool Headers { get; set; }
|
||||
[JsonPropertyName("jetstream")] public bool JetStream { get; set; }
|
||||
[JsonPropertyName("deny_pub")] public string[] DenyPub { get; set; } = [];
|
||||
[JsonPropertyName("isolate")] public bool Isolate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compression mode string. The legacy boolean field was never used; this
|
||||
/// string field uses a different JSON tag to avoid conflicts.
|
||||
/// </summary>
|
||||
[JsonPropertyName("compress_mode")] public string Compression { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Used only to detect wrong-port connections (client connecting to leaf port).
|
||||
/// </summary>
|
||||
[JsonPropertyName("gateway")] public string Gateway { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Account name the remote is binding to on the accept side.</summary>
|
||||
[JsonPropertyName("remote_account")] public string RemoteAccount { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Protocol version sent by the soliciting side so the accepting side knows
|
||||
/// which features are supported (e.g. message tracing).
|
||||
/// </summary>
|
||||
[JsonPropertyName("protocol")] public int Proto { get; set; }
|
||||
}
|
||||
@@ -225,11 +225,7 @@ public sealed class JetStreamConfig
|
||||
public string UniqueTag { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Stub for server gateway state (session 16).</summary>
|
||||
internal sealed class SrvGateway
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
// SrvGateway — full class is in Gateway/GatewayTypes.cs (session 16).
|
||||
|
||||
/// <summary>Stub for server websocket state (session 23).</summary>
|
||||
internal sealed class SrvWebsocket
|
||||
@@ -249,8 +245,7 @@ internal interface IOcspResponseCache { }
|
||||
/// <summary>Stub for IP queue (session 02 — already ported as IpQueue).</summary>
|
||||
// IpQueue is already in session 02 internals — used here via object.
|
||||
|
||||
/// <summary>Stub for leaf node config (session 15).</summary>
|
||||
internal sealed class LeafNodeCfg { }
|
||||
// LeafNodeCfg — full class is in LeafNode/LeafNodeTypes.cs (session 15).
|
||||
|
||||
/// <summary>
|
||||
/// Network resolver used by <see cref="NatsServer.GetRandomIP"/>.
|
||||
|
||||
185
dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs
Normal file
185
dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2013-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/route.go in the NATS server Go source.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// ============================================================================
|
||||
// Session 14: Routes
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Designates whether a route was explicitly configured or discovered via gossip.
|
||||
/// Mirrors Go <c>RouteType</c> iota in route.go.
|
||||
/// Note: Go defines Implicit=0, Explicit=1 — we keep TombStone=2 for future use.
|
||||
/// </summary>
|
||||
public enum RouteType : int
|
||||
{
|
||||
/// <summary>This route was learned from speaking to other routes.</summary>
|
||||
Implicit = 0,
|
||||
/// <summary>This route was explicitly configured.</summary>
|
||||
Explicit = 1,
|
||||
/// <summary>Reserved tombstone marker for removed routes.</summary>
|
||||
TombStone = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gossip mode constants exchanged between route servers.
|
||||
/// Mirrors the const block immediately after <c>routeInfo</c> in route.go.
|
||||
/// Do not change values — they are part of the wire protocol.
|
||||
/// </summary>
|
||||
internal static class GossipMode
|
||||
{
|
||||
public const byte Default = 0;
|
||||
public const byte Disabled = 1;
|
||||
public const byte Override = 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-connection route state embedded in <see cref="ClientConnection"/> when the
|
||||
/// connection kind is <c>Router</c>.
|
||||
/// Mirrors Go <c>route</c> struct in route.go.
|
||||
/// </summary>
|
||||
internal sealed class Route
|
||||
{
|
||||
/// <summary>Remote server ID string.</summary>
|
||||
public string RemoteId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Remote server name.</summary>
|
||||
public string RemoteName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>True if this server solicited the outbound connection.</summary>
|
||||
public bool DidSolicit { get; set; }
|
||||
|
||||
/// <summary>True if the connection should be retried on failure.</summary>
|
||||
public bool Retry { get; set; }
|
||||
|
||||
/// <summary>Leaf-node origin cluster flag (lnoc).</summary>
|
||||
public bool Lnoc { get; set; }
|
||||
|
||||
/// <summary>Leaf-node origin cluster with unsub support (lnocu).</summary>
|
||||
public bool Lnocu { get; set; }
|
||||
|
||||
/// <summary>Whether this is an explicit or implicit route.</summary>
|
||||
public RouteType RouteType { get; set; }
|
||||
|
||||
/// <summary>Remote URL used to establish the connection.</summary>
|
||||
public Uri? Url { get; set; }
|
||||
|
||||
/// <summary>True if the remote requires authentication.</summary>
|
||||
public bool AuthRequired { get; set; }
|
||||
|
||||
/// <summary>True if the remote requires TLS.</summary>
|
||||
public bool TlsRequired { get; set; }
|
||||
|
||||
/// <summary>True if JetStream is enabled on the remote.</summary>
|
||||
public bool JetStream { get; set; }
|
||||
|
||||
/// <summary>List of client connect URLs advertised by the remote.</summary>
|
||||
public List<string> ConnectUrls { get; set; } = [];
|
||||
|
||||
/// <summary>List of WebSocket connect URLs advertised by the remote.</summary>
|
||||
public List<string> WsConnUrls { get; set; } = [];
|
||||
|
||||
/// <summary>Gateway URL advertised by the remote.</summary>
|
||||
public string GatewayUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Leaf-node URL advertised by the remote.</summary>
|
||||
public string LeafnodeUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Cluster hash used for routing.</summary>
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Server ID hash (6 bytes encoded).</summary>
|
||||
public string IdHash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Index of this route in the <c>s.routes[remoteID]</c> slice.
|
||||
/// Initialized to -1 to indicate the route has not yet been registered.
|
||||
/// </summary>
|
||||
public int PoolIdx { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// When set, this route is pinned to a specific account and the account
|
||||
/// name will not be included in routed protocols.
|
||||
/// </summary>
|
||||
public byte[]? AccName { get; set; }
|
||||
|
||||
/// <summary>True if this is a connection to an old server or one with pooling disabled.</summary>
|
||||
public bool NoPool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selected compression mode, which may differ from the server-configured mode.
|
||||
/// </summary>
|
||||
public string Compression { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Transient gossip mode byte sent when initiating an implicit route.
|
||||
/// </summary>
|
||||
public byte GossipMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set in a pooling scenario, signals that the route should trigger
|
||||
/// creation of the next pooled connection after receiving the first PONG.
|
||||
/// </summary>
|
||||
public RouteInfo? StartNewRoute { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal descriptor used to create a new route connection, including
|
||||
/// the target URL, its type, and gossip mode.
|
||||
/// Mirrors Go <c>routeInfo</c> struct (the small inner type) in route.go.
|
||||
/// </summary>
|
||||
internal sealed class RouteInfo
|
||||
{
|
||||
public Uri? Url { get; set; }
|
||||
public RouteType RouteType { get; set; }
|
||||
public byte GossipMode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CONNECT protocol payload exchanged between cluster servers.
|
||||
/// Fields map 1-to-1 with the JSON tags in Go's <c>connectInfo</c>.
|
||||
/// Mirrors Go <c>connectInfo</c> struct in route.go.
|
||||
/// </summary>
|
||||
internal sealed class ConnectInfo
|
||||
{
|
||||
[JsonPropertyName("echo")] public bool Echo { get; set; }
|
||||
[JsonPropertyName("verbose")] public bool Verbose { get; set; }
|
||||
[JsonPropertyName("pedantic")] public bool Pedantic { get; set; }
|
||||
[JsonPropertyName("user")] public string User { get; set; } = string.Empty;
|
||||
[JsonPropertyName("pass")] public string Pass { get; set; } = string.Empty;
|
||||
[JsonPropertyName("tls_required")] public bool Tls { get; set; }
|
||||
[JsonPropertyName("headers")] public bool Headers { get; set; }
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
|
||||
[JsonPropertyName("cluster")] public string Cluster { get; set; } = string.Empty;
|
||||
[JsonPropertyName("cluster_dynamic")] public bool Dynamic { get; set; }
|
||||
[JsonPropertyName("lnoc")] public bool Lnoc { get; set; }
|
||||
[JsonPropertyName("lnocu")] public bool Lnocu { get; set; }
|
||||
[JsonPropertyName("gateway")] public string Gateway { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds a set of subscriptions for a single account, used when fanning out
|
||||
/// route subscription interest.
|
||||
/// Mirrors Go <c>asubs</c> struct in route.go.
|
||||
/// </summary>
|
||||
internal sealed class ASubs
|
||||
{
|
||||
public Account? Account { get; set; }
|
||||
public List<Internal.Subscription> Subs { get; set; } = [];
|
||||
}
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 20:46:14 UTC
|
||||
Generated: 2026-02-26 20:50:51 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
@@ -13,9 +13,9 @@ Generated: 2026-02-26 20:46:14 UTC
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 1382 |
|
||||
| complete | 1601 |
|
||||
| n_a | 82 |
|
||||
| not_started | 2116 |
|
||||
| not_started | 1897 |
|
||||
| stub | 93 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
@@ -36,4 +36,4 @@ Generated: 2026-02-26 20:46:14 UTC
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**1975/6942 items complete (28.5%)**
|
||||
**2194/6942 items complete (31.6%)**
|
||||
|
||||
39
reports/report_ce45dff.md
Normal file
39
reports/report_ce45dff.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 20:50:51 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 11 |
|
||||
| not_started | 1 |
|
||||
|
||||
## Features (3673 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 1601 |
|
||||
| n_a | 82 |
|
||||
| not_started | 1897 |
|
||||
| stub | 93 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 319 |
|
||||
| n_a | 181 |
|
||||
| not_started | 2533 |
|
||||
| stub | 224 |
|
||||
|
||||
## Library Mappings (36 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| mapped | 36 |
|
||||
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**2194/6942 items complete (31.6%)**
|
||||
Reference in New Issue
Block a user