fix(centralui): short negative TTL after a faulted pending-updates flight (review 2026-10-21 04/C4)
This commit is contained in:
@@ -115,10 +115,10 @@ public class PendingUpdatesCacheTests : IDisposable
|
||||
await _evaluator.Received(1).EvaluateAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failed evaluation surfaces to the caller and is not memoized.</summary>
|
||||
/// <summary>Verifies that a failed evaluation surfaces to the caller and is remembered only for the short back-off (review 2026-10-21 04/C4).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FailedEvaluation_SurfacesToTheCaller_AndIsNotMemoized()
|
||||
public async Task FailedEvaluation_SurfacesToTheCaller_AndIsBackedOffNotRetriedPerPoll()
|
||||
{
|
||||
_evaluator.EvaluateAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
@@ -128,10 +128,12 @@ public class PendingUpdatesCacheTests : IDisposable
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetAsync());
|
||||
|
||||
// Same instant, well inside the TTL — a cached fault would starve every dashboard
|
||||
// for the rest of the window, so the next caller must start a new evaluation.
|
||||
var recovered = await sut.GetAsync();
|
||||
// Inside the negative TTL a poll is served the same fault with no new flatten.
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetAsync());
|
||||
await _evaluator.Received(1).EvaluateAsync(Arg.Any<CancellationToken>());
|
||||
|
||||
// An operator Re-check bypasses the back-off.
|
||||
var recovered = await sut.GetAsync(forceRefresh: true);
|
||||
Assert.NotNull(recovered);
|
||||
await _evaluator.Received(2).EvaluateAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
@@ -141,6 +141,26 @@ public class SingleFlightMemoTests
|
||||
Assert.Equal(2, memo.FlightCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the opt-in fault back-off memoizes a fault briefly, then retries.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FaultBackoff_ReservesTheFault_ThenRetriesAfterWindow()
|
||||
{
|
||||
var now = T0;
|
||||
var invocations = 0;
|
||||
var memo = new SingleFlightMemo<int>(TimeSpan.FromSeconds(60), () => now, TimeSpan.FromSeconds(10));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
memo.GetAsync(() => { invocations++; return Task.FromException<int>(new InvalidOperationException("x")); }));
|
||||
now = T0.AddSeconds(5);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => memo.GetAsync(() => Task.FromResult(++invocations)));
|
||||
Assert.Equal(1, memo.FlightCount);
|
||||
|
||||
now = T0.AddSeconds(11);
|
||||
Assert.Equal(2, await memo.GetAsync(() => Task.FromResult(++invocations)));
|
||||
Assert.Equal(2, memo.FlightCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a TTL override applies to the flight it was supplied for.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user