春江暮客

春江暮客的个人学习分享网站

用 Python 检查定时任务产物是否过期:修改时间、空文件与退出码

2026-10-16 技术
用 Python 检查定时任务产物是否过期:修改时间、空文件与退出码

定时任务的日志写着“已启动”,输出目录里也有 report.json,但那可能是昨天留下的旧文件。只看进程退出码或文件是否存在,很容易漏掉“任务没有生成新结果”这类故障。

本文面向初学者到中级读者,用 Python 标准库检查产物的修改时间、字节数和文件类型,输出 JSON,并用退出码让后续命令停止。适合本地报表、导出文件和静态站点构建后的辅助检查,不需要安装依赖或改调度器。

1. 方法:明确什么叫“产物正常”

这里采用一组简单规则:

  1. 路径必须能取得元数据,且目标是普通文件。
  2. 文件至少有 --min-bytes 字节,默认 1,空文件报警。
  3. 距离最后修改不能超过 --max-age 秒,等于阈值时允许通过。
  4. 修改时间领先当前时间超过 5 秒也报警,避免未来时间让旧产物长期被判为正常。

Python 的 stat 结果文档说明,st_mtime 是最后内容修改时间,st_size 是字节数。在 Linux 上,st_ctime 表示元数据变更时间,不能拿它当文件创建时间。下面使用 stat.S_ISREG()识别普通文件。

“文件是否存在”和“任务是否按时产出”是两回事。如果脚本在定时环境下找错了目录,先参考定时任务路径与环境排查,固定工作目录和解释器路径。

2. 准备环境和测试文件

需要 Python 3.9 或更新版本,无第三方依赖。示例已在 Linux、Python 3.11.4 下测试,命令适用于 bash 或 zsh。

  1. 新建演示目录,后续命令在同一个终端执行。重复实验时使用新的目录名:
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
  1. 把下一节代码保存为当前目录的 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. 运行与退出码

  1. 查看帮助,再检查新文件和旧文件:
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;同一文件可以有多个问题。

  1. 按下面的约定接入调用方:
退出码 含义
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. 接入已有流程与排错

  1. 用绝对路径检查实际结果。本例仍然只检查演示文件,不修改任何调度设置:
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 站内链接与图片检查做内容层验证。

  1. 根据问题码修复原因,再重新检查:
现象 具体处理
too_old 核对生产任务最近的真实成功记录、输入和输出路径,再重新生成产物。复制工具保留旧修改时间时,应改查本次成功后生成的状态文件。
too_small 查看生产任务错误日志和剩余磁盘;只有业务允许空结果时,才显式使用 --min-bytes 0。字节阈值不代表记录数。
future_mtime 核对生产主机与检查主机的时钟,以及复制过程保留的时间戳。不要通过无限增大阈值掩盖问题。
No such file 用绝对路径确认挂载位置、产物名称和生产步骤。
Permission denied 用执行检查器的账号检查父目录访问权限。能 stat 不等于能读文件内容。
not a regular file 指向具体文件,不能用输出目录的时间代替报表文件的时间。
  1. 在演示中重新生成旧产物,确认恢复:
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. 小结

给产物检查同时设置最大年龄和最小字节数,可以发现旧文件残留与空输出,并让自动化流程获得明确的退出码。先用可控时间戳验证失败分支,再检查生产路径;把内容校验和成功后的文件替换留在生产任务中,避免把“近期更新”误当成“结果正确”。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它