Files
bj_power/bj_power_mes/docs/superpowers/specs/2026-05-08-db-ssot-event-loop-design.md
T

11 KiB
Raw Blame History

数据库 SSOT 单线程事件循环重构设计

背景

当前后端同时包含 PostgreSQL、Redis Streams EventBus、RedisStateManager、RedisToolLocker、ZPopLoop、Dispatcher、ReadyQueue 和多个运行时锁。目标是重构为数据库唯一真相源:单进程单实例部署下,所有生产状态写入由单线程事件循环串行处理,Redis 完全移除,数据库 ORM 使用 entgo。

目标

  • PostgreSQL 作为唯一运行时真相源。
  • 移除 Redis 运行时依赖,包括 RedisBus、RedisStateManager、RedisToolLocker、WriteThroughHelper、ZPopLoop、SSOT 配置路径。
  • 所有 job、equipment_slot、task、work_order、manual_action、alarm、event_log 状态变更通过单线程 event loop 串行执行。
  • 机器人、PLC、扫码、打标等长耗时动作不阻塞 event loop,由 worker 执行后回投结果。
  • 保留并改造现有 scheduler 三层 Generator/Filter/Policy,使其基于 DB/loop 快照调度。
  • 重启恢复直接以 PostgreSQL 当前状态为准,采用保守人工确认策略。

非目标

  • 不支持多后端实例并发调度写入。
  • 不引入分布式锁、租约、选主或 Redis 替代品。
  • 不把暂存台建成独立槽位表。
  • 不让 HTTP 请求等待真实硬件动作完成。

总体架构

新增 ProductionEventLoop 作为产线运行核心。HTTP logic、SignalWatcher、恢复流程、人工操作和定时器只向 event loop 投递消息,不直接修改生产状态。

组件边界:

  • HTTP handler / logic:提交 command。控制类命令短超时等待结果;长流程通过 SSE 或查询接口观察。
  • SignalWatcher:轮询 PLC 后投递 MachineDoneInspectionResult 等事件到 event loop。
  • ProductionEventLoop:唯一状态写路径。串行处理 command、硬件事件、worker result、调度 tick、补料触发。
  • Hardware Worker:执行机器人、PLC、扫码、打标等长耗时动作,不修改 DB;完成或失败后回投结果。
  • DBStateevent loop 内部使用的 ent 状态访问层,封装条件更新和事务。
  • PostgreSQL:唯一真相源。
  • SSE/EventLog:由 event loop 在状态变更后发布/写入。

事件与命令模型

event loop 接收统一的 EventLoopMessage

  • ID:事件日志和去重标识。
  • Type:消息类型。
  • Payload:业务数据。
  • CorrelationID:动作关联标识。对 worker result 必须等于对应 task.id 或 event loop 生成的 action ID。
  • Reply:可选,用于 API 同步等待。
  • CreatedAt:创建时间。

消息分为三类。

Command

来自 API、恢复流程或人工操作:

  • StartOrder
  • PauseOrder
  • ResumeOrder
  • CancelOrder
  • SuspendJob
  • ResumeJob
  • ReworkJob
  • ConfirmRecovery
  • ResolveManualAction

ExternalEvent

来自硬件轮询或系统定时器:

  • MachineDone
  • InspectionResult
  • PalletArrived
  • StepTimeout
  • RefillRequested
  • ScheduleTick

WorkerResult

来自硬件 worker

  • RobotActionSucceeded
  • RobotActionFailed
  • ToolActionSucceeded
  • ToolActionFailed
  • MachineStartSucceeded
  • MachineStartFailed

每条消息处理流程固定为:读取必要 DB 状态,校验当前状态,执行 ent 条件更新或事务,写事件日志和 SSE,再触发调度评估。

Worker result 必须按 CorrelationID 关联到 task.id。event loop 只接受当前仍处于 RUNNING 状态的 task result;如果 task 已完成、失败、取消,或 job 已进入终态,则把 result 记录为 stale 事件并忽略状态推进。这样暂停、取消、恢复后迟到的硬件结果不会污染 DB 状态。

API 返回语义

