用 Python 检查定时任务产物是否过期:修改时间、空文件与退出码
定时任务的日志写着“已启动”,输出目录里也有 report.json,但那可能是昨天留下的旧文件。只看进程退出码或文件是否存在,很容易漏掉“任务没有生成新结果”这类故障。
本文面向初学者到中级读者,用 Python 标准库检查产物的修改时间、字节数和文件类型,输出 JSON,并用退出码让后续命令停止。适合本地报表、导出文件和静态站点构建后的辅助检查,不需要安装依赖或改调度器。
1. 方法:明确什么叫“产物正常”
这里采用一组简单规则:
- 路径必须能取得元数据,且目标是普通文件。
- 文件至少有
--min-bytes字节,默认 1,空文件报警。 - 距离最后修改不能超过
--max-age秒,等于阈值时允许通过。 - 修改时间领先当前时间超过 5 秒也报警,避免未来时间让旧产物长期被判为正常。
Python 的 stat 结果文档说明,st_mtime 是最后内容修改时间,st_size 是字节数。在 Linux 上,st_ctime 表示元数据变更时间,不能拿它当文件创建时间。下面使用 stat.S_ISREG()识别普通文件。
“文件是否存在”和“任务是否按时产出”是两回事。如果脚本在定时环境下找错了目录,先参考定时任务路径与环境排查,固定工作目录和解释器路径。
2. 准备环境和测试文件
需要 Python 3.9 或更新版本,无第三方依赖。示例已在 Linux、Python 3.11.4 下测试,命令适用于 bash 或 zsh。
- 新建演示目录,后续命令在同一个终端执行。重复实验时使用新的目录名:
python3 --version
mkdir freshness-demo
cd freshness-demo
freshness_root="$PWD"
python3 - <<'PY_DATA'
from pathlib import Path
import os
import time
now = time.time()
for name, content, modified in [
("fresh.txt", "ready\n", now),
("stale.txt", "ready\n", now - 7200),
("empty.txt", "", now),
("future.txt", "ready\n", now + 3600),
]:
Path(name).write_text(content, encoding="utf-8")
os.utime(name, (modified, modified))
PY_DATA
- 把下一节代码保存为当前目录的
check_freshness.py。fresh.txt是新文件,stale.txt被设为两小时前,empty.txt为空,future.txt的时间在一小时后。
os.utime()可设置访问和修改时间。这里只改演示文件,因此无需等待一小时或调整系统时钟。请在创建测试文件后一小时内完成第 4、5 节;超过一小时就重新运行上面的 PY_DATA 片段。
3. 完整实现:只读检查,输出 JSON
"""Check a completed output file's age and size. Python 3.9+, standard library."""
import argparse
import json
from pathlib import Path
import stat
import sys
import time
def nonnegative_int(value):
number = int(value)
if number < 0:
raise argparse.ArgumentTypeError("must be zero or greater")
return number
def inspect_file(path, max_age, min_bytes):
info = path.stat()
if not stat.S_ISREG(info.st_mode):
raise ValueError("not a regular file")
age = time.time() - info.st_mtime
problems = []
if info.st_size < min_bytes:
problems.append("too_small")
if age < -5:
problems.append("future_mtime")
elif age > max_age:
problems.append("too_old")
return {
"file": str(path),
"status": "alert" if problems else "ok",
"age_seconds": round(age, 3),
"size_bytes": info.st_size,
"problems": problems,
}
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("file", type=Path, help="completed output to inspect")
parser.add_argument("--max-age", type=nonnegative_int, required=True,
help="maximum age in seconds; must be greater than zero")
parser.add_argument("--min-bytes", type=nonnegative_int, default=1,
help="minimum size in bytes (default: 1)")
args = parser.parse_args()
if args.max_age == 0:
parser.error("--max-age must be greater than zero")
try:
result = inspect_file(args.file, args.max_age, args.min_bytes)
except (OSError, ValueError) as exc:
print(f"ERROR: {args.file}: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False))
return 1 if result["problems"] else 0
if __name__ == "__main__":
raise SystemExit(main())
脚本只读取元数据,不读取或修改产物内容。Path.stat() 会跟随符号链接,检查的是目标文件;断开的链接会报错。目录、设备和 FIFO 都不能充当正常产物。
文件时间和 time.time()使用可比较的时间基准;不要把文件时间戳与 time.monotonic() 相减。系统时钟校正会影响年龄计算,本例只容忍最多 5 秒的未来偏差。age_seconds 为便于阅读四舍五入到三位小数,判断用的是舍入前的值。
4. 运行与退出码
- 查看帮助,再检查新文件和旧文件:
python3 check_freshness.py --help
python3 check_freshness.py fresh.txt --max-age 3600
python3 check_freshness.py stale.txt --max-age 3600
新文件的输出类似下面,age_seconds 取决于实际运行时间:
{"file": "fresh.txt", "status": "ok", "age_seconds": 2.314, "size_bytes": 6, "problems": []}
旧文件会输出 status: "alert" 和 problems: ["too_old"],退出码为 1。空文件和未来时间分别对应 too_small、future_mtime;同一文件可以有多个问题。
- 按下面的约定接入调用方:
| 退出码 | 含义 |
|---|---|
| 0 | 普通文件,年龄和大小均通过检查。 |
| 1 | 产物报警,或缺失、权限错误、类型不对,无法完成检查。 |
| 2 | 命令行参数错误,例如 --max-age 0 或负数。 |
能取得普通文件元数据时,标准输出是一行 JSON,包括报警结果;缺失、访问错误和类型错误只向标准错误输出 ERROR:。参数错误由 argparse 输出到标准错误,规则见 argparse 官方文档。不要把“没有成功 JSON”当作“没有问题”。
5. 验证故障分支,无需真实等待
运行下面的自检,确认结果和退出码都符合约定。代码显式允许子进程失败,再检查其实际返回值:
python3 - <<'PY_CHECK'
import json
import subprocess
import sys
cases = [
(["fresh.txt", "--max-age", "3600"], 0, []),
(["stale.txt", "--max-age", "3600"], 1, ["too_old"]),
(["empty.txt", "--max-age", "3600"], 1, ["too_small"]),
(["future.txt", "--max-age", "3600"], 1, ["future_mtime"]),
(["missing.txt", "--max-age", "3600"], 1, None),
([".", "--max-age", "3600"], 1, None),
(["fresh.txt", "--max-age", "0"], 2, None),
]
for arguments, expected_code, expected_problems in cases:
result = subprocess.run(
[sys.executable, "check_freshness.py", *arguments],
capture_output=True, text=True, check=False,
)
assert result.returncode == expected_code, (arguments, result)
if expected_problems is None:
assert result.stdout == "" and result.stderr, (arguments, result)
else:
report = json.loads(result.stdout)
assert report["problems"] == expected_problems, report
assert report["status"] == ("alert" if expected_problems else "ok"), report
print("OK: fresh, stale, empty, future, missing, directory, bad arguments")
PY_CHECK
预期输出:
OK: fresh, stale, empty, future, missing, directory, bad arguments
自检不会重写这些产物。缺失路径和目录必须返回非零,错误参数也不能悄悄变成成功。需要在 shell 中继续观察预期失败时,用条件语句处理;开启 set -e 的脚本可能会在未处理的退出码 1 处提前结束。
6. 接入已有流程与排错
- 用绝对路径检查实际结果。本例仍然只检查演示文件,不修改任何调度设置:
python3 "$freshness_root/check_freshness.py" \
"$freshness_root/fresh.txt" --max-age 3600 --min-bytes 1 \
&& printf 'Output is recent and nonempty\n'
&& 右边只在检查成功时执行,失败的检查会保留非零退出码。若要发现生产任务一直没有运行,应让检查器由独立的现有监控入口定期调用;把它放在生产任务内部,生产任务不启动时它也不会启动。每小时生成一次报表的任务可以先设 --max-age 5400,给正常运行时长和调度延迟留出余量,再按实际运行情况调整。
Hugo 构建后也可检查本次输出目录里的目标 HTML,但文件时间不能证明页面内容和链接正确;继续使用 Hugo 站内链接与图片检查做内容层验证。
- 根据问题码修复原因,再重新检查:
| 现象 | 具体处理 |
|---|---|
too_old |
核对生产任务最近的真实成功记录、输入和输出路径,再重新生成产物。复制工具保留旧修改时间时,应改查本次成功后生成的状态文件。 |
too_small |
查看生产任务错误日志和剩余磁盘;只有业务允许空结果时,才显式使用 --min-bytes 0。字节阈值不代表记录数。 |
future_mtime |
核对生产主机与检查主机的时钟,以及复制过程保留的时间戳。不要通过无限增大阈值掩盖问题。 |
No such file |
用绝对路径确认挂载位置、产物名称和生产步骤。 |
Permission denied |
用执行检查器的账号检查父目录访问权限。能 stat 不等于能读文件内容。 |
not a regular file |
指向具体文件,不能用输出目录的时间代替报表文件的时间。 |
- 在演示中重新生成旧产物,确认恢复:
python3 - <<'PY_REPAIR'
from pathlib import Path
Path("stale.txt").write_text("ready\n", encoding="utf-8")
PY_REPAIR
python3 check_freshness.py stale.txt --max-age 3600
这次应返回 status: "ok" 和退出码 0。生产环境不要仅用 touch 刷新时间来消除报警,它不会生成缺失的业务数据。
时间新、文件非空,也可能只是写了一半或内容错误。生产任务应先校验内容,成功后再替换正式文件;具体保存方式见 Python 原子写入 JSON。对正在写入的文件,本次元数据只是某一时刻的观察,不能证明任务已完成。保留修改时间的复制、文件系统时间精度和网络文件系统缓存,也会影响结果。
7. 小结
给产物检查同时设置最大年龄和最小字节数,可以发现旧文件残留与空输出,并让自动化流程获得明确的退出码。先用可控时间戳验证失败分支,再检查生产路径;把内容校验和成功后的文件替换留在生产任务中,避免把“近期更新”误当成“结果正确”。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-output-freshness-check.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。