156 lines
9.0 KiB
C#
156 lines
9.0 KiB
C#
using GatewayKeepalive.Core;
|
|
|
|
namespace GatewayKeepalive.Tests;
|
|
|
|
internal static class SwitchPolicyTests
|
|
{
|
|
public static Task SuccessThresholdAndCooldownAsync()
|
|
{
|
|
var clock = new ManualTimeProvider();
|
|
var policy = new SwitchPolicy(clock);
|
|
var settings = Fixtures.Settings() with { SuccessThreshold = 3, RecoveryCooldownSeconds = 10 };
|
|
var healthy = Fixtures.Health(true, true);
|
|
policy.Switched();
|
|
|
|
Check.Equal(false, policy.Evaluate(settings, NetworkMode.Direct, healthy) is not null, "The first success must remain below threshold");
|
|
Check.Equal(1, policy.SuccessCount, "One success should be counted");
|
|
clock.AdvanceSeconds(5);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "The second success must remain below threshold");
|
|
clock.AdvanceSeconds(4);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "The recovery cooldown must still block at 9 seconds");
|
|
Check.Equal(3, policy.SuccessCount, "Success count should cap at the configured threshold");
|
|
clock.AdvanceSeconds(1);
|
|
Check.Equal(true, policy.Evaluate(settings, NetworkMode.Direct, healthy), "The switch must fire exactly when the recovery cooldown expires");
|
|
|
|
var resetting = new SwitchPolicy(clock);
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "First healthy observation should not switch");
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Second healthy observation should not switch");
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(false, true)) is null, "An unhealthy observation must reset consecutive success");
|
|
Check.Equal(0, resetting.SuccessCount, "Unhealthy observation should reset success count");
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "First post-reset success must not switch");
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Second post-reset success must not switch");
|
|
Check.Equal(2, resetting.SuccessCount, "Only consecutive successes should count");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public static Task FailureThresholdAndCooldownAsync()
|
|
{
|
|
var clock = new ManualTimeProvider();
|
|
var policy = new SwitchPolicy(clock);
|
|
var settings = Fixtures.Settings() with
|
|
{
|
|
FailureThreshold = 3,
|
|
FailureCooldownSeconds = 5,
|
|
RecoveryCooldownSeconds = 3600,
|
|
};
|
|
policy.Switched();
|
|
|
|
var unhealthy = Fixtures.Health(true, false);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "First failed probe must not switch");
|
|
clock.AdvanceSeconds(1);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "Second failed probe must not switch");
|
|
clock.AdvanceSeconds(3);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "Failure cooldown must block before 5 seconds");
|
|
Check.Equal(3, policy.FailureCount, "Failure count should cap at threshold");
|
|
clock.AdvanceSeconds(1);
|
|
Check.Equal(false, policy.Evaluate(settings, NetworkMode.Proxy, unhealthy), "Proxy must fail over to direct at its own cooldown");
|
|
|
|
var recoveryClock = new ManualTimeProvider();
|
|
var recovery = new SwitchPolicy(recoveryClock);
|
|
var recoverySettings = settings with { SuccessThreshold = 1, RecoveryCooldownSeconds = 40, FailureCooldownSeconds = 3600 };
|
|
recovery.Switched();
|
|
recoveryClock.AdvanceSeconds(39);
|
|
Check.True(recovery.Evaluate(recoverySettings, NetworkMode.Direct, Fixtures.Health(true, true)) is null, "Recovery cooldown must be independent of failure cooldown");
|
|
recoveryClock.AdvanceSeconds(1);
|
|
Check.Equal(true, recovery.Evaluate(recoverySettings, NetworkMode.Direct, Fixtures.Health(true, true)), "Direct should recover at the configured recovery cooldown");
|
|
|
|
var resetting = new SwitchPolicy(new ManualTimeProvider());
|
|
resetting.Switched();
|
|
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
|
|
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
|
|
Check.Equal(2, resetting.FailureCount, "Failure fixture should be consecutive");
|
|
Check.True(resetting.Evaluate(settings, NetworkMode.Proxy, Fixtures.Health(true, true)) is null, "A healthy proxy check must not switch");
|
|
Check.Equal(0, resetting.FailureCount, "A healthy proxy check must reset failure count");
|
|
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
|
|
Check.Equal(1, resetting.FailureCount, "Only consecutive failures should count after a healthy check");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public static Task FailureRetryUsesThirtySecondFloorAsync()
|
|
{
|
|
var clock = new ManualTimeProvider();
|
|
var policy = new SwitchPolicy(clock);
|
|
var settings = Fixtures.Settings() with { SuccessThreshold = 1, RecoveryCooldownSeconds = 0 };
|
|
var healthy = Fixtures.Health(true, true);
|
|
policy.Switched();
|
|
clock.AdvanceSeconds(1);
|
|
policy.Failed();
|
|
Check.Equal(0, policy.SuccessCount, "Failed() should reset success count");
|
|
Check.Equal(0, policy.FailureCount, "Failed() should reset failure count");
|
|
|
|
clock.AdvanceSeconds(29);
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Recovery must remain blocked 29 seconds after a failure");
|
|
Check.Equal(1, policy.SuccessCount, "The blocked attempt should still be observed");
|
|
clock.AdvanceSeconds(1);
|
|
Check.Equal(true, policy.Evaluate(settings, NetworkMode.Direct, healthy), "Recovery must become available at the 30 second failure backoff");
|
|
|
|
var failureClock = new ManualTimeProvider();
|
|
var failurePolicy = new SwitchPolicy(failureClock);
|
|
var failureSettings = settings with { FailureThreshold = 1, FailureCooldownSeconds = 0 };
|
|
failurePolicy.Failed();
|
|
failureClock.AdvanceSeconds(29);
|
|
Check.True(failurePolicy.Evaluate(failureSettings, NetworkMode.Proxy, Fixtures.Health(false, true)) is null, "Failover must also wait 29 seconds");
|
|
failureClock.AdvanceSeconds(1);
|
|
Check.Equal(false, failurePolicy.Evaluate(failureSettings, NetworkMode.Proxy, Fixtures.Health(false, true)), "Failover must be allowed at the 30 second floor");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public static Task NonAutoModesResetCountsAsync()
|
|
{
|
|
var policy = new SwitchPolicy(new ManualTimeProvider());
|
|
var settings = Fixtures.Settings();
|
|
var healthy = Fixtures.Health(true, true);
|
|
policy.Evaluate(settings, NetworkMode.Direct, healthy);
|
|
policy.Evaluate(settings, NetworkMode.Direct, healthy);
|
|
Check.Equal(2, policy.SuccessCount, "Fixture should accumulate success before changing mode");
|
|
|
|
foreach (var mode in new[] { ControlPolicy.Paused, ControlPolicy.ForceDirect, ControlPolicy.ForceProxy })
|
|
{
|
|
Check.True(policy.Evaluate(settings with { Policy = mode }, NetworkMode.Direct, healthy) is null, $"{mode} must request no automatic switch");
|
|
Check.Equal(0, policy.SuccessCount, $"{mode} must clear success count");
|
|
Check.Equal(0, policy.FailureCount, $"{mode} must clear failure count");
|
|
}
|
|
|
|
policy.Evaluate(settings, NetworkMode.Proxy, Fixtures.Health(false, true));
|
|
Check.True(policy.Evaluate(settings, NetworkMode.Degraded, healthy) is null, "Observe-only network modes must not switch");
|
|
Check.Equal(0, policy.FailureCount, "Observe-only modes must clear failure count");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public static Task CountsResetAfterEventsAsync()
|
|
{
|
|
var clock = new ManualTimeProvider();
|
|
var policy = new SwitchPolicy(clock);
|
|
var settings = Fixtures.Settings();
|
|
var sick = Fixtures.Health(true, false);
|
|
policy.Evaluate(settings, NetworkMode.Proxy, sick);
|
|
policy.Evaluate(settings, NetworkMode.Proxy, sick);
|
|
Check.Equal(2, policy.FailureCount, "Failure count fixture");
|
|
policy.Switched();
|
|
Check.Equal(0, policy.FailureCount, "Switched() should clear failure count");
|
|
Check.Equal(0, policy.SuccessCount, "Switched() should clear success count");
|
|
|
|
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
|
|
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
|
|
policy.Failed();
|
|
Check.Equal(0, policy.SuccessCount, "Failed() should clear success count");
|
|
Check.Equal(0, policy.FailureCount, "Failed() should clear failure count");
|
|
|
|
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
|
|
policy.Reset();
|
|
Check.Equal(0, policy.SuccessCount, "Reset() should clear success count");
|
|
Check.Equal(0, policy.FailureCount, "Reset() should clear failure count");
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|