Files
EchoChat/docs/plans/2026-04-21-phase2e-2-task8-lifecycle.plan.md
bujinyuan e7f6a32dfe docs(phase2e-2): 归档 Task 8 实施 plan 文件至 docs/plans
将 Cursor Plan 模式下的临时产物 task_8_会议生命周期状态机_3a0bc37b.plan.md
归档至 docs/plans/ 目录,统一命名风格为日期前缀 + 阶段/任务描述:
2026-04-21-phase2e-2-task8-lifecycle.plan.md

用途:
- 与 docs/plans/2026-04-21-phase2e-2-{design,implementation.plan}.md 共同构成
  Phase 2e-2 Task 8 的完整设计 → 实施 → 归档三件套
- 保留 15 个 todo 的详细拆分 + 参考附录,作为后续复杂 Task 的实施样本
- 头部补充 archived_at / archived_note,指向实际交付结果与 commit

不改动文件主体内容,仅追加归档元数据。

Made-with: Cursor
2026-04-21 18:24:49 +08:00

11 KiB
Raw Permalink Blame History

name, overview, archived_at, archived_note, todos, isProject
name overview archived_at archived_note todos isProject
Task 8 会议生命周期状态机 落地 Phase 2e-2 Task 8host 掉线 2 分钟宽限期 + 自动转让、空房 5 分钟 TTL、后台定时清理兜底任务同步修复 Task 7 遗留的 CreateRouter 重复调用。整体 0.5 人日。 2026-04-21 本文件原为 Cursor Plan 模式临时产物(.cursor/plans/task_8_*.plan.md Task 8 实施完成后归档至 docs/plans/ 供后续审计追溯。 实施结果与实际偏差见 docs/plans/2026-04-21-phase2e-2-implementation.plan.md §Task 8 与 docs/progress/CURRENT_STATUS.md 的 Task 8 交付条目E2E 验证 20/20 PASS docs/verify/meeting_t8_verify.mjs最终 commit: 3b83c79。
id content status
t8-config config.go + yaml 新增 MeetingConfigHostGrace/EmptyTTL/CleanupInterval/StaleRoomHours completed
id content status
t8-lifecycle 新建 meeting_lifecycle_service.go5 个钩子 + sync.Map timer + RescheduleFromRedis completed
id content status
t8-dao MeetingRoomDAO 新增 ListStaleActive2 行 SQL completed
id content status
t8-service-fix meeting_service.goJoinRoom 移除 CreateRouter + LeaveRoom 空房改调 OnAllMembersLeft + JoinRoom 复活 empty_ttl completed
id content status
t8-signal-fix meeting_signal_service.go新增 OnWSDisconnect + OnRoomJoin 钩 host 重连 completed
id content status
t8-http-idempotent HTTPMediaOrchestrator.CreateRouter 加 sync.Map 幂等防御 completed
id content status
t8-ws-hook ws.handler 新增 MeetingDisconnectHook 接口注入 + wire.Bind completed
id content status
t8-task 新建 meeting_cleanup_task.go30s 扰动过期 + 10min stale + 24h chat 清理) completed
id content status
t8-wire provider.go + wire.go + wire_gen.go 注入新加 5 个类型 + main.go 启动 CleanupTask completed
id content status
t8-build go build ./... + go vet + ReadLints 全绿 completed
id content status
t8-verify E2E 脚本 meeting_t8_verify.mjshost 宽限期转让 / 重连 / 空房 TTL 复活 / 过期 Ended / Router 不再重复建) completed
id content status
t8-doc 更新 5 份文档CURRENT_STATUS / implementation.plan / design / api/frontend/meeting / project-context completed
id content status
t8-commit git commit feat(phase2e-2): Task 8 会议生命周期状态机 completed
false

一、核心决策(回答已确认)

  • 普通成员 WS 断线Q1=A:维持现状,仅清 WS 媒体资源,不动 meeting_participants;长期不活跃依赖 4h 兜底清理Task 8 内实现)
  • Router 去重Q2=C:业务层 JoinRoom 不再调 CreateRouter + HTTPMediaOrchestrator.CreateRouter 加 sync.Map 幂等防御
  • 过期检测time.AfterFunc 为主(低延迟)+ 后台 meeting_cleanup_task 每 30s 扫描兜底(服务重启/机器迁移场景)
  • 并发保护HandleHostGraceExpired 入口先 DEL host_grace keyDEL 返回 0 视为已被重连清走,直接 return天然互斥
  • 可测性config.MeetingConfig{HostGraceSeconds, EmptyRoomTTLSeconds, CleanupIntervalSeconds},默认 120/300/30E2E 测试可覆盖为 2/3/1

