11 KiB
数据库 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 后投递
MachineDone、InspectionResult等事件到 event loop。 - ProductionEventLoop:唯一状态写路径。串行处理 command、硬件事件、worker result、调度 tick、补料触发。
- Hardware Worker:执行机器人、PLC、扫码、打标等长耗时动作,不修改 DB;完成或失败后回投结果。
- DBState:event 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、恢复流程或人工操作:
StartOrderPauseOrderResumeOrderCancelOrderSuspendJobResumeJobReworkJobConfirmRecoveryResolveManualAction
ExternalEvent
来自硬件轮询或系统定时器:
MachineDoneInspectionResultPalletArrivedStepTimeoutRefillRequestedScheduleTick
WorkerResult
来自硬件 worker:
RobotActionSucceededRobotActionFailedToolActionSucceededToolActionFailedMachineStartSucceededMachineStartFailed
每条消息处理流程固定为:读取必要 DB 状态,校验当前状态,执行 ent 条件更新或事务,写事件日志和 SSE,再触发调度评估。
Worker result 必须按 CorrelationID 关联到 task.id。event loop 只接受当前仍处于 RUNNING 状态的 task result;如果 task 已完成、失败、取消,或 job 已进入终态,则把 result 记录为 stale 事件并忽略状态推进。这样暂停、取消、恢复后迟到的硬件结果不会污染 DB 状态。
API 返回语义
采用混合模式:
PauseOrder、ResumeOrder、CancelOrder、SuspendJob、ResumeJob、ReworkJob:投递 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_no、position_type=ON_BUFFER、position_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=FAULT、alarm和manual_action。 - 两者必须由 event loop 在同一事务内更新。
- 恢复时发现不一致则挂起并人工确认。
设备完成信号语义
MachineDone payload 至少包含 machineID,可选包含 slotNo、jobID、pass。
处理规则:
- 批量设备:以
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。
调度流程:
- event loop 从 DB 和 loop 内存快照构建
SystemState。 - 复用 scheduler 的 Generator/Filter/Policy 选出候选动作。
- event loop 对候选动作执行 DB 条件迁移,并创建/启动 task。
- event loop 创建硬件动作请求,发送给单 robot worker。
- worker 执行动作。
- worker 回投成功或失败。
- 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 约束中移除。
恢复策略
采用保守人工确认。
启动时:
- 加载所有非终态 work_order、job、equipment_slot、task。
- 根据
job.temp_slot_no重建暂存台占用。 - 查询 PLC/设备实际状态。
- 执行一致性校验:
- job 在设备上,但
equipment_slot.currentJobId/status不匹配。 - 多个非终态 job 使用同一
temp_slot_no。 temp_slot_no越界。- DB 显示设备运行,但 PLC 无对应状态。
- job 在设备上,但
- 不一致项写 manual_action,状态改为
SUSPENDED或PAUSED。 - 人工确认后通过
ConfirmRecoverycommand 回到 event loop,再恢复调度。
不再把 event_log 重放到 Redis。
错误处理
- 硬件 worker 失败:回投失败结果,event loop 标记 task failed,job 进入
SUSPENDED或ERROR,并创建报警/人工动作。 - DB 条件更新失败:event loop 重新读取 DB 快照,决定跳过、重新调度或挂起,不做盲目重试。
- API command 超时:返回超时或处理中,前端以 SSE/查询为准。
- Step timeout:只投递
StepTimeout消息到 event loop,不在 timer goroutine 中直接改 DB。 - 终态计数:DB 事务内根据 job 旧状态条件保证只计一次。
测试策略
首版测试重点是新核心的回归安全网:
- EventLoop 单元测试:命令顺序、同步 reply、超时、worker result 推进。
- DBState ent 集成测试:条件更新、跨表事务、终态计数防重复。
- Scheduler integration:从 DB 快照生成候选动作,覆盖卸料优先、换料优先、补料规则。
- Recovery 测试:重复 temp slot、设备槽位/job 不一致、PLC 不一致时创建 manual action。
- Mock E2E:覆盖完整工单流程。
Windows 上保持现有限制:不要求 -race,部分硬件相关测试仍可隔离。
验收标准
- 核心生产流程不依赖 Redis。
- 配置不需要
Redis、EventBus、SSOT才能启动核心流程。 - 所有运行时状态可从 PostgreSQL 恢复。
- 生产状态写入路径只在 event loop。
- 硬件动作不阻塞 event loop。
- 暂存台占用只由
job.temp_slot_no表示。 - 真实设备槽位由
equipment_slot表表示,并与 job 位置在事务中保持一致。