Port of Go server/reload.go option interface and diffing logic. Compares NatsOptions property-by-property to detect changes, tags each with category flags (logging, auth, TLS, non-reloadable), validates that non-reloadable options (Host, Port, ServerName) are not changed at runtime, and provides MergeCliOverrides to ensure CLI flags always take precedence over config file values during hot reload.
342 lines
16 KiB
C#
342 lines
16 KiB
C#
// Port of Go server/reload.go — config diffing, validation, and CLI override merging
|
|
// for hot reload support. Reference: golang/nats-server/server/reload.go.
|
|
|
|
namespace NATS.Server.Configuration;
|
|
|
|
/// <summary>
|
|
/// Provides static methods for comparing two <see cref="NatsOptions"/> instances,
|
|
/// validating that detected changes are reloadable, and merging CLI overrides
|
|
/// so that command-line flags always take precedence over config file values.
|
|
/// </summary>
|
|
public static class ConfigReloader
|
|
{
|
|
// Non-reloadable options (match Go server — Host, Port, ServerName require restart)
|
|
private static readonly HashSet<string> NonReloadable = ["Host", "Port", "ServerName"];
|
|
|
|
// Logging-related options
|
|
private static readonly HashSet<string> LoggingOptions =
|
|
["Debug", "Trace", "TraceVerbose", "Logtime", "LogtimeUTC", "LogFile",
|
|
"LogSizeLimit", "LogMaxFiles", "Syslog", "RemoteSyslog"];
|
|
|
|
// Auth-related options
|
|
private static readonly HashSet<string> AuthOptions =
|
|
["Username", "Password", "Authorization", "Users", "NKeys",
|
|
"NoAuthUser", "AuthTimeout"];
|
|
|
|
// TLS-related options
|
|
private static readonly HashSet<string> TlsOptions =
|
|
["TlsCert", "TlsKey", "TlsCaCert", "TlsVerify", "TlsMap",
|
|
"TlsTimeout", "TlsHandshakeFirst", "TlsHandshakeFirstFallback",
|
|
"AllowNonTls", "TlsRateLimit", "TlsPinnedCerts"];
|
|
|
|
/// <summary>
|
|
/// Compares two <see cref="NatsOptions"/> instances property by property and returns
|
|
/// a list of <see cref="IConfigChange"/> for every property that differs. Each change
|
|
/// is tagged with the appropriate category flags.
|
|
/// </summary>
|
|
public static List<IConfigChange> Diff(NatsOptions oldOpts, NatsOptions newOpts)
|
|
{
|
|
var changes = new List<IConfigChange>();
|
|
|
|
// Non-reloadable
|
|
CompareAndAdd(changes, "Host", oldOpts.Host, newOpts.Host);
|
|
CompareAndAdd(changes, "Port", oldOpts.Port, newOpts.Port);
|
|
CompareAndAdd(changes, "ServerName", oldOpts.ServerName, newOpts.ServerName);
|
|
|
|
// Logging
|
|
CompareAndAdd(changes, "Debug", oldOpts.Debug, newOpts.Debug);
|
|
CompareAndAdd(changes, "Trace", oldOpts.Trace, newOpts.Trace);
|
|
CompareAndAdd(changes, "TraceVerbose", oldOpts.TraceVerbose, newOpts.TraceVerbose);
|
|
CompareAndAdd(changes, "Logtime", oldOpts.Logtime, newOpts.Logtime);
|
|
CompareAndAdd(changes, "LogtimeUTC", oldOpts.LogtimeUTC, newOpts.LogtimeUTC);
|
|
CompareAndAdd(changes, "LogFile", oldOpts.LogFile, newOpts.LogFile);
|
|
CompareAndAdd(changes, "LogSizeLimit", oldOpts.LogSizeLimit, newOpts.LogSizeLimit);
|
|
CompareAndAdd(changes, "LogMaxFiles", oldOpts.LogMaxFiles, newOpts.LogMaxFiles);
|
|
CompareAndAdd(changes, "Syslog", oldOpts.Syslog, newOpts.Syslog);
|
|
CompareAndAdd(changes, "RemoteSyslog", oldOpts.RemoteSyslog, newOpts.RemoteSyslog);
|
|
|
|
// Auth
|
|
CompareAndAdd(changes, "Username", oldOpts.Username, newOpts.Username);
|
|
CompareAndAdd(changes, "Password", oldOpts.Password, newOpts.Password);
|
|
CompareAndAdd(changes, "Authorization", oldOpts.Authorization, newOpts.Authorization);
|
|
CompareCollectionAndAdd(changes, "Users", oldOpts.Users, newOpts.Users);
|
|
CompareCollectionAndAdd(changes, "NKeys", oldOpts.NKeys, newOpts.NKeys);
|
|
CompareAndAdd(changes, "NoAuthUser", oldOpts.NoAuthUser, newOpts.NoAuthUser);
|
|
CompareAndAdd(changes, "AuthTimeout", oldOpts.AuthTimeout, newOpts.AuthTimeout);
|
|
|
|
// TLS
|
|
CompareAndAdd(changes, "TlsCert", oldOpts.TlsCert, newOpts.TlsCert);
|
|
CompareAndAdd(changes, "TlsKey", oldOpts.TlsKey, newOpts.TlsKey);
|
|
CompareAndAdd(changes, "TlsCaCert", oldOpts.TlsCaCert, newOpts.TlsCaCert);
|
|
CompareAndAdd(changes, "TlsVerify", oldOpts.TlsVerify, newOpts.TlsVerify);
|
|
CompareAndAdd(changes, "TlsMap", oldOpts.TlsMap, newOpts.TlsMap);
|
|
CompareAndAdd(changes, "TlsTimeout", oldOpts.TlsTimeout, newOpts.TlsTimeout);
|
|
CompareAndAdd(changes, "TlsHandshakeFirst", oldOpts.TlsHandshakeFirst, newOpts.TlsHandshakeFirst);
|
|
CompareAndAdd(changes, "TlsHandshakeFirstFallback", oldOpts.TlsHandshakeFirstFallback, newOpts.TlsHandshakeFirstFallback);
|
|
CompareAndAdd(changes, "AllowNonTls", oldOpts.AllowNonTls, newOpts.AllowNonTls);
|
|
CompareAndAdd(changes, "TlsRateLimit", oldOpts.TlsRateLimit, newOpts.TlsRateLimit);
|
|
CompareCollectionAndAdd(changes, "TlsPinnedCerts", oldOpts.TlsPinnedCerts, newOpts.TlsPinnedCerts);
|
|
|
|
// Limits
|
|
CompareAndAdd(changes, "MaxConnections", oldOpts.MaxConnections, newOpts.MaxConnections);
|
|
CompareAndAdd(changes, "MaxPayload", oldOpts.MaxPayload, newOpts.MaxPayload);
|
|
CompareAndAdd(changes, "MaxPending", oldOpts.MaxPending, newOpts.MaxPending);
|
|
CompareAndAdd(changes, "WriteDeadline", oldOpts.WriteDeadline, newOpts.WriteDeadline);
|
|
CompareAndAdd(changes, "PingInterval", oldOpts.PingInterval, newOpts.PingInterval);
|
|
CompareAndAdd(changes, "MaxPingsOut", oldOpts.MaxPingsOut, newOpts.MaxPingsOut);
|
|
CompareAndAdd(changes, "MaxControlLine", oldOpts.MaxControlLine, newOpts.MaxControlLine);
|
|
CompareAndAdd(changes, "MaxSubs", oldOpts.MaxSubs, newOpts.MaxSubs);
|
|
CompareAndAdd(changes, "MaxSubTokens", oldOpts.MaxSubTokens, newOpts.MaxSubTokens);
|
|
CompareAndAdd(changes, "MaxTracedMsgLen", oldOpts.MaxTracedMsgLen, newOpts.MaxTracedMsgLen);
|
|
CompareAndAdd(changes, "MaxClosedClients", oldOpts.MaxClosedClients, newOpts.MaxClosedClients);
|
|
|
|
// Misc
|
|
CompareCollectionAndAdd(changes, "Tags", oldOpts.Tags, newOpts.Tags);
|
|
CompareAndAdd(changes, "LameDuckDuration", oldOpts.LameDuckDuration, newOpts.LameDuckDuration);
|
|
CompareAndAdd(changes, "LameDuckGracePeriod", oldOpts.LameDuckGracePeriod, newOpts.LameDuckGracePeriod);
|
|
CompareAndAdd(changes, "ClientAdvertise", oldOpts.ClientAdvertise, newOpts.ClientAdvertise);
|
|
CompareAndAdd(changes, "DisableSublistCache", oldOpts.DisableSublistCache, newOpts.DisableSublistCache);
|
|
CompareAndAdd(changes, "ConnectErrorReports", oldOpts.ConnectErrorReports, newOpts.ConnectErrorReports);
|
|
CompareAndAdd(changes, "ReconnectErrorReports", oldOpts.ReconnectErrorReports, newOpts.ReconnectErrorReports);
|
|
CompareAndAdd(changes, "NoHeaderSupport", oldOpts.NoHeaderSupport, newOpts.NoHeaderSupport);
|
|
CompareAndAdd(changes, "NoSystemAccount", oldOpts.NoSystemAccount, newOpts.NoSystemAccount);
|
|
CompareAndAdd(changes, "SystemAccount", oldOpts.SystemAccount, newOpts.SystemAccount);
|
|
|
|
return changes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a list of config changes and returns error messages for any
|
|
/// non-reloadable changes (properties that require a server restart).
|
|
/// </summary>
|
|
public static List<string> Validate(List<IConfigChange> changes)
|
|
{
|
|
var errors = new List<string>();
|
|
foreach (var change in changes)
|
|
{
|
|
if (change.IsNonReloadable)
|
|
{
|
|
errors.Add($"Config reload: '{change.Name}' cannot be changed at runtime (requires restart)");
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges CLI overrides into a freshly-parsed config so that command-line flags
|
|
/// always take precedence. Only properties whose names appear in <paramref name="cliFlags"/>
|
|
/// are copied from <paramref name="cliValues"/> to <paramref name="fromConfig"/>.
|
|
/// </summary>
|
|
public static void MergeCliOverrides(NatsOptions fromConfig, NatsOptions cliValues, HashSet<string> cliFlags)
|
|
{
|
|
foreach (var flag in cliFlags)
|
|
{
|
|
switch (flag)
|
|
{
|
|
// Non-reloadable
|
|
case "Host":
|
|
fromConfig.Host = cliValues.Host;
|
|
break;
|
|
case "Port":
|
|
fromConfig.Port = cliValues.Port;
|
|
break;
|
|
case "ServerName":
|
|
fromConfig.ServerName = cliValues.ServerName;
|
|
break;
|
|
|
|
// Logging
|
|
case "Debug":
|
|
fromConfig.Debug = cliValues.Debug;
|
|
break;
|
|
case "Trace":
|
|
fromConfig.Trace = cliValues.Trace;
|
|
break;
|
|
case "TraceVerbose":
|
|
fromConfig.TraceVerbose = cliValues.TraceVerbose;
|
|
break;
|
|
case "Logtime":
|
|
fromConfig.Logtime = cliValues.Logtime;
|
|
break;
|
|
case "LogtimeUTC":
|
|
fromConfig.LogtimeUTC = cliValues.LogtimeUTC;
|
|
break;
|
|
case "LogFile":
|
|
fromConfig.LogFile = cliValues.LogFile;
|
|
break;
|
|
case "LogSizeLimit":
|
|
fromConfig.LogSizeLimit = cliValues.LogSizeLimit;
|
|
break;
|
|
case "LogMaxFiles":
|
|
fromConfig.LogMaxFiles = cliValues.LogMaxFiles;
|
|
break;
|
|
case "Syslog":
|
|
fromConfig.Syslog = cliValues.Syslog;
|
|
break;
|
|
case "RemoteSyslog":
|
|
fromConfig.RemoteSyslog = cliValues.RemoteSyslog;
|
|
break;
|
|
|
|
// Auth
|
|
case "Username":
|
|
fromConfig.Username = cliValues.Username;
|
|
break;
|
|
case "Password":
|
|
fromConfig.Password = cliValues.Password;
|
|
break;
|
|
case "Authorization":
|
|
fromConfig.Authorization = cliValues.Authorization;
|
|
break;
|
|
case "Users":
|
|
fromConfig.Users = cliValues.Users;
|
|
break;
|
|
case "NKeys":
|
|
fromConfig.NKeys = cliValues.NKeys;
|
|
break;
|
|
case "NoAuthUser":
|
|
fromConfig.NoAuthUser = cliValues.NoAuthUser;
|
|
break;
|
|
case "AuthTimeout":
|
|
fromConfig.AuthTimeout = cliValues.AuthTimeout;
|
|
break;
|
|
|
|
// TLS
|
|
case "TlsCert":
|
|
fromConfig.TlsCert = cliValues.TlsCert;
|
|
break;
|
|
case "TlsKey":
|
|
fromConfig.TlsKey = cliValues.TlsKey;
|
|
break;
|
|
case "TlsCaCert":
|
|
fromConfig.TlsCaCert = cliValues.TlsCaCert;
|
|
break;
|
|
case "TlsVerify":
|
|
fromConfig.TlsVerify = cliValues.TlsVerify;
|
|
break;
|
|
case "TlsMap":
|
|
fromConfig.TlsMap = cliValues.TlsMap;
|
|
break;
|
|
case "TlsTimeout":
|
|
fromConfig.TlsTimeout = cliValues.TlsTimeout;
|
|
break;
|
|
case "TlsHandshakeFirst":
|
|
fromConfig.TlsHandshakeFirst = cliValues.TlsHandshakeFirst;
|
|
break;
|
|
case "TlsHandshakeFirstFallback":
|
|
fromConfig.TlsHandshakeFirstFallback = cliValues.TlsHandshakeFirstFallback;
|
|
break;
|
|
case "AllowNonTls":
|
|
fromConfig.AllowNonTls = cliValues.AllowNonTls;
|
|
break;
|
|
case "TlsRateLimit":
|
|
fromConfig.TlsRateLimit = cliValues.TlsRateLimit;
|
|
break;
|
|
case "TlsPinnedCerts":
|
|
fromConfig.TlsPinnedCerts = cliValues.TlsPinnedCerts;
|
|
break;
|
|
|
|
// Limits
|
|
case "MaxConnections":
|
|
fromConfig.MaxConnections = cliValues.MaxConnections;
|
|
break;
|
|
case "MaxPayload":
|
|
fromConfig.MaxPayload = cliValues.MaxPayload;
|
|
break;
|
|
case "MaxPending":
|
|
fromConfig.MaxPending = cliValues.MaxPending;
|
|
break;
|
|
case "WriteDeadline":
|
|
fromConfig.WriteDeadline = cliValues.WriteDeadline;
|
|
break;
|
|
case "PingInterval":
|
|
fromConfig.PingInterval = cliValues.PingInterval;
|
|
break;
|
|
case "MaxPingsOut":
|
|
fromConfig.MaxPingsOut = cliValues.MaxPingsOut;
|
|
break;
|
|
case "MaxControlLine":
|
|
fromConfig.MaxControlLine = cliValues.MaxControlLine;
|
|
break;
|
|
case "MaxSubs":
|
|
fromConfig.MaxSubs = cliValues.MaxSubs;
|
|
break;
|
|
case "MaxSubTokens":
|
|
fromConfig.MaxSubTokens = cliValues.MaxSubTokens;
|
|
break;
|
|
case "MaxTracedMsgLen":
|
|
fromConfig.MaxTracedMsgLen = cliValues.MaxTracedMsgLen;
|
|
break;
|
|
case "MaxClosedClients":
|
|
fromConfig.MaxClosedClients = cliValues.MaxClosedClients;
|
|
break;
|
|
|
|
// Misc
|
|
case "Tags":
|
|
fromConfig.Tags = cliValues.Tags;
|
|
break;
|
|
case "LameDuckDuration":
|
|
fromConfig.LameDuckDuration = cliValues.LameDuckDuration;
|
|
break;
|
|
case "LameDuckGracePeriod":
|
|
fromConfig.LameDuckGracePeriod = cliValues.LameDuckGracePeriod;
|
|
break;
|
|
case "ClientAdvertise":
|
|
fromConfig.ClientAdvertise = cliValues.ClientAdvertise;
|
|
break;
|
|
case "DisableSublistCache":
|
|
fromConfig.DisableSublistCache = cliValues.DisableSublistCache;
|
|
break;
|
|
case "ConnectErrorReports":
|
|
fromConfig.ConnectErrorReports = cliValues.ConnectErrorReports;
|
|
break;
|
|
case "ReconnectErrorReports":
|
|
fromConfig.ReconnectErrorReports = cliValues.ReconnectErrorReports;
|
|
break;
|
|
case "NoHeaderSupport":
|
|
fromConfig.NoHeaderSupport = cliValues.NoHeaderSupport;
|
|
break;
|
|
case "NoSystemAccount":
|
|
fromConfig.NoSystemAccount = cliValues.NoSystemAccount;
|
|
break;
|
|
case "SystemAccount":
|
|
fromConfig.SystemAccount = cliValues.SystemAccount;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Comparison helpers ─────────────────────────────────────────
|
|
|
|
private static void CompareAndAdd<T>(List<IConfigChange> changes, string name, T oldVal, T newVal)
|
|
{
|
|
if (!Equals(oldVal, newVal))
|
|
{
|
|
changes.Add(new ConfigChange(
|
|
name,
|
|
isLoggingChange: LoggingOptions.Contains(name),
|
|
isAuthChange: AuthOptions.Contains(name),
|
|
isTlsChange: TlsOptions.Contains(name),
|
|
isNonReloadable: NonReloadable.Contains(name)));
|
|
}
|
|
}
|
|
|
|
private static void CompareCollectionAndAdd<T>(List<IConfigChange> changes, string name, T? oldVal, T? newVal)
|
|
where T : class
|
|
{
|
|
// For collections we compare by reference and null state.
|
|
// A change from null to non-null (or vice versa), or a different reference, counts as changed.
|
|
if (ReferenceEquals(oldVal, newVal))
|
|
return;
|
|
|
|
if (oldVal is null || newVal is null || !ReferenceEquals(oldVal, newVal))
|
|
{
|
|
changes.Add(new ConfigChange(
|
|
name,
|
|
isLoggingChange: LoggingOptions.Contains(name),
|
|
isAuthChange: AuthOptions.Contains(name),
|
|
isTlsChange: TlsOptions.Contains(name),
|
|
isNonReloadable: NonReloadable.Contains(name)));
|
|
}
|
|
}
|
|
}
|