mirror of
https://github.com/alexta69/metube.git
synced 2026-07-23 13:22:48 +00:00
fix: limit atomic-write fallback to atomic-unsupported errnos
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -17,6 +18,25 @@ STATE_SCHEMA_VERSION = 2
|
|||||||
_BYTES_MARKER = "__metube_bytes__"
|
_BYTES_MARKER = "__metube_bytes__"
|
||||||
_DATETIME_MARKER = "__metube_datetime__"
|
_DATETIME_MARKER = "__metube_datetime__"
|
||||||
|
|
||||||
|
# Errnos that signal the filesystem cannot support the temp-file + rename
|
||||||
|
# atomic-write strategy (for example an NFS-backed state dir returning EPERM on
|
||||||
|
# mkstemp). These are safe to fall back on because they mean the atomic
|
||||||
|
# mechanism is unavailable, not that the data write itself failed. Errors like
|
||||||
|
# ENOSPC/EIO are deliberately excluded so a genuine storage failure surfaces
|
||||||
|
# instead of silently truncating an existing good state file.
|
||||||
|
_ATOMIC_UNSUPPORTED_ERRNOS = frozenset(
|
||||||
|
e
|
||||||
|
for e in (
|
||||||
|
errno.EPERM,
|
||||||
|
errno.EACCES,
|
||||||
|
errno.ENOSYS,
|
||||||
|
errno.EINVAL,
|
||||||
|
getattr(errno, "EOPNOTSUPP", None),
|
||||||
|
getattr(errno, "ENOTSUP", None),
|
||||||
|
)
|
||||||
|
if e is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_json_compatible(value: Any) -> Any:
|
def to_json_compatible(value: Any) -> Any:
|
||||||
if value is None or isinstance(value, (bool, int, float, str)):
|
if value is None or isinstance(value, (bool, int, float, str)):
|
||||||
@@ -100,6 +120,8 @@ class AtomicJsonStore:
|
|||||||
try:
|
try:
|
||||||
self._atomic_write(payload)
|
self._atomic_write(payload)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
|
if exc.errno not in _ATOMIC_UNSUPPORTED_ERRNOS:
|
||||||
|
raise
|
||||||
self._warn_direct_write_fallback(exc)
|
self._warn_direct_write_fallback(exc)
|
||||||
self._direct_write(payload)
|
self._direct_write(payload)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,28 @@ class StateStoreTests(unittest.TestCase):
|
|||||||
self.assertEqual(ctx.exception.errno, 13)
|
self.assertEqual(ctx.exception.errno, 13)
|
||||||
self.assertFalse(os.path.exists(path))
|
self.assertFalse(os.path.exists(path))
|
||||||
|
|
||||||
|
def test_save_reraises_and_preserves_state_on_non_atomic_errno(self):
|
||||||
|
# A storage failure such as ENOSPC is not an "atomic unavailable"
|
||||||
|
# signal, so it must surface instead of falling back to a direct write
|
||||||
|
# that would truncate the existing good state file.
|
||||||
|
import errno as _errno
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
path = os.path.join(tmp, "queue.json")
|
||||||
|
store = AtomicJsonStore(path, kind="persistent_queue:queue")
|
||||||
|
store.save({"items": [{"key": "good"}]})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"state_store.tempfile.mkstemp",
|
||||||
|
side_effect=OSError(_errno.ENOSPC, "No space left on device"),
|
||||||
|
):
|
||||||
|
with self.assertRaises(OSError) as ctx:
|
||||||
|
store.save({"items": [{"key": "new"}]})
|
||||||
|
|
||||||
|
self.assertEqual(ctx.exception.errno, _errno.ENOSPC)
|
||||||
|
# Existing state is untouched.
|
||||||
|
self.assertEqual(store.load()["items"], [{"key": "good"}])
|
||||||
|
|
||||||
def test_invalid_file_is_quarantined(self):
|
def test_invalid_file_is_quarantined(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
path = os.path.join(tmp, "queue.json")
|
path = os.path.join(tmp, "queue.json")
|
||||||
|
|||||||
Reference in New Issue
Block a user