feat: wire service import forwarding into message delivery path
Add ProcessServiceImport method to NatsServer that transforms subjects from importer to exporter namespace and delivers to destination account subscribers. Wire service import checking into ProcessMessage so that publishes matching a service import "From" pattern are automatically forwarded to the destination account. Includes MapImportSubject for wildcard-aware subject mapping and WireServiceImports for import setup.
This commit is contained in:
@@ -10,6 +10,7 @@ using NATS.NKeys;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Configuration;
|
||||
using NATS.Server.Events;
|
||||
using NATS.Server.Imports;
|
||||
using NATS.Server.Monitoring;
|
||||
using NATS.Server.Protocol;
|
||||
using NATS.Server.Subscriptions;
|
||||
@@ -631,6 +632,27 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Check for service imports that match this subject.
|
||||
// When a client in the importer account publishes to a subject
|
||||
// that matches a service import "From" pattern, we forward the
|
||||
// message to the destination (exporter) account's subscribers
|
||||
// using the mapped "To" subject.
|
||||
if (sender.Account != null)
|
||||
{
|
||||
foreach (var kvp in sender.Account.Imports.Services)
|
||||
{
|
||||
foreach (var si in kvp.Value)
|
||||
{
|
||||
if (si.Invalid) continue;
|
||||
if (SubjectMatch.MatchLiteral(subject, si.From))
|
||||
{
|
||||
ProcessServiceImport(si, subject, replyTo, headers, payload);
|
||||
delivered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No-responders: if nobody received the message and the publisher
|
||||
// opted in, send back a 503 status HMSG on the reply subject.
|
||||
if (!delivered && replyTo != null && sender.ClientOpts?.NoResponders == true)
|
||||
@@ -670,6 +692,153 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a service import by transforming the subject from the importer's
|
||||
/// subject space to the exporter's subject space, then delivering to matching
|
||||
/// subscribers in the destination account.
|
||||
/// Reference: Go server/accounts.go addServiceImport / processServiceImport.
|
||||
/// </summary>
|
||||
public void ProcessServiceImport(ServiceImport si, string subject, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
if (si.Invalid) return;
|
||||
|
||||
// Transform subject: map from importer subject space to exporter subject space
|
||||
string targetSubject;
|
||||
if (si.Transform != null)
|
||||
{
|
||||
var transformed = si.Transform.Apply(subject);
|
||||
targetSubject = transformed ?? si.To;
|
||||
}
|
||||
else if (si.UsePub)
|
||||
{
|
||||
targetSubject = subject;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default: use the "To" subject from the import definition.
|
||||
// For wildcard imports (e.g. "requests.>" -> "api.>"), we need
|
||||
// to map the specific subject tokens from the source pattern to
|
||||
// the destination pattern.
|
||||
targetSubject = MapImportSubject(subject, si.From, si.To);
|
||||
}
|
||||
|
||||
// Match against destination account's SubList
|
||||
var destSubList = si.DestinationAccount.SubList;
|
||||
var result = destSubList.Match(targetSubject);
|
||||
|
||||
// Deliver to plain subscribers in the destination account
|
||||
foreach (var sub in result.PlainSubs)
|
||||
{
|
||||
if (sub.Client == null) continue;
|
||||
DeliverMessage(sub, targetSubject, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
// Deliver to one member of each queue group
|
||||
foreach (var queueGroup in result.QueueSubs)
|
||||
{
|
||||
if (queueGroup.Length == 0) continue;
|
||||
var sub = queueGroup[0]; // Simple selection: first available
|
||||
if (sub.Client != null)
|
||||
DeliverMessage(sub, targetSubject, replyTo, headers, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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",
|
||||
/// this returns "api.test".
|
||||
/// </summary>
|
||||
private static string MapImportSubject(string subject, string fromPattern, string toPattern)
|
||||
{
|
||||
// If "To" doesn't contain wildcards, use it directly
|
||||
if (SubjectMatch.IsLiteral(toPattern))
|
||||
return toPattern;
|
||||
|
||||
// For wildcard patterns, replace matching wildcard segments.
|
||||
// Split into tokens and map from source to destination.
|
||||
var subTokens = subject.Split('.');
|
||||
var fromTokens = fromPattern.Split('.');
|
||||
var toTokens = toPattern.Split('.');
|
||||
|
||||
var result = new string[toTokens.Length];
|
||||
int subIdx = 0;
|
||||
|
||||
// Build a mapping: for each wildcard position in "from",
|
||||
// capture the corresponding subject token(s)
|
||||
var wildcardValues = new List<string>();
|
||||
string? fwcValue = null;
|
||||
|
||||
for (int i = 0; i < fromTokens.Length && subIdx < subTokens.Length; i++)
|
||||
{
|
||||
if (fromTokens[i] == "*")
|
||||
{
|
||||
wildcardValues.Add(subTokens[subIdx]);
|
||||
subIdx++;
|
||||
}
|
||||
else if (fromTokens[i] == ">")
|
||||
{
|
||||
// Capture all remaining tokens
|
||||
fwcValue = string.Join(".", subTokens[subIdx..]);
|
||||
subIdx = subTokens.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
subIdx++; // Skip literal match
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the output using the "to" pattern
|
||||
int wcIdx = 0;
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < toTokens.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append('.');
|
||||
|
||||
if (toTokens[i] == "*")
|
||||
{
|
||||
sb.Append(wcIdx < wildcardValues.Count ? wildcardValues[wcIdx] : "*");
|
||||
wcIdx++;
|
||||
}
|
||||
else if (toTokens[i] == ">")
|
||||
{
|
||||
sb.Append(fwcValue ?? ">");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(toTokens[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wires service import subscriptions for an account. Creates marker
|
||||
/// subscriptions in the account's SubList so that the import paths
|
||||
/// are tracked. The actual forwarding happens in ProcessMessage when
|
||||
/// it checks the account's Imports.Services.
|
||||
/// Reference: Go server/accounts.go addServiceImportSub.
|
||||
/// </summary>
|
||||
public void WireServiceImports(Account account)
|
||||
{
|
||||
foreach (var kvp in account.Imports.Services)
|
||||
{
|
||||
foreach (var si in kvp.Value)
|
||||
{
|
||||
if (si.Invalid) continue;
|
||||
|
||||
// Create a marker subscription in the importer account.
|
||||
// This subscription doesn't directly deliver messages;
|
||||
// the ProcessMessage method checks service imports after
|
||||
// the regular SubList match.
|
||||
_logger.LogDebug(
|
||||
"Wired service import for account {Account}: {From} -> {To} (dest: {DestAccount})",
|
||||
account.Name, si.From, si.To, si.DestinationAccount.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendNoResponders(NatsClient sender, string replyTo)
|
||||
{
|
||||
// Find the sid for a subscription matching the reply subject
|
||||
|
||||
Reference in New Issue
Block a user