fix: cap automatic playout refreshes

This commit is contained in:
2026-07-12 19:28:15 +09:00
parent 6e00955f1b
commit df60e09884
16 changed files with 603 additions and 37 deletions

View File

@@ -181,11 +181,145 @@ public sealed class LegacyRefreshSchedulerTests
Assert.Equal(0, delays.CallCount);
}
private static LegacyRefreshScheduler CreateScheduler(ControlledDelay delays) =>
[Fact]
public async Task MaximumOne_StopsBeforeASecondDelayOrRefreshCanBeScheduled()
{
var delays = new ControlledDelay();
var scheduler = CreateScheduler(delays, maximumRefreshCount: 1);
var untrackedCalls = 0;
await Assert.ThrowsAsync<InvalidOperationException>(() =>
scheduler.ExecuteRefreshAsync(
_ =>
{
untrackedCalls++;
return Task.FromResult(true);
},
CancellationToken.None));
Assert.Equal(0, untrackedCalls);
Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
var firstOpportunity = scheduler.WaitForNextRefreshAsync(
_ => Task.FromResult(true),
null,
CancellationToken.None);
Assert.Equal(TimeSpan.FromSeconds(2), await delays.WaitForCallAsync(0));
delays.Complete(0);
Assert.True(await firstOpportunity);
Assert.True(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
_ => Task.FromResult(true),
static result => result,
CancellationToken.None));
Assert.Equal(1, scheduler.CompletedRefreshCount);
Assert.Equal(1, scheduler.MaximumRefreshCount);
Assert.True(scheduler.HasReachedMaximum);
Assert.False(scheduler.IsMaximumCallbackDrained);
var finalCallback = NewCompletion<bool>();
var drain = scheduler.DrainMaximumCallbackAsync(
cancellationToken => finalCallback.Task.WaitAsync(cancellationToken),
CancellationToken.None);
await Task.Yield();
Assert.False(drain.IsCompleted);
Assert.False(scheduler.IsMaximumCallbackDrained);
finalCallback.SetResult(true);
Assert.True(await drain);
Assert.True(scheduler.IsMaximumCallbackDrained);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
scheduler.WaitForNextRefreshAsync(
_ => Task.FromResult(true),
null,
CancellationToken.None));
var unexpectedRefreshCalls = 0;
await Assert.ThrowsAsync<InvalidOperationException>(() =>
scheduler.ExecuteRefreshAsync(
_ =>
{
unexpectedRefreshCalls++;
return Task.FromResult(true);
},
CancellationToken.None));
Assert.Equal(0, unexpectedRefreshCalls);
Assert.Equal(1, delays.CallCount);
Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
}
[Fact]
public void NullMaximum_PreservesUnlimitedLegacyScheduling()
{
var scheduler = CreateScheduler(new ControlledDelay());
for (var index = 0; index < 10_000; index++)
{
scheduler.MarkRefreshSucceeded();
}
Assert.Null(scheduler.MaximumRefreshCount);
Assert.Equal(10_000, scheduler.CompletedRefreshCount);
Assert.False(scheduler.HasReachedMaximum);
}
[Fact]
public void ZeroMaximum_DisablesAutomaticRefreshBeforeTheFirstDelay()
{
var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 0);
Assert.Equal(0, scheduler.CompletedRefreshCount);
Assert.Equal(0, scheduler.MaximumRefreshCount);
Assert.True(scheduler.HasReachedMaximum);
Assert.True(scheduler.IsMaximumCallbackDrained);
Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
}
[Fact]
public async Task MaximumCallbackTimeout_DoesNotConfirmTheLimit()
{
var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 1);
Assert.True(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
_ => Task.FromResult(true),
static result => result,
CancellationToken.None));
Assert.False(await scheduler.DrainMaximumCallbackAsync(
_ => Task.FromResult(false),
CancellationToken.None));
Assert.True(scheduler.HasReachedMaximum);
Assert.False(scheduler.IsMaximumCallbackDrained);
}
[Fact]
public async Task FailedTrackedRefresh_DoesNotConsumeTheMaximum()
{
var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 1);
Assert.False(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
_ => Task.FromResult(false),
static result => result,
CancellationToken.None));
Assert.Equal(0, scheduler.CompletedRefreshCount);
Assert.False(scheduler.HasReachedMaximum);
Assert.False(scheduler.IsMaximumCallbackDrained);
}
[Fact]
public void NegativeMaximum_IsRejected()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
new LegacyRefreshScheduler(
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3),
maximumRefreshCount: -1));
}
private static LegacyRefreshScheduler CreateScheduler(
ControlledDelay delays,
int? maximumRefreshCount = null) =>
new(
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3),
delays.DelayAsync);
delays.DelayAsync,
maximumRefreshCount);
private static async Task<bool> RunOneRefreshAsync(
LegacyRefreshScheduler scheduler,