fix: run dashboard queries sequentially to avoid DbContext concurrency

DbContext is not thread-safe. Task.WhenAll caused concurrent access
within the same scoped DbContext, throwing 'A second operation was
started on this context instance' errors on slower machines.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
2026-06-05 11:49:48 +08:00
co-authored by Claude Opus 4.8 noreply@anthropic.com
parent a819322559
commit f5ad1dec00
@@ -44,48 +44,34 @@ public sealed class DashboardService
var todayUtcEnd = new DateTimeOffset(todayBeijingDate.ToDateTime(TimeOnly.MaxValue), ChinaTime.Zone.GetUtcOffset(beijingNow.DateTime));
var recentErrorSince = now.AddHours(-24);
// Run independent queries in parallel for efficiency
var activeRecordingTask = _recordSessionRepository.CountByStatusAsync(RecordSessionStatus.Running, cancellationToken);
var liveRoomCountTask = _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Live, cancellationToken);
var offlineRoomCountTask = _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Offline, cancellationToken);
var totalRoomCountTask = _liveRoomRepository.CountAsync(cancellationToken);
var activeSessionCountTask = _recordSessionRepository.CountActiveAsync(cancellationToken);
var recentErrorCountTask = _systemLogRepository.CountRecentErrorsAsync(recentErrorSince, cancellationToken);
var todayDurationTask = _recordTaskRepository.SumDurationSecondsAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var todayAggregateTask = _recordResultRepository.GetTodayAggregateAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var recentSessionsTask = _recordSessionRepository.ListRecentAsync(5, cancellationToken);
var todaySessionsTask = _recordSessionRepository.ListInDateRangeAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var settingsTask = _systemSettingsService.GetAsync(cancellationToken);
await Task.WhenAll(
activeRecordingTask,
liveRoomCountTask,
offlineRoomCountTask,
totalRoomCountTask,
activeSessionCountTask,
recentErrorCountTask,
todayDurationTask,
todayAggregateTask,
recentSessionsTask,
todaySessionsTask,
(Task)settingsTask
);
var (todayTotalBytes, todayTotalDanmaku) = todayAggregateTask.Result;
var settings = settingsTask.Result;
// Run queries sequentially — DbContext is not thread-safe
var activeRecordingCount = await _recordSessionRepository.CountByStatusAsync(RecordSessionStatus.Running, cancellationToken);
var liveRoomCount = await _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Live, cancellationToken);
var offlineRoomCount = await _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Offline, cancellationToken);
var totalRoomCount = await _liveRoomRepository.CountAsync(cancellationToken);
var activeSessionCount = await _recordSessionRepository.CountActiveAsync(cancellationToken);
var recentErrorCount = await _systemLogRepository.CountRecentErrorsAsync(recentErrorSince, cancellationToken);
var todayRecordingSeconds = await _recordTaskRepository.SumDurationSecondsAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var (todayTotalBytes, todayTotalDanmaku) = await _recordResultRepository.GetTodayAggregateAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var recentSessions = await _recordSessionRepository.ListRecentAsync(5, cancellationToken);
var todaySessions = await _recordSessionRepository.ListInDateRangeAsync(todayUtcStart, todayUtcEnd, cancellationToken);
var settings = await _systemSettingsService.GetAsync(cancellationToken);
var storageCheck = _storageGuardService.CheckCanStartOrResume(settings);
var pendingTranscodeCount = await _recordTaskRepository.CountByStatusAsync(RecordTaskStatus.Processing, cancellationToken);
var pendingUploadCount = await _recordResultRepository.CountPendingUploadAsync(cancellationToken);
var queuedDataBytes = await _recordResultRepository.SumPendingUploadBytesAsync(cancellationToken);
return new DashboardDto
{
ActiveRecordingCount = activeRecordingTask.Result,
LiveRoomCount = liveRoomCountTask.Result,
OfflineRoomCount = offlineRoomCountTask.Result,
TotalRoomCount = totalRoomCountTask.Result,
TodayRecordingSeconds = todayDurationTask.Result,
ActiveRecordingCount = activeRecordingCount,
LiveRoomCount = liveRoomCount,
OfflineRoomCount = offlineRoomCount,
TotalRoomCount = totalRoomCount,
TodayRecordingSeconds = todayRecordingSeconds,
TodayDataBytes = todayTotalBytes,
TodayDanmakuCount = todayTotalDanmaku,
ActiveSessionCount = activeSessionCountTask.Result,
RecentErrorCount = recentErrorCountTask.Result,
ActiveSessionCount = activeSessionCount,
RecentErrorCount = recentErrorCount,
StorageStatus = new StorageStatusDto
{
HasEnoughSpace = storageCheck.HasEnoughSpace,
@@ -94,13 +80,13 @@ public sealed class DashboardService
Tier = storageCheck.Tier.ToString(),
UsagePercent = storageCheck.UsagePercent
},
PendingTranscodeCount = await _recordTaskRepository.CountByStatusAsync(Domain.Enums.RecordTaskStatus.Processing, cancellationToken),
PendingUploadCount = await _recordResultRepository.CountPendingUploadAsync(cancellationToken),
QueuedDataBytes = await _recordResultRepository.SumPendingUploadBytesAsync(cancellationToken),
RecentSessions = recentSessionsTask.Result
PendingTranscodeCount = pendingTranscodeCount,
PendingUploadCount = pendingUploadCount,
QueuedDataBytes = queuedDataBytes,
RecentSessions = recentSessions
.Select(MapRecentSession)
.ToList(),
TopRooms = ComputeTopRooms(todaySessionsTask.Result)
TopRooms = ComputeTopRooms(todaySessions)
};
}