feat(gateways): implement GatewayInterestTracker for interest-only mode state machine (D1)
Ports the gateway interest-only mode from Go (gateway.go:100-150, 1500-1600): - Add GatewayInterestTracker with Optimistic/Transitioning/InterestOnly modes - In Optimistic mode, track no-interest set; switch to InterestOnly when set exceeds threshold (default 1000, matching Go defaultGatewayMaxRUnsubThreshold) - In InterestOnly mode, only forward subjects with tracked RS+ interest; use SubjectMatch.MatchLiteral for wildcard pattern support - Integrate tracker into GatewayConnection: A+/A- messages update tracker, SendMessageAsync skips send when ShouldForward returns false - Expose InterestTracker property on GatewayConnection for observability - Add 13 unit tests covering all 8 specified behaviors plus edge cases
This commit is contained in:
190
src/NATS.Server/Gateways/GatewayInterestTracker.cs
Normal file
190
src/NATS.Server/Gateways/GatewayInterestTracker.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
// Go: gateway.go:100-150 (InterestMode enum)
|
||||
// Go: gateway.go:1500-1600 (switchToInterestOnlyMode)
|
||||
using System.Collections.Concurrent;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Gateways;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the interest mode for each account on a gateway connection.
|
||||
/// In Optimistic mode, all messages are forwarded unless a subject is in the
|
||||
/// no-interest set. Once the no-interest set exceeds the threshold (1000),
|
||||
/// the account switches to InterestOnly mode where only subjects with tracked
|
||||
/// RS+ interest are forwarded.
|
||||
/// </summary>
|
||||
public enum GatewayInterestMode
|
||||
{
|
||||
/// <summary>Forward everything (initial state). Track subjects with no interest.</summary>
|
||||
Optimistic,
|
||||
|
||||
/// <summary>Mode transition in progress.</summary>
|
||||
Transitioning,
|
||||
|
||||
/// <summary>Only forward subjects with known remote interest (RS+ received).</summary>
|
||||
InterestOnly,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-account interest state machine for a gateway connection.
|
||||
/// Go reference: gateway.go:100-150 (struct srvGateway, interestMode fields),
|
||||
/// gateway.go:1500-1600 (switchToInterestOnlyMode, processGatewayAccountUnsub).
|
||||
/// </summary>
|
||||
public sealed class GatewayInterestTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of no-interest subjects before switching to InterestOnly mode.
|
||||
/// Go: gateway.go:134 (defaultGatewayMaxRUnsubThreshold = 1000)
|
||||
/// </summary>
|
||||
public const int DefaultNoInterestThreshold = 1000;
|
||||
|
||||
private readonly int _noInterestThreshold;
|
||||
|
||||
// Per-account state: mode + no-interest set (Optimistic) or positive interest set (InterestOnly)
|
||||
private readonly ConcurrentDictionary<string, AccountState> _accounts = new(StringComparer.Ordinal);
|
||||
|
||||
public GatewayInterestTracker(int noInterestThreshold = DefaultNoInterestThreshold)
|
||||
{
|
||||
_noInterestThreshold = noInterestThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current interest mode for the given account.
|
||||
/// Accounts default to Optimistic until the no-interest threshold is exceeded.
|
||||
/// </summary>
|
||||
public GatewayInterestMode GetMode(string account)
|
||||
=> _accounts.TryGetValue(account, out var state) ? state.Mode : GatewayInterestMode.Optimistic;
|
||||
|
||||
/// <summary>
|
||||
/// Track a positive interest (RS+ received from remote) for an account/subject.
|
||||
/// Go: gateway.go:1540 (processGatewayAccountSub — adds to interest set)
|
||||
/// </summary>
|
||||
public void TrackInterest(string account, string subject)
|
||||
{
|
||||
var state = GetOrCreateState(account);
|
||||
lock (state)
|
||||
{
|
||||
// In Optimistic mode, remove from no-interest set if present
|
||||
if (state.Mode == GatewayInterestMode.Optimistic)
|
||||
{
|
||||
state.NoInterestSet.Remove(subject);
|
||||
return;
|
||||
}
|
||||
|
||||
// In InterestOnly mode, add to the positive interest set
|
||||
if (state.Mode == GatewayInterestMode.InterestOnly)
|
||||
{
|
||||
state.InterestSet.Add(subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Track a no-interest event (RS- received from remote) for an account/subject.
|
||||
/// When the no-interest set crosses the threshold, switches to InterestOnly mode.
|
||||
/// Go: gateway.go:1560 (processGatewayAccountUnsub — tracks no-interest, triggers switch)
|
||||
/// </summary>
|
||||
public void TrackNoInterest(string account, string subject)
|
||||
{
|
||||
var state = GetOrCreateState(account);
|
||||
lock (state)
|
||||
{
|
||||
if (state.Mode == GatewayInterestMode.InterestOnly)
|
||||
{
|
||||
// In InterestOnly mode, remove from positive interest set
|
||||
state.InterestSet.Remove(subject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.Mode == GatewayInterestMode.Optimistic)
|
||||
{
|
||||
state.NoInterestSet.Add(subject);
|
||||
|
||||
if (state.NoInterestSet.Count >= _noInterestThreshold)
|
||||
DoSwitchToInterestOnly(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a message should be forwarded to the remote gateway
|
||||
/// for the given account and subject.
|
||||
/// Go: gateway.go:2900 (shouldForwardMsg — checks mode and interest)
|
||||
/// </summary>
|
||||
public bool ShouldForward(string account, string subject)
|
||||
{
|
||||
if (!_accounts.TryGetValue(account, out var state))
|
||||
return true; // Optimistic by default — no state yet means forward
|
||||
|
||||
lock (state)
|
||||
{
|
||||
return state.Mode switch
|
||||
{
|
||||
GatewayInterestMode.Optimistic =>
|
||||
// Forward unless subject is in no-interest set
|
||||
!state.NoInterestSet.Contains(subject),
|
||||
|
||||
GatewayInterestMode.Transitioning =>
|
||||
// During transition, be conservative and forward
|
||||
true,
|
||||
|
||||
GatewayInterestMode.InterestOnly =>
|
||||
// Only forward if at least one interest pattern matches
|
||||
MatchesAnyInterest(state, subject),
|
||||
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly switch an account to InterestOnly mode.
|
||||
/// Called when the remote signals it is in interest-only mode.
|
||||
/// Go: gateway.go:1500 (switchToInterestOnlyMode)
|
||||
/// </summary>
|
||||
public void SwitchToInterestOnly(string account)
|
||||
{
|
||||
var state = GetOrCreateState(account);
|
||||
lock (state)
|
||||
{
|
||||
if (state.Mode != GatewayInterestMode.InterestOnly)
|
||||
DoSwitchToInterestOnly(state);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────
|
||||
|
||||
private AccountState GetOrCreateState(string account)
|
||||
=> _accounts.GetOrAdd(account, _ => new AccountState());
|
||||
|
||||
private static void DoSwitchToInterestOnly(AccountState state)
|
||||
{
|
||||
// Go: gateway.go:1510-1530 — clear no-interest, build positive interest from what remains
|
||||
state.Mode = GatewayInterestMode.InterestOnly;
|
||||
state.NoInterestSet.Clear();
|
||||
// InterestSet starts empty; subsequent RS+ events will populate it
|
||||
}
|
||||
|
||||
private static bool MatchesAnyInterest(AccountState state, string subject)
|
||||
{
|
||||
foreach (var pattern in state.InterestSet)
|
||||
{
|
||||
// Use SubjectMatch.MatchLiteral to support wildcard patterns in the interest set
|
||||
if (SubjectMatch.MatchLiteral(subject, pattern))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Per-account mutable state. All access must be under the instance lock.</summary>
|
||||
private sealed class AccountState
|
||||
{
|
||||
public GatewayInterestMode Mode { get; set; } = GatewayInterestMode.Optimistic;
|
||||
|
||||
/// <summary>Subjects with no remote interest (used in Optimistic mode).</summary>
|
||||
public HashSet<string> NoInterestSet { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Subjects/patterns with positive remote interest (used in InterestOnly mode).</summary>
|
||||
public HashSet<string> InterestSet { get; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user