Merge branch 'codex/jetstream-full-parity-executeplan' into main
# Conflicts: # differences.md # docs/plans/2026-02-23-jetstream-full-parity-plan.md # src/NATS.Server/Auth/Account.cs # src/NATS.Server/Configuration/ConfigProcessor.cs # src/NATS.Server/Monitoring/VarzHandler.cs # src/NATS.Server/NatsClient.cs # src/NATS.Server/NatsOptions.cs # src/NATS.Server/NatsServer.cs
This commit is contained in:
9
src/NATS.Server/Configuration/ClusterOptions.cs
Normal file
9
src/NATS.Server/Configuration/ClusterOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
public sealed class ClusterOptions
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string Host { get; set; } = "0.0.0.0";
|
||||
public int Port { get; set; } = 6222;
|
||||
public List<string> Routes { get; set; } = [];
|
||||
}
|
||||
@@ -217,6 +217,26 @@ public static class ConfigProcessor
|
||||
opts.AllowNonTls = ToBool(value);
|
||||
break;
|
||||
|
||||
// Cluster / inter-server / JetStream
|
||||
case "cluster":
|
||||
if (value is Dictionary<string, object?> clusterDict)
|
||||
opts.Cluster = ParseCluster(clusterDict, errors);
|
||||
break;
|
||||
case "gateway":
|
||||
if (value is Dictionary<string, object?> gatewayDict)
|
||||
opts.Gateway = ParseGateway(gatewayDict, errors);
|
||||
break;
|
||||
case "leaf":
|
||||
case "leafnode":
|
||||
case "leafnodes":
|
||||
if (value is Dictionary<string, object?> leafDict)
|
||||
opts.LeafNode = ParseLeafNode(leafDict, errors);
|
||||
break;
|
||||
case "jetstream":
|
||||
if (value is Dictionary<string, object?> jsDict)
|
||||
opts.JetStream = ParseJetStream(jsDict, errors);
|
||||
break;
|
||||
|
||||
// Tags
|
||||
case "server_tags":
|
||||
if (value is Dictionary<string, object?> tagsDict)
|
||||
@@ -348,6 +368,9 @@ public static class ConfigProcessor
|
||||
private static readonly Regex DurationPattern = new(
|
||||
@"^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h)$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex ByteSizePattern = new(
|
||||
@"^(\d+)\s*(b|kb|mb|gb|tb)?$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static TimeSpan ParseDurationString(string s)
|
||||
{
|
||||
@@ -368,6 +391,133 @@ public static class ConfigProcessor
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Cluster / gateway / leafnode / JetStream parsing ────────
|
||||
|
||||
private static ClusterOptions ParseCluster(Dictionary<string, object?> dict, List<string> errors)
|
||||
{
|
||||
var options = new ClusterOptions();
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
options.Name = ToString(value);
|
||||
break;
|
||||
case "listen":
|
||||
try
|
||||
{
|
||||
var (host, port) = ParseHostPort(value);
|
||||
if (host is not null)
|
||||
options.Host = host;
|
||||
if (port is not null)
|
||||
options.Port = port.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Invalid cluster.listen: {ex.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static GatewayOptions ParseGateway(Dictionary<string, object?> dict, List<string> errors)
|
||||
{
|
||||
var options = new GatewayOptions();
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
options.Name = ToString(value);
|
||||
break;
|
||||
case "listen":
|
||||
try
|
||||
{
|
||||
var (host, port) = ParseHostPort(value);
|
||||
if (host is not null)
|
||||
options.Host = host;
|
||||
if (port is not null)
|
||||
options.Port = port.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Invalid gateway.listen: {ex.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static LeafNodeOptions ParseLeafNode(Dictionary<string, object?> dict, List<string> errors)
|
||||
{
|
||||
var options = new LeafNodeOptions();
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
if (key.Equals("listen", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var (host, port) = ParseHostPort(value);
|
||||
if (host is not null)
|
||||
options.Host = host;
|
||||
if (port is not null)
|
||||
options.Port = port.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Invalid leafnode.listen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static JetStreamOptions ParseJetStream(Dictionary<string, object?> dict, List<string> errors)
|
||||
{
|
||||
var options = new JetStreamOptions();
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "store_dir":
|
||||
options.StoreDir = ToString(value);
|
||||
break;
|
||||
case "max_mem_store":
|
||||
try
|
||||
{
|
||||
options.MaxMemoryStore = ParseByteSize(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Invalid jetstream.max_mem_store: {ex.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
case "max_file_store":
|
||||
try
|
||||
{
|
||||
options.MaxFileStore = ParseByteSize(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Invalid jetstream.max_file_store: {ex.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// ─── Authorization parsing ─────────────────────────────────────
|
||||
|
||||
private static void ParseAuthorization(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
|
||||
@@ -785,6 +935,40 @@ public static class ConfigProcessor
|
||||
_ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to long"),
|
||||
};
|
||||
|
||||
private static long ParseByteSize(object? value)
|
||||
{
|
||||
if (value is long l)
|
||||
return l;
|
||||
if (value is int i)
|
||||
return i;
|
||||
if (value is double d)
|
||||
return (long)d;
|
||||
if (value is not string s)
|
||||
throw new FormatException($"Cannot parse byte size from {value?.GetType().Name ?? "null"}");
|
||||
|
||||
var trimmed = s.Trim();
|
||||
var match = ByteSizePattern.Match(trimmed);
|
||||
if (!match.Success)
|
||||
throw new FormatException($"Cannot parse byte size: '{s}'");
|
||||
|
||||
var amount = long.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups[2].Value.ToLowerInvariant();
|
||||
var multiplier = unit switch
|
||||
{
|
||||
"" or "b" => 1L,
|
||||
"kb" => 1024L,
|
||||
"mb" => 1024L * 1024L,
|
||||
"gb" => 1024L * 1024L * 1024L,
|
||||
"tb" => 1024L * 1024L * 1024L * 1024L,
|
||||
_ => throw new FormatException($"Unknown byte-size unit: '{unit}'"),
|
||||
};
|
||||
|
||||
checked
|
||||
{
|
||||
return amount * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ToBool(object? value) => value switch
|
||||
{
|
||||
bool b => b,
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace NATS.Server.Configuration;
|
||||
public static class ConfigReloader
|
||||
{
|
||||
// Non-reloadable options (match Go server — Host, Port, ServerName require restart)
|
||||
private static readonly HashSet<string> NonReloadable = ["Host", "Port", "ServerName"];
|
||||
private static readonly HashSet<string> NonReloadable =
|
||||
["Host", "Port", "ServerName", "Cluster", "JetStream.StoreDir"];
|
||||
|
||||
// Logging-related options
|
||||
private static readonly HashSet<string> LoggingOptions =
|
||||
@@ -102,6 +103,13 @@ public static class ConfigReloader
|
||||
CompareAndAdd(changes, "NoSystemAccount", oldOpts.NoSystemAccount, newOpts.NoSystemAccount);
|
||||
CompareAndAdd(changes, "SystemAccount", oldOpts.SystemAccount, newOpts.SystemAccount);
|
||||
|
||||
// Cluster and JetStream (restart-required boundaries)
|
||||
if (!ClusterEquivalent(oldOpts.Cluster, newOpts.Cluster))
|
||||
changes.Add(new ConfigChange("Cluster", isNonReloadable: true));
|
||||
|
||||
if (JetStreamStoreDirChanged(oldOpts.JetStream, newOpts.JetStream))
|
||||
changes.Add(new ConfigChange("JetStream.StoreDir", isNonReloadable: true));
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -338,4 +346,35 @@ public static class ConfigReloader
|
||||
isNonReloadable: NonReloadable.Contains(name)));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ClusterEquivalent(ClusterOptions? oldCluster, ClusterOptions? newCluster)
|
||||
{
|
||||
if (oldCluster is null && newCluster is null)
|
||||
return true;
|
||||
|
||||
if (oldCluster is null || newCluster is null)
|
||||
return false;
|
||||
|
||||
if (!string.Equals(oldCluster.Name, newCluster.Name, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (!string.Equals(oldCluster.Host, newCluster.Host, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (oldCluster.Port != newCluster.Port)
|
||||
return false;
|
||||
|
||||
return oldCluster.Routes.SequenceEqual(newCluster.Routes, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static bool JetStreamStoreDirChanged(JetStreamOptions? oldJetStream, JetStreamOptions? newJetStream)
|
||||
{
|
||||
if (oldJetStream is null && newJetStream is null)
|
||||
return false;
|
||||
|
||||
if (oldJetStream is null || newJetStream is null)
|
||||
return true;
|
||||
|
||||
return !string.Equals(oldJetStream.StoreDir, newJetStream.StoreDir, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
8
src/NATS.Server/Configuration/GatewayOptions.cs
Normal file
8
src/NATS.Server/Configuration/GatewayOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
public sealed class GatewayOptions
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string Host { get; set; } = "0.0.0.0";
|
||||
public int Port { get; set; }
|
||||
}
|
||||
8
src/NATS.Server/Configuration/JetStreamOptions.cs
Normal file
8
src/NATS.Server/Configuration/JetStreamOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
public sealed class JetStreamOptions
|
||||
{
|
||||
public string StoreDir { get; set; } = string.Empty;
|
||||
public long MaxMemoryStore { get; set; }
|
||||
public long MaxFileStore { get; set; }
|
||||
}
|
||||
7
src/NATS.Server/Configuration/LeafNodeOptions.cs
Normal file
7
src/NATS.Server/Configuration/LeafNodeOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
public sealed class LeafNodeOptions
|
||||
{
|
||||
public string Host { get; set; } = "0.0.0.0";
|
||||
public int Port { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user