From ad81d2b9cfef8033eecab9ce1f35b5697935e82d Mon Sep 17 00:00:00 2001 From: kira1928 Date: Tue, 18 Aug 2026 01:10:20 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A0=B9=E6=8D=AE=20review=20=E6=94=B6?= =?UTF-8?q?=E7=B4=A7=E5=BD=95=E5=88=B6=E5=99=A8=E5=81=9C=E6=AD=A2=E4=B8=8E?= =?UTF-8?q?=20E2E=20=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/recorders/event.go | 7 +-- src/recorders/manager.go | 3 ++ src/recorders/manager_test.go | 22 +++++++++ src/recorders/recorder.go | 8 ++-- src/recorders/recorder_test.go | 5 +- .../fixtures/ffmpeg-test-config.template.yml | 2 +- tests/e2e/fixtures/test-config.template.yml | 2 +- tests/e2e/recording.spec.ts | 47 ++++++++++++++++--- 8 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/recorders/event.go b/src/recorders/event.go index 346b953..afd9dcb 100644 --- a/src/recorders/event.go +++ b/src/recorders/event.go @@ -3,7 +3,8 @@ package recorders import "github.com/bililive-go/bililive-go/src/pkg/events" const ( - RecorderStart events.EventType = "RecorderStart" - RecorderStop events.EventType = "RecorderStop" - RecorderRestart events.EventType = "RecorderRestart" + RecorderStart events.EventType = "RecorderStart" + RecorderStop events.EventType = "RecorderStop" + RecorderRestart events.EventType = "RecorderRestart" + RecorderStopRequested events.EventType = "RecorderStopRequested" ) diff --git a/src/recorders/manager.go b/src/recorders/manager.go index 6925772..6138716 100644 --- a/src/recorders/manager.go +++ b/src/recorders/manager.go @@ -133,6 +133,9 @@ func (m *manager) registryListener(ctx context.Context, ed events.Dispatcher) { }) ed.AddEventListener(listeners.LiveEnd, removeEvtListener) ed.AddEventListener(listeners.ListenStop, removeEvtListener) + // RecorderStopRequested 仅表示 recorder 自身遇到不可重试错误,需要被回收; + // 它不具有 LiveEnd 的全局直播状态语义,避免错误结束仍在进行的直播会话。 + ed.AddEventListener(RecorderStopRequested, removeEvtListener) } func (m *manager) Start(ctx context.Context) error { diff --git a/src/recorders/manager_test.go b/src/recorders/manager_test.go index f292349..87ee6f5 100644 --- a/src/recorders/manager_test.go +++ b/src/recorders/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/bililive-go/bililive-go/src/instance" "github.com/bililive-go/bililive-go/src/live" livemock "github.com/bililive-go/bililive-go/src/live/mock" + "github.com/bililive-go/bililive-go/src/pkg/events" "github.com/bililive-go/bililive-go/src/pkg/livelogger" "github.com/bililive-go/bililive-go/src/types" ) @@ -56,6 +57,27 @@ func TestManagerAddAndRemoveRecorder(t *testing.T) { assert.False(t, m.HasRecorder(context.Background(), "test")) } +// TestRecorderStopRequestedRemovesOnlyRecorder 验证 recorder 的内部停止请求会被 +// manager 消费并回收 recorder,无需伪造具有全局直播状态语义的 LiveEnd。 +func TestRecorderStopRequestedRemovesOnlyRecorder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + ed := events.NewDispatcher(ctx) + l := livemock.NewMockLive(ctrl) + l.EXPECT().GetLiveId().Return(types.LiveID("test")).AnyTimes() + + r := NewMockRecorder(ctrl) + r.EXPECT().Close() + m := &manager{savers: map[types.LiveID]Recorder{"test": r}} + m.registryListener(ctx, ed) + + ed.DispatchEventSync(events.NewEvent(RecorderStopRequested, l)) + + assert.False(t, m.HasRecorder(ctx, "test")) +} + // TestRestartRecorderRaceWithLiveEnd 验证 RestartRecorder 和 LiveEnd(RemoveRecorder) // 并发执行时不会产生僵尸录制器。 // diff --git a/src/recorders/recorder.go b/src/recorders/recorder.go index f031319..ebb383e 100644 --- a/src/recorders/recorder.go +++ b/src/recorders/recorder.go @@ -382,11 +382,11 @@ func (r *recorder) tryRecord(ctx context.Context) { buf := new(bytes.Buffer) if err = tmpl.Execute(buf, info); err != nil { // 文件名模板渲染失败通常是配置错误(如引用了不存在的字段),每次重试都会 - // 重复失败。派发一次 LiveEnd 让 manager 回收该 recorder,避免无意义的 5s - // 重试风暴与日志刷屏,也不再被误计入活跃录制数。listener 的内部状态不受 - // 影响,房间真正下播再开播时仍会以修正后的配置重新创建 recorder。 + // 重复失败。请求 manager 仅回收该 recorder,避免无意义的 5s 重试风暴与 + // 日志刷屏,也不再被误计入活跃录制数;不要派发具有全局直播状态语义的 + // LiveEnd,否则会错误地提前结束仍在进行的 live session。 r.getLogger().WithError(err).Error("failed to render filename, stopping recorder (check out_put_tmpl)") - r.ed.DispatchEvent(events.NewEvent(listeners.LiveEnd, r.Live)) + r.ed.DispatchEvent(events.NewEvent(RecorderStopRequested, r.Live)) return } // 使用层级配置的 OutPutPath diff --git a/src/recorders/recorder_test.go b/src/recorders/recorder_test.go index 4f85450..b41d29f 100644 --- a/src/recorders/recorder_test.go +++ b/src/recorders/recorder_test.go @@ -11,7 +11,6 @@ import ( gomock "go.uber.org/mock/gomock" "github.com/bililive-go/bililive-go/src/configs" - "github.com/bililive-go/bililive-go/src/listeners" "github.com/bililive-go/bililive-go/src/live" livemock "github.com/bililive-go/bililive-go/src/live/mock" "github.com/bililive-go/bililive-go/src/pkg/events" @@ -37,9 +36,9 @@ func TestTryRecordStopsWithoutPanicWhenFilenameRenderFails(t *testing.T) { l.EXPECT().GetStreamInfos().Return([]*live.StreamUrlInfo{{Url: streamURL}}, nil) l.EXPECT().GetLogger().Return(logger) - // 渲染失败时 recorder 应派发一次 LiveEnd 交由 manager 回收,而不是无限重试。 + // 渲染失败时 recorder 应请求 manager 仅回收自身,而不是错误派发 LiveEnd。 ed := eventsmock.NewMockDispatcher(ctrl) - ed.EXPECT().DispatchEvent(events.NewEvent(listeners.LiveEnd, l)) + ed.EXPECT().DispatchEvent(events.NewEvent(RecorderStopRequested, l)) cache := gcache.New(1).LRU().Build() if err := cache.Set(l, &live.Info{Live: l}); err != nil { diff --git a/tests/e2e/fixtures/ffmpeg-test-config.template.yml b/tests/e2e/fixtures/ffmpeg-test-config.template.yml index cb464ea..9b67e55 100644 --- a/tests/e2e/fixtures/ffmpeg-test-config.template.yml +++ b/tests/e2e/fixtures/ffmpeg-test-config.template.yml @@ -16,7 +16,7 @@ log: rotate_days: 7 feature: remove_symbol_other_character: false -out_put_tmpl: '{{.RoomName}}-{{.HostName}}/{{ now | date "2006-01-02 15-04-05" }}' +out_put_tmpl: '{{.RoomName}}-{{.HostName}}/{{ now | date "2006-01-02 15-04-05" }}.flv' video_split_strategies: on_room_name_changed: false max_duration: 0s diff --git a/tests/e2e/fixtures/test-config.template.yml b/tests/e2e/fixtures/test-config.template.yml index fc3197c..9320baf 100644 --- a/tests/e2e/fixtures/test-config.template.yml +++ b/tests/e2e/fixtures/test-config.template.yml @@ -18,7 +18,7 @@ feature: # '{{ .Live.GetPlatformCNName }}/{{ .HostName | filenameFilter }}/[{{ now | date "2006-01-02 15-04-05"}}][{{ .HostName | filenameFilter }}][{{ .RoomName | filenameFilter }}].flv' # ./平台名称/主播名字/[时间戳][主播名字][房间名字].flv # https://github.com/bililive-go/bililive-go/wiki/More-Tips -out_put_tmpl: '{{.RoomName}}-{{.HostName}}/{{ now | date "2006-01-02 15-04-05" }}' +out_put_tmpl: '{{.RoomName}}-{{.HostName}}/{{ now | date "2006-01-02 15-04-05" }}.flv' video_split_strategies: on_room_name_changed: false max_duration: 0s diff --git a/tests/e2e/recording.spec.ts b/tests/e2e/recording.spec.ts index 0f8d2e8..4308cac 100644 --- a/tests/e2e/recording.spec.ts +++ b/tests/e2e/recording.spec.ts @@ -13,19 +13,55 @@ const DEV_STREAM_URL = `${DEV_STREAM_SERVER}/live/test.flv`; // bililive-go 后端地址 const BGO_SERVER = 'http://127.0.0.1:8080'; +interface LiveInfo { + id: string; + live_url?: string; +} + +const preExistingLiveIds = new Set(); +let baselineCaptured = false; + +// 本地运行时 Playwright 可能复用开发者已经启动的 bililive-go。先记录既有房间, +// 避免测试清理误删开发者的持久化配置。 +test.beforeAll(async () => { + const ctx = await apiRequest.newContext(); + try { + const res = await ctx.get(`${BGO_SERVER}/api/lives`); + if (!res.ok()) { + return; + } + const lives: LiveInfo[] = await res.json(); + if (!Array.isArray(lives)) { + return; + } + for (const live of lives) { + if (live.id) { + preExistingLiveIds.add(live.id); + } + } + baselineCaptured = true; + } finally { + await ctx.dispose(); + } +}); + // 本文件会通过 UI 添加 dev 直播间;由于所有 spec 共用同一个后端进程(workers=1), // 若不清理,残留的监控房间会一直被判定为"在播",导致后续 spec(如 update-*)的 -// active_recordings 断言失败。这里在文件所有用例结束后统一删除所有直播间,保证隔离。 +// active_recordings 断言失败。这里只删除本文件新建的 dev 直播间,既保证测试隔离, +// 也不会在复用本地服务时误删开发者已有的监控房间。 test.afterAll(async () => { + if (!baselineCaptured) { + return; + } const ctx = await apiRequest.newContext(); try { const res = await ctx.get(`${BGO_SERVER}/api/lives`); if (res.ok()) { - const lives = await res.json(); + const lives: LiveInfo[] = await res.json(); if (Array.isArray(lives)) { - for (const l of lives) { - if (l?.id) { - await ctx.delete(`${BGO_SERVER}/api/lives/${l.id}`); + for (const live of lives) { + if (live.id && live.live_url === DEV_STREAM_URL && !preExistingLiveIds.has(live.id)) { + await ctx.delete(`${BGO_SERVER}/api/lives/${live.id}`); } } } @@ -248,4 +284,3 @@ test.describe('紧急情况:停止所有录制', () => { expect(count).toBeGreaterThanOrEqual(0); }); }); -