fix(cron): tolerate unsupported directory fsync
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user