二、状态机流转

stateDiagram-v2
  [*] --> Active: CreateRoom
  Active --> HostGrace: OnHostDisconnect (host WS 掉线)
  HostGrace --> Active: OnHostReconnect (120s 内重连DEL grace key)
  HostGrace --> Transferring: host_grace 过期 AfterFunc 触发
  Transferring --> Active: 事务转让最早加入者 + 广播 host.changed
  Transferring --> EmptyTTL: 无其他活跃成员
  Active --> EmptyTTL: OnAllMembersLeft (全员 Leave)
  EmptyTTL --> Active: JoinRoom 命中 empty_ttl key 复活
  EmptyTTL --> Ended: 300s 过期 + CloseRouter + status=2 reason=empty_ttl
  Active --> Ended: host EndRoom (status=2 reason=host_ended)
  Ended --> [*]

三、文件变更清单

3.1 新建

  • backend/go-service/app/meeting/service/meeting_lifecycle_service.go(约 350 行)
    • 统一封装 5 个钩子:OnHostDisconnect / OnHostReconnect / HandleHostGraceExpired / OnAllMembersLeft / HandleEmptyRoomExpired
    • 内部 sync.Map[roomCode]*time.Timer 管理本地 AfterFunc
    • 暴露 RescheduleFromRedis(ctx)(服务启动时扫描已存在的 grace/empty_ttl key 补装 timer
  • backend/go-service/app/meeting/task/meeting_cleanup_task.go(约 180 行)
    • 启动时先 lifecycleSvc.RescheduleFromRedis 一次
    • 每 30secho:meeting:host_grace:* 找出 TTL ≤ 0 或 timer 已丢失的 key → 再次触发 HandleHostGraceExpired;同理处理 empty_ttl key
    • 每 10 分钟:roomDAO.ListStaleActive(hoursNoActivity=4)EndRoom(..., reason=system_error) 兜底
    • 每 24 小时:chatDAO.DeleteByRoomIDs(olderThan=24h) 聊天清理
  • docs/verify/meeting_t8_verify.mjs(约 250 行E2E 脚本

3.2 修改(业务逻辑)

  • backend/go-service/app/meeting/service/meeting_service.go
    • JoinRoom L307-316移除 mediaOrchestrator.CreateRouter 调用,改为复用已存在 Router查 Redis echo:meeting:room:{code} router_id 字段或直接从 HTTPMediaOrchestrator sync.Map 读)
    • LeaveRoom L381-384全员退出时从"立即 MarkEnded+CloseRouter"改为lifecycleSvc.OnAllMembersLeft(ctx, code)
    • JoinRoom 新增 L260 后:检查 echo:meeting:empty_ttl:{code} 存在即 lifecycleSvc.CancelEmptyTTL(ctx, code) 恢复房间
    • CreateRoom L213 保留(房间首创 Router
  • backend/go-service/app/meeting/service/http_media_orchestrator.go
    • CreateRouter 前置查 roomRouterCache sync.Map命中即直接 return底层幂等防御对业务层误调零影响
  • backend/go-service/app/meeting/service/meeting_signal_service.go
    • 新增公开方法 OnWSDisconnect(ctx context.Context, userID int64)participantDAO.FindActiveByUser → 存在则调 cleanupUserResources;若 room.HostID == userID 则额外 lifecycleSvc.OnHostDisconnect
    • OnRoomJoin 新增 L112 前:若该用户是 room.HostID 则调 lifecycleSvc.OnHostReconnect(支持主持人重连恢复)

3.3 WS 断线钩子集成

  • backend/go-service/app/ws/handler.go
    • 新增字段 meetingDisconnectHook MeetingDisconnectHook(接口,可选,避免循环依赖)
    • SetOnDisconnect 回调 L115-123UserOffline 之后追加 hook.OnWSDisconnect(ctx, userID)
  • app/ws/interfaces.go(新增 10 行)定义接口:
    type MeetingDisconnectHook interface {
        OnWSDisconnect(ctx context.Context, userID int64)
    }
    
  • app/provider/wire.gowire.Bind(new(ws.MeetingDisconnectHook), new(*service.MeetingSignalService))

3.4 配置 + 启动

  • backend/go-service/config/config.go 新增 MeetingConfig { HostGraceSeconds int; EmptyRoomTTLSeconds int; CleanupIntervalSeconds int; StaleRoomHours int }
  • config.dev.yaml / config.docker.yaml 默认值120 / 300 / 30 / 4
  • cmd/server/main.go L82 后追加:app.MeetingCleanupTask.Start() + defer Stop()
  • app/provider/provider.goApp 结构体新增 MeetingCleanupTask *task.MeetingCleanupTask

3.5 DAO 扩展

  • backend/go-service/app/meeting/dao/meeting_room_dao.go
    • ListStaleActive(ctx, hoursNoActivity int, limit int) ([]int64, error)status != ended AND started_at < NOW() - hoursNoActivity
    • FindActiveByCode(ctx, code) (*MeetingRoom, error)(若不存在直接复用已有 GetByCode

3.6 Redis key 增补

key TTL 作用
echo:meeting:host_grace:{code} 120s {host_id, grace_until, started_at} JSON 已在设计 §5.4 定义Task 8 落地
echo:meeting:empty_ttl:{code} 300s 1(占位) 空房 TTL key设计 §5.4 echo:meeting:room:{code} 的 TTL 语义,本 Task 改为独立 key 语义更清晰)
echo:meeting:grace_lock:{code} 5s 1 SETNX 锁防止多实例部署并发处理同一过期事件可选MVP 单实例不强依赖)

四、并发/容错要点

  1. host 重连与过期竞争HandleHostGraceExpired 入口先 DEL host_grace + 检查返回值;返回 0 即已被 OnHostReconnect 清走,跳过本次处理
  2. AfterFunc 取消lifecycleSvcsync.Map*time.TimerOnHostReconnect / CancelEmptyTTL 时调 timer.Stop() + sync.Map.Delete
  3. 服务重启兜底RescheduleFromRedisSCAN echo:meeting:host_grace:*echo:meeting:empty_ttl:*,按 PTTL 剩余毫秒数重新 AfterFunc
  4. TransferHost 事务幂等DAO 层已是事务;外层在 DEL grace_key 成功后才调 TransferHost天然串行
  5. 4 小时兜底 EndRoomcleanupTask 用独立 context.Background + 5min timeout失败仅日志不 panic

五、测试/验收

  • 单元psql 手工脚本验证 TransferHost 事务在冲突时回滚(复用 Task 3 风格)
  • E2E 脚本 docs/verify/meeting_t8_verify.mjsTEST 模式下 HostGraceSeconds=2EmptyRoomTTLSeconds=3CleanupIntervalSeconds=1
    1. host 掉线 → 2s 内读 host_grace 存在 → 第二成员 3s 后收 meeting.host.changedroom.HostID 在 DB 已更新
    2. host 掉线 → 1s 内重连 → host_grace 被 DEL → host 身份保留
    3. 全员 leave → empty_ttl 存在 → 新用户 join → key 被 DEL + 房间 status 仍 Active
    4. 全员 leave → 不 join → 3s 后 MarkEnded + CloseRouterNode 日志印证)
    5. JoinRoom 两次不再产生两次 router created Node 日志Task 7 遗留修复验收)
  • go build ./... && go vet ./... 全绿ReadLints 零问题

六、文档同步commit 前)

  • docs/progress/CURRENT_STATUS.md 新增 Task 8 章节,总进度切 Task 0-8 ✅ / Task 9-16 待执行
  • docs/plans/2026-04-21-phase2e-2-implementation.plan.md Task 8 标记已完成 + 实际产出
  • docs/plans/2026-04-21-phase2e-2-design.md §十六变更记录追加 Task 8 偏差(若有)
  • docs/api/frontend/meeting.md 补充 host 宽限期 UI 建议 + 空房复活行为
  • .cursor/rules/project-context.mdc 进度同步

七、风险与回退

  • 本地 timer 失效(宿主时钟跳变)→ 30s 扫描兜底接管
  • 多实例部署并发处理同一过期事件 → MVP 单实例可容忍,后续用 grace_lock SETNX 修复
  • JoinRoom 移除 CreateRouter 后如果 Router 丢失Node 重启)→ HTTPMediaOrchestrator 层可加"404 → 重建"兜底(作为 Task 8 可选增强或 Task 16 再议)