diff --git a/README.md b/README.md index cbcdbbc..0176dcf 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,11 @@ Prefer `AioKV` in new code. ## Durability Writes are **atomic**: data is written to a temp file in the same directory and -`os.replace()`d over the target (atomic on POSIX). A crash mid-write leaves the -previous good file intact, and a reader never observes a partial file. A single +`os.replace()`d over the target (atomic on POSIX). A **process** crash mid-write leaves +the previous good file intact, and a reader never observes a partial file. (This is +process-crash safety, not power-loss durability — there's no `fsync`, so an OS/power +failure could still lose the last write; fine for reconstructible single-process state.) +A single `asyncio.Lock` guards every read and write, so concurrent operations on one instance are consistent and no update is lost. All blocking filesystem calls run via `asyncio.to_thread`, so nothing stalls the event loop. diff --git a/src/aiokv/aiokv.py b/src/aiokv/aiokv.py index 522f463..061d3ea 100644 --- a/src/aiokv/aiokv.py +++ b/src/aiokv/aiokv.py @@ -82,11 +82,17 @@ class AioKV: return False async def clear(self) -> bool: - """remove the backing file entirely; returns True on success, False on error""" + """remove the backing file entirely; returns True on success, False on error + + a file already absent (or removed concurrently between the check and the remove) + is success — the goal state, no file, is reached. + """ try: async with self.lock: - if await asyncio.to_thread(os.path.exists, self.file): + try: await asyncio.to_thread(os.remove, self.file) + except FileNotFoundError: + pass return True except Exception: log.exception("aiokv.clear() failed") @@ -107,7 +113,7 @@ class AioKV: """ if not await asyncio.to_thread(os.path.exists, self.file): return {} - async with aiofiles.open(self.file, mode="r") as f: + async with aiofiles.open(self.file, mode="r", encoding="utf-8") as f: data = await f.read() if not data: return {} @@ -120,7 +126,10 @@ class AioKV: """write the store atomically: temp file in the same dir, then os.replace os.replace is atomic on POSIX, so a reader never sees a partial file and a - crash mid-write leaves the previous good file intact. + process crash mid-write leaves the previous good file intact. note this is + process-crash safety, NOT power-loss durability — there is no fsync of the temp + file or the directory, so an OS/power failure could still lose the most recent + write (acceptable here: this is reconstructible single-process state, not a db). """ directory = os.path.dirname(self.file) or "." await asyncio.to_thread(os.makedirs, directory, exist_ok=True) @@ -128,7 +137,7 @@ class AioKV: payload = json.dumps(cache) tmp = f"{self.file}.{os.getpid()}.tmp" try: - async with aiofiles.open(tmp, mode="w") as f: + async with aiofiles.open(tmp, mode="w", encoding="utf-8") as f: await f.write(payload) await asyncio.to_thread(os.replace, tmp, self.file) except Exception: