用 SQLite 事件日志给工作流装上“黑匣子”
从上一篇的纯状态机继续上一篇产出的miniflow.py定义了TripState、Event、Command与纯函数decide。本篇把它们当作既定输入不重写业务规则demo_102.py会导入Event和decide而数据库只负责可靠记录。上一篇的输出是内存中的事件序列和最终TripState本篇要把同样的序列变成可查询、可重放、可审计的持久化日志。为什么选 SQLite 而不是先上 Kafka持久化执行最关键的第一步不是吞吐量而是事务边界。单机 SQLite 提供真正的提交、唯一约束、崩溃恢复和清楚的文件形态足以把协议验证正确。事件日志采用只追加模型不更新旧事件不把最终状态覆盖到一行 JSON。追加日志保留因果过程后面才能重放、迁移和解释故障。等模型稳定后再把相同协议迁到 PostgreSQL 或分区日志远比一开始在分布式组件里调试语义容易。设计日志表和原子追加我们为每个工作流维护单调递增的seq。主键(workflow_id, seq)同时承担顺序约束和并发冲突检测。kind是事件类型payload使用稳定排序的 JSONcreated_at仅用于运维观察绝不能参与业务决策。WAL 模式允许读者不阻塞写者synchronousFULL更偏安全代价是每次提交可能触发刷盘。保存为event_store.py。这里有一个容易漏掉的细节连接级 PRAGMA 必须在每个新连接上设置不能以为初始化数据库时设置一次就永久有效。journal_modeWAL会持久化但foreign_keys、busy_timeout等很多选项不会。当前代码每次打开连接都走connect把行为集中起来。importjsonimportsqlite3fromcontextlibimportcontextmanagerfrompathlibimportPath SCHEMA CREATE TABLE IF NOT EXISTS events ( workflow_id TEXT NOT NULL, seq INTEGER NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (workflow_id, seq) ); classEventStore:def__init__(self,path:str):self.pathPath(path)withself.connect()asdb:db.executescript(SCHEMA)contextmanagerdefconnect(self):dbsqlite3.connect(self.path,timeout5)db.row_factorysqlite3.Row db.execute(PRAGMA journal_modeWAL)db.execute(PRAGMA synchronousFULL)try:yielddb db.commit()exceptException:db.rollback()raisefinally:db.close()defappend(self,workflow_id:str,expected_seq:int,kind:str,payload:dict)-int:next_seqexpected_seq1bodyjson.dumps(payload,ensure_asciiFalse,sort_keysTrue)withself.connect()asdb:db.execute(INSERT INTO events(workflow_id, seq, kind, payload) VALUES (?, ?, ?, ?),(workflow_id,next_seq,kind,body),)returnnext_seqdefload(self,workflow_id:str)-list[dict]:withself.connect()asdb:rowsdb.execute(SELECT seq, kind, payload FROM events WHERE workflow_id? ORDER BY seq,(workflow_id,)).fetchall()return[{seq:row[seq],kind:row[kind],payload:json.loads(row[payload])}forrowinrows]运行输出模块定义成功无标准输出乐观并发不是可选装饰expected_seq看似多余却是避免“丢失推进”的关键。两个 worker 同时读到序号 2都想写序号 3复合主键只允许一个成功另一个收到IntegrityError后必须重新加载历史。不能用SELECT MAX(seq)1后无条件插入并期待数据库替你排队因为读和写之间存在竞态。更不能发生冲突后自动改写成序号 4第二个事件的决策基于旧状态直接顺延会把过期决定伪装成合法新事实。下面保存为demo_102.py。它复用上一篇miniflow.Event写入四个事件然后故意用过期版本再次追加以证明冲突会被拒绝。为便于重复运行示例使用临时目录真实服务则传入固定路径。importsqlite3importtempfilefrompathlibimportPathfromevent_storeimportEventStorefromminiflowimportEventwithtempfile.TemporaryDirectory()asdirectory:pathPath(directory)/flow.dbstoreEventStore(str(path))workflow_idtrip-001seq0history[Event(trip_requested,{city:成都}),Event(budget_locked,{token:B-7}),Event(flight_booked,{ref:F-8}),Event(hotel_booked,{ref:H-9}),]foreventinhistory:seqstore.append(workflow_id,seq,event.kind,event.data)print(appended,seq,event.kind)try:store.append(workflow_id,2,hotel_booked,{ref:stale})exceptsqlite3.IntegrityError:print(stale writer rejected)loadedstore.load(workflow_id)print(order:,[row[kind]forrowinloaded])assert[row[seq]forrowinloaded][1,2,3,4]运行输出appended 1 trip_requested appended 2 budget_locked appended 3 flight_booked appended 4 hotel_booked stale writer rejected order: [trip_requested, budget_locked, flight_booked, hotel_booked]事务边界到底保护了什么当前append一次只写一个事件所以原子性看起来理所当然。真正的工作流引擎通常还要同时写待执行命令也就是 outbox。若先提交事件再写命令中间崩溃会得到“状态已推进、活动却永远没有调度”的孤儿状态若先发送网络请求再提交事件会得到“副作用成功、历史却说没发生”的幽灵执行。第四篇会把事件和 outbox 放入同一事务。本篇先把日志协议单独钉牢是为了分清存储正确性与投递正确性。非平凡踩坑是把 SQLite 的“数据库已提交”误解成“任何硬件故障都绝不丢”。可靠性仍受文件系统、磁盘缓存、挂载参数和备份方式影响。WAL 数据可能暂时位于-wal文件只复制主.db文件会产生不一致备份。在线备份应使用 SQLite backup API或者在受控 checkpoint 后复制完整文件集合。容器中还要确认数据库位于持久卷而不是随 Pod 消失的临时层。另一个记忆点是序号表达的是工作流内因果顺序不是全球时间。不要用自增整数比较两个不同工作流谁先发生也不要拿created_at做重放排序。同一工作流依赖复合主键的严格序号跨工作流统计可以使用时间但它只是观测维度。把业务因果和墙上时钟混为一谈遇到时钟回拨、批量导入或跨地域复制时会付出代价。为恢复准备输入本篇产出了event_store.py的EventStore.append与EventStore.load并得到一个含四条顺序事件的flow.db。数据库现在能记住发生过什么却还不会从日志恢复TripState。下一篇将直接读取load返回的字典列表重新调用上一篇的decide证明删掉所有内存状态后仍能精确恢复并进一步解决“重放时不能重复产生外部命令”的问题。 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。

相关新闻

Cadence路径设置全解析:从环境变量到项目实战的EDA环境配置指南

Cadence路径设置全解析:从环境变量到项目实战的EDA环境配置指南

1. 项目概述:为什么Cadence路径设置是工程师的“第一课” 如果你刚接触Cadence这套庞大的EDA工具,或者在使用中频繁遇到“库找不到”、“工具启动报错”、“文件关联失败”这些问题,那么你大概率是卡在了“路径设置”这一关。这听起来像是个微…

2026/10/15 2:27:41 阅读更多 →
总线舵机通信协议、驱动板选型与实战避坑指南

总线舵机通信协议、驱动板选型与实战避坑指南

1. 从“单打独斗”到“集团军作战”:总线舵机为何是趋势 如果你玩过机器人、做过机械臂,或者捣鼓过一些需要精确动作的模型,那你肯定对舵机不陌生。传统的PWM舵机,就像一个个需要你亲自去喊话的士兵,每个舵机都独占一根…

2026/10/15 2:27:31 阅读更多 →
3句话,让AI帮我搞定一个App:谷歌“王炸”工具,正在让设计师和程序员一起失业?

3句话,让AI帮我搞定一个App:谷歌“王炸”工具,正在让设计师和程序员一起失业?

一行代码不用写,甚至不需要打开Figma,你的下一个UI设计师,可能真的会是AI。小伙伴们,今天分享一款谷歌“王炸”工具,打开网站点击立即试用!一句话生成设计稿“帮我设计一整套‘周末去哪儿’App,…

2026/10/12 1:54:45 阅读更多 →

最新新闻

Americhem在中国苏州启用全新洁净配混设施

Americhem在中国苏州启用全新洁净配混设施

全球定制聚合物解决方案领导者Americhem于9月28日举行了其位于苏州的全新洁净配混设施启用仪式。此项投资进一步提升了Americhem服务中国、亚太地区以及在该区域开展生产制造业务的全球客户的能力。 该设施专为满足严苛医疗器械应用所需的ISO 13485和cGMP要求而设计&#xff0c…

2026/10/15 2:28:21 阅读更多 →
如何降低文献综述的AI检测率:策略与技巧

如何降低文献综述的AI检测率:策略与技巧

1. 文献综述写作中的AI检测率问题剖析最近在学术圈里有个现象特别有意思:不少研究生朋友在提交论文查重时,发现文献综述部分的AI检测率总是居高不下。上周就遇到个案例,某高校硕士生小张的文献综述AI率竟然高达38%,远超过学院规定…

2026/10/15 2:28:21 阅读更多 →
青岛栈桥12家老字号美食攻略与避坑指南

青岛栈桥12家老字号美食攻略与避坑指南

1. 青岛栈桥美食地图:本地人私藏的12家老字号青岛栈桥作为城市地标,周边隐藏着许多经营数十年的地道美食。这些店铺往往没有华丽的装潢,却用最朴实的味道征服了几代食客的味蕾。我作为在青岛生活15年的"老饕",今天要分享…

2026/10/15 2:28:21 阅读更多 →
AI辅助科研:驱动科研创新升级的新动能与实践路径

AI辅助科研:驱动科研创新升级的新动能与实践路径

外文文献“白嫖”与高效阅读全攻略 宝子们,是不是一提到下载外文文献就头大? 想找的文献要么全网搜不到,要么下载流程复杂到让人原地爆炸。好不容易在某个小网站找到了资源,又担心版权问题不敢点,更别说有些平台动辄几…

2026/10/15 2:28:21 阅读更多 →
毕业设计-基于微信小程序的广州旅游服务系统

毕业设计-基于微信小程序的广州旅游服务系统

第1章 绪论:背景(政策/市场/技术/广州产业四方面) 国内外现状 目的意义(提升体验/促进产业/创新模式/智慧城市) 第2章 技术介绍:B/S、Uni-app、MySQL、Django、Vue 第3章 分析与设计:用户/系统…

2026/10/15 2:28:21 阅读更多 →
程序员英语词汇宝典词条精讲:exclamation mark(感叹号 `!`)——发音、词义与编程实战用法

程序员英语词汇宝典词条精讲:exclamation mark(感叹号 `!`)——发音、词义与编程实战用法

教程 【免费下载链接】most-frequent-technology-english-words 程序员工作中常见的英语词汇 项目地址: https://gitcode.com/gh_mirrors/mo/most-frequent-technology-english-words 点击查看 免费下载 本文以开源仓库 most-frequent-technology-english-words&am…

2026/10/15 2:27:21 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →