// 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);
}
}