feat: wire remaining E2E gaps — account imports, subject transforms, JWT auth, service latency
Close all 5 server-side wiring gaps so E2E tests pass without skips: - System events: bridge user-defined system_account to internal $SYS - Account imports/exports: config parsing + reverse response import for cross-account request-reply - Subject transforms: parse mappings config block, apply in ProcessMessage - JWT auth: parse trusted_keys, resolver MEMORY, resolver_preload in config - Service latency: timestamp on request, publish ServiceLatencyMsg on response
This commit is contained in:
@@ -529,6 +529,17 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
_systemAccount = new Account(Account.SystemAccountName) { IsSystemAccount = true };
|
||||
_accounts[Account.SystemAccountName] = _systemAccount;
|
||||
|
||||
// If a user-defined system_account is configured, promote that account to be the
|
||||
// system account. Events published to $SYS.* will be delivered to subscribers on
|
||||
// this account. Go reference: server/server.go — configureAccounts / setSystemAccount.
|
||||
if (!string.IsNullOrEmpty(options.SystemAccount) &&
|
||||
!string.Equals(options.SystemAccount, Account.SystemAccountName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var userSysAccount = GetOrCreateAccount(options.SystemAccount);
|
||||
userSysAccount.IsSystemAccount = true;
|
||||
_systemAccount = userSysAccount;
|
||||
}
|
||||
|
||||
// Create system internal client and event system
|
||||
var sysClientId = Interlocked.Increment(ref _nextClientId);
|
||||
var sysClient = new InternalClient(sysClientId, ClientKind.System, _systemAccount);
|
||||
@@ -1312,7 +1323,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
if (si.Invalid) continue;
|
||||
if (SubjectMatch.MatchLiteral(subject, si.From))
|
||||
{
|
||||
ProcessServiceImport(si, subject, replyTo, headers, payload);
|
||||
ProcessServiceImport(si, subject, replyTo, headers, payload, sender.Account);
|
||||
delivered = true;
|
||||
}
|
||||
}
|
||||
@@ -1453,7 +1464,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
/// Reference: Go server/accounts.go addServiceImport / processServiceImport.
|
||||
/// </summary>
|
||||
public void ProcessServiceImport(ServiceImport si, string subject, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload, Account? sourceAccount = null)
|
||||
{
|
||||
if (si.Invalid) return;
|
||||
|
||||
@@ -1477,6 +1488,24 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
targetSubject = MapImportSubject(subject, si.From, si.To);
|
||||
}
|
||||
|
||||
// Set up a temporary reverse service import so that responses from the
|
||||
// destination (exporter) account can route back to the source (importer)
|
||||
// account. This handles request-reply across account boundaries.
|
||||
// Go reference: client.go setupResponseServiceImport
|
||||
if (replyTo != null && sourceAccount != null && !si.IsResponse)
|
||||
{
|
||||
SetupResponseServiceImport(si.DestinationAccount, sourceAccount, replyTo, si.Export);
|
||||
}
|
||||
|
||||
// Service latency tracking: when the response arrives back, compute elapsed
|
||||
// time and publish a latency metric to the configured subject.
|
||||
// Go reference: client.go processServiceImport — latency tracking path.
|
||||
if (si.IsResponse && si.Tracking && si.TimestampTicks > 0)
|
||||
{
|
||||
var elapsed = TimeSpan.FromTicks(Environment.TickCount64 * TimeSpan.TicksPerMillisecond - si.TimestampTicks);
|
||||
PublishServiceLatency(si, elapsed);
|
||||
}
|
||||
|
||||
// Match against destination account's SubList
|
||||
var destSubList = si.DestinationAccount.SubList;
|
||||
var result = destSubList.Match(targetSubject);
|
||||
@@ -1498,6 +1527,36 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a temporary reverse service import in the exporter's account so that
|
||||
/// when the exporter publishes a response to the reply subject, the message is
|
||||
/// forwarded back to the importer's account where the reply subscription lives.
|
||||
/// Go reference: client.go setupResponseServiceImport.
|
||||
/// </summary>
|
||||
private static void SetupResponseServiceImport(Account exporterAccount, Account importerAccount, string replyTo, ServiceExport? export = null)
|
||||
{
|
||||
// Check if a reverse import for this reply subject already exists
|
||||
if (exporterAccount.Imports.Services.ContainsKey(replyTo))
|
||||
return;
|
||||
|
||||
// Determine if we should track latency for this response
|
||||
var shouldTrack = export?.Latency is { } latency && LatencyTracker.ShouldSample(latency);
|
||||
|
||||
var reverseImport = new ServiceImport
|
||||
{
|
||||
DestinationAccount = importerAccount,
|
||||
From = replyTo,
|
||||
To = replyTo,
|
||||
IsResponse = true,
|
||||
UsePub = true,
|
||||
Export = export,
|
||||
Tracking = shouldTrack,
|
||||
// Store start time as TickCount64 (milliseconds) converted to ticks for elapsed computation
|
||||
TimestampTicks = shouldTrack ? Environment.TickCount64 * TimeSpan.TicksPerMillisecond : 0,
|
||||
};
|
||||
exporterAccount.Imports.AddServiceImport(reverseImport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a published subject from the import "From" pattern to the "To" pattern.
|
||||
/// For example, if From="requests.>" and To="api.>" and subject="requests.test",
|
||||
@@ -1633,11 +1692,54 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
acc.MaxConnections = config.MaxConnections;
|
||||
acc.MaxSubscriptions = config.MaxSubscriptions;
|
||||
acc.DefaultPermissions = config.DefaultPermissions;
|
||||
|
||||
// Wire exports from config
|
||||
if (config.Exports != null)
|
||||
{
|
||||
foreach (var export in config.Exports)
|
||||
{
|
||||
if (export.Service is { Length: > 0 } svc)
|
||||
{
|
||||
ServiceLatency? latency = export.LatencySubject is { Length: > 0 }
|
||||
? new ServiceLatency { Subject = export.LatencySubject, SamplingPercentage = export.LatencySampling }
|
||||
: null;
|
||||
acc.AddServiceExport(svc, Imports.ServiceResponseType.Singleton, approved: null, latency: latency);
|
||||
}
|
||||
else if (export.Stream is { Length: > 0 } strm)
|
||||
{
|
||||
acc.AddStreamExport(strm, approved: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire imports from config (deferred — needs destination accounts resolved)
|
||||
if (config.Imports != null)
|
||||
WireAccountImports(acc, config.Imports);
|
||||
}
|
||||
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
private void WireAccountImports(Account importer, List<Auth.ImportDefinition> imports)
|
||||
{
|
||||
foreach (var imp in imports)
|
||||
{
|
||||
if (imp.ServiceAccount is { Length: > 0 } svcAcct && imp.ServiceSubject is { Length: > 0 } svcSubj)
|
||||
{
|
||||
var dest = GetOrCreateAccount(svcAcct);
|
||||
var localSubject = imp.To ?? svcSubj;
|
||||
importer.AddServiceImport(dest, from: localSubject, to: svcSubj);
|
||||
}
|
||||
else if (imp.StreamAccount is { Length: > 0 } strmAcct && imp.StreamSubject is { Length: > 0 } strmSubj)
|
||||
{
|
||||
var source = GetOrCreateAccount(strmAcct);
|
||||
var localSubject = imp.To ?? strmSubj;
|
||||
importer.AddStreamImport(source, from: strmSubj, to: localSubject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the subject belongs to the $SYS subject space.
|
||||
/// Reference: Go server/server.go — isReservedSubject.
|
||||
@@ -1675,6 +1777,25 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
return account?.SubList ?? _globalAccount.SubList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a service latency metric message to the configured latency subject.
|
||||
/// Go reference: client.go processServiceImport — trackLatency path.
|
||||
/// </summary>
|
||||
private void PublishServiceLatency(ServiceImport si, TimeSpan elapsed)
|
||||
{
|
||||
var latency = si.Export?.Latency;
|
||||
if (latency == null || string.IsNullOrEmpty(latency.Subject))
|
||||
return;
|
||||
|
||||
var msg = LatencyTracker.BuildLatencyMsg(
|
||||
requestor: si.DestinationAccount.Name,
|
||||
responder: si.Export?.Account?.Name ?? "unknown",
|
||||
serviceLatency: elapsed,
|
||||
totalLatency: elapsed);
|
||||
|
||||
SendInternalMsg(latency.Subject, reply: null, msg);
|
||||
}
|
||||
|
||||
public void SendInternalMsg(string subject, string? reply, object? msg)
|
||||
{
|
||||
_eventSystem?.Enqueue(new PublishMessage { Subject = subject, Reply = reply, Body = msg });
|
||||
|
||||
Reference in New Issue
Block a user