fix: make AdaptiveStore.save() atomic via tempfile + os.replace (#138) - #139
Conversation
Write to a temp file in the same directory, then atomically rename over the target. A crash or concurrent write can no longer leave adaptive.json truncated or corrupt. Closes mldsveda#138
There was a problem hiding this comment.
Pull request overview
This PR improves the resilience of the adaptive-selector fingerprint persistence by making AdaptiveStore.save() write updates atomically (write to a temp file, then swap into place) and adds a regression test to ensure failed writes don’t corrupt existing stored data.
Changes:
- Updated
AdaptiveStore.save()to write JSON to a temp file and atomically replace the target viaos.replace. - Added a test that simulates a failure during the replace step and verifies the original store remains intact and no temp files are left behind.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/pyscrappy/generic/adaptive_store.py |
Switches store persistence to a temp-write + atomic replace approach to avoid partial/corrupt writes. |
tests/test_generic/test_adaptive.py |
Adds a failure-mode test to validate atomic-save behavior and cleanup. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Hi @mldsveda 👋 I've addressed the automated review feedback in the latest commit:
Ready for review/merge when you have a moment! 🚀 |
|
Hi @mldsveda 👋 I've updated the PR with all feedback addressed:
Ready for review & merge when you have a moment! 🚀 |
The prior format commits did not fully satisfy ruff format (likely a ruff version mismatch): the retrieve() signature and test_adaptive.py still needed reformatting, so CI lint stayed red. Run ruff format to clear it. No logic change.
|
Thanks @devkamani9313-lab, the atomic save is done right, temp file in the same dir, handle closed before os.replace, and failure cleanup that unlinks the temp file without masking the original exception. The failure-mode test is a good guard too. One thing: CI lint stayed red because ruff format still flagged adaptive_store.py and test_adaptive.py on your latest, likely a ruff version mismatch with CI, so your format commits did not fully clear it. I pushed a ruff format commit on top to green it, no logic change. 480 passed. Nice contribution. |
|
Thank you so much @vedaant00 and @mldsveda for the super fast review and merge! Glad to contribute to PyScrappy! 🎉🙌 |
Summary
Makes
AdaptiveStore.save()atomic by writing to a temporary file (tempfile.mkstemp) and then renaming it over the target withos.replace().Fixes #138
Problem
The current
save()method usesPath.write_text(), which is a non-atomic write:adaptive.jsontruncated/corrupt_load()silently returns{}on corrupt JSON, wiping all saved fingerprintsChanges
src/pyscrappy/generic/adaptive_store.pyimport tempfile(stdlib, no new dependencies)self.path.write_text(json.dumps(...))with:tempfile.mkstemp(dir=self.path.parent)— temp file in same directoryjson.dump(data, f)— write to temp fileos.replace(tmp, self.path)— atomic rename (POSIX + Windows)except BaseException: os.unlink(tmp); raise— cleanup on failuretests/test_generic/test_adaptive.pytest_save_atomic_no_corruption_on_write_failure:mock.patch("os.replace").tmpfilesChecklist
ruff check src/passestempfile+os)