可靠的定时内容发布:原计划时间、版本条件与单篇隔离

可靠的定时内容发布:原计划时间、版本条件与单篇隔离原创封面

发布范围由不可变批次清单限定

定时发布器不应查询“所有到期草稿”后统一改状态,因为后台可能存在私人草稿、人工排期和其他业务类型。受控批次在代码或签名 manifest 中精确列出 slug、标题、内容版本和封面哈希,导入与发布都只接受这个集合。数据库再保存 batchId、slug 列表和指纹,执行器启动时核对一致;批次记录缺失或不同就整体停止,不猜测哪个来源更可信。

slug 是稳定外部身份,导入前与现有文章全集比较,任何碰撞都失败,不能 update 覆盖。批次版本一旦导入不允许原地换正文或封面;确需修改时由后台编辑产生新 version,并让发布前完整性策略决定继续或退回人工。白名单查询同时匹配 slug、SCHEDULED 和到期时间,即使有人创建同名 DRAFT,也不会被通用扫描顺手发布。

时间同步是最后的运行前提。应用和数据库主机启用可靠时钟同步并监控偏差,发布判断尽量以数据库 now 为事实;即使 systemd 提前或延后几十秒唤醒,条件查询也不会越过 scheduledAt。测试通过注入 now 参数验证边界,不修改宿主时钟影响其他服务。值班发现时钟异常时先暂停 publisher 和校正基础设施,再恢复安全扫描,不能手工批量改 PUBLISHED 补偿。最终批次报告逐个列出 slug 的计划、版本与最终状态,任何缺口都必须人工关闭。

上海排期在导入时转换成绝对时间

“部署次日 09:30”先以 Asia/Shanghai 取得部署时刻的日历年月日,再从下一日开始生成每天 09:30 和 19:30 两个时刻,连续十八天。生成结果存为带时区含义的绝对时间,数据库使用 timestamptz 或等价 UTC 时刻。不能用服务器本地 Date 加二十四小时推断日历日,部署主机时区改变或未来迁移到其他地区都会让计划偏移。

导入保存原 deployedAt、timeZone 和全部计算后时间,重跑时读取首批记录而不是用当前时间重新排期。测试覆盖上海午夜前后、月末与年末,明确首个 UTC 时刻对应本地 09:30。发布器比较数据库当前时间与 scheduledAt,publishedAt 写原 scheduledAt 而不是工作进程实际醒来的 now;这样短暂宕机后补发仍保留内容原计划顺序,RSS 和归档不会显示错误补发时间。

幂等导入先验证全部内容和媒体

连接数据库写入前,对三十六篇执行数量、分类配比、slug 唯一、正文长度、引用、HTML 安全和重复段落检查。封面 manifest 必须逐个对应 slug,包含原图与响应式格式的真实字节、尺寸、用途、提示词和 SHA-256;导入器重新读取文件计算哈希,缺失或不符使整批失败。占位图和空哈希不能进入最终 manifest,也不能因为一张失败而先写入其他三十五篇。

文件预先复制到不可公开或受控上传位置时,记录本次新建的精确路径。目标存在则比较哈希,相同视为幂等,不同立即冲突,绝不覆盖未知文件。数据库事务创建分类、媒体、文章、标签、初始修订、逐篇审计和批次 setting;其中任何一步失败整体回滚,并只清理本次新文件。已存在同指纹批次返回 already imported,已有不同指纹批次拒绝,重跑不会重新计算日期。

单篇发布使用版本与状态条件原子提交

执行器读取白名单中 status=SCHEDULED 且 scheduledAt 不晚于 now 的文章,按计划时间和 slug 稳定排序。每篇在短事务里用 id、slug、version、SCHEDULED 和原 scheduledAt 作为 updateMany 条件,成功时改为 PUBLISHED、publishedAt=scheduledAt、清空 scheduledAt 并递增 version。影响一行才继续创建发布修订和审计;影响零行说明管理员或另一执行器已改变状态,记录冲突但不覆盖。

两个 systemd 进程偶然重叠时,它们可能都读到同一候选,但只有一个版本条件更新成功。修订或审计创建失败会回滚主状态,不留下已经发布却无证据的记录。事务中不执行网络、图片转换或长内容生成,所有不可变资源在导入阶段完成。公共 HTTP 请求只读取 PUBLISHED 且 publishedAt 已到的内容,绝不承担“顺便扫描并发布”,否则低流量会延迟,高流量会制造并发写。

内容失败与系统失败采用不同状态语义

发布前重新渲染 manifest 文章或验证数据库保存的正文、引用、分类与封面批次。标题被篡改、正文不完整、引用丢失或封面哈希元数据无效属于当前文章内容失败;执行器以该文章版本条件原子退回 DRAFT、清空计划、递增版本,并创建包含脱敏错误列表的修订与审计。随后继续处理其他到期文章,使一篇坏内容不会打乱剩余十八天排期。

