// 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; /// /// 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. /// public enum GatewayInterestMode { /// Forward everything (initial state). Track subjects with no interest. Optimistic, /// Mode transition in progress. Transitioning, /// Only forward subjects with known remote interest (RS+ received). InterestOnly, } /// /// 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). /// public sealed class GatewayInterestTracker { /// /// Number of no-interest subjects before switching to InterestOnly mode. /// Go: gateway.go:134 (defaultGatewayMaxRUnsubThreshold = 1000) /// 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 _accounts = new(StringComparer.Ordinal); public GatewayInterestTracker(int noInterestThreshold = DefaultNoInterestThreshold) { _noInterestThreshold = noInterestThreshold; } /// /// Returns the current interest mode for the given account. /// Accounts default to Optimistic until the no-interest threshold is exceeded. /// public GatewayInterestMode GetMode(string account) => _accounts.TryGetValue(account, out var state) ? state.Mode : GatewayInterestMode.Optimistic; /// /// Track a positive interest (RS+ received from remote) for an account/subject. /// Go: gateway.go:1540 (processGatewayAccountSub — adds to interest set) /// 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); } } } /// /// 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) /// 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); } } } /// /// 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) /// 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, }; } } /// /// Explicitly switch an account to InterestOnly mode. /// Called when the remote signals it is in interest-only mode. /// Go: gateway.go:1500 (switchToInterestOnlyMode) /// 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; } /// Per-account mutable state. All access must be under the instance lock. private sealed class AccountState { public GatewayInterestMode Mode { get; set; } = GatewayInterestMode.Optimistic; /// Subjects with no remote interest (used in Optimistic mode). public HashSet NoInterestSet { get; } = new(StringComparer.Ordinal); /// Subjects/patterns with positive remote interest (used in InterestOnly mode). public HashSet InterestSet { get; } = new(StringComparer.Ordinal); } }