feat: improve polling recovery and session cleanup
This commit is contained in:
@@ -19,8 +19,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LiveRecorder.Infrastructure.Services;
|
||||
|
||||
public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveRoomPollingSignal
|
||||
{
|
||||
private static readonly TimeSpan TransientRetryDelay = TimeSpan.FromSeconds(10);
|
||||
private static readonly TimeSpan OfflineGracefulStopTimeout = TimeSpan.FromSeconds(20);
|
||||
private static readonly TimeSpan OfflineForcedStopTimeout = TimeSpan.FromSeconds(8);
|
||||
private static readonly TimeSpan ExceptionEmailCooldown = TimeSpan.FromHours(6);
|
||||
@@ -32,6 +33,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
private readonly ILogger<LiveRoomPollingBackgroundService> _logger;
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _exceptionEmailSentAt = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<Guid, DateTimeOffset> _nextPollDueAt = new();
|
||||
private readonly SemaphoreSlim _wakeSignal = new(0);
|
||||
|
||||
public LiveRoomPollingBackgroundService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
@@ -41,6 +43,23 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void RequestImmediatePoll(Guid liveRoomId, TimeSpan? delay = null)
|
||||
{
|
||||
var requestedDueAt = DateTimeOffset.UtcNow + (delay ?? MinimumIdleDelay);
|
||||
_nextPollDueAt.AddOrUpdate(
|
||||
liveRoomId,
|
||||
requestedDueAt,
|
||||
(_, current) => requestedDueAt < current ? requestedDueAt : current);
|
||||
|
||||
try
|
||||
{
|
||||
_wakeSignal.Release();
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
@@ -61,7 +80,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
|
||||
if (!settings.EnableBackgroundPolling)
|
||||
{
|
||||
await DelayAsync(TimeSpan.FromSeconds(Math.Clamp(settings.PollingIntervalSeconds, 10, 3600)), stoppingToken);
|
||||
await WaitForNextRunAsync(TimeSpan.FromSeconds(Math.Clamp(settings.PollingIntervalSeconds, 10, 3600)), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -160,12 +179,23 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
await DelayAsync(delay, stoppingToken);
|
||||
await WaitForNextRunAsync(delay, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||
Task.Delay(ClampDelay(delay), cancellationToken);
|
||||
private async Task WaitForNextRunAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
while (_wakeSignal.CurrentCount > 0)
|
||||
{
|
||||
await _wakeSignal.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var effectiveDelay = ClampDelay(delay);
|
||||
var delayTask = Task.Delay(effectiveDelay, cancellationToken);
|
||||
var wakeTask = _wakeSignal.WaitAsync(cancellationToken);
|
||||
var completedTask = await Task.WhenAny(delayTask, wakeTask);
|
||||
await completedTask;
|
||||
}
|
||||
|
||||
private static TimeSpan ClampDelay(TimeSpan delay)
|
||||
{
|
||||
@@ -254,6 +284,8 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
return;
|
||||
}
|
||||
|
||||
var nextDueAt = DateTimeOffset.UtcNow.AddSeconds(Math.Clamp(intervalSeconds, 10, 3600));
|
||||
|
||||
try
|
||||
{
|
||||
var adapter = adapterFactory.GetByPlatform(liveRoom.Platform);
|
||||
@@ -380,6 +412,21 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
_logger.LogWarning(ex, "Polling room {RoomId} failed", liveRoom.RoomId);
|
||||
|
||||
var isTransient = IsTransientPollingException(ex, cancellationToken);
|
||||
if (isTransient)
|
||||
{
|
||||
nextDueAt = DateTimeOffset.UtcNow.Add(TransientRetryDelay);
|
||||
}
|
||||
|
||||
await UpdateAutoStartDecisionAsync(
|
||||
dbContext,
|
||||
liveRoom,
|
||||
isTransient ? AutoStartDecisionCodes.PollFailedTransient : AutoStartDecisionCodes.PollFailed,
|
||||
isTransient
|
||||
? "Auto-start is pending because live status polling failed temporarily."
|
||||
: "Auto-start failed because live status polling failed.",
|
||||
BuildPollingFailureDetail(ex),
|
||||
cancellationToken);
|
||||
|
||||
await logService.WriteAsync(
|
||||
isTransient ? SystemLogLevel.Warning : SystemLogLevel.Error,
|
||||
"Scheduler",
|
||||
@@ -408,7 +455,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nextPollDueAt[liveRoomId] = DateTimeOffset.UtcNow.AddSeconds(Math.Clamp(intervalSeconds, 10, 3600));
|
||||
_nextPollDueAt[liveRoomId] = nextDueAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,6 +652,12 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
IsTransientPollingException(exception.InnerException, cancellationToken);
|
||||
}
|
||||
|
||||
private static string BuildPollingFailureDetail(Exception exception)
|
||||
{
|
||||
var root = exception.GetBaseException();
|
||||
return Truncate($"{root.GetType().Name}: {root.Message}", 2048) ?? exception.GetType().Name;
|
||||
}
|
||||
|
||||
private bool ShouldSendExceptionEmail(string key)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user