refactor: extract NATS.Server.Mqtt.Tests project
Move 29 MQTT test files from NATS.Server.Tests into a dedicated NATS.Server.Mqtt.Tests project. Update namespaces, add InternalsVisibleTo, and replace Task.Delay calls with PollHelper.WaitUntilAsync for proper synchronization.
This commit is contained in:
248
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttFlapperDetectionTests.cs
Normal file
248
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttFlapperDetectionTests.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
// MQTT flapper detection tests — exponential backoff for rapid reconnectors.
|
||||
// Go reference: golang/nats-server/server/mqtt.go mqttCheckFlapper ~lines 300–360.
|
||||
|
||||
using NATS.Server.Mqtt;
|
||||
using Shouldly;
|
||||
|
||||
namespace NATS.Server.Mqtt.Tests.Mqtt;
|
||||
|
||||
public class MqttFlapperDetectionTests
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper: FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static MqttSessionStore CreateStore(
|
||||
FakeTimeProvider? time = null,
|
||||
int flapThreshold = 3,
|
||||
TimeSpan? flapWindow = null) =>
|
||||
new(
|
||||
flapWindow: flapWindow ?? TimeSpan.FromSeconds(10),
|
||||
flapThreshold: flapThreshold,
|
||||
timeProvider: time);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. TrackConnectDisconnect_counts_events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TrackConnectDisconnect_counts_events()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — each connect increments the counter.
|
||||
var store = CreateStore();
|
||||
|
||||
var s1 = store.TrackConnectDisconnect("client-a");
|
||||
s1.ConnectDisconnectCount.ShouldBe(1);
|
||||
|
||||
var s2 = store.TrackConnectDisconnect("client-a");
|
||||
s2.ConnectDisconnectCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Not_flapper_below_threshold
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Not_flapper_below_threshold()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — threshold is 3; 2 events should not mark as flapper.
|
||||
var store = CreateStore();
|
||||
|
||||
store.TrackConnectDisconnect("client-b");
|
||||
store.TrackConnectDisconnect("client-b");
|
||||
|
||||
store.IsFlapper("client-b").ShouldBeFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Becomes_flapper_at_threshold
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Becomes_flapper_at_threshold()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — 3 events within window marks the client.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
store.TrackConnectDisconnect("client-c");
|
||||
time.Advance(TimeSpan.FromSeconds(1));
|
||||
store.TrackConnectDisconnect("client-c");
|
||||
time.Advance(TimeSpan.FromSeconds(1));
|
||||
store.TrackConnectDisconnect("client-c");
|
||||
|
||||
store.IsFlapper("client-c").ShouldBeTrue();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Backoff_increases_exponentially
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Backoff_increases_exponentially()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — backoff doubles on each new flap trigger.
|
||||
// Level 0 → 1 s, Level 1 → 2 s, Level 2 → 4 s.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
// First flap at level 0 (1 s backoff)
|
||||
store.TrackConnectDisconnect("client-d");
|
||||
store.TrackConnectDisconnect("client-d");
|
||||
var s1 = store.TrackConnectDisconnect("client-d");
|
||||
s1.BackoffLevel.ShouldBe(1); // incremented after applying level 0
|
||||
s1.BackoffUntil.ShouldNotBeNull();
|
||||
var backoff1 = s1.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
|
||||
backoff1.TotalMilliseconds.ShouldBeInRange(900, 1100); // ~1 000 ms
|
||||
|
||||
// Advance past the backoff and trigger again — level 1 (2 s)
|
||||
time.Advance(TimeSpan.FromSeconds(2));
|
||||
var s2 = store.TrackConnectDisconnect("client-d");
|
||||
s2.BackoffLevel.ShouldBe(2);
|
||||
var backoff2 = s2.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
|
||||
backoff2.TotalMilliseconds.ShouldBeInRange(1900, 2100); // ~2 000 ms
|
||||
|
||||
// Advance past and trigger once more — level 2 (4 s)
|
||||
time.Advance(TimeSpan.FromSeconds(3));
|
||||
var s3 = store.TrackConnectDisconnect("client-d");
|
||||
s3.BackoffLevel.ShouldBe(3);
|
||||
var backoff3 = s3.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
|
||||
backoff3.TotalMilliseconds.ShouldBeInRange(3900, 4100); // ~4 000 ms
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Backoff_capped_at_60_seconds
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Backoff_capped_at_60_seconds()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — cap the maximum backoff at 60 s.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
// Trigger enough flaps to overflow past 60 s (level 6 = 64 s, which should cap at 60 s)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
store.TrackConnectDisconnect("client-e");
|
||||
time.Advance(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
var state = store.TrackConnectDisconnect("client-e");
|
||||
var remaining = state.BackoffUntil!.Value - time.GetUtcNow().UtcDateTime;
|
||||
remaining.TotalMilliseconds.ShouldBeLessThanOrEqualTo(60_001); // max 60 s (±1 ms tolerance)
|
||||
remaining.TotalMilliseconds.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. GetBackoffMs_returns_remaining
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetBackoffMs_returns_remaining()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — caller can query remaining backoff time.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
store.TrackConnectDisconnect("client-f");
|
||||
store.TrackConnectDisconnect("client-f");
|
||||
store.TrackConnectDisconnect("client-f"); // threshold hit
|
||||
|
||||
var ms = store.GetBackoffMs("client-f");
|
||||
ms.ShouldBeGreaterThan(0);
|
||||
ms.ShouldBeLessThanOrEqualTo(1000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 7. GetBackoffMs_zero_when_not_flapping
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetBackoffMs_zero_when_not_flapping()
|
||||
{
|
||||
// Not enough events to trigger backoff — remaining ms should be 0.
|
||||
var store = CreateStore();
|
||||
|
||||
store.TrackConnectDisconnect("client-g");
|
||||
store.TrackConnectDisconnect("client-g");
|
||||
|
||||
store.GetBackoffMs("client-g").ShouldBe(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 8. ClearFlapperState_removes_tracking
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ClearFlapperState_removes_tracking()
|
||||
{
|
||||
// Go reference: server/mqtt.go — stable clients should have state purged.
|
||||
var store = CreateStore();
|
||||
|
||||
store.TrackConnectDisconnect("client-h");
|
||||
store.TrackConnectDisconnect("client-h");
|
||||
store.TrackConnectDisconnect("client-h");
|
||||
store.IsFlapper("client-h").ShouldBeTrue();
|
||||
|
||||
store.ClearFlapperState("client-h");
|
||||
|
||||
store.IsFlapper("client-h").ShouldBeFalse();
|
||||
store.GetBackoffMs("client-h").ShouldBe(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 9. Window_resets_after_10_seconds
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Window_resets_after_10_seconds()
|
||||
{
|
||||
// Go reference: server/mqtt.go mqttCheckFlapper — window-based detection resets.
|
||||
// Track 2 events, advance past the window, add 1 more — should NOT be a flapper.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
store.TrackConnectDisconnect("client-i");
|
||||
store.TrackConnectDisconnect("client-i");
|
||||
|
||||
// Advance past the 10 s flap window
|
||||
time.Advance(TimeSpan.FromSeconds(11));
|
||||
|
||||
// Directly set the WindowStart via the returned state to simulate the old window
|
||||
// being in the past. A single new event in a new window should not cross threshold.
|
||||
store.TrackConnectDisconnect("client-i");
|
||||
|
||||
store.IsFlapper("client-i").ShouldBeFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10. CheckAndClearStableClients_clears_old
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void CheckAndClearStableClients_clears_old()
|
||||
{
|
||||
// Go reference: server/mqtt.go — periodic sweep clears long-stable flapper records.
|
||||
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
var store = CreateStore(time);
|
||||
|
||||
// Trigger flap
|
||||
store.TrackConnectDisconnect("client-j");
|
||||
store.TrackConnectDisconnect("client-j");
|
||||
var state = store.TrackConnectDisconnect("client-j");
|
||||
store.IsFlapper("client-j").ShouldBeTrue();
|
||||
|
||||
// Manually backdate BackoffUntil so it's already expired
|
||||
lock (state)
|
||||
{
|
||||
state.BackoffUntil = time.GetUtcNow().UtcDateTime - TimeSpan.FromSeconds(61);
|
||||
}
|
||||
|
||||
// A stable-threshold sweep of 60 s should evict the now-expired entry
|
||||
store.CheckAndClearStableClients(TimeSpan.FromSeconds(60));
|
||||
|
||||
store.IsFlapper("client-j").ShouldBeFalse();
|
||||
store.GetBackoffMs("client-j").ShouldBe(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user