实现 .NET 10 托盘版网关保活
包含分层探测、可补偿的 IPv4 路由/DNS 切换、自动策略、托盘与分组设置界面、只读诊断工具、离线测试和发布脚本。
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user