feat(batch18): implement group-b server core helpers
This commit is contained in:
@@ -638,6 +638,14 @@ public sealed partial class NatsServer
|
|||||||
return (claims, claimJwt, null);
|
return (claims, claimJwt, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches an account from the resolver, registers it, and returns it.
|
||||||
|
/// Mirrors Go <c>Server.fetchAccount</c>.
|
||||||
|
/// Lock must NOT be held on entry.
|
||||||
|
/// </summary>
|
||||||
|
public (Account? Account, Exception? Error) FetchAccount(string name) =>
|
||||||
|
FetchAccountFromResolver(name);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches an account from the resolver, registers it, and returns it.
|
/// Fetches an account from the resolver, registers it, and returns it.
|
||||||
/// Mirrors Go <c>Server.fetchAccount</c>.
|
/// Mirrors Go <c>Server.fetchAccount</c>.
|
||||||
|
|||||||
@@ -331,10 +331,12 @@ public sealed partial class NatsServer
|
|||||||
public int NumRemotes()
|
public int NumRemotes()
|
||||||
{
|
{
|
||||||
_mu.EnterReadLock();
|
_mu.EnterReadLock();
|
||||||
try { return _routes.Count; }
|
try { return NumRemotesInternal(); }
|
||||||
finally { _mu.ExitReadLock(); }
|
finally { _mu.ExitReadLock(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int NumRemotesInternal() => _routes.Count;
|
||||||
|
|
||||||
/// <summary>Returns the number of leaf-node connections. Mirrors Go <c>Server.NumLeafNodes()</c>.</summary>
|
/// <summary>Returns the number of leaf-node connections. Mirrors Go <c>Server.NumLeafNodes()</c>.</summary>
|
||||||
public int NumLeafNodes()
|
public int NumLeafNodes()
|
||||||
{
|
{
|
||||||
@@ -475,27 +477,56 @@ public sealed partial class NatsServer
|
|||||||
/// Mirrors Go <c>Server.readyForConnections()</c>.
|
/// Mirrors Go <c>Server.readyForConnections()</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Exception? ReadyForConnectionsError(TimeSpan d)
|
public Exception? ReadyForConnectionsError(TimeSpan d)
|
||||||
|
=> ReadyForConnectionsInternal(d);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polls until all expected listeners are up or the deadline expires.
|
||||||
|
/// Returns an error description if not ready within <paramref name="d"/>.
|
||||||
|
/// Mirrors Go <c>Server.readyForConnections()</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal Exception? ReadyForConnectionsInternal(TimeSpan d)
|
||||||
{
|
{
|
||||||
var opts = GetOpts();
|
var opts = GetOpts();
|
||||||
var end = DateTime.UtcNow.Add(d);
|
var end = DateTime.UtcNow.Add(d);
|
||||||
|
|
||||||
|
var checks = new Dictionary<string, (bool ok, Exception? err)>(StringComparer.Ordinal);
|
||||||
while (DateTime.UtcNow < end)
|
while (DateTime.UtcNow < end)
|
||||||
{
|
{
|
||||||
bool serverOk, routeOk, gatewayOk, leafOk, wsOk;
|
bool serverOk, routeOk, gatewayOk, leafOk, wsOk, mqttOk;
|
||||||
|
Exception? serverErr, routeErr, gatewayErr, leafErr;
|
||||||
_mu.EnterReadLock();
|
_mu.EnterReadLock();
|
||||||
serverOk = _listener != null || opts.DontListen;
|
serverOk = _listener != null || opts.DontListen;
|
||||||
|
serverErr = _listenerErr;
|
||||||
routeOk = opts.Cluster.Port == 0 || _routeListener != null;
|
routeOk = opts.Cluster.Port == 0 || _routeListener != null;
|
||||||
|
routeErr = _routeListenerErr;
|
||||||
gatewayOk = string.IsNullOrEmpty(opts.Gateway.Name) || _gatewayListener != null;
|
gatewayOk = string.IsNullOrEmpty(opts.Gateway.Name) || _gatewayListener != null;
|
||||||
|
gatewayErr = _gatewayListenerErr;
|
||||||
leafOk = opts.LeafNode.Port == 0 || _leafNodeListener != null;
|
leafOk = opts.LeafNode.Port == 0 || _leafNodeListener != null;
|
||||||
wsOk = opts.Websocket.Port == 0; // stub — websocket listener not tracked until session 23
|
leafErr = _leafNodeListenerErr;
|
||||||
|
wsOk = opts.Websocket.Port == 0;
|
||||||
|
mqttOk = opts.Mqtt.Port == 0;
|
||||||
_mu.ExitReadLock();
|
_mu.ExitReadLock();
|
||||||
|
|
||||||
if (serverOk && routeOk && gatewayOk && leafOk && wsOk)
|
checks["server"] = (serverOk, serverErr);
|
||||||
|
checks["route"] = (routeOk, routeErr);
|
||||||
|
checks["gateway"] = (gatewayOk, gatewayErr);
|
||||||
|
checks["leafnode"] = (leafOk, leafErr);
|
||||||
|
checks["websocket"] = (wsOk, null);
|
||||||
|
checks["mqtt"] = (mqttOk, null);
|
||||||
|
|
||||||
|
var numOk = checks.Values.Count(v => v.ok);
|
||||||
|
if (numOk == checks.Count)
|
||||||
{
|
{
|
||||||
if (opts.DontListen)
|
if (opts.DontListen)
|
||||||
{
|
{
|
||||||
try { _startupComplete.Task.Wait((int)d.TotalMilliseconds); }
|
try { _startupComplete.Task.Wait((int)d.TotalMilliseconds); }
|
||||||
catch { /* timeout */ }
|
catch { }
|
||||||
|
|
||||||
|
if (!_startupComplete.Task.IsCompleted)
|
||||||
|
{
|
||||||
|
return new InvalidOperationException(
|
||||||
|
$"failed to be ready for connections after {d}: startup did not complete");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -504,8 +535,19 @@ public sealed partial class NatsServer
|
|||||||
Thread.Sleep(25);
|
Thread.Sleep(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var failed = new List<string>(checks.Count);
|
||||||
|
foreach (var (name, info) in checks)
|
||||||
|
{
|
||||||
|
if (info.ok && info.err != null)
|
||||||
|
failed.Add($"{name}(ok, but {info.err.Message})");
|
||||||
|
else if (!info.ok && info.err == null)
|
||||||
|
failed.Add(name);
|
||||||
|
else if (!info.ok)
|
||||||
|
failed.Add($"{name}({info.err!.Message})");
|
||||||
|
}
|
||||||
|
|
||||||
return new InvalidOperationException(
|
return new InvalidOperationException(
|
||||||
$"failed to be ready for connections after {d}");
|
$"failed to be ready for connections after {d}: {string.Join(", ", failed)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -528,7 +570,10 @@ public sealed partial class NatsServer
|
|||||||
public string Name() => _info.Name;
|
public string Name() => _info.Name;
|
||||||
|
|
||||||
/// <summary>Returns the server name as a string. Mirrors Go <c>Server.String()</c>.</summary>
|
/// <summary>Returns the server name as a string. Mirrors Go <c>Server.String()</c>.</summary>
|
||||||
public override string ToString() => _info.Name;
|
public string String() => _info.Name;
|
||||||
|
|
||||||
|
/// <summary>Returns the server name as a string. Mirrors Go <c>Server.String()</c>.</summary>
|
||||||
|
public override string ToString() => String();
|
||||||
|
|
||||||
/// <summary>Returns the number of currently-stored closed connections. Mirrors Go <c>Server.numClosedConns()</c>.</summary>
|
/// <summary>Returns the number of currently-stored closed connections. Mirrors Go <c>Server.numClosedConns()</c>.</summary>
|
||||||
internal int NumClosedConns()
|
internal int NumClosedConns()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Reflection;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using ZB.MOM.NatsNet.Server;
|
using ZB.MOM.NatsNet.Server;
|
||||||
using ZB.MOM.NatsNet.Server.Internal;
|
using ZB.MOM.NatsNet.Server.Internal;
|
||||||
@@ -6,6 +7,43 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
|||||||
|
|
||||||
public sealed class NatsServerTests
|
public sealed class NatsServerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void String_WhenCalled_ReturnsServerNameAndMatchesToString()
|
||||||
|
{
|
||||||
|
var options = new ServerOptions { ServerName = "batch18-node" };
|
||||||
|
var (server, err) = NatsServer.NewServer(options);
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var stringMethod = server!.GetType()
|
||||||
|
.GetMethod("String", BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
stringMethod.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var value = stringMethod!.Invoke(server, null).ShouldBeOfType<string>();
|
||||||
|
value.ShouldBe("batch18-node");
|
||||||
|
server.ToString().ShouldBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FetchAccount_WhenResolverClaimsAreInvalid_ReturnsValidationError()
|
||||||
|
{
|
||||||
|
var (server, err) = NatsServer.NewServer(new ServerOptions());
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var resolver = new MemoryAccountResolver();
|
||||||
|
resolver.StoreAsync("A", "invalid-jwt").GetAwaiter().GetResult();
|
||||||
|
SetField(server!, "_accResolver", resolver);
|
||||||
|
|
||||||
|
var fetchAccountMethod = server.GetType()
|
||||||
|
.GetMethod("FetchAccount", BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
fetchAccountMethod.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var result = ((Account? Account, Exception? Error))fetchAccountMethod!.Invoke(server, ["A"])!;
|
||||||
|
result.Account.ShouldBeNull();
|
||||||
|
result.Error.ShouldBe(ServerErrors.ErrAccountValidation);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // T:2886
|
[Fact] // T:2886
|
||||||
public void CustomRouterAuthentication_ShouldSucceed()
|
public void CustomRouterAuthentication_ShouldSucceed()
|
||||||
{
|
{
|
||||||
@@ -518,4 +556,10 @@ public sealed class NatsServerTests
|
|||||||
"TestServerShutdownDuringStart".ShouldNotBeNullOrWhiteSpace();
|
"TestServerShutdownDuringStart".ShouldNotBeNullOrWhiteSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SetField(object target, string name, object? value)
|
||||||
|
{
|
||||||
|
target.GetType()
|
||||||
|
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.SetValue(target, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
|||||||
|
|
||||||
public sealed partial class RouteHandlerTests
|
public sealed partial class RouteHandlerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NumRemotesInternal_WhenRoutesExist_ReturnsCount()
|
||||||
|
{
|
||||||
|
var (server, err) = NatsServer.NewServer(new ServerOptions());
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var routesField = typeof(NatsServer).GetField("_routes", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
routesField.ShouldNotBeNull();
|
||||||
|
routesField!.SetValue(
|
||||||
|
server,
|
||||||
|
new Dictionary<string, List<ClientConnection>>
|
||||||
|
{
|
||||||
|
["one"] = [],
|
||||||
|
["two"] = [],
|
||||||
|
});
|
||||||
|
|
||||||
|
var method = typeof(NatsServer).GetMethod("NumRemotesInternal", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
method.ShouldNotBeNull();
|
||||||
|
var count = (int)method!.Invoke(server, null)!;
|
||||||
|
count.ShouldBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // T:2854
|
[Fact] // T:2854
|
||||||
public void RouteCompressionAuto_ShouldSucceed()
|
public void RouteCompressionAuto_ShouldSucceed()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,59 @@ public sealed class ServerLifecycleStubFeaturesTests
|
|||||||
after.ShouldBeGreaterThanOrEqualTo(before);
|
after.ShouldBeGreaterThanOrEqualTo(before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NumRemotesInternal_WhenRoutesRegistered_ReturnsRouteBucketCount()
|
||||||
|
{
|
||||||
|
var (server, err) = NatsServer.NewServer(new ServerOptions());
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.ShouldNotBeNull();
|
||||||
|
|
||||||
|
SetField(
|
||||||
|
server!,
|
||||||
|
"_routes",
|
||||||
|
new Dictionary<string, List<ClientConnection>>
|
||||||
|
{
|
||||||
|
["a"] = [],
|
||||||
|
["b"] = [],
|
||||||
|
["c"] = [],
|
||||||
|
});
|
||||||
|
|
||||||
|
var numRemotesInternal = server.GetType()
|
||||||
|
.GetMethod("NumRemotesInternal", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
numRemotesInternal.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var count = (int)numRemotesInternal!.Invoke(server, null)!;
|
||||||
|
count.ShouldBe(3);
|
||||||
|
server.NumRemotes().ShouldBe(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadyForConnectionsInternal_WhenRouteListenerErrorPresent_ReturnsDetailedFailure()
|
||||||
|
{
|
||||||
|
var opts = new ServerOptions
|
||||||
|
{
|
||||||
|
DontListen = true,
|
||||||
|
Cluster = new ClusterOpts { Port = 6222 },
|
||||||
|
LeafNode = new LeafNodeOpts { Port = 0 },
|
||||||
|
Websocket = new WebsocketOpts { Port = 0 },
|
||||||
|
Mqtt = new MqttOpts { Port = 0 },
|
||||||
|
};
|
||||||
|
var (server, err) = NatsServer.NewServer(opts);
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.ShouldNotBeNull();
|
||||||
|
|
||||||
|
SetField(server!, "_routeListenerErr", new InvalidOperationException("route listener failed"));
|
||||||
|
|
||||||
|
var readyForConnectionsInternal = server.GetType()
|
||||||
|
.GetMethod("ReadyForConnectionsInternal", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
readyForConnectionsInternal.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var readyError = readyForConnectionsInternal!.Invoke(server, [TimeSpan.FromMilliseconds(40)]) as Exception;
|
||||||
|
readyError.ShouldNotBeNull();
|
||||||
|
readyError!.Message.ShouldContain("route(");
|
||||||
|
readyError.Message.ShouldContain("route listener failed");
|
||||||
|
}
|
||||||
|
|
||||||
private static object GetField(object target, string name)
|
private static object GetField(object target, string name)
|
||||||
{
|
{
|
||||||
return target.GetType()
|
return target.GetType()
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user