采用混合模式:

  • PauseOrderResumeOrderCancelOrderSuspendJobResumeJobReworkJob:投递 command 后短超时等待执行结果。
  • StartOrder:等待 DB 初始化和首轮调度完成后返回;真实生产动作通过 SSE/查询观察。
  • 硬件动作不阻塞 HTTP 请求。
  • 如果 API 等待超时,前端以 SSE/查询结果为准。

DBState 与 ent 原子状态更新

新增 DB 状态访问层,只给 event loop 使用。它不暴露通用 CRUD,而暴露领域状态迁移方法:

  • ClaimJobForAction(ctx, jobID, expectedStatus, nextStatus, position)
  • MoveJobToEquipment(ctx, jobID, fromPosition, equipmentID, slotNo)
  • MoveJobToBuffer(ctx, jobID, slotNo)
  • CompleteStep(ctx, jobID, expectedStepIndex, nextStepIndex, contextPatch)
  • FinishJob(ctx, jobID, terminalStatus)
  • SetEquipmentSlot(ctx, equipmentID, slotNo, expectedState, nextState, jobID)
  • CreateTask(ctx, ...)
  • StartTask(ctx, ...)
  • FinishTask(ctx, ...)
  • FailTask(ctx, ...)
  • AppendEventLog(ctx, message) 或由 event loop 统一写。
  • RaiseAlarm(ctx, alarm) / ResolveAlarm(ctx, alarmID)

实现优先使用 ent Update().Where(...).Save/Exec。需要 RETURNING 或多表条件更新时,可在 DBState 内部使用 ent 事务和原生 SQL,但 SQL 不散落到 processor 业务代码。

原则:

  • 不使用 Redis version。
  • job.version 保留为事件版本号,每次状态更新递增。
  • 不使用悲观锁,也不依赖乐观锁重试。
  • SQL WHERE status/position/step 是防御性条件,主要一致性由单 event loop 串行化保证。
  • 跨表变更使用事务。
  • 终态计数用 DB 条件保证只计一次,不能依赖运行时 terminalCounted

槽位状态设计

采用混合模型。

暂存台

暂存台不建独立槽位表,也不使用 Redis bitmap。以 job.temp_slot_no 作为占用记录:

job.position_type = ON_BUFFER
job.position_ref_id = temp_slot_no
job.temp_slot_no = 1..8

规则:

  • event loop 查询非终态且 temp_slot_no IS NOT NULL 的 job,计算 1..8 空位。
  • 补料分配槽位时设置 temp_slot_noposition_type=ON_BUFFERposition_ref_id=slotNo
  • 工件上设备时保留 temp_slot_no 不变,只改当前位置。
  • 下料回原暂存位时使用原 temp_slot_no
  • 完成或报废时清空 temp_slot_no
  • 恢复时若发现重复槽位或越界槽位,相关 job 挂起并创建人工恢复动作。

真实设备槽位

真实设备槽位使用 equipment_slot 表,同时 job 记录当前位置:

job.position_type = ON_EQUIPMENT
job.position_ref_id = equipment_id:slot_no

equipment_slot.status = EMPTY / OCCUPIED / DONE
equipment_slot.currentJobId = job.id

职责划分:

  • job.position_type/position_ref_id/temp_slot_no 回答“工件在哪里”。
  • equipment_slot.status/currentJobId 回答“设备槽位当前物理/工艺状态是什么”。
  • 槽位状态沿用现有枚举 EMPTY / OCCUPIED / DONE。设备故障不扩展到槽位枚举,记录在 equipment.status=FAULTalarmmanual_action
  • 两者必须由 event loop 在同一事务内更新。
  • 恢复时发现不一致则挂起并人工确认。

设备完成信号语义

MachineDone payload 至少包含 machineID,可选包含 slotNojobIDpass

处理规则:

  • 批量设备:以 equipment.batch=true 为准,event loop 将该设备所有 OCCUPIED 槽位改为 DONE,并把对应 job 状态改为 WAITING_UNLOAD
  • 非批量单槽设备:忽略缺失的 slotNo,唯一槽位从 OCCUPIED 改为 DONE
  • 非批量多槽设备:优先使用 payload 中的 slotNo;如果缺失或小于等于 1,则按现有规则选择最早进入的 OCCUPIED 槽位作为 FIFO 完成槽位。
  • 如果 payload 中包含 jobID,必须与目标槽位 currentJobId 一致;不一致时不推进状态,创建报警和人工恢复动作。
  • 检测设备的 InspectionResult 同样进入 event loop,由 loop 更新 job context、决定通过/报废/人工判定。

调度数据流

调度触发来源:

  • 工单启动。
  • 工件步骤完成。
  • 设备完成信号。
  • 暂存台槽位释放。
  • 人工恢复。
  • 周期性 ScheduleTick

调度流程:

  1. event loop 从 DB 和 loop 内存快照构建 SystemState
  2. 复用 scheduler 的 Generator/Filter/Policy 选出候选动作。
  3. event loop 对候选动作执行 DB 条件迁移,并创建/启动 task。
  4. event loop 创建硬件动作请求,发送给单 robot worker。
  5. worker 执行动作。
  6. worker 回投成功或失败。
  7. event loop 推进步骤,更新 job、equipment_slot、task、work_order,写 event_log,发送 SSE,再触发下一轮调度。

补料

补料不再依赖 TempSlotAllocator bitmap 或 Redis。event loop 扫描 job.temp_slot_no 得到暂存台占用:

  • 启动时补满 8 个。
  • 后续空位大于等于 4 时补 4 个。
  • 补料动作由 robot worker 串行执行。
  • 补料分配槽位和 job 状态更新都在 event loop 内完成。
  • 不再需要 Dispatcher.replenishing 原子标志。

Replenisher 可保留为“选择接驳台 job、生成补料动作”的辅助,不再维护真相状态。

工具互斥

由于只有一个 robot worker,扫码和打标天然串行:

  • 删除 RedisToolLocker 运行时依赖。
  • MemoryToolLocker 可临时作为兼容 no-op,但最终不参与核心调度。
  • ToolLockConstraint 从默认 scheduler 约束中移除。

恢复策略

采用保守人工确认。

启动时:

  1. 加载所有非终态 work_order、job、equipment_slot、task。
  2. 根据 job.temp_slot_no 重建暂存台占用。
  3. 查询 PLC/设备实际状态。
  4. 执行一致性校验:
    • job 在设备上,但 equipment_slot.currentJobId/status 不匹配。
    • 多个非终态 job 使用同一 temp_slot_no
    • temp_slot_no 越界。
    • DB 显示设备运行,但 PLC 无对应状态。
  5. 不一致项写 manual_action,状态改为 SUSPENDEDPAUSED
  6. 人工确认后通过 ConfirmRecovery command 回到 event loop,再恢复调度。

不再把 event_log 重放到 Redis。

错误处理

  • 硬件 worker 失败:回投失败结果,event loop 标记 task failedjob 进入 SUSPENDEDERROR,并创建报警/人工动作。
  • DB 条件更新失败:event loop 重新读取 DB 快照,决定跳过、重新调度或挂起,不做盲目重试。
  • API command 超时:返回超时或处理中,前端以 SSE/查询为准。
  • Step timeout:只投递 StepTimeout 消息到 event loop,不在 timer goroutine 中直接改 DB。
  • 终态计数:DB 事务内根据 job 旧状态条件保证只计一次。

测试策略

首版测试重点是新核心的回归安全网:

  1. EventLoop 单元测试:命令顺序、同步 reply、超时、worker result 推进。
  2. DBState ent 集成测试:条件更新、跨表事务、终态计数防重复。
  3. Scheduler integration:从 DB 快照生成候选动作,覆盖卸料优先、换料优先、补料规则。
  4. Recovery 测试:重复 temp slot、设备槽位/job 不一致、PLC 不一致时创建 manual action。
  5. Mock E2E:覆盖完整工单流程。

Windows 上保持现有限制:不要求 -race,部分硬件相关测试仍可隔离。

验收标准

  • 核心生产流程不依赖 Redis。
  • 配置不需要 RedisEventBusSSOT 才能启动核心流程。
  • 所有运行时状态可从 PostgreSQL 恢复。
  • 生产状态写入路径只在 event loop。
  • 硬件动作不阻塞 event loop。
  • 暂存台占用只由 job.temp_slot_no 表示。
  • 真实设备槽位由 equipment_slot 表表示,并与 job 位置在事务中保持一致。