fix: add max_alloc to prevent overlong headers error and throttle startup failure notifications

- Add -max_alloc 100000000 to FFmpeg arguments for HTTP inputs to avoid
  'overlong headers' error when CDN (e.g. Douyin) returns oversized HTTP
  response headers exceeding FFmpeg's default 4096-byte buffer.

- Add exponential backoff for repeated startup failures (30s → 15min cap)
  to break the tight fail→retry→re-poll loop that floods notifications.

- Throttle startup failure notifications to at most one per 30 minutes per
  room to prevent email/webhook storms during persistent failures.

- Reset backoff counter when a session successfully opens its first segment.
This commit is contained in:
2026-07-09 11:51:52 +08:00
parent 48da49ac72
commit 8a079b4698
3 changed files with 119 additions and 14 deletions
@@ -324,6 +324,7 @@ public sealed partial class FfmpegService
if (runtime.SaveMode == RecordSaveMode.SingleFile)
{
runtime.HasOpenedFirstSegment = true;
ResetStartupFailureBackoff(runtime.LiveRoomId);
currentTask.AttachProcess(runtime.ProcessId, now);
currentTask.MarkRunning(now);
session.AttachProcess(runtime.ProcessId, now);
@@ -361,6 +362,7 @@ public sealed partial class FfmpegService
if (segmentIndex == runtime.CurrentSegmentIndex)
{
runtime.HasOpenedFirstSegment = true;
ResetStartupFailureBackoff(runtime.LiveRoomId);
var finalOutputPath = NormalizeAbsolutePath(
currentTask.OutputFilePath ??
ResolveSegmentOutputPath(session.OutputPathPattern ?? runtime.OutputPathPattern, session.SaveMode, segmentIndex));
@@ -435,6 +437,7 @@ public sealed partial class FfmpegService
runtime.CurrentOutputFilePath = normalizedOpenedPath;
runtime.ResetCurrentRecorderSegmentPaths(normalizedOpenedPath);
runtime.HasOpenedFirstSegment = true;
ResetStartupFailureBackoff(runtime.LiveRoomId);
if (runtime.DanmakuRecorder is not null)
{
@@ -1213,24 +1216,38 @@ public sealed partial class FfmpegService
if (!runtime.StopRequested && session.LiveRoomId != Guid.Empty)
{
_liveRoomPollingSignal.RequestImmediatePoll(session.LiveRoomId, TimeSpan.FromSeconds(2));
var isStartupFailure = !runtime.HasOpenedFirstSegment &&
runtime.StartupFailureKind != StartupFailureKind.None;
var pollDelay = isStartupFailure
? RecordStartupFailure(session.LiveRoomId)
: TimeSpan.FromSeconds(2);
_liveRoomPollingSignal.RequestImmediatePoll(session.LiveRoomId, pollDelay);
}
if (session.Status == RecordSessionStatus.Failed && session.LiveRoom is not null)
{
var failureDetail = BuildExitLogDetail(runtime, process.ExitCode, effectiveOutputPath, toleratedNonZeroExit: false, finalizationError);
await emailNotificationService.SendExceptionAsync(
"FFmpeg",
"Recording session exited abnormally.",
failureDetail,
session.LiveRoom,
currentTask);
await webhookNotificationService.SendExceptionAsync(
"FFmpeg",
"Recording session exited abnormally.",
failureDetail,
session.LiveRoom,
currentTask);
var isStartupFailure = !runtime.HasOpenedFirstSegment &&
runtime.StartupFailureKind != StartupFailureKind.None;
if (isStartupFailure && ShouldThrottleStartupFailureNotification(session.LiveRoomId))
{
// Notification suppressed — the failure has already been logged above.
}
else
{
var failureDetail = BuildExitLogDetail(runtime, process.ExitCode, effectiveOutputPath, toleratedNonZeroExit: false, finalizationError);
await emailNotificationService.SendExceptionAsync(
"FFmpeg",
"Recording session exited abnormally.",
failureDetail,
session.LiveRoom,
currentTask);
await webhookNotificationService.SendExceptionAsync(
"FFmpeg",
"Recording session exited abnormally.",
failureDetail,
session.LiveRoom,
currentTask);
}
}
}