fix(communication): failback probation is time-boxed and cleared on address change (review 2026-10-21 03/P-2 follow-up)
This commit is contained in:
@@ -58,6 +58,13 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan ChannelDisposalGrace = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// How long a probe-driven failback stays on probation without a confirming real call; a
|
||||
/// NodeA failure after this window is treated as a fresh failover (backoff restarts at the
|
||||
/// initial value). Settable per instance for tests.
|
||||
/// </summary>
|
||||
internal TimeSpan FailbackProbationWindow { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
private readonly ISitePskProvider _pskProvider;
|
||||
private readonly CommunicationOptions _options;
|
||||
private readonly ILogger<SitePairChannelProvider> _logger;
|
||||
@@ -169,6 +176,7 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
|
||||
// Addresses changed — return to the preferred node and reset failback backoff.
|
||||
pair.CurrentIsA = pair.ChannelA is not null;
|
||||
pair.FailbackOnProbation = false;
|
||||
pair.ResetFailback();
|
||||
_logger.LogInformation(
|
||||
"gRPC channel pair for site {SiteId} refreshed (nodeA={HasA}, nodeB={HasB})",
|
||||
@@ -373,6 +381,7 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
// node that answers the probe but not real traffic is retried ever less often.
|
||||
pair.CurrentIsA = true;
|
||||
pair.FailbackOnProbation = true;
|
||||
pair.ProbationSinceUtc = DateTime.UtcNow;
|
||||
pair.NextProbeUtc = DateTime.MaxValue;
|
||||
_logger.LogInformation("Site {SiteId} preferred node A ready again; failing back", siteId);
|
||||
return true;
|
||||
@@ -535,7 +544,12 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
pair.CurrentIsA = toIsA;
|
||||
if (!toIsA)
|
||||
{
|
||||
if (pair.FailbackOnProbation)
|
||||
// Probation is time-boxed: a site quiet for longer than FailbackProbationWindow
|
||||
// after a probe-driven failback (no real call to confirm it) is treated as
|
||||
// confirmed, so a fresh NodeA failure much later starts at InitialBackoff again
|
||||
// instead of inheriting a stale, grown backoff.
|
||||
if (pair.FailbackOnProbation
|
||||
&& DateTime.UtcNow - pair.ProbationSinceUtc < FailbackProbationWindow)
|
||||
{
|
||||
// A failed us again right after a probe-driven failback: it is not really
|
||||
// ready yet — grow the backoff rather than restarting it.
|
||||
@@ -545,6 +559,7 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
else
|
||||
{
|
||||
// Failed over off the preferred node → schedule the first failback probe.
|
||||
pair.FailbackOnProbation = false;
|
||||
pair.ScheduleFailback(InitialBackoff);
|
||||
}
|
||||
}
|
||||
@@ -660,6 +675,9 @@ public sealed class SitePairChannelProvider : IDisposable
|
||||
/// </summary>
|
||||
public bool FailbackOnProbation;
|
||||
|
||||
/// <summary>When the current probation began (meaningful only while <see cref="FailbackOnProbation"/>).</summary>
|
||||
public DateTime ProbationSinceUtc;
|
||||
|
||||
/// <summary>The current failback backoff.</summary>
|
||||
public TimeSpan Backoff => _backoff;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user