refactor(daemon): revisión crítica del reporte de errores de sesión
- failSession usa un contexto fresco (no el del daemon): los fallos se concentran justo cuando el daemon se apaga (la cancelación mata arranques en vuelo) y un report derivado de ese contexto moría antes de llegar a la web; el cap de 10s sigue acotándolo - consts sessErrFfmpegMissing/sessErrStartFailed sustituyen los 7 literales inline (un typo habría producido un code que el z.enum de la web rechaza con 400 — exactamente el fallo mudo que este canal elimina) - markReady() unifica los tres goroutines idénticos de MarkSessionReady de los caminos sin transcode (direct-play, remux, debrid direct)
This commit is contained in:
parent
0dca296fec
commit
898fe80f4e
1 changed files with 36 additions and 31 deletions
|
|
@ -684,7 +684,11 @@ func runDaemonStart() error {
|
||||||
failSession := func(sessionID, code, message string) {
|
failSession := func(sessionID, code, message string) {
|
||||||
log.Printf("[hls %s] failed (%s): %s", agent.ShortID(sessionID), code, message)
|
log.Printf("[hls %s] failed (%s): %s", agent.ShortID(sessionID), code, message)
|
||||||
go func() {
|
go func() {
|
||||||
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
// Fresh context on purpose: failures cluster exactly when the
|
||||||
|
// daemon ctx is being cancelled (shutdown kills in-flight
|
||||||
|
// session starts), and a report derived from it would die
|
||||||
|
// before reaching the web. The 10s cap still bounds it.
|
||||||
|
rctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := agentClient.ReportSessionError(rctx, sessionID, code, message); err != nil {
|
if err := agentClient.ReportSessionError(rctx, sessionID, code, message); err != nil {
|
||||||
log.Printf("[hls %s] session error report failed: %v", agent.ShortID(sessionID), err)
|
log.Printf("[hls %s] session error report failed: %v", agent.ShortID(sessionID), err)
|
||||||
|
|
@ -692,6 +696,20 @@ func runDaemonStart() error {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markReady reports "first bytes are servable" for the no-transcode
|
||||||
|
// paths (direct-play, remux, debrid direct) — one place instead of a
|
||||||
|
// copy per branch. HLS sessions report via watchSessionReady instead
|
||||||
|
// (they wait for seg-0 + attach a health snapshot).
|
||||||
|
markReady := func(sessionID string) {
|
||||||
|
go func() {
|
||||||
|
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := agentClient.MarkSessionReady(rctx, sessionID, nil); err != nil {
|
||||||
|
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sessionID), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// startHLSPlayback starts an HLS encode (local file or debrid URL) and
|
// startHLSPlayback starts an HLS encode (local file or debrid URL) and
|
||||||
// wires it into the StreamServer. Shared by the local-file HLS path and
|
// wires it into the StreamServer. Shared by the local-file HLS path and
|
||||||
// the debrid HLS-from-URL path (hueco #2 / 2b) so both register, probe
|
// the debrid HLS-from-URL path (hueco #2 / 2b) so both register, probe
|
||||||
|
|
@ -743,7 +761,7 @@ func runDaemonStart() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
playerSessionRegistry.remove(hlsCfg.SessionID)
|
playerSessionRegistry.remove(hlsCfg.SessionID)
|
||||||
hlsCancel()
|
hlsCancel()
|
||||||
failSession(hlsCfg.SessionID, "start_failed", err.Error())
|
failSession(hlsCfg.SessionID, sessErrStartFailed, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if prewarm {
|
if prewarm {
|
||||||
|
|
@ -783,17 +801,13 @@ func runDaemonStart() error {
|
||||||
provider, perr := engine.NewDebridFileProvider(bctx, sess.DirectURL, sess.FileName, sess.FileSize, refresh)
|
provider, perr := engine.NewDebridFileProvider(bctx, sess.DirectURL, sess.FileName, sess.FileSize, refresh)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
playerSessionRegistry.remove(sess.SessionID)
|
playerSessionRegistry.remove(sess.SessionID)
|
||||||
failSession(sess.SessionID, "start_failed", fmt.Sprintf("debrid provider: %v", perr))
|
failSession(sess.SessionID, sessErrStartFailed, fmt.Sprintf("debrid provider: %v", perr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
streamSrv.SetFile(provider, sess.TaskID)
|
streamSrv.SetFile(provider, sess.TaskID)
|
||||||
log.Printf("[stream %s] debrid direct-play: %s (%d bytes)",
|
log.Printf("[stream %s] debrid direct-play: %s (%d bytes)",
|
||||||
agent.ShortID(sess.SessionID), provider.FileName(), provider.FileSize())
|
agent.ShortID(sess.SessionID), provider.FileName(), provider.FileSize())
|
||||||
rctx, rcancel := context.WithTimeout(ctx, 10*time.Second)
|
markReady(sess.SessionID)
|
||||||
defer rcancel()
|
|
||||||
if err := agentClient.MarkSessionReady(rctx, sess.SessionID, nil); err != nil {
|
|
||||||
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sess.SessionID), err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -806,7 +820,7 @@ func runDaemonStart() error {
|
||||||
if sess.DirectURL != "" { // playMethod == "hls" implied (2a returned above)
|
if sess.DirectURL != "" { // playMethod == "hls" implied (2a returned above)
|
||||||
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
||||||
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
||||||
failSession(sess.SessionID, "ffmpeg_unavailable", "ffmpeg/ffprobe unavailable (debrid HLS)")
|
failSession(sess.SessionID, sessErrFfmpegMissing, "ffmpeg/ffprobe unavailable (debrid HLS)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hlsCtx, hlsCancel := context.WithCancel(ctx)
|
hlsCtx, hlsCancel := context.WithCancel(ctx)
|
||||||
|
|
@ -833,7 +847,7 @@ func runDaemonStart() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if sess.FilePath == "" {
|
if sess.FilePath == "" {
|
||||||
failSession(sess.SessionID, "start_failed", "empty file path")
|
failSession(sess.SessionID, sessErrStartFailed, "empty file path")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// SAME base-path self-heal + stat-retry + dir resolution as the raw
|
// SAME base-path self-heal + stat-retry + dir resolution as the raw
|
||||||
|
|
@ -864,19 +878,13 @@ func runDaemonStart() error {
|
||||||
log.Printf("[stream %s] direct-play: %s", agent.ShortID(sess.SessionID), filepath.Base(filePath))
|
log.Printf("[stream %s] direct-play: %s", agent.ShortID(sess.SessionID), filepath.Base(filePath))
|
||||||
// File is on disk → ready immediately. Tell the web so the player
|
// File is on disk → ready immediately. Tell the web so the player
|
||||||
// attaches <video src> without burning its HEAD-probe retry budget.
|
// attaches <video src> without burning its HEAD-probe retry budget.
|
||||||
go func() {
|
markReady(sess.SessionID)
|
||||||
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := agentClient.MarkSessionReady(rctx, sess.SessionID, nil); err != nil {
|
|
||||||
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sess.SessionID), err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
||||||
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
||||||
failSession(sess.SessionID, "ffmpeg_unavailable", "ffmpeg/ffprobe unavailable")
|
failSession(sess.SessionID, sessErrFfmpegMissing, "ffmpeg/ffprobe unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,7 +898,7 @@ func runDaemonStart() error {
|
||||||
probe, perr := engine.ProbeFile(probeCtx, tcRuntime.FFprobePath, filePath)
|
probe, perr := engine.ProbeFile(probeCtx, tcRuntime.FFprobePath, filePath)
|
||||||
cancelProbe()
|
cancelProbe()
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
failSession(sess.SessionID, "start_failed", fmt.Sprintf("remux probe: %v", perr))
|
failSession(sess.SessionID, sessErrStartFailed, fmt.Sprintf("remux probe: %v", perr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tProbe := time.Now()
|
tProbe := time.Now()
|
||||||
|
|
@ -898,7 +906,7 @@ func runDaemonStart() error {
|
||||||
src, serr := engine.NewRemuxSource(remuxCtx, filePath, probe, tcRuntime.FFmpegPath, sess.FileName)
|
src, serr := engine.NewRemuxSource(remuxCtx, filePath, probe, tcRuntime.FFmpegPath, sess.FileName)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
remuxCancel()
|
remuxCancel()
|
||||||
failSession(sess.SessionID, "start_failed", fmt.Sprintf("remux start: %v", serr))
|
failSession(sess.SessionID, sessErrStartFailed, fmt.Sprintf("remux start: %v", serr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
streamSrv.SetGrowingFile(src, sess.TaskID)
|
streamSrv.SetGrowingFile(src, sess.TaskID)
|
||||||
|
|
@ -912,13 +920,7 @@ func runDaemonStart() error {
|
||||||
log.Printf("[stream %s] remux (copy) → fMP4: %s [probe=%v spawn=%v]",
|
log.Printf("[stream %s] remux (copy) → fMP4: %s [probe=%v spawn=%v]",
|
||||||
agent.ShortID(sess.SessionID), filepath.Base(filePath),
|
agent.ShortID(sess.SessionID), filepath.Base(filePath),
|
||||||
tProbe.Sub(tStart).Round(time.Millisecond), time.Since(tProbe).Round(time.Millisecond))
|
tProbe.Sub(tStart).Round(time.Millisecond), time.Since(tProbe).Round(time.Millisecond))
|
||||||
go func() {
|
markReady(sess.SessionID)
|
||||||
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := agentClient.MarkSessionReady(rctx, sess.SessionID, nil); err != nil {
|
|
||||||
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sess.SessionID), err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1153,13 +1155,16 @@ func relocateUnreachable(filePath string, allowedRoots []string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stable machine codes for the web's session-error channel
|
// Stable machine codes for the web's session-error channel
|
||||||
// (POST /api/internal/agent/session-error). Only "file_missing" triggers
|
// (POST /api/internal/agent/session-error) — mirrored by
|
||||||
|
// SESSION_ERROR_CODES in the web repo. Only "file_missing" triggers
|
||||||
// destructive self-heal on the web (it prunes the stale library row + task
|
// destructive self-heal on the web (it prunes the stale library row + task
|
||||||
// pointer), so the resolver must never return it while the file may exist.
|
// pointer), so the resolver must never return it while the file may exist.
|
||||||
const (
|
const (
|
||||||
pathErrRejected = "path_rejected"
|
pathErrRejected = "path_rejected"
|
||||||
pathErrMissing = "file_missing"
|
pathErrMissing = "file_missing"
|
||||||
pathErrNoVideo = "no_video_file"
|
pathErrNoVideo = "no_video_file"
|
||||||
|
sessErrFfmpegMissing = "ffmpeg_unavailable"
|
||||||
|
sessErrStartFailed = "start_failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resolvePlayableFile validates and self-heals a web-provided source path into
|
// resolvePlayableFile validates and self-heals a web-provided source path into
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue