遇码头像

给自动提交加三道保险:进程内互斥锁、状态检查与幂等写区分,让后台写入失败不再静默

给自动提交加三道保险:进程内互斥锁、状态检查与幂等写区分,让后台写入失败不再静默

学习目标

读完这篇,你能给自己的桌面应用或后台服务加一套「自动提交不再静默」的保险:外部进程调用失败时不再被 let _ = 悄悄丢掉,而是串行化、查状态、记日志,并且把「本来就没东西可提交」和「提交真的失败了」区分开。做完之后,你的自动备份、自动归档、知识库自动入库这类 fire-and-forget 流程,失败时第一时间就能在日志里看见,而不是几周后对账时才发现历史断了一大截。

先给大家科普一个概念「fire-and-forget」。翻译成人话:程序调了一个外部命令,调完就不管了,既不看它成功没有,也不记它失败没有,就像把信扔进邮筒却从不确认对方有没有收到。对一次性的、不重要的调用,这么写没问题;但对「每次写入都要留痕」的自动提交,这么写等于把数据的历史交给了运气。

daw 就踩过这个坑。它的本地知识库每次写入后会自动 git 提交,提交信息里带板块名和行数变化,本来是一套完整的变更追溯。结果一轮复盘里,新上线的 git 审计一查就现形:工作区里 80 多个表骨架从 8 月 13 日起就从未入库——骨架生成时连发几十次提交,撞上 git 的 index.lock 全数失败,而代码里每步都是 let _ = cmd.status(),失败被原样丢掉,没有任何日志。更糟的是,没人发现,因为工具每次都照常报「已写入」。

1. 前置知识:一次自动提交要过几道手,失败会在哪几处丢

动手之前,先把 daw 里一次知识库自动提交的旅程说清楚。写入函数调 run_git_commit,它在知识库目录里依次做三件事:首次使用时 git init 并配好换行设置;git add 要提交的文件;git commit -m 写提交信息。每一件都是调外部 git 进程完成的。

失败有三个藏身点,缺一不可地堵:

第一,并发撞锁。骨架生成一次能连发几十次提交,同一个进程里多个提交流程同时跑 git add / git commit,第二个进程发现 .git/index.lock 已被占用就直接失败。这是 daw 当时的根因,不是磁盘满了、不是权限问题,就是自己跟自己抢锁。

第二,状态没人看。原来的代码是 let _ = cmd.status();,下划线的意思是「返回值我不要了」。进程退出的状态码、是成功还是失败,全被扔掉。失败没有声音,调用方自然以为成功。

第三,幂等写会被误报成失败。内容和上次完全一致时,git commit 会以「nothing to commit」退出,状态码非成功——但这根本不是失败,本来就没有需要落库的差异。如果不把这一类区分出来,日志里会全是误报,真的失败反而淹没在误报里,没人看了。

所以修法是三道保险,缺一道都不完整:串行化解决撞锁,状态检查解决静默,幂等区分解决误报。

2. 分步骤实操

步骤 1:先给提交函数加一把进程内互斥锁

第一步不是改日志,是先让并发提交排队。做法是在 run_git_commit 里放一把进程内静态互斥锁:

use std::sync::Mutex;

static GIT_LOCK: Mutex<()> = Mutex::new(());
let _guard = GIT_LOCK.lock();

翻译成人话:这把锁像一间只有一个座位的等候室,同一个进程里不管多少路写入想提交,都必须一个一个来,前一个的 add + commit 全跑完,下一个才能进门。守卫变量 _guard 只要还活着,锁就一直占着,函数返回时自动释放,不用手写解锁,也就不怕中途 return 漏解锁。

有人可能会问,为什么是进程内锁,而不是文件锁或跨进程锁?因为 daw 的场景里,撞锁的几十次提交都来自同一个进程内的骨架生成,复盘脚本对仓库只是只读,不会跟写入抢锁。锁的范围要跟真实的冲突范围对齐:冲突只发生在进程内,就用进程内锁;如果你的场景里多个进程都会写同一个仓库,那就得升级成文件锁或重试策略,别照搬。

步骤 2:把 let _ = 换成状态检查,失败写进日志

第二步,把原来丢掉返回值的地方改成接住状态:

let add_ok = cmd.status().map(|s| s.success()).unwrap_or(false);
// ……
let commit_ok = cmd.status().map(|s| s.success()).unwrap_or(false);

cmd.status() 返回的是进程退出状态,.success() 只在退出码为 0 时为真;命令根本没跑起来(比如找不到 git)时返回 Err,用 unwrap_or(false) 统一算成失败,不让它 panic,也不让它悄悄算成功。

接住状态之后,失败要进日志,而不是只返回给调用方。daw 用的是 tracing::warn!,带上分类和提交信息:

if !add_ok || (!commit_ok && !git_worktree_clean(okf_dir)) {
    tracing::warn!(
        category = "system",
        "OKF git 提交未生效(add={add_ok} commit={commit_ok}),知识变更历史断档风险:{commit_msg}"
    );
}

注意这个判断里已经埋了第三步的伏笔:commit_ok 为假时,还要再看工作区是不是干净的——不干净才是真失败,干净就是幂等写。

步骤 3:用 git status --porcelain 把幂等写区分出来

第三步,写一个很小的工作区检查函数:

fn git_worktree_clean(okf_dir: &Path) -> bool {
    use std::process::Command;
    Command::new("git")
        .args(["status", "--porcelain"])
        .current_dir(okf_dir)
        .output()
        .map(|o| o.stdout.is_empty())
        .unwrap_or(false)
}

git status --porcelain 是给脚本用的稳定输出格式,有任何未提交变更就会输出一行,没有就是空输出。逻辑是:提交失败、但工作区已经干净,说明这次写入的内容和上次完全一致,属于「nothing to commit」,不算失败、不记警告;提交失败、工作区还不干净,说明确实有变更没落库,这才记警告。

这个区分为什么重要?因为没有它,两种结果都会摆在你面前:要么把幂等写也记成警告,日志很快被误报淹没,运维会把警告当噪音关掉;要么为了少报警告干脆不记,回到静默的老路。区分开之后,警告只出现在真失败时,才值得半夜被叫起来看。

步骤 4:给这套保险配一个「审计」,让历史断档自己现形

光有日志还不够,daw 的教训里还有一环:失败日志写出来了,也得有人定期去看。它的复盘脚本里加了「git 审计」:检查最近几次知识变更的提交记录,并检测有没有未提交变更——手工改了知识文件却没补提交,历史就会断档,审计要把它揪出来。

你不一定要照搬复盘脚本,但至少要做到两点之一:要么在写入回执里带上行数统计(新增几行、删除几行),让调用方一眼看出「这次写入是不是空的」;要么定期跑一次 git status,把未提交变更当成告警项。daw 当时正是靠这道审计,才让 80 多个从未入库的骨架现形,之后一次性回填了 83 个文件。

3. 为什么这样设计

这套做法背后只有一个判断标准:「自动提交」不等于「提交成功」。自动化的部分只是「不用人动手去提交」,成功与否仍然需要证据——状态码是证据,日志是证据,工作区干净与否是区分幂等和失败的证据。没有证据的自动化,等于把确认工作推迟到出事那天,而出事那天往往已经断档几周,补都补不全。

反过来想,为什么不直接重试?因为重试治标不治本:撞锁的根因是并发提交自己打架,重试只是让它们换个时间再打一架;而且重试会把一次写入变成多次提交,提交历史里出现重复记录,追溯时反而更乱。先串行化,再把失败亮出来,远比「默默多试几次」干净。

还有一个细节值得学:锁的粒度。daw 的锁只包住 run_git_commit 这一次提交的 add + commit 两步,没有把整个写入流程都锁住。粒度太大,骨架生成会被串行拖慢;粒度太小,add 和 commit 之间插进另一个提交,照样撞锁。锁住「一次完整提交」这个最小完整单元,是这套做法里最容易抄错的地方。

4. 避坑清单

  • 别用 let _ = 处理任何「失败了会有数据损失」的外部调用。能检查状态就检查状态;实在不能检查(比如某些平台接口不返回状态),要在注释里写明为什么可以不检查,而不是默认丢掉。
  • 互斥锁要先确认冲突范围。单进程内冲突用进程内锁就够了;多进程写同一仓库时,进程内锁拦不住跨进程冲突,得换文件锁或带退避的重试。
  • 幂等写一定要单独区分。凡是「没变化就不产生副作用」的操作,失败判断里都要留一个「本来就没事」的出口,否则误报会吃掉真告警的注意力。
  • 日志里要带足定位信息:哪一步失败(add 还是 commit)、提交信息是什么。只记一句「提交失败」,事后根本没法定位是哪笔变更丢了。
  • 审计不能只靠人记性。定期跑一次未提交变更检测,或者在写入回执里带行数统计,让「历史断档」自己浮出水面,而不是等用户发现数据对不上才去查。

5. 总结

给自动提交加保险,记住三句话:并发提交先排队,别让自己跟自己抢锁;外部调用的状态码必须接住,失败要进日志;幂等写要单独区分,别让误报淹没真失败。daw 的 80 多个骨架断档几周无人知晓,根因不是 git 难用,而是三处都缺了证据。把这三道保险补上,你的自动入库、自动备份、自动归档,才算真正「自动」而不是「碰运气」。

项目开源在 GitHub:tsingliuwin/daw,觉得有用欢迎点个 star。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/tsingwin/article/details/169534684

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--