合并任务排队串行,不再互相顶掉进度和杀错 ffmpeg
审查发现:全局只有一份任务进度状态,但没有任何东西拦住两个合并同时跑。
1. 为什么必须串行
jobStart() 原来是无条件重置全部字段的。多个主播同时下播(很常见),
或者自动合并跑着的时候在页面上点一次手动合并,就会有两个任务同时跑:
后启动的把前一个的输出名、进度、已渲染时长全部清零;更糟的是 jobStop()
只认最后注册的那个 ffmpeg,点「停止」杀的是别人家任务的进程,
而自己那个继续跑;两个任务还会互相读到 stopped 标记而提前退出。
界面上看只是一个进度条乱跳,背后是合并结果可能张冠李戴。
2. 怎么串起来的
加一把 mergeSlot 锁占任务位:
- 手动合并用 TryLock,抢不到直接 409 并告诉用户现在谁在跑 —— 它是一次
HTTP 请求,不能让人干等几十分钟。
- 自动合并用 Lock 排队等待,它本来就是后台任务,丢掉白录一场。
jobStart 改成返回 bool:已经有任务在跑时返回 false 且一个字段都不动。
这样即使以后有调用方忘了先拿任务位,也只是失败而不会互相覆盖。
3. 顺带修「panic 之后卡片一直转」
原来 jobFinish() 写在 autoMerge / mergeHandler 的正常返回路径上,任务内部
一旦 panic(外部 safeGo 的 recover 兜在外层)它就永远不会执行,任务会一直
停在 active,界面上的进度条再也不动,ffmpeg 也失联了。
现在统一走 runMergeExclusive():任务位用 defer 释放,panic 翻译成 error,
保证 jobFinish 一定能跑到。
4. 之前 ban 掉的 jobActive() 接回来了
它本来就该是这道护栏,写了却没人调用,现在用于判断要不要提示「在排队」
以及拒绝新任务时报出正在忙的任务名。jobState 的锁改成 RWMutex 供它读。
已验证:
- 新增 4 个用例:TestJobStartRejectsWhenBusy(被拒时 Must 一个字段都不改,
包括进度必须还是原任务的)、TestMergeSlotTryLockExclusive、
TestMergeSlotSerializes(4 个并发任务必须全跑完且不重不漏)、
TestMergeExclusiveReleasesOnPanic(panic 后必须拿到任务位 + 翻出错误)。
- 反向验证:把 jobStart 的占用判断临时改成 `if false && job.active`,
TestJobStartRejectsWhenBusy 立刻报「应当返回 false」。
- 现有测试跟着走真实链路(progress_test 加了 startJobForTest 统一先收尾、
manualmerge_test 走 execManualMerge 独占执行)—— 改之前这些用例单独跑过、
连着跑会失败,正是因为前一个用例把任务状态留在了 active。
- gofmt / go vet / go build / GOOS=linux go build / go test / go test -race 全过。
This commit is contained in:
+63
-11
@@ -1,10 +1,12 @@
|
||||
// 转换任务(合并 / 烧录弹幕)的实时进度与暂停、停止控制。
|
||||
// 进度只保留一份全局状态:同一时间只允许一个合并任务在跑(mergeRun 已经拦住了)。
|
||||
// 进度只保留一份全局状态:同一时间只允许一个合并任务在跑,串行靠下面的 mergeSlot 保证
|
||||
// (mergeRun.busy 只拦得住手动合并之间,拦不住自动合并,也拦不住手动与自动互相穿插)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
@@ -77,7 +79,7 @@ type jobInfo struct {
|
||||
|
||||
// 全局唯一的任务状态
|
||||
type jobState struct {
|
||||
sync.Mutex
|
||||
sync.RWMutex
|
||||
active bool
|
||||
kind string
|
||||
outName string
|
||||
@@ -115,10 +117,49 @@ type jobState struct {
|
||||
|
||||
var job jobState
|
||||
|
||||
// 开始一个新任务。kind 是任务类型,totalSec 是视频总时长(拿不到填 0)
|
||||
func jobStart(kind, outName string, inputBytes int64, totalSec float64) {
|
||||
// 合并任务的串行锁。全局 job 状态只有一份,两个任务同时跑的话后启动的会把前一个的
|
||||
// 进度和输出名清掉,「停止」也会杀掉别家任务的 ffmpeg,所以必须排队。
|
||||
// 手动合并用 TryLock(抢不到立刻告诉用户),自动合并用 Lock(后台任务,排队等就行)。
|
||||
var mergeSlot sync.Mutex
|
||||
|
||||
// 尝试占用合并任务位,被占着返回 false。给手动合并用:不能让 HTTP 请求干等着
|
||||
func tryTakeMergeSlot() bool {
|
||||
return mergeSlot.TryLock()
|
||||
}
|
||||
|
||||
// 占用合并任务位,被占着就等到前一个跑完。给自动合并用
|
||||
func takeMergeSlot() {
|
||||
mergeSlot.Lock()
|
||||
}
|
||||
|
||||
func releaseMergeSlot() {
|
||||
mergeSlot.Unlock()
|
||||
}
|
||||
|
||||
// 排队跑完一整个合并任务:先独占任务位,跑完后一定释放,被调用方 panic 也不漏。
|
||||
// 内部出错会被翻成 error 而不是让上层去 recover —— 任务卡片只有走到 jobFinish 才会收尾,
|
||||
// 漏掉的话进度会永远停在界面上(之前 panic 后卡片一直转就是这么来的)
|
||||
func runMergeExclusive(f func() error) (err error) {
|
||||
takeMergeSlot()
|
||||
defer releaseMergeSlot()
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("合并任务内部出错:%v", p)
|
||||
lPrintErrf("合并任务内部出错,已兜住:%v", p)
|
||||
}
|
||||
}()
|
||||
return f()
|
||||
}
|
||||
|
||||
// 开始一个新任务。kind 是任务类型,totalSec 是视频总时长(拿不到填 0)。
|
||||
// 已经有任务在跑时返回 false 并且什么都不动:调用方应该先拿到 mergeSlot,
|
||||
// 这里再做一次判断是为了让误用(忘记排队)只会失败而不会互相覆盖
|
||||
func jobStart(kind, outName string, inputBytes int64, totalSec float64) bool {
|
||||
job.Lock()
|
||||
defer job.Unlock()
|
||||
if job.active {
|
||||
return false
|
||||
}
|
||||
job.active = true
|
||||
job.kind = kind
|
||||
job.outName = outName
|
||||
@@ -143,6 +184,24 @@ func jobStart(kind, outName string, inputBytes int64, totalSec float64) {
|
||||
job.errText = ""
|
||||
job.message = ""
|
||||
job.doneAt = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// 当前是否有任务在跑。给界面提示和「现在能不能接新任务」的判断用
|
||||
func jobActive() bool {
|
||||
job.RLock()
|
||||
defer job.RUnlock()
|
||||
return job.active
|
||||
}
|
||||
|
||||
// 当前任务的名字(没任务时为空),拒绝新任务时用它告诉用户在忙什么
|
||||
func jobBusyName() string {
|
||||
job.RLock()
|
||||
defer job.RUnlock()
|
||||
if !job.active {
|
||||
return ""
|
||||
}
|
||||
return job.outName
|
||||
}
|
||||
|
||||
// 任务结束。errText 为空表示成功,errJobStopped.Error() 表示被手动停止
|
||||
@@ -168,13 +227,6 @@ func jobFinish(errText string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 任务是否正在跑
|
||||
func jobActive() bool {
|
||||
job.Lock()
|
||||
defer job.Unlock()
|
||||
return job.active
|
||||
}
|
||||
|
||||
// 是否已经被要求停止
|
||||
func jobStopRequested() bool {
|
||||
job.Lock()
|
||||
|
||||
Reference in New Issue
Block a user