// 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);
}
}