From 88d19cc07f811569591ea24620ed4ad695dde8a8 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 9 Jan 2025 01:39:49 -0500 Subject: [PATCH] write atomically in DirectoryBasedExampleDatabase --- hypothesis-python/RELEASE.rst | 3 +++ hypothesis-python/src/hypothesis/database.py | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..b05d7ffd59 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +:class:`~hypothesis.database.DirectoryBasedExampleDatabase` now creates files representing database entries atomically, avoiding a very brief intermediary state where a file could be created but not yet written to. diff --git a/hypothesis-python/src/hypothesis/database.py b/hypothesis-python/src/hypothesis/database.py index 5a15b66eea..c3542abdf6 100644 --- a/hypothesis-python/src/hypothesis/database.py +++ b/hypothesis-python/src/hypothesis/database.py @@ -9,11 +9,11 @@ # obtain one at https://mozilla.org/MPL/2.0/. import abc -import binascii import json import os import struct import sys +import tempfile import warnings import weakref from collections.abc import Iterable @@ -235,14 +235,19 @@ def save(self, key: bytes, value: bytes) -> None: self._key_path(key).mkdir(exist_ok=True, parents=True) path = self._value_path(key, value) if not path.exists(): - suffix = binascii.hexlify(os.urandom(16)).decode("ascii") - tmpname = path.with_suffix(f"{path.suffix}.{suffix}") - tmpname.write_bytes(value) + # to mimic an atomic write, create and write in a temporary + # directory, and only move to the final path after. This avoids + # any intermediate state where the file is created (and empty) + # but not yet written to. + fd, tmpname = tempfile.mkstemp() + tmppath = Path(tmpname) + os.write(fd, value) + os.close(fd) try: - tmpname.rename(path) + tmppath.rename(path) except OSError: # pragma: no cover - tmpname.unlink() - assert not tmpname.exists() + tmppath.unlink() + assert not tmppath.exists() except OSError: # pragma: no cover pass