数据库连接失败、事务超时、进程退出、磁盘或运行时错误属于系统失败,此时不能把候选改成 DRAFT 或 FAILED,因为内容本身没有被证明错误。事务自然回滚,进程以非零退出,让 timer 下次安全重试。错误分类只捕获明确的内容校验结果,不用宽泛 catch 把任何异常包装成“文章无效”。若系统在前几篇成功后故障,已提交文章保持发布,未处理文章仍为 SCHEDULED。

systemd timer 只负责唤醒短生命周期进程

timer 每分钟触发 oneshot service,Persistent 让主机停机期间错过的检查在恢复后补跑。业务准点由数据库 scheduledAt 判断,不依赖 OnCalendar 精确列出两个发布时间,因此改期和补发不需要重装 unit。服务使用专用低权限用户、受限环境文件、固定 WorkingDirectory 和启动超时,文件系统默认只读,仅开放必要运行目录。ExecStart 调用仓库中的明确 publish 脚本,不执行通用“发布全部”入口。

安装 release 时复制 unit 到系统目录、daemon-reload 并 enable --now timer,随后检查 list-timers、service 最近退出码和 journal。应用 release 切换使用稳定 current 链接,oneshot 启动后加载同一版本代码;切换期间若旧进程已启动,数据库版本条件仍阻止重复。部署回滚要恢复与数据库 schema 兼容的 release,timer 保持受控或在迁移失败期间暂停,不能让半部署代码继续处理。

审计和指标证明每个计划的最终去向

导入审计记录 batchId、slug、scheduledAt 与初始 version;发布审计记录 expectedVersion、原计划时间和结果;内容失败记录字段级错误但不复制正文。修订保存当时标题和 contentJson,使后台能追踪自动动作。普通日志只输出批次、候选数量、published、returnedToDraft、conflicts 与 request/run id,不包含数据库 URL、Cookie、凭据或完整文章。

运行指标包括最老已到期仍 SCHEDULED 的延迟、每次扫描耗时、版本冲突、内容退回和系统退出码。零候选是正常状态,不应告警;到期延迟超过两分钟或连续 service 失败才需要值班处理。后台计数来自真实数据库成功响应,加载失败不能显示成零。值班人员按 slug 查看审计和修订,修复 DRAFT 后必须显式重新排期,发布器不会私自恢复已经暂停的文章。

验证矩阵覆盖时区、并发、隔离与回滚

纯函数测试断言三十六个时刻从部署次日开始、每天两个、连续十八天,并核对上海本地与 UTC。导入测试运行两次得到首次创建和第二次零创建,修改任一 slug、文件字节或哈希都在数据库写入前失败。发布并发测试让两个执行器处理同一版本,只能产生一次状态转换、一次发布修订和一次审计,publishedAt 必须等于原 scheduledAt。

隔离测试篡改其中一篇正文,断言它退回 DRAFT 而下一篇正常发布;让 findMany 或事务抛数据库错误,所有尚未提交状态保持不变并允许下次重试。强杀 oneshot 后重新运行,已发布文章不重复,剩余文章继续。部署验收检查 timer、PostgreSQL、真实文章数、RSS 与公开页面;任一健康检查失败按 release 和数据备份恢复,不调用会扩大到私人草稿的管理批量接口。

编辑冲突、缓存与灾难恢复不能改变发布事实

管理员在排期后编辑文章会递增 version。若产品允许修改仍自动发布,编辑保存必须重新运行全部质量校验并更新批次内容指纹;若批次要求不可变,保存动作应明确把文章退回 DRAFT 并取消计划。不能让发布器在到点时偷偷选择 manifest 旧正文覆盖管理员新版本,也不能忽略差异照常发布。冲突策略在后台界面提前说明。

公开页面、RSS 和 sitemap 只查询 PUBLISHED 且 publishedAt 不晚于当前时间,发布事务提交后按内容版本失效缓存。缓存失效失败不回滚数据库发布,因为跨系统没有共同事务,而是记录可重试任务;缓存 TTL 提供最终恢复上限。publishedAt 仍是原计划时间,updatedAt 与版本帮助读者和缓存区分补发与后续编辑。

数据库从旧备份恢复后,某些已发布文章可能重新变成 SCHEDULED。执行器再次处理时,外部副作用主要是缓存和订阅更新,也应使用文章 id 与发布版本作为操作键。恢复运行 timer 前比较备份快照、审计和外部 feed 状态,必要时只重建可再生输出,不重复发送已经完成的推送通知。

十八天结束后 timer 可以继续每分钟空扫描,成本很低但仍要保留监控;若系统只为该批次存在,可在确认所有 slug 最终为 PUBLISHED 或人工 DRAFT 后停用。停用是受审计部署动作,不删除批次 setting、修订和封面 manifest。未来新批次使用新 batchId 和明确清单,不能修改旧常量把历史证据变成另一组内容。

实现片段

WHERE id = $id AND version = $version AND status = 'SCHEDULED'

一手参考资料

评论 · 0

还没有评论留下第一句经过思考的话。

游客评论需审核。注册后可直接公开,无需审核。

SHARE / 分享

分享这篇文章

WECHAT / 微信

用微信扫一扫

在手机微信中打开文章后,再从微信右上角分享给朋友或朋友圈。