// Copyright 2017-2026 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/reload.go in the NATS server Go source. using System.Reflection; using System.Net.Security; using ZB.MOM.NatsNet.Server.Auth; using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server; // ============================================================================= // IReloadOption — mirrors Go `option` interface in reload.go // ============================================================================= /// /// Represents a hot-swappable configuration setting that can be applied to a /// running server. Mirrors Go option interface in server/reload.go. /// public interface IReloadOption { /// Apply this option to the running server. void Apply(NatsServer server); /// Returns true if this option requires reloading the logger. bool IsLoggingChange(); /// /// Returns true if this option requires reloading the cached trace level. /// Clients store trace level separately. /// bool IsTraceLevelChange(); /// Returns true if this option requires reloading authorization. bool IsAuthChange(); /// Returns true if this option requires reloading TLS. bool IsTlsChange(); /// Returns true if this option requires reloading cluster permissions. bool IsClusterPermsChange(); /// /// Returns true if this option requires special handling for changes in /// cluster pool size or accounts list. /// bool IsClusterPoolSizeOrAccountsChange(); /// /// Returns true if this option indicates a change in the server's JetStream config. /// Account changes are handled separately in reloadAuthorization. /// bool IsJetStreamChange(); /// Returns true if this change requires publishing the server's statz. bool IsStatszChange(); } // ============================================================================= // NoopReloadOption — mirrors Go `noopOption` struct in reload.go // ============================================================================= /// /// Base class providing no-op implementations for all /// methods. Concrete option types override only the methods relevant to them. /// Mirrors Go noopOption struct in server/reload.go. /// public abstract class NoopReloadOption : IReloadOption { /// public virtual void Apply(NatsServer server) => _ = server; /// public virtual bool IsLoggingChange() => false; /// public virtual bool IsTraceLevelChange() => false; /// public virtual bool IsAuthChange() => false; /// public virtual bool IsTlsChange() => false; /// public virtual bool IsClusterPermsChange() => false; /// public virtual bool IsClusterPoolSizeOrAccountsChange() => false; /// public virtual bool IsJetStreamChange() => false; /// public virtual bool IsStatszChange() => false; } // ============================================================================= // Intermediate base classes (mirrors Go loggingOption / traceLevelOption) // ============================================================================= /// /// Base for all logging-related reload options. /// Mirrors Go loggingOption struct. /// internal abstract class LoggingReloadOption : NoopReloadOption { public override bool IsLoggingChange() => true; } /// /// Base for all trace-level reload options. /// Mirrors Go traceLevelOption struct. /// internal abstract class TraceLevelReloadOption : LoggingReloadOption { public override bool IsTraceLevelChange() => true; } /// /// Base for all authorization-related reload options. /// Mirrors Go authOption struct. /// internal abstract class AuthReloadOption : NoopReloadOption { public override bool IsAuthChange() => true; } /// /// Base for TLS reload options. /// Mirrors Go tlsOption (as a base, not the concrete type). /// internal abstract class TlsBaseReloadOption : NoopReloadOption { public override bool IsTlsChange() => true; } // ============================================================================= // Logging & Trace option types // ============================================================================= /// /// Reload option for the trace setting. /// Mirrors Go traceOption struct in reload.go. /// internal sealed class TraceReloadOption : TraceLevelReloadOption { private readonly bool _newValue; public TraceReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: trace = {0}", _newValue); } /// /// Reload option for the trace_verbose setting. /// Mirrors Go traceVerboseOption struct in reload.go. /// internal sealed class TraceVerboseReloadOption : TraceLevelReloadOption { private readonly bool _newValue; public TraceVerboseReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: trace_verbose = {0}", _newValue); } /// /// Reload option for the trace_headers setting. /// Mirrors Go traceHeadersOption struct in reload.go. /// internal sealed class TraceHeadersReloadOption : TraceLevelReloadOption { private readonly bool _newValue; public TraceHeadersReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: trace_headers = {0}", _newValue); } /// /// Reload option for the debug setting. /// Mirrors Go debugOption struct in reload.go. /// internal sealed class DebugReloadOption : LoggingReloadOption { private readonly bool _newValue; public DebugReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) { server.Noticef("Reloaded: debug = {0}", _newValue); // DEFERRED: session 13 — call server.ReloadDebugRaftNodes(_newValue) } } /// /// Reload option for the logtime setting. /// Mirrors Go logtimeOption struct in reload.go. /// internal sealed class LogtimeReloadOption : LoggingReloadOption { private readonly bool _newValue; public LogtimeReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: logtime = {0}", _newValue); } /// /// Reload option for the logtime_utc setting. /// Mirrors Go logtimeUTCOption struct in reload.go. /// internal sealed class LogtimeUTCOption : LoggingReloadOption { private readonly bool _newValue; public LogtimeUTCOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: logtime_utc = {0}", _newValue); } /// /// Reload option for the log_file setting. /// Mirrors Go logfileOption struct in reload.go. /// internal sealed class LogfileOption : LoggingReloadOption { private readonly string _newValue; public LogfileOption(string newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: log_file = {0}", _newValue); } /// /// Reload option for the syslog setting. /// Mirrors Go syslogOption struct in reload.go. /// internal sealed class SyslogReloadOption : LoggingReloadOption { private readonly bool _newValue; public SyslogReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: syslog = {0}", _newValue); } /// /// Reload option for the remote_syslog setting. /// Mirrors Go remoteSyslogOption struct in reload.go. /// internal sealed class RemoteSyslogReloadOption : LoggingReloadOption { private readonly string _newValue; public RemoteSyslogReloadOption(string newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: remote_syslog = {0}", _newValue); } // ============================================================================= // TLS option types // ============================================================================= /// /// Reload option for the tls setting. /// Mirrors Go tlsOption struct in reload.go. /// The TLS config is stored as object? because the full /// TlsConfig type is not yet ported. /// DEFERRED: session 13 — replace object? with the ported TlsConfig type. /// internal sealed class TlsReloadOption : NoopReloadOption { // DEFERRED: session 13 — replace object? with ported TlsConfig type private readonly object? _newValue; public TlsReloadOption(object? newValue) => _newValue = newValue; public override bool IsTlsChange() => true; public override void Apply(NatsServer server) { var message = _newValue is null ? "disabled" : "enabled"; server.Noticef("Reloaded: tls = {0}", message); // DEFERRED: session 13 — update server.Info.TLSRequired / TLSVerify } } /// /// Reload option for the TLS timeout setting. /// Mirrors Go tlsTimeoutOption struct in reload.go. /// internal sealed class TlsTimeoutReloadOption : NoopReloadOption { private readonly double _newValue; public TlsTimeoutReloadOption(double newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: tls timeout = {0}", _newValue); } /// /// Reload option for the TLS pinned_certs setting. /// Mirrors Go tlsPinnedCertOption struct in reload.go. /// The pinned cert set is stored as object? pending the port /// of the PinnedCertSet type. /// DEFERRED: session 13 — replace object? with ported PinnedCertSet type. /// internal sealed class TlsPinnedCertReloadOption : NoopReloadOption { // DEFERRED: session 13 — replace object? with ported PinnedCertSet type private readonly object? _newValue; public TlsPinnedCertReloadOption(object? newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: pinned_certs"); } /// /// Reload option for the TLS handshake_first setting. /// Mirrors Go tlsHandshakeFirst struct in reload.go. /// internal sealed class TlsHandshakeFirstReloadOption : NoopReloadOption { private readonly bool _newValue; public TlsHandshakeFirstReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: Client TLS handshake first: {0}", _newValue); } /// /// Reload option for the TLS handshake_first_fallback delay setting. /// Mirrors Go tlsHandshakeFirstFallback struct in reload.go. /// internal sealed class TlsHandshakeFirstFallbackReloadOption : NoopReloadOption { private readonly TimeSpan _newValue; public TlsHandshakeFirstFallbackReloadOption(TimeSpan newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: Client TLS handshake first fallback delay: {0}", _newValue); } // ============================================================================= // Authorization option types // ============================================================================= /// /// Reload option for the username authorization setting. /// Mirrors Go usernameOption struct in reload.go. /// internal sealed class UsernameReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization username"); } /// /// Reload option for the password authorization setting. /// Mirrors Go passwordOption struct in reload.go. /// internal sealed class PasswordReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization password"); } /// /// Reload option for the token authorization setting. /// Mirrors Go authorizationOption struct in reload.go. /// internal sealed class AuthorizationReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization token"); } /// /// Reload option for the authorization timeout setting. /// Note: this is a NoopReloadOption (not auth) because authorization /// will be reloaded with options separately. /// Mirrors Go authTimeoutOption struct in reload.go. /// internal sealed class AuthTimeoutReloadOption : NoopReloadOption { private readonly double _newValue; public AuthTimeoutReloadOption(double newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization timeout = {0}", _newValue); } /// /// Reload option for the tags setting. /// Mirrors Go tagsOption struct in reload.go. /// internal sealed class TagsReloadOption : NoopReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: tags"); public override bool IsStatszChange() => true; } /// /// Reload option for the metadata setting. /// Mirrors Go metadataOption struct in reload.go. /// internal sealed class MetadataReloadOption : NoopReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: metadata"); public override bool IsStatszChange() => true; } /// /// Reload option for the authorization users setting. /// Mirrors Go usersOption struct in reload.go. /// internal sealed class UsersReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization users"); } /// /// Reload option for the authorization nkeys setting. /// Mirrors Go nkeysOption struct in reload.go. /// internal sealed class NkeysReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: authorization nkey users"); } /// /// Reload option for the accounts setting. /// Mirrors Go accountsOption struct in reload.go. /// internal sealed class AccountsReloadOption : AuthReloadOption { public override void Apply(NatsServer server) => server.Noticef("Reloaded: accounts"); } // ============================================================================= // Cluster option types // ============================================================================= /// /// Reload option for the cluster setting. /// Stores cluster options as object? pending the port of ClusterOpts. /// Mirrors Go clusterOption struct in reload.go. /// DEFERRED: session 13 — replace object? with ported ClusterOpts type. /// internal sealed class ClusterReloadOption : AuthReloadOption { // DEFERRED: session 13 — replace object? with ported ClusterOpts type private readonly object? _newValue; private readonly bool _permsChanged; private bool _poolSizeChanged; private readonly bool _compressChanged; private string[] _accsAdded; private string[] _accsRemoved; public ClusterReloadOption( object? newValue, bool permsChanged, bool poolSizeChanged, bool compressChanged, string[] accsAdded, string[] accsRemoved) { _newValue = newValue; _permsChanged = permsChanged; _poolSizeChanged = poolSizeChanged; _compressChanged = compressChanged; _accsAdded = accsAdded; _accsRemoved = accsRemoved; } public override bool IsClusterPermsChange() => _permsChanged; public override bool IsClusterPoolSizeOrAccountsChange() => _poolSizeChanged || _accsAdded.Length > 0 || _accsRemoved.Length > 0; internal ClusterOpts? ClusterValue => _newValue as ClusterOpts; internal bool PoolSizeChanged => _poolSizeChanged; internal bool CompressChanged => _compressChanged; internal IReadOnlyList AccountsAdded => _accsAdded; internal IReadOnlyList AccountsRemoved => _accsRemoved; /// /// Computes pool/account deltas used by reload orchestration. /// Mirrors Go clusterOption.diffPoolAndAccounts. /// public void DiffPoolAndAccounts(ClusterOpts oldValue) { ArgumentNullException.ThrowIfNull(oldValue); if (_newValue is not ClusterOpts newValue) { _poolSizeChanged = false; _accsAdded = []; _accsRemoved = []; return; } _poolSizeChanged = newValue.PoolSize != oldValue.PoolSize; var oldAccounts = new HashSet(oldValue.PinnedAccounts, StringComparer.Ordinal); var newAccounts = new HashSet(newValue.PinnedAccounts, StringComparer.Ordinal); _accsAdded = newAccounts.Except(oldAccounts).ToArray(); _accsRemoved = oldAccounts.Except(newAccounts).ToArray(); } public override void Apply(NatsServer server) { // DEFERRED: session 13 — full cluster apply logic (TLS, route info, compression) server.Noticef("Reloaded: cluster"); } } /// /// Reload option for the cluster routes setting. /// Routes to add/remove are stored as object[] pending the port of URL handling. /// Mirrors Go routesOption struct in reload.go. /// DEFERRED: session 13 — replace object[] with Uri[] when route types are ported. /// internal sealed class RoutesReloadOption : NoopReloadOption { // DEFERRED: session 13 — replace object[] with Uri[] when route URL types are ported private readonly object[] _add; private readonly object[] _remove; public RoutesReloadOption(object[] add, object[] remove) { _add = add; _remove = remove; } public override void Apply(NatsServer server) { // DEFERRED: session 13 — add/remove routes, update varzUpdateRouteURLs server.Noticef("Reloaded: cluster routes"); } } // ============================================================================= // Connection limit & network option types // ============================================================================= /// /// Reload option for the max_connections setting. /// Mirrors Go maxConnOption struct in reload.go. /// internal sealed class MaxConnReloadOption : NoopReloadOption { private readonly int _newValue; public MaxConnReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — close random connections if over limit server.Noticef("Reloaded: max_connections = {0}", _newValue); } } /// /// Reload option for the pid_file setting. /// Mirrors Go pidFileOption struct in reload.go. /// internal sealed class PidFileReloadOption : NoopReloadOption { private readonly string _newValue; public PidFileReloadOption(string newValue) => _newValue = newValue; public override void Apply(NatsServer server) { if (string.IsNullOrEmpty(_newValue)) return; // DEFERRED: session 13 — call server.LogPid() server.Noticef("Reloaded: pid_file = {0}", _newValue); } } /// /// Reload option for the ports_file_dir setting. /// Mirrors Go portsFileDirOption struct in reload.go. /// internal sealed class PortsFileDirReloadOption : NoopReloadOption { private readonly string _oldValue; private readonly string _newValue; public PortsFileDirReloadOption(string oldValue, string newValue) { _oldValue = oldValue; _newValue = newValue; } public override void Apply(NatsServer server) { // DEFERRED: session 13 — call server.DeletePortsFile(_oldValue) and server.LogPorts() server.Noticef("Reloaded: ports_file_dir = {0}", _newValue); } } /// /// Reload option for the max_control_line setting. /// Mirrors Go maxControlLineOption struct in reload.go. /// internal sealed class MaxControlLineReloadOption : NoopReloadOption { private readonly int _newValue; public MaxControlLineReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — update mcl on each connected client server.Noticef("Reloaded: max_control_line = {0}", _newValue); } } /// /// Reload option for the max_payload setting. /// Mirrors Go maxPayloadOption struct in reload.go. /// internal sealed class MaxPayloadReloadOption : NoopReloadOption { private readonly int _newValue; public MaxPayloadReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — update server info and mpay on each client server.Noticef("Reloaded: max_payload = {0}", _newValue); } } /// /// Reload option for the ping_interval setting. /// Mirrors Go pingIntervalOption struct in reload.go. /// internal sealed class PingIntervalReloadOption : NoopReloadOption { private readonly TimeSpan _newValue; public PingIntervalReloadOption(TimeSpan newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: ping_interval = {0}", _newValue); } /// /// Reload option for the ping_max setting. /// Mirrors Go maxPingsOutOption struct in reload.go. /// internal sealed class MaxPingsOutReloadOption : NoopReloadOption { private readonly int _newValue; public MaxPingsOutReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: ping_max = {0}", _newValue); } /// /// Reload option for the write_deadline setting. /// Mirrors Go writeDeadlineOption struct in reload.go. /// internal sealed class WriteDeadlineReloadOption : NoopReloadOption { private readonly TimeSpan _newValue; public WriteDeadlineReloadOption(TimeSpan newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: write_deadline = {0}", _newValue); } /// /// Reload option for the client_advertise setting. /// Mirrors Go clientAdvertiseOption struct in reload.go. /// internal sealed class ClientAdvertiseReloadOption : NoopReloadOption { private readonly string _newValue; public ClientAdvertiseReloadOption(string newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — call server.SetInfoHostPort() server.Noticef("Reload: client_advertise = {0}", _newValue); } } // ============================================================================= // JetStream option type // ============================================================================= /// /// Reload option for the jetstream setting. /// Mirrors Go jetStreamOption struct in reload.go. /// internal sealed class JetStreamReloadOption : NoopReloadOption { private readonly bool _newValue; public JetStreamReloadOption(bool newValue) => _newValue = newValue; internal bool Value => _newValue; public override bool IsJetStreamChange() => true; public override bool IsStatszChange() => true; public override void Apply(NatsServer server) => server.Noticef("Reloaded: JetStream"); } // ============================================================================= // Miscellaneous option types // ============================================================================= /// /// Reload option for the default_sentinel setting. /// Mirrors Go defaultSentinelOption struct in reload.go. /// internal sealed class DefaultSentinelReloadOption : NoopReloadOption { private readonly string _newValue; public DefaultSentinelReloadOption(string newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: default_sentinel = {0}", _newValue); } /// /// Reload option for the OCSP setting. /// The new value is stored as object? pending the port of OCSPConfig. /// Mirrors Go ocspOption struct in reload.go. /// DEFERRED: session 13 — replace object? with ported OcspConfig type. /// internal sealed class OcspReloadOption : TlsBaseReloadOption { // DEFERRED: session 13 — replace object? with ported OcspConfig type private readonly object? _newValue; public OcspReloadOption(object? newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: OCSP"); } /// /// Reload option for the OCSP response cache setting. /// The new value is stored as object? pending the port of /// OCSPResponseCacheConfig. /// Mirrors Go ocspResponseCacheOption struct in reload.go. /// DEFERRED: session 13 — replace object? with ported OcspResponseCacheConfig type. /// internal sealed class OcspResponseCacheReloadOption : TlsBaseReloadOption { // DEFERRED: session 13 — replace object? with ported OcspResponseCacheConfig type private readonly object? _newValue; public OcspResponseCacheReloadOption(object? newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded OCSP peer cache"); } /// /// Reload option for the connect_error_reports setting. /// Mirrors Go connectErrorReports struct in reload.go. /// internal sealed class ConnectErrorReportsReloadOption : NoopReloadOption { private readonly int _newValue; public ConnectErrorReportsReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: connect_error_reports = {0}", _newValue); } /// /// Reload option for the reconnect_error_reports setting. /// Mirrors Go reconnectErrorReports struct in reload.go. /// internal sealed class ReconnectErrorReportsReloadOption : NoopReloadOption { private readonly int _newValue; public ReconnectErrorReportsReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: reconnect_error_reports = {0}", _newValue); } /// /// Reload option for the max_traced_msg_len setting. /// Mirrors Go maxTracedMsgLenOption struct in reload.go. /// internal sealed class MaxTracedMsgLenReloadOption : NoopReloadOption { private readonly int _newValue; public MaxTracedMsgLenReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — update server.Opts.MaxTracedMsgLen under lock server.Noticef("Reloaded: max_traced_msg_len = {0}", _newValue); } } // ============================================================================= // MQTT option types // ============================================================================= /// /// Reload option for the MQTT ack_wait setting. /// Mirrors Go mqttAckWaitReload struct in reload.go. /// internal sealed class MqttAckWaitReloadOption : NoopReloadOption { private readonly TimeSpan _newValue; public MqttAckWaitReloadOption(TimeSpan newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: MQTT ack_wait = {0}", _newValue); } /// /// Reload option for the MQTT max_ack_pending setting. /// Mirrors Go mqttMaxAckPendingReload struct in reload.go. /// internal sealed class MqttMaxAckPendingReloadOption : NoopReloadOption { private readonly ushort _newValue; public MqttMaxAckPendingReloadOption(ushort newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — call server.MqttUpdateMaxAckPending(_newValue) server.Noticef("Reloaded: MQTT max_ack_pending = {0}", _newValue); } } /// /// Reload option for the MQTT stream_replicas setting. /// Mirrors Go mqttStreamReplicasReload struct in reload.go. /// internal sealed class MqttStreamReplicasReloadOption : NoopReloadOption { private readonly int _newValue; public MqttStreamReplicasReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: MQTT stream_replicas = {0}", _newValue); } /// /// Reload option for the MQTT consumer_replicas setting. /// Mirrors Go mqttConsumerReplicasReload struct in reload.go. /// internal sealed class MqttConsumerReplicasReloadOption : NoopReloadOption { private readonly int _newValue; public MqttConsumerReplicasReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: MQTT consumer_replicas = {0}", _newValue); } /// /// Reload option for the MQTT consumer_memory_storage setting. /// Mirrors Go mqttConsumerMemoryStorageReload struct in reload.go. /// internal sealed class MqttConsumerMemoryStorageReloadOption : NoopReloadOption { private readonly bool _newValue; public MqttConsumerMemoryStorageReloadOption(bool newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: MQTT consumer_memory_storage = {0}", _newValue); } /// /// Reload option for the MQTT consumer_inactive_threshold setting. /// Mirrors Go mqttInactiveThresholdReload struct in reload.go. /// internal sealed class MqttInactiveThresholdReloadOption : NoopReloadOption { private readonly TimeSpan _newValue; public MqttInactiveThresholdReloadOption(TimeSpan newValue) => _newValue = newValue; public override void Apply(NatsServer server) => server.Noticef("Reloaded: MQTT consumer_inactive_threshold = {0}", _newValue); } // ============================================================================= // Profiling option type // ============================================================================= /// /// Reload option for the prof_block_rate setting. /// Mirrors Go profBlockRateReload struct in reload.go. /// internal sealed class ProfBlockRateReloadOption : NoopReloadOption { private readonly int _newValue; public ProfBlockRateReloadOption(int newValue) => _newValue = newValue; public override void Apply(NatsServer server) { // DEFERRED: session 13 — call server.SetBlockProfileRate(_newValue) server.Noticef("Reloaded: prof_block_rate = {0}", _newValue); } } // ============================================================================= // LeafNode option type // ============================================================================= /// /// Reload option for leaf-node settings (TLS handshake-first, compression, disabled). /// Mirrors Go leafNodeOption struct in reload.go. /// internal sealed class LeafNodeReloadOption : NoopReloadOption { private readonly bool _tlsFirstChanged; private readonly bool _compressionChanged; private readonly bool _disabledChanged; public LeafNodeReloadOption(bool tlsFirstChanged, bool compressionChanged, bool disabledChanged) { _tlsFirstChanged = tlsFirstChanged; _compressionChanged = compressionChanged; _disabledChanged = disabledChanged; } public override void Apply(NatsServer server) { // DEFERRED: session 13 — full leaf-node apply logic from Go leafNodeOption.Apply() if (_tlsFirstChanged) server.Noticef("Reloaded: LeafNode TLS HandshakeFirst settings"); if (_compressionChanged) server.Noticef("Reloaded: LeafNode compression settings"); if (_disabledChanged) server.Noticef("Reloaded: LeafNode disabled/enabled state"); } } // ============================================================================= // NoFastProducerStall option type // ============================================================================= /// /// Reload option for the no_fast_producer_stall setting. /// Mirrors Go noFastProdStallReload struct in reload.go. /// internal sealed class NoFastProducerStallReloadOption : NoopReloadOption { private readonly bool _noStall; public NoFastProducerStallReloadOption(bool noStall) => _noStall = noStall; public override void Apply(NatsServer server) { var not = _noStall ? "not " : string.Empty; server.Noticef("Reloaded: fast producers will {0}be stalled", not); } } // ============================================================================= // Proxies option type // ============================================================================= /// /// Reload option for the proxies trusted keys setting. /// Mirrors Go proxiesReload struct in reload.go. /// internal sealed class ProxiesReloadOption : NoopReloadOption { private readonly string[] _add; private readonly string[] _del; public ProxiesReloadOption(string[] add, string[] del) { _add = add; _del = del; } public override void Apply(NatsServer server) { // DEFERRED: session 13 — disconnect proxied clients for removed keys, // call server.ProcessProxiesTrustedKeys() if (_del.Length > 0) server.Noticef("Reloaded: proxies trusted keys {0} were removed", string.Join(", ", _del)); if (_add.Length > 0) server.Noticef("Reloaded: proxies trusted keys {0} were added", string.Join(", ", _add)); } } // ============================================================================= // ConfigReloader — stub for server/reload.go Reload() / ReloadOptions() // ============================================================================= /// /// Stub for the configuration reloader. /// Full reload logic (diffOptions, applyOptions, recheckPinnedCerts) will be /// implemented in a future session. /// Mirrors Go Server.Reload() and Server.ReloadOptions() in /// server/reload.go. /// internal sealed class ConfigReloader { // DEFERRED: session 13 — full reload logic // Mirrors Go server.Reload() / server.ReloadOptions() in server/reload.go /// /// Stub: read and apply the server config file. /// Returns null on success; a non-null Exception describes the failure. /// public Exception? Reload(NatsServer server) => null; /// /// Applies bool-valued config precedence for reload: /// config-file explicit values first, then explicit command-line flags. /// Mirrors Go applyBoolFlags. /// public static void ApplyBoolFlags(ServerOptions newOptions, ServerOptions flagOptions) { ArgumentNullException.ThrowIfNull(newOptions); ArgumentNullException.ThrowIfNull(flagOptions); foreach (var (name, value) in newOptions.InConfig) SetBooleanMember(newOptions, name, value); foreach (var (name, value) in flagOptions.InCmdLine) SetBooleanMember(newOptions, name, value); } /// /// Sorts order-insensitive values in place prior to deep comparisons. /// Mirrors Go imposeOrder. /// public static Exception? ImposeOrder(object? value) { switch (value) { case List accounts: accounts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name)); break; case List users: users.Sort((a, b) => string.CompareOrdinal(a.Username, b.Username)); break; case List nkeys: nkeys.Sort((a, b) => string.CompareOrdinal(a.Nkey, b.Nkey)); break; case List urls: urls.Sort((a, b) => string.CompareOrdinal(a.ToString(), b.ToString())); break; case List strings: strings.Sort(StringComparer.Ordinal); break; case GatewayOpts gateway: gateway.Gateways.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name)); break; case WebsocketOpts websocket: websocket.AllowedOrigins.Sort(StringComparer.Ordinal); break; case null: case string: case bool: case byte: case ushort: case uint: case ulong: case int: case long: case TimeSpan: case float: case double: case LeafNodeOpts: case ClusterOpts: case SslServerAuthenticationOptions: case PinnedCertSet: case IAccountResolver: case MqttOpts: case Dictionary: case JsLimitOpts: case StoreCipher: case OcspResponseCacheConfig: case ProxiesConfig: case WriteTimeoutPolicy: case AuthCalloutOpts: case JsTpmOpts: break; default: return new InvalidOperationException( $"OnReload, sort or explicitly skip type: {value.GetType().FullName}"); } return null; } /// /// Returns remote gateway configs copied for diffing, with TLS runtime fields stripped. /// Mirrors Go copyRemoteGWConfigsWithoutTLSConfig. /// public static List? CopyRemoteGWConfigsWithoutTLSConfig(List? current) { if (current is not { Count: > 0 }) return null; var copied = new List(current.Count); foreach (var config in current) { copied.Add(new RemoteGatewayOpts { Name = config.Name, TlsConfig = null, TlsConfigOpts = null, TlsTimeout = config.TlsTimeout, Urls = [.. config.Urls.Select(static u => new Uri(u.ToString(), UriKind.Absolute))], }); } return copied; } /// /// Returns remote leaf-node configs copied for diffing, with runtime-mutated /// fields stripped or normalized. /// Mirrors Go copyRemoteLNConfigForReloadCompare. /// public static List? CopyRemoteLNConfigForReloadCompare(List? current) { if (current is not { Count: > 0 }) return null; var copied = new List(current.Count); foreach (var config in current) { copied.Add(new RemoteLeafOpts { LocalAccount = config.LocalAccount, NoRandomize = config.NoRandomize, Urls = [.. config.Urls.Select(static u => new Uri(u.ToString(), UriKind.Absolute))], Credentials = config.Credentials, Nkey = config.Nkey, SignatureCb = config.SignatureCb, Tls = false, TlsConfig = null, TlsConfigOpts = null, TlsTimeout = config.TlsTimeout, TlsHandshakeFirst = false, Hub = config.Hub, DenyImports = [], DenyExports = [], FirstInfoTimeout = config.FirstInfoTimeout, Compression = new CompressionOpts(), Websocket = new RemoteLeafWebsocketOpts { Compression = config.Websocket.Compression, NoMasking = config.Websocket.NoMasking, }, Proxy = new RemoteLeafProxyOpts { Url = config.Proxy.Url, Username = config.Proxy.Username, Password = config.Proxy.Password, Timeout = config.Proxy.Timeout, }, JetStreamClusterMigrate = config.JetStreamClusterMigrate, JetStreamClusterMigrateDelay = config.JetStreamClusterMigrateDelay, LocalIsolation = config.LocalIsolation, RequestIsolation = config.RequestIsolation, Disabled = false, }); } return copied; } /// /// Validates non-reloadable cluster settings and advertise syntax. /// Mirrors Go validateClusterOpts. /// public static Exception? ValidateClusterOpts(ClusterOpts oldValue, ClusterOpts newValue) { ArgumentNullException.ThrowIfNull(oldValue); ArgumentNullException.ThrowIfNull(newValue); if (!string.Equals(oldValue.Host, newValue.Host, StringComparison.Ordinal)) { return new InvalidOperationException( $"config reload not supported for cluster host: old={oldValue.Host}, new={newValue.Host}"); } if (oldValue.Port != newValue.Port) { return new InvalidOperationException( $"config reload not supported for cluster port: old={oldValue.Port}, new={newValue.Port}"); } if (!string.IsNullOrEmpty(newValue.Advertise)) { var (_, _, err) = ServerUtilities.ParseHostPort(newValue.Advertise, 0); if (err != null) return new InvalidOperationException( $"invalid Cluster.Advertise value of {newValue.Advertise}, err={err.Message}", err); } return null; } /// /// Diffs old/new route lists and returns routes to add/remove. /// Mirrors Go diffRoutes. /// public static (List Add, List Remove) DiffRoutes(IReadOnlyList oldRoutes, IReadOnlyList newRoutes) { ArgumentNullException.ThrowIfNull(oldRoutes); ArgumentNullException.ThrowIfNull(newRoutes); var add = new List(); var remove = new List(); foreach (var oldRoute in oldRoutes) { if (!newRoutes.Any(newRoute => ServerUtilities.UrlsAreEqual(oldRoute, newRoute))) remove.Add(oldRoute); } foreach (var newRoute in newRoutes) { if (!oldRoutes.Any(oldRoute => ServerUtilities.UrlsAreEqual(oldRoute, newRoute))) add.Add(newRoute); } return (add, remove); } /// /// Diffs proxy trusted keys and returns added/removed key sets. /// Mirrors Go diffProxiesTrustedKeys. /// public static (List Add, List Del) DiffProxiesTrustedKeys( IReadOnlyList? oldTrusted, IReadOnlyList? newTrusted) { var oldList = oldTrusted ?? []; var newList = newTrusted ?? []; var add = new List(); var del = new List(); foreach (var oldProxy in oldList) { if (!newList.Any(np => string.Equals(np.Key, oldProxy.Key, StringComparison.Ordinal))) del.Add(oldProxy.Key); } foreach (var newProxy in newList) { if (!oldList.Any(op => string.Equals(op.Key, newProxy.Key, StringComparison.Ordinal))) add.Add(newProxy.Key); } return (add, del); } private static void SetBooleanMember(ServerOptions options, string path, bool value) { var segments = path.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (segments.Length == 0) return; object? target = options; for (int i = 0; i < segments.Length - 1; i++) { if (target == null) return; var segment = segments[i]; var targetType = target.GetType(); var prop = targetType.GetProperty(segment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (prop != null) { target = prop.GetValue(target); continue; } var field = targetType.GetField(segment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { target = field.GetValue(target); continue; } return; } if (target == null) return; var leaf = segments[^1]; var leafType = target.GetType(); var leafProperty = leafType.GetProperty(leaf, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (leafProperty?.PropertyType == typeof(bool) && leafProperty.CanWrite) { leafProperty.SetValue(target, value); return; } var leafField = leafType.GetField(leaf, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (leafField?.FieldType == typeof(bool)) leafField.SetValue(target, value); } }