fix(utils): EXDEV/EBUSY copy fallback in atomic_replace (port from gemini-cli#21541) - #43852
Conversation
🔎 Lint report:
|
|
Verification review — reviewed the full diff (2 files, +157/-1). Clean port of the EXDEV/EBUSY fallback from gemini-cli#21541:
No issues found. Clean utility fix. |
Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
4c57760 to
3cc8909
Compare
|
Refreshed against current Cleanup:
Validation:
|
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
…arch#43852) Fallback from `os.replace` on EXDEV/EBUSY using copy+fsync+unlink while preserving symlink target semantics and metadata.
Summary
atomic_replace()now falls back to copy + fsync + unlink whenos.replace()fails with EXDEV (cross-device link) or EBUSY (busy bind mount), instead of crashing every atomic write site.Port of the rename-fallback pattern from google-gemini/gemini-cli#21541 (their policy-file persistence hit the same failure class on Docker bind mounts / Linux aarch64).
Root cause:
atomic_replaceresolves symlinked targets to their real path (#16743 symlink preservation) before renaming. When a managed deployment symlinks~/.hermes/config.yaml/auth.json/jobs.jsonto a file on a different filesystem (Docker bind mount, NFS/SMB share, dotfiles repo on another partition), the rename crosses devices and raisesOSError(EXDEV)— reproduced E2E on current main with a/dev/shm-backed symlink target. Every caller ofatomic_replace/atomic_json_write/atomic_yaml_write(~24 sites: config, auth, cron jobs, webhooks, model catalog, skills sync, …) is affected.Changes
utils.py: catch EXDEV/EBUSY fromos.replace, fall back toshutil.copyfile+copymode+ fsync + unlink. All other OSErrors still propagate. Fallback is non-atomic but strictly better than failing the write and leaking the temp file.tests/test_atomic_replace_symlinks.py: 5 new tests — EXDEV/EBUSY fallback (parametrized), symlink preserved on the fallback path, non-EXDEV errors propagate untouched, and a real-filesystem E2E using a tmpfs (/dev/shm) symlink target.Validation
atomic_replaceonto cross-device symlinkOSError: [Errno 18] Invalid cross-device linktests/test_atomic_replace_symlinks.pySource PR: google-gemini/gemini-cli#21541