fix(cron): tolerate unsupported directory fsync

This commit is contained in:
chengyongru
2026-07-01 19:51:43 +08:00
committed by Xubin Ren
parent 2ec4044217
commit f6d1dba32a
2 changed files with 36 additions and 2 deletions
+7 -2
View File
@@ -1,6 +1,7 @@
"""Cron service for scheduling agent tasks."""
import asyncio
import errno
import json
import os
import time
@@ -456,11 +457,15 @@ class CronService:
os.replace(tmp_path, path)
# fsync the parent directory so the rename itself is durable.
# Skip on Windows where opening a directory raises PermissionError;
# NTFS journals metadata synchronously so this is a no-op there.
# some shared filesystems reject directory fsync with EINVAL.
with suppress(PermissionError):
fd = os.open(str(path.parent), os.O_RDONLY)
try:
os.fsync(fd)
try:
os.fsync(fd)
except OSError as exc:
if exc.errno != errno.EINVAL:
raise
finally:
os.close(fd)
except BaseException:
+29
View File
@@ -8,6 +8,7 @@ jobs.json + don't silently overwrite corrupt store``.
from __future__ import annotations
import errno
import json
from pathlib import Path
from typing import Callable
@@ -107,6 +108,34 @@ def test_save_store_failure_does_not_corrupt_existing_file(
assert store_path.read_bytes() == original
def test_atomic_write_ignores_unsupported_directory_fsync(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""vboxsf-like filesystems can open directories but reject directory fsync."""
store_path = tmp_path / "cron" / "jobs.json"
dir_fd = 987654
def fake_open(path: str, flags: int) -> int:
assert Path(path) == store_path.parent
return dir_fd
def fake_fsync(fd: int) -> None:
if fd == dir_fd:
raise OSError(errno.EINVAL, "Invalid argument")
def fake_close(fd: int) -> None:
assert fd == dir_fd
monkeypatch.setattr("os.open", fake_open)
monkeypatch.setattr("os.fsync", fake_fsync)
monkeypatch.setattr("os.close", fake_close)
CronService._atomic_write(store_path, '{"version": 1, "jobs": []}')
assert store_path.read_text(encoding="utf-8") == '{"version": 1, "jobs": []}'
assert list(store_path.parent.glob("*.tmp")) == []
def test_load_jobs_preserves_corrupt_store_and_returns_none(
tmp_path: Path,
) -> None: