feat: execute post-baseline jetstream parity plan

This commit is contained in:
Joseph Doherty
2026-02-23 12:11:19 -05:00
parent c3763e83d6
commit b41e6ff320
58 changed files with 1430 additions and 102 deletions

View File

@@ -42,11 +42,11 @@ public sealed class LeafConnection(Socket socket) : IAsyncDisposable
public Task WaitUntilClosedAsync(CancellationToken ct)
=> _loopTask?.WaitAsync(ct) ?? Task.CompletedTask;
public Task SendLsPlusAsync(string subject, string? queue, CancellationToken ct)
=> WriteLineAsync(queue is { Length: > 0 } ? $"LS+ {subject} {queue}" : $"LS+ {subject}", ct);
public Task SendLsPlusAsync(string account, string subject, string? queue, CancellationToken ct)
=> WriteLineAsync(queue is { Length: > 0 } ? $"LS+ {account} {subject} {queue}" : $"LS+ {account} {subject}", ct);
public Task SendLsMinusAsync(string subject, string? queue, CancellationToken ct)
=> WriteLineAsync(queue is { Length: > 0 } ? $"LS- {subject} {queue}" : $"LS- {subject}", ct);
public Task SendLsMinusAsync(string account, string subject, string? queue, CancellationToken ct)
=> WriteLineAsync(queue is { Length: > 0 } ? $"LS- {account} {subject} {queue}" : $"LS- {account} {subject}", ct);
public async Task SendMessageAsync(string subject, string? replyTo, ReadOnlyMemory<byte> payload, CancellationToken ct)
{
@@ -94,10 +94,9 @@ public sealed class LeafConnection(Socket socket) : IAsyncDisposable
if (line.StartsWith("LS+ ", StringComparison.Ordinal))
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2 && RemoteSubscriptionReceived != null)
if (RemoteSubscriptionReceived != null && TryParseAccountScopedInterest(parts, out var account, out var parsedSubject, out var queue))
{
var queue = parts.Length >= 3 ? parts[2] : null;
await RemoteSubscriptionReceived(new RemoteSubscription(parts[1], queue, RemoteId ?? string.Empty));
await RemoteSubscriptionReceived(new RemoteSubscription(parsedSubject, queue, RemoteId ?? string.Empty, account));
}
continue;
}
@@ -105,10 +104,9 @@ public sealed class LeafConnection(Socket socket) : IAsyncDisposable
if (line.StartsWith("LS- ", StringComparison.Ordinal))
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2 && RemoteSubscriptionReceived != null)
if (RemoteSubscriptionReceived != null && TryParseAccountScopedInterest(parts, out var account, out var parsedSubject, out var queue))
{
var queue = parts.Length >= 3 ? parts[2] : null;
await RemoteSubscriptionReceived(RemoteSubscription.Removal(parts[1], queue, RemoteId ?? string.Empty));
await RemoteSubscriptionReceived(RemoteSubscription.Removal(parsedSubject, queue, RemoteId ?? string.Empty, account));
}
continue;
}
@@ -186,6 +184,35 @@ public sealed class LeafConnection(Socket socket) : IAsyncDisposable
throw new InvalidOperationException("Leaf handshake missing id");
return id;
}
private static bool TryParseAccountScopedInterest(string[] parts, out string account, out string subject, out string? queue)
{
account = "$G";
subject = string.Empty;
queue = null;
if (parts.Length < 2)
return false;
// New format: LS+ <account> <subject> [queue]
// Legacy format: LS+ <subject> [queue]
if (parts.Length >= 3 && !LooksLikeSubject(parts[1]))
{
account = parts[1];
subject = parts[2];
queue = parts.Length >= 4 ? parts[3] : null;
return true;
}
subject = parts[1];
queue = parts.Length >= 3 ? parts[2] : null;
return true;
}
private static bool LooksLikeSubject(string token)
=> token.Contains('.', StringComparison.Ordinal)
|| token.Contains('*', StringComparison.Ordinal)
|| token.Contains('>', StringComparison.Ordinal);
}
public sealed record LeafMessage(string Subject, string? ReplyTo, ReadOnlyMemory<byte> Payload);

View File

@@ -0,0 +1,26 @@
namespace NATS.Server.LeafNodes;
public static class LeafLoopDetector
{
private const string LeafLoopPrefix = "$LDS.";
public static string Mark(string subject, string serverId)
=> $"{LeafLoopPrefix}{serverId}.{subject}";
public static bool IsLooped(string subject, string localServerId)
=> subject.StartsWith($"{LeafLoopPrefix}{localServerId}.", StringComparison.Ordinal);
public static bool TryUnmark(string subject, out string unmarked)
{
unmarked = subject;
if (!subject.StartsWith(LeafLoopPrefix, StringComparison.Ordinal))
return false;
var serverSeparator = subject.IndexOf('.', LeafLoopPrefix.Length);
if (serverSeparator < 0 || serverSeparator == subject.Length - 1)
return false;
unmarked = subject[(serverSeparator + 1)..];
return true;
}
}

View File

@@ -64,16 +64,16 @@ public sealed class LeafNodeManager : IAsyncDisposable
await connection.SendMessageAsync(subject, replyTo, payload, ct);
}
public void PropagateLocalSubscription(string subject, string? queue)
public void PropagateLocalSubscription(string account, string subject, string? queue)
{
foreach (var connection in _connections.Values)
_ = connection.SendLsPlusAsync(subject, queue, _cts?.Token ?? CancellationToken.None);
_ = connection.SendLsPlusAsync(account, subject, queue, _cts?.Token ?? CancellationToken.None);
}
public void PropagateLocalUnsubscription(string subject, string? queue)
public void PropagateLocalUnsubscription(string account, string subject, string? queue)
{
foreach (var connection in _connections.Values)
_ = connection.SendLsMinusAsync(subject, queue, _cts?.Token ?? CancellationToken.None);
_ = connection.SendLsMinusAsync(account, subject, queue, _cts?.Token ?? CancellationToken.None);
}
public async ValueTask DisposeAsync()