feat: add live room metadata, uploads, proxies and backups
This commit is contained in:
@@ -25,11 +25,13 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
private static readonly TimeSpan OfflineForcedStopTimeout = TimeSpan.FromSeconds(8);
|
||||
private static readonly TimeSpan ExceptionEmailCooldown = TimeSpan.FromHours(6);
|
||||
private static readonly TimeSpan PollDispatchSpacing = TimeSpan.FromMilliseconds(400);
|
||||
private static readonly TimeSpan MinimumIdleDelay = TimeSpan.FromSeconds(2);
|
||||
private const int MaxConcurrentLiveRoomPolls = 2;
|
||||
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<LiveRoomPollingBackgroundService> _logger;
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _exceptionEmailSentAt = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<Guid, DateTimeOffset> _nextPollDueAt = new();
|
||||
|
||||
public LiveRoomPollingBackgroundService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
@@ -43,15 +45,13 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var delaySeconds = 60;
|
||||
var delay = TimeSpan.FromSeconds(60);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISystemSettingsService>();
|
||||
var settings = await settingsService.GetAsync(stoppingToken);
|
||||
|
||||
delaySeconds = settings.PollingIntervalSeconds;
|
||||
var ffmpegService = scope.ServiceProvider.GetRequiredService<IFfmpegService>();
|
||||
var storageGuardService = scope.ServiceProvider.GetRequiredService<IStorageGuardService>();
|
||||
if (storageGuardService.CheckCanStartOrResume(settings).HasEnoughSpace)
|
||||
@@ -61,21 +61,64 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
|
||||
if (!settings.EnableBackgroundPolling)
|
||||
{
|
||||
await DelayAsync(delaySeconds, stoppingToken);
|
||||
await DelayAsync(TimeSpan.FromSeconds(Math.Clamp(settings.PollingIntervalSeconds, 10, 3600)), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
|
||||
var liveRoomIds = (await dbContext.LiveRooms
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var candidates = (await dbContext.LiveRooms
|
||||
.AsNoTracking()
|
||||
.Where(static item => item.IsEnabled)
|
||||
.Select(static item => new { item.Id, item.UpdatedAt })
|
||||
.Select(static item => new
|
||||
{
|
||||
item.Id,
|
||||
item.UpdatedAt,
|
||||
item.LastCheckedAt,
|
||||
item.IsPriority,
|
||||
item.PollingIntervalSecondsOverride
|
||||
})
|
||||
.ToListAsync(stoppingToken))
|
||||
.OrderBy(static item => item.UpdatedAt)
|
||||
.Select(static item => item.Id)
|
||||
.ToList();
|
||||
|
||||
await PollLiveRoomsAsync(liveRoomIds, settings, stoppingToken);
|
||||
var pollCandidates = candidates
|
||||
.Select(item =>
|
||||
{
|
||||
var intervalSeconds = GetEffectivePollingIntervalSeconds(settings.PollingIntervalSeconds, item.PollingIntervalSecondsOverride);
|
||||
var baselineDueAt = item.LastCheckedAt?.AddSeconds(intervalSeconds) ?? DateTimeOffset.MinValue;
|
||||
var dueAt = _nextPollDueAt.TryGetValue(item.Id, out var scheduledDueAt) && scheduledDueAt > baselineDueAt
|
||||
? scheduledDueAt
|
||||
: baselineDueAt;
|
||||
return new PollCandidate(item.Id, dueAt, item.IsPriority, item.UpdatedAt, intervalSeconds);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (pollCandidates.Count == 0)
|
||||
{
|
||||
delay = TimeSpan.FromSeconds(Math.Clamp(settings.PollingIntervalSeconds, 10, 3600));
|
||||
}
|
||||
else
|
||||
{
|
||||
var dueCandidates = pollCandidates
|
||||
.Where(item => item.DueAt <= now)
|
||||
.OrderByDescending(static item => item.IsPriority)
|
||||
.ThenBy(static item => item.DueAt)
|
||||
.ThenBy(static item => item.UpdatedAt)
|
||||
.ToList();
|
||||
|
||||
if (dueCandidates.Count == 0)
|
||||
{
|
||||
var nextDueAt = pollCandidates.Min(static item => item.DueAt);
|
||||
delay = nextDueAt <= now
|
||||
? MinimumIdleDelay
|
||||
: ClampDelay(nextDueAt - now);
|
||||
}
|
||||
else
|
||||
{
|
||||
await PollLiveRoomsAsync(dueCandidates, settings, stoppingToken);
|
||||
delay = MinimumIdleDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -117,35 +160,52 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
await DelayAsync(delaySeconds, stoppingToken);
|
||||
await DelayAsync(delay, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task DelayAsync(int delaySeconds, CancellationToken cancellationToken) =>
|
||||
Task.Delay(TimeSpan.FromSeconds(Math.Clamp(delaySeconds, 10, 3600)), cancellationToken);
|
||||
private static Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||
Task.Delay(ClampDelay(delay), cancellationToken);
|
||||
|
||||
private static TimeSpan ClampDelay(TimeSpan delay)
|
||||
{
|
||||
if (delay <= TimeSpan.Zero)
|
||||
{
|
||||
return MinimumIdleDelay;
|
||||
}
|
||||
|
||||
return delay < MinimumIdleDelay
|
||||
? MinimumIdleDelay
|
||||
: delay > TimeSpan.FromHours(1)
|
||||
? TimeSpan.FromHours(1)
|
||||
: delay;
|
||||
}
|
||||
|
||||
private static int GetEffectivePollingIntervalSeconds(int globalIntervalSeconds, int? overrideIntervalSeconds) =>
|
||||
Math.Clamp(overrideIntervalSeconds ?? globalIntervalSeconds, 10, 3600);
|
||||
|
||||
private async Task PollLiveRoomsAsync(
|
||||
IReadOnlyList<Guid> liveRoomIds,
|
||||
IReadOnlyList<PollCandidate> liveRooms,
|
||||
SystemSettingsDto settings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (liveRoomIds.Count == 0)
|
||||
if (liveRooms.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var semaphore = new SemaphoreSlim(Math.Min(MaxConcurrentLiveRoomPolls, liveRoomIds.Count));
|
||||
var tasks = new List<Task>(liveRoomIds.Count);
|
||||
using var semaphore = new SemaphoreSlim(Math.Min(MaxConcurrentLiveRoomPolls, liveRooms.Count));
|
||||
var tasks = new List<Task>(liveRooms.Count);
|
||||
|
||||
for (var index = 0; index < liveRoomIds.Count; index++)
|
||||
for (var index = 0; index < liveRooms.Count; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken);
|
||||
var liveRoomId = liveRoomIds[index];
|
||||
tasks.Add(PollLiveRoomWithReleaseAsync(liveRoomId, settings, semaphore, cancellationToken));
|
||||
var liveRoom = liveRooms[index];
|
||||
tasks.Add(PollLiveRoomWithReleaseAsync(liveRoom, settings, semaphore, cancellationToken));
|
||||
|
||||
if (index < liveRoomIds.Count - 1)
|
||||
if (index < liveRooms.Count - 1)
|
||||
{
|
||||
await Task.Delay(PollDispatchSpacing, cancellationToken);
|
||||
}
|
||||
@@ -155,14 +215,14 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
}
|
||||
|
||||
private async Task PollLiveRoomWithReleaseAsync(
|
||||
Guid liveRoomId,
|
||||
PollCandidate liveRoom,
|
||||
SystemSettingsDto settings,
|
||||
SemaphoreSlim semaphore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PollLiveRoomAsync(liveRoomId, settings, cancellationToken);
|
||||
await PollLiveRoomAsync(liveRoom.LiveRoomId, liveRoom.IntervalSeconds, settings, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -170,7 +230,11 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PollLiveRoomAsync(Guid liveRoomId, SystemSettingsDto settings, CancellationToken cancellationToken)
|
||||
private async Task PollLiveRoomAsync(
|
||||
Guid liveRoomId,
|
||||
int intervalSeconds,
|
||||
SystemSettingsDto settings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
|
||||
@@ -186,6 +250,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
var liveRoom = await dbContext.LiveRooms.FirstOrDefaultAsync(item => item.Id == liveRoomId, cancellationToken);
|
||||
if (liveRoom is null || !liveRoom.IsEnabled)
|
||||
{
|
||||
_nextPollDueAt.TryRemove(liveRoomId, out _);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,6 +406,10 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nextPollDueAt[liveRoomId] = DateTimeOffset.UtcNow.AddSeconds(Math.Clamp(intervalSeconds, 10, 3600));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CompleteActiveSessionsForOfflineRoomAsync(
|
||||
@@ -642,4 +711,11 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
||||
var trimmed = value.Trim();
|
||||
return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength];
|
||||
}
|
||||
|
||||
private sealed record PollCandidate(
|
||||
Guid LiveRoomId,
|
||||
DateTimeOffset DueAt,
|
||||
bool IsPriority,
|
||||
DateTimeOffset UpdatedAt,
|
||||
int IntervalSeconds);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user