fix: 根据 review 收紧录制器停止与 E2E 清理

This commit is contained in:
kira1928 committed 2026-08-18 01:10:20 +09:00
1 parent 524f3329c7
commit ad81d2b9cf
8 files changed
+78 -18

No files matched your search

+4 -3
View File
@@ -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"
)
+3
View File
@@ -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 {
+22
View File
@@ -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)
// 并发执行时不会产生僵尸录制器。
//
+4 -4
View File
@@ -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
+2 -3
View File
@@ -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 {
@@ -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
+1 -1
View File
@@ -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
+41 -6
View File
@@ -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<string>();
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);
});
});