← 返回首页

AI 自动化流水线并发写入冲突复盘:5 个真实场景与 flock 锁修复

AIAutomationCronConcurrencyflockLinuxPythonPipelineDebugDevOps

去年我搭的 OpenClaw AI 自动化流水线(4 个 cron 任务 × 每日 4 次 × isolated session × 流水线生成 → 生成 HTML → 推送 GitHub Pages)在 W21-W26 平稳跑了 5 周,结果在 W30(2026-07-20)单日内连续踩进同一个坑 **3 次**:6AM cron 启动后被 22PM cron 残留文件覆盖,22PM cron 自己又被别的 cron subagent 覆盖,最终我以为发布的 4 篇新文章(Thunderbolt 4 / WordPress 7.0 / NAS 网卡 / Apple USB-C)实际上 0 篇来自我写的 cn.txt,三次写入三次被替换。本文把这次完整的崩溃-修复-再崩溃-最终锁定的全过程拆开讲,重点是为什么 v2 的"双源同步"修复(corrections.md 6/23)根本不够,以及 v3 用 flock -xn /tmp/article-gen.lock + 隔离文件名是怎么彻底解决的。如果你也在跑多个 cron 任务共享临时目录,这 5 个真实崩溃场景 + flock 锁 + 8 项生产验证清单值得从头到尾读完。

⏳ 太长不看版(TL;DR)

为什么 /tmp/article-gen/ 成了多 Session 战场

OpenClaw 流水线的设计里,run-pipeline.py 的 Phase 1 调 generate-html.py,而 generate-html.py 硬编码读 /tmp/article-gen/cn.txt/tmp/article-gen/en.txt(不是 drafts/cn.txt)。原因很简单:临时目录比工作目录快,跨 session 不用每次解析相对路径。

但 W21 之后调度策略变了:**6AM cron(每日 6 点跑一次)启动时间不固定**,取决于 subagent 队列深度。当 6AM cron 还在写 cn.txt 时,22PM cron(实际 21:53 启动)已经启动 run-pipeline.py。两者共享同一文件名 /tmp/article-gen/cn.txt,导致:

时间线(2026-07-20 真实)
21:42  22PM isolated session 启动,写 /tmp/article-gen/cn.txt (10559 bytes)
21:44  6AM isolated session 启动,覆盖写 cn.txt (14752 bytes, NAS 网卡主题)
21:47  6AM Phase 0 读 cn.txt,发现主题被改成 NAS 网卡 → 重新写
21:48  6AM 重新写又被 22PM 覆盖(22PM 此时正在生成 HTML)
21:50  6AM 等 22PM last_run 完成后重新写(16752 bytes WordPress 7.0)
21:51  6AM 跑 pipeline 成功,发布 4 篇(含本次 CN+EN 2 篇 + drafts 队列 10 篇历史补推)
21:53  22PM 也跑完了,但 22PM 写的"AI 自动化流水线 5 个 Bug"主题 0 篇被推送

这就是 v2 修复(6/23 双源同步)无法解决的问题:你的 cn.txt 在被另一个人覆盖,不是一致性问题,是所有权问题

💣 踩坑录:5 个真实崩溃场景(按时间倒序)

崩溃场景 1:22PM 21:53 第三次踩坑(最严重,2026-07-20)

现象:22PM isolated session 21:42 写 cn.txt("AI 自动化流水线 5 个 Bug"主题)→ 21:46 跑 pipeline → Phase 1 generate-html.py 读到的 cn.txt 主题是 NAS 网卡(来自 6AM 21:44 覆盖写入)→ 生成的 12 篇文章全部来自 6AM 主题或 drafts 队列历史补推,0 篇是 22PM 写的"AI 自动化流水线 5 个 Bug"

**根因**:run-pipeline.py Phase 0 读 /tmp/article-gen/cn.txt 后,**中间 30 秒窗口**被 6AM isolated session 写入覆盖。Phase 2 generate-html.py 读到的不是 22PM 自己的文件。

解决方案:v3 flock 锁(见下文)—— 整个写-跑流程必须在一个锁内,期间不允许其他 session 介入。

崩溃场景 2:6AM 与 22PM 互相覆盖(21:48,2026-07-20)

现象:6AM 21:44 开始写 cn.txt(WP 7.0 Playground 主题,14752 bytes),写到 21:46 完成 → 但 22PM cron 21:47 启动 run-pipeline.py,覆盖 cn.txt 为 NAS 网卡主题 → 6AM Phase 0 读发现"主题被换",21:48 重新写又被 22PM 覆盖。

根因:两个 cron session 完全没有协调,每个 session 都假设自己是唯一的写者。当 6AM 等待 30 秒重写时,22PM 正好在跑 Phase 2,又一次覆盖。

解决方案:v3 flock 锁 + 调度时差 ≥30 分钟(已升格永久标准 #24)。

崩溃场景 3:v2「双源同步」根本不管用(2026-06-23)

现象:6/23 22PM cron 第二次掉进同一坑,generate-html.py 路径 vs drafts/ 路径不一致。

修复:v2 修复方案——把 drafts/cn.txt 拷贝到 /tmp/article-gen/cn.txt 后再跑 pipeline。

为什么不够:v2 假设「drafts/ 是真相源,/tmp/article-gen/ 是临时副本」,但实际 7/20 案例里 drafts/cn.txt 是 22PM 写的"5 个 Bug",/tmp/article-gen/cn.txt 已经被 6AM 改成 NAS 网卡。Phase 0 读 drafts/cn.txt 是对的,但 Phase 1 generate-html.py 读 /tmp/article-gen/cn.txt 是错的,而 generate-html.py 没参数指定输入文件,无法指向 drafts/。

教训:v2 只解决了「drafts/ 残留污染 /tmp/article-gen/」这一种场景,对「多 session 并发写同一文件」无效。

崩溃场景 4:6AM 6:13 残留污染(2026-07-17,W29)

现象:6AM cron 跑完后留下 /tmp/article-gen/cn.txt(Ansible 主题,06:13 mtime),当晚 22PM cron 启动 run-pipeline.py,generate-html.py 读到 Ansible 主题,22PM 写的 WordPress 主题文章 0 篇被推送。

**修复**:6AM cron 跑完后**手动** rm /tmp/article-gen/*.txtmv 到 backup。

教训:v2 修复的「单次残留」场景,v3 必须扩展到「并发覆盖」场景。

崩溃场景 5:cron subagent 队列意外写入(2026-07-20)

现象:22PM cron 启动后,6AM 的 subagent 队列还残留了 12PM/18PM 之前未完成的 subagent,这些 subagent 在 22PM 21:47 写入 /tmp/article-gen/。没人叫它们写,是调度系统的 subagent 自动恢复机制。

根因:OpenClaw 调度器在 cron 触发时,会先恢复所有未完成的 subagent(默认 1 小时窗口)。如果 12PM cron 的 subagent 因故未完成,它会在 22PM cron 启动时自动恢复并执行剩余任务,其中可能包括写 /tmp/article-gen/。

解决方案:v3 flock 锁 + 调度时差必须 ≥ 30 分钟(避免 subagent 恢复窗口重叠)。

🛠️ 实战修复:v3 flock 锁 + 隔离文件名(完整脚本)

Step 1: 写文件用隔离文件名(带 cron 名 + 时间戳)

#!/bin/bash
# 22PM cron 开头:先取隔离文件名
STAMP="$(date +%Y-%m-%d-%H%M)-22pm"  # 例:2026-07-20-2153-22pm
CN_FILE="/tmp/article-gen/${STAMP}-cn.txt"
EN_FILE="/tmp/article-gen/${STAMP}-en.txt"

# 检查其他 cron 是否在跑(v3 永久标准 #24)
if ps auxf | grep -E "publish-via-api|run-pipeline|generate-html" | grep -v grep > /dev/null; then
  echo "⚠️ 其他 cron 流水线在跑,等待 30s 重检"
  sleep 30
  if ps auxf | grep -E "publish-via-api|run-pipeline|generate-html" | grep -v grep > /dev/null; then
    echo "❌ 仍有活跃进程,放弃本次发布"
    exit 1
  fi
fi

# 用 flock 包裹整个写-跑流程(关键!)
exec 9>/tmp/article-gen.lock
if ! flock -xn 9; then
  echo "⚠️ flock 锁被占用(其他 cron 正在写),放弃本次"
  exit 1
fi

# 在锁内写文件
cat > "$CN_FILE" <<'EOF'
# 你的 CN 文章内容(pipe 格式元数据 + 正文)
EOF

cat > "$EN_FILE" <<'EOF'
# 你的 EN 翻译版本
EOF

# Phase 1: 调 generate-html.py,**指定输入文件**(v3 新参数)
python3 generate-html.py --cn "$CN_FILE" --en "$EN_FILE" \
  --out-cn /root/.openclaw/workspace/yaohehe.github.io/2026-07-20-xxx.html \
  --out-en /root/.openclaw/workspace/yaohehe.github.io/2026-07-20-xxx-en.html

# Phase 2: 推送(同样在锁内)
python3 /root/.openclaw/workspace/yaohehe.github.io/publish-via-api.py 2026-07-20-xxx.html
python3 /root/.openclaw/workspace/yaohehe.github.io/publish-via-api.py 2026-07-20-xxx-en.html

# 清理(保留 24h 用于排查)
find /tmp/article-gen/ -name "${STAMP}-*" -mmin +1440 -delete

# 释放锁(脚本结束自动释放)

Step 2: generate-html.py 增加 --input-file 参数

# generate-html.py 关键修改(patch)
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--cn', default='/tmp/article-gen/cn.txt',
                    help='CN 输入文件路径(v3 新参数)')
parser.add_argument('--en', default='/tmp/article-gen/en.txt',
                    help='EN 输入文件路径(v3 新参数)')
parser.add_argument('--out-cn', required=True, help='CN 输出 HTML 路径')
parser.add_argument('--out-en', required=True, help='EN 输出 HTML 路径')
args = parser.parse_args()

# 读取 v3 指定的输入文件(不再硬编码)
with open(args.cn, 'r', encoding='utf-8') as f:
    cn_content = f.read()
with open(args.en, 'r', encoding='utf-8') as f:
    en_content = f.read()

# ... 后续 HTML 生成逻辑不变

Step 3: 调度时差检查(永久标准 #24)

# 在 22PM cron 触发前,检查最近 30 分钟是否有其他 cron 完成
LAST_RUN=$(stat -c '%Y' /root/.openclaw/workspace/affiliate-blog/drafts/.last_run 2>/dev/null || echo 0)
NOW=$(date +%s)
DIFF=$((NOW - LAST_RUN))

if [ "$DIFF" -lt 1800 ]; then
  echo "⚠️ 距离上次 cron 完成仅 ${DIFF}s,等待 $((1800 - DIFF))s"
  sleep $((1800 - DIFF))
fi

🔒 为什么 flock 是正确解(而不是 mv 原子替换)

很多人第一反应是「用 mv 原子替换不就完了?」——**不对**。

方案单写者多写者跨 session锁释放失败回滚
`mv` 原子替换❌ 后写者覆盖前写者N/A❌ 文件残留
`cp + mv`(v2)N/A
flock 包裹✅ 阻塞等待✅ 自动✅ 超时退出
`flock -xn` 非阻塞✅ 立即放弃✅ 自动✅ 立即退出

flock -xn 的关键优势:**非阻塞语义**——如果锁被占,立刻失败退出,不让 cron 等 5 分钟。否则两个 cron 互相等锁会导致**死锁的近亲**(互相阻塞 30 秒后超时,但中间 30 秒双方都无法工作)。

🛡️ 8 项生产验证清单

升级到 v3 后,必须验证:

🔗 相关内链

总结与下一步

AI 自动化流水线的并发冲突不是「双源不一致」问题,是「多 session 写同一文件所有权」问题。v3 用 flock -xn + 隔离文件名 + generate-html.py --input-file 参数三个机制叠加,才能彻底解决。**W30(本周)已升格为永久标准 #24**,所有 4 个 cron 任务(6AM/12PM/18PM/22PM)的 prompt 头部必须固化这套流程。

下一步方向:

1. W31 验证:连续 7 天观察 4 个 cron 是否还出现类似冲突(V8 验证清单)

2. run-pipeline.py v2 升级:Phase 0 增加 flock 锁自动包裹,cron prompt 不用手写锁

3. subagent 调度窗口调整:把 OpenClaw 调度器的 subagent 恢复窗口从 1 小时缩短到 30 分钟

👉 Join MiniMax Token Plan: AI coding acceleration for businesses

👉 Join Zhipu Coding Plan: GLM-4.6/GLM-5 coding packages, China-stable, pay-per-token unlimited

👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation

📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews

🔗 Recommended Tools

These are carefully selected tools. Using our affiliate links supports us to keep producing quality content:

☁️ DigitalOcean Cloud ⚡ Vultr VPS ⭐ MiniMax Token Plan 🧩 Zhipu Coding Plan 🎁 Zhipu 20M Tokens Gift 🤖 QoderWork CN (Refer & Earn) ☁️ Aliyun AI Products 📚 WordPress Books 🔍 WordPress SEO Books 🌐 Web Hosting Books 🐳 Docker Books 🐧 Linux Books 🐍 Python Books 💰 Affiliate Marketing 💵 Passive Income Books 🖥️ Server Books ☁️ Cloud Computing Books 🚀 DevOps Books
← 返回首页