fix: cap automatic playout refreshes
This commit is contained in:
@@ -12,33 +12,59 @@ public sealed class LegacyRefreshScheduler
|
||||
{
|
||||
private readonly TimeSpan _recurringDelay;
|
||||
private readonly Func<TimeSpan, CancellationToken, Task> _delayAsync;
|
||||
private readonly int? _maximumRefreshCount;
|
||||
private TimeSpan _nextDelay;
|
||||
private int _refreshInFlight;
|
||||
private int _completedRefreshCount;
|
||||
private int _maximumCallbackDrained;
|
||||
|
||||
public LegacyRefreshScheduler(TimeSpan initialDelay, TimeSpan recurringDelay)
|
||||
public LegacyRefreshScheduler(
|
||||
TimeSpan initialDelay,
|
||||
TimeSpan recurringDelay,
|
||||
int? maximumRefreshCount = null)
|
||||
: this(
|
||||
initialDelay,
|
||||
recurringDelay,
|
||||
static (delay, cancellationToken) => Task.Delay(delay, cancellationToken))
|
||||
static (delay, cancellationToken) => Task.Delay(delay, cancellationToken),
|
||||
maximumRefreshCount)
|
||||
{
|
||||
}
|
||||
|
||||
internal LegacyRefreshScheduler(
|
||||
TimeSpan initialDelay,
|
||||
TimeSpan recurringDelay,
|
||||
Func<TimeSpan, CancellationToken, Task> delayAsync)
|
||||
Func<TimeSpan, CancellationToken, Task> delayAsync,
|
||||
int? maximumRefreshCount = null)
|
||||
{
|
||||
ValidateDelay(initialDelay, nameof(initialDelay));
|
||||
ValidateDelay(recurringDelay, nameof(recurringDelay));
|
||||
ArgumentNullException.ThrowIfNull(delayAsync);
|
||||
if (maximumRefreshCount is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(maximumRefreshCount),
|
||||
"A legacy refresh limit cannot be negative.");
|
||||
}
|
||||
|
||||
_nextDelay = initialDelay;
|
||||
_recurringDelay = recurringDelay;
|
||||
_delayAsync = delayAsync;
|
||||
_maximumRefreshCount = maximumRefreshCount;
|
||||
_maximumCallbackDrained = maximumRefreshCount == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
public bool IsRefreshInFlight => Volatile.Read(ref _refreshInFlight) != 0;
|
||||
|
||||
public int CompletedRefreshCount => Volatile.Read(ref _completedRefreshCount);
|
||||
|
||||
public int? MaximumRefreshCount => _maximumRefreshCount;
|
||||
|
||||
public bool HasReachedMaximum =>
|
||||
_maximumRefreshCount is { } maximum && CompletedRefreshCount >= maximum;
|
||||
|
||||
public bool IsMaximumCallbackDrained =>
|
||||
Volatile.Read(ref _maximumCallbackDrained) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the prior Play callback first, then starts the entire one-shot
|
||||
/// delay. A false callback result prevents any delay or refresh dispatch.
|
||||
@@ -49,6 +75,11 @@ public sealed class LegacyRefreshScheduler
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(waitForPlayCompletionAsync);
|
||||
if (HasReachedMaximum)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The configured legacy refresh limit has already been reached.");
|
||||
}
|
||||
|
||||
if (!await waitForPlayCompletionAsync(cancellationToken))
|
||||
{
|
||||
@@ -66,11 +97,78 @@ public sealed class LegacyRefreshScheduler
|
||||
/// Executes at most one refresh at a time, even if the scheduler is used
|
||||
/// incorrectly by more than one caller.
|
||||
/// </summary>
|
||||
public async Task<TResult> ExecuteRefreshAsync<TResult>(
|
||||
internal async Task<TResult> ExecuteRefreshAsync<TResult>(
|
||||
Func<CancellationToken, Task<TResult>> refreshAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_maximumRefreshCount is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A bounded legacy refresh must use the tracked execution path.");
|
||||
}
|
||||
|
||||
return await ExecuteRefreshCoreAsync(
|
||||
refreshAsync,
|
||||
successPredicate: null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one refresh and records a successful dispatch before releasing the
|
||||
/// in-flight latch. This closes the dispatch/count gap at a configured ceiling.
|
||||
/// </summary>
|
||||
public async Task<TResult> ExecuteRefreshAndTrackSuccessAsync<TResult>(
|
||||
Func<CancellationToken, Task<TResult>> refreshAsync,
|
||||
Func<TResult, bool> successPredicate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(successPredicate);
|
||||
return await ExecuteRefreshCoreAsync(
|
||||
refreshAsync,
|
||||
successPredicate,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms the final Play callback after the dispatch ceiling is reached.
|
||||
/// A false callback result leaves the ceiling unconfirmed for fail-closed status.
|
||||
/// </summary>
|
||||
public async Task<bool> DrainMaximumCallbackAsync(
|
||||
Func<CancellationToken, Task<bool>> waitForPlayCompletionAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(waitForPlayCompletionAsync);
|
||||
if (!HasReachedMaximum)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The configured legacy refresh limit has not been reached.");
|
||||
}
|
||||
|
||||
if (IsMaximumCallbackDrained)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!await waitForPlayCompletionAsync(cancellationToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _maximumCallbackDrained, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<TResult> ExecuteRefreshCoreAsync<TResult>(
|
||||
Func<CancellationToken, Task<TResult>> refreshAsync,
|
||||
Func<TResult, bool>? successPredicate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(refreshAsync);
|
||||
if (HasReachedMaximum)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The configured legacy refresh limit has already been reached.");
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _refreshInFlight, 1, 0) != 0)
|
||||
{
|
||||
@@ -79,7 +177,13 @@ public sealed class LegacyRefreshScheduler
|
||||
|
||||
try
|
||||
{
|
||||
return await refreshAsync(cancellationToken);
|
||||
var result = await refreshAsync(cancellationToken);
|
||||
if (successPredicate?.Invoke(result) == true)
|
||||
{
|
||||
TrackRefreshSucceeded();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -87,7 +191,28 @@ public sealed class LegacyRefreshScheduler
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkRefreshSucceeded() => _nextDelay = _recurringDelay;
|
||||
internal void MarkRefreshSucceeded()
|
||||
{
|
||||
if (_maximumRefreshCount is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A bounded legacy refresh is tracked by its execution path.");
|
||||
}
|
||||
|
||||
TrackRefreshSucceeded();
|
||||
}
|
||||
|
||||
private void TrackRefreshSucceeded()
|
||||
{
|
||||
if (HasReachedMaximum)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The configured legacy refresh limit has already been reached.");
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _completedRefreshCount);
|
||||
_nextDelay = _recurringDelay;
|
||||
}
|
||||
|
||||
private static void ValidateDelay(TimeSpan delay, string parameterName)
|
||||
{
|
||||
|
||||
@@ -75,4 +75,13 @@ public sealed class PlayoutOptions
|
||||
public int MaximumReconnectAttempts { get; set; } = 3;
|
||||
|
||||
public bool ReconnectEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional trusted local safety ceiling for timer-driven same-scene refreshes
|
||||
/// started by one TAKE IN. Null preserves the legacy continuous-refresh behavior;
|
||||
/// zero disables automatic refresh; a positive value stops after that many
|
||||
/// successful refresh dispatches and drains the final play callback before
|
||||
/// reporting that the limit was reached. Web content cannot change this value.
|
||||
/// </summary>
|
||||
public int? MaximumAutomaticRefreshesPerTakeIn { get; set; }
|
||||
}
|
||||
|
||||
@@ -98,6 +98,14 @@ internal sealed record ValidatedPlayoutOptions(
|
||||
0,
|
||||
1_000,
|
||||
nameof(options.MaximumReconnectAttempts));
|
||||
if (options.MaximumAutomaticRefreshesPerTakeIn is { } maximumAutomaticRefreshes)
|
||||
{
|
||||
RequireRange(
|
||||
maximumAutomaticRefreshes,
|
||||
0,
|
||||
1_000_000,
|
||||
nameof(options.MaximumAutomaticRefreshesPerTakeIn));
|
||||
}
|
||||
|
||||
Regex? titleRegex = null;
|
||||
var allowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Reference in New Issue
Block a user