fix(cron): tolerate unsupported directory fsync
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""Cron service for scheduling agent tasks."""
|
"""Cron service for scheduling agent tasks."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
@@ -456,11 +457,15 @@ class CronService:
|
|||||||
os.replace(tmp_path, path)
|
os.replace(tmp_path, path)
|
||||||
# fsync the parent directory so the rename itself is durable.
|
# fsync the parent directory so the rename itself is durable.
|
||||||
# Skip on Windows where opening a directory raises PermissionError;
|
# 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):
|
with suppress(PermissionError):
|
||||||
fd = os.open(str(path.parent), os.O_RDONLY)
|
fd = os.open(str(path.parent), os.O_RDONLY)
|
||||||
try:
|
try:
|
||||||
os.fsync(fd)
|
try:
|
||||||
|
os.fsync(fd)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno != errno.EINVAL:
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ jobs.json + don't silently overwrite corrupt store``.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -107,6 +108,34 @@ def test_save_store_failure_does_not_corrupt_existing_file(
|
|||||||
assert store_path.read_bytes() == original
|
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(
|
def test_load_jobs_preserves_corrupt_store_and_returns_none(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user