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
242 lines
9.4 KiB
C#
242 lines
9.4 KiB
C#
// Go: gateway.go:100-150 (InterestMode enum), gateway.go:1500-1600 (switchToInterestOnlyMode)
|
|
using NATS.Server.Gateways;
|
|
|
|
namespace NATS.Server.Tests.Gateways;
|
|
|
|
/// <summary>
|
|
/// Unit tests for GatewayInterestTracker — the per-connection interest mode state machine.
|
|
/// Covers Optimistic/InterestOnly modes, threshold-based switching, and per-account isolation.
|
|
/// Go reference: gateway_test.go, TestGatewaySwitchToInterestOnlyModeImmediately (line 6934),
|
|
/// TestGatewayAccountInterest (line 1794), TestGatewayAccountUnsub (line 1912).
|
|
/// </summary>
|
|
public class GatewayInterestTrackerTests
|
|
{
|
|
// Go: TestGatewayBasic server/gateway_test.go:399 — initial state is Optimistic
|
|
[Fact]
|
|
public void StartsInOptimisticMode()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
|
|
tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.Optimistic);
|
|
tracker.GetMode("ANY_ACCOUNT").ShouldBe(GatewayInterestMode.Optimistic);
|
|
}
|
|
|
|
// Go: TestGatewayBasic server/gateway_test.go:399 — optimistic mode forwards everything
|
|
[Fact]
|
|
public void OptimisticForwardsEverything()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
tracker.ShouldForward("$G", "any.subject").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "deeply.nested.subject.path").ShouldBeTrue();
|
|
tracker.ShouldForward("ACCT", "foo").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestGatewayAccountUnsub server/gateway_test.go:1912 — RS- adds to no-interest
|
|
[Fact]
|
|
public void TrackNoInterest_AddsToNoInterestSet()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
tracker.TrackNoInterest("$G", "orders.created");
|
|
|
|
// Should not forward that specific subject in Optimistic mode
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
|
|
// Other subjects still forwarded
|
|
tracker.ShouldForward("$G", "orders.updated").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "payments.created").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934 — threshold switch
|
|
[Fact]
|
|
public void SwitchesToInterestOnlyAfterThreshold()
|
|
{
|
|
const int threshold = 10;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
|
|
|
|
// Add subjects up to (but not reaching) the threshold
|
|
for (int i = 0; i < threshold - 1; i++)
|
|
tracker.TrackNoInterest("$G", $"subject.{i}");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
|
|
|
|
// One more crosses the threshold
|
|
tracker.TrackNoInterest("$G", $"subject.{threshold - 1}");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
}
|
|
|
|
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
|
|
[Fact]
|
|
public void InterestOnlyMode_OnlyForwardsTrackedSubjects()
|
|
{
|
|
const int threshold = 5;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
// Trigger mode switch
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("$G", $"noise.{i}");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
|
|
// Nothing forwarded until interest is explicitly tracked
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
|
|
|
|
// Track a positive interest
|
|
tracker.TrackInterest("$G", "orders.created");
|
|
|
|
// Now only that subject is forwarded
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "orders.updated").ShouldBeFalse();
|
|
tracker.ShouldForward("$G", "payments.done").ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestGatewaySubjectInterest server/gateway_test.go:1972 — wildcard interest in InterestOnly
|
|
[Fact]
|
|
public void InterestOnlyMode_SupportsWildcards()
|
|
{
|
|
const int threshold = 3;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
// Trigger InterestOnly mode
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("$G", $"x.{i}");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
|
|
// Register a wildcard interest
|
|
tracker.TrackInterest("$G", "foo.>");
|
|
|
|
// Matching subjects are forwarded
|
|
tracker.ShouldForward("$G", "foo.bar").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "foo.bar.baz").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "foo.anything.deep.nested").ShouldBeTrue();
|
|
|
|
// Non-matching subjects are not forwarded
|
|
tracker.ShouldForward("$G", "other.subject").ShouldBeFalse();
|
|
tracker.ShouldForward("$G", "foo").ShouldBeFalse(); // "foo.>" requires at least one token after "foo"
|
|
}
|
|
|
|
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — per-account mode isolation
|
|
[Fact]
|
|
public void ModePerAccount()
|
|
{
|
|
const int threshold = 5;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
// Switch ACCT_A to InterestOnly
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("ACCT_A", $"noise.{i}");
|
|
|
|
tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
|
|
// ACCT_B remains Optimistic
|
|
tracker.GetMode("ACCT_B").ShouldBe(GatewayInterestMode.Optimistic);
|
|
|
|
// ACCT_A blocks unknown subjects, ACCT_B forwards
|
|
tracker.ShouldForward("ACCT_A", "orders.created").ShouldBeFalse();
|
|
tracker.ShouldForward("ACCT_B", "orders.created").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
|
|
[Fact]
|
|
public void ModePersistsAfterSwitch()
|
|
{
|
|
const int threshold = 3;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
// Trigger switch
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("$G", $"y.{i}");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
|
|
// TrackInterest in InterestOnly mode — mode stays InterestOnly
|
|
tracker.TrackInterest("$G", "orders.created");
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
|
|
// TrackNoInterest in InterestOnly mode — mode stays InterestOnly
|
|
tracker.TrackNoInterest("$G", "something.else");
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
}
|
|
|
|
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — explicit SwitchToInterestOnly
|
|
[Fact]
|
|
public void ExplicitSwitchToInterestOnly_SetsMode()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
|
|
|
|
tracker.SwitchToInterestOnly("$G");
|
|
|
|
tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
|
|
}
|
|
|
|
// Go: TestGatewayAccountUnsub server/gateway_test.go:1912 — RS+ restores interest after RS-
|
|
[Fact]
|
|
public void TrackInterest_InOptimisticMode_RemovesFromNoInterestSet()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
// Mark no interest
|
|
tracker.TrackNoInterest("$G", "orders.created");
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
|
|
|
|
// Remote re-subscribes — track interest again
|
|
tracker.TrackInterest("$G", "orders.created");
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
|
|
[Fact]
|
|
public void InterestOnlyMode_TrackNoInterest_RemovesFromInterestSet()
|
|
{
|
|
const int threshold = 3;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
// Trigger InterestOnly
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("$G", $"z.{i}");
|
|
|
|
tracker.TrackInterest("$G", "orders.created");
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
|
|
|
|
// Remote unsubscribes — subject removed from interest set
|
|
tracker.TrackNoInterest("$G", "orders.created");
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestGatewaySubjectInterest server/gateway_test.go:1972 — pwc wildcard in InterestOnly
|
|
[Fact]
|
|
public void InterestOnlyMode_SupportsPwcWildcard()
|
|
{
|
|
const int threshold = 3;
|
|
var tracker = new GatewayInterestTracker(noInterestThreshold: threshold);
|
|
|
|
for (int i = 0; i < threshold; i++)
|
|
tracker.TrackNoInterest("$G", $"n.{i}");
|
|
|
|
tracker.TrackInterest("$G", "orders.*");
|
|
|
|
tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "orders.deleted").ShouldBeTrue();
|
|
tracker.ShouldForward("$G", "orders.deep.nested").ShouldBeFalse(); // * is single token
|
|
tracker.ShouldForward("$G", "payments.created").ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestGatewayAccountInterest server/gateway_test.go:1794 — unknown account defaults optimistic
|
|
[Fact]
|
|
public void UnknownAccount_DefaultsToOptimisticForwarding()
|
|
{
|
|
var tracker = new GatewayInterestTracker();
|
|
|
|
// Account never seen — should forward everything
|
|
tracker.ShouldForward("BRAND_NEW_ACCOUNT", "any.subject").ShouldBeTrue();
|
|
}
|
|
}
|