Skip to content

fix(store): relocate LocalStore temporary files to configurable tmp_dir#4173

Open
omar-merhebi wants to merge 1 commit into
zarr-developers:mainfrom
omar-merhebi:fix/localstore-configurable-tempdir
Open

fix(store): relocate LocalStore temporary files to configurable tmp_dir#4173
omar-merhebi wants to merge 1 commit into
zarr-developers:mainfrom
omar-merhebi:fix/localstore-configurable-tempdir

Conversation

@omar-merhebi

Copy link
Copy Markdown

Summary

LocalStore now writes temporary files to a dedicated tmp_dir rather than directly inside the store path. Previously, _atomic_write wrote temp files ending in .partial inside the store which triggered ZarrUserWarning during concurrent reading and listing of the store.

The location defaults to the system temporary directory (via tempfile.gettempdir()), and is overridable via the tmp_dir argument to LocalStore or globally via the store.local.tmp_dir config option (env var ZARR_STORE__LOCAL__TMP_DIR). The temporary directory must be on the same filesystem as the store, otherwise _atomic_write will raise OSError with EXDEV. The pre-existing try/except now also has a specific catch for EXDEV and re-raises, informing the user to change the tmp_dir value to a location on the same filesystem rather than emitting a raw OSError. Documented this new option in config.md and storage.md. Fixes #4161.

For reviewers

I added a specific catch in the try/except block of _atomic_write() that specifically catches OSError caused by EXDEV and instructs the user to pick a temporary directory on the same filesystem if the atomic write fails. Happy to modify the message, change logic, or remove it entirely if preferred. I also verified that errno.EXDEV is the errno that is raised on both Unix and Windows. Used the convention of naming the config arg and vars as tmp_dir to copy the old tmp_path variable in _atomic_write(). Also happy to change that naming scheme if preferable. I specifically avoided shutil.move() here to keep LocalStore atomic (shutil.move() attempts os.rename() and falls back to copying if it fails). This follows the design discussed in #4161.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

TODO

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/user-guide/*.md
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

`LocalStore` now writes temporary files to a dedicated `tmp_dir` rather than
directly inside the store path. Previously, `_atomic_write` wrote temp files
ending in `.partial` inside the store which triggered `ZarrUserWarning` during
concurrent reading and listing of the store.

The location defaults to the system temporary directory (via
`tempfile.gettempdir()`), and is overridable via the `tmp_dir` argument to
`LocalStore` or globally via the `store.local.tmp_dir` config option (env var
`ZARR_STORE__LOCAL__TMP_DIR`). The temporary directory must be on the same
filesystem as the store, otherwise `_atomic_write` will raise `OSError` with
`EXDEV`. The pre-existing try/except now also has a specific catch for `EXDEV`
and re-raises, informing the user to change the `tmp_dir` value to a location on
the same filesystem rather than emitting a raw `OSError`. Documented this new
option in config.md and storage.md. Fixes zarr-developers#4161.
@omar-merhebi
omar-merhebi force-pushed the fix/localstore-configurable-tempdir branch from 4ae0f4e to a8f6db7 Compare July 22, 2026 04:12
@omar-merhebi

omar-merhebi commented Jul 22, 2026

Copy link
Copy Markdown
Author

Flagging this before anyone digs into the failing CIs

Both failures have the same root cause: temps default to the system temp dir, which is on a different filesystem than the store, so the os.replace() in _atomic_write() raises EXDEV. The new error handling is catching and re-raising it as designed, but it's happening on ordinary writes.

RTD (Linux): temp dir /tmp vs the checkout volume under /home/docs/checkouts/.... is leading to EXDEV on all the code blocks that write LocalStore. So /tmp is set up as a different filesystem.

Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/zarr/envs/4173/lib/python3.12/site-packages/zarr/storage/_local.py", line 78, in _atomic_write
    tmp_path.replace(path)
  File "/home/docs/.asdf/installs/python/3.12.13/lib/python3.12/pathlib.py", line 1376, in replace
    os.replace(self, target)
OSError: [Errno 18] Invalid cross-device link: '/tmp/7437a6df7e014a488d0b2fb54780f3c7.partial' -> 'data/example-1.zarr/zarr.json'

Windows (windows-latest, py3.14): the temp dir is on C: (C:\Users\...\Temp) and the checkout is on D:\. The OS itself reports this with WinError 17: OSError: [WinError 17] The system cannot move the file to a different disk drive:

Traceback (most recent call last):
  File "D:\a\zarr-python\zarr-python\src\zarr\storage\_local.py", line 78, in _atomic_write
    tmp_path.replace(path)
  File "C:\hostedtoolcache\windows\Python\3.14.6\x64\Lib\pathlib\__init__.py", line 1091, in replace
    os.replace(self, target)
OSError: [WinError 17] The system cannot move the file to a different disk drive: 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\7a611477900f439a92b4a924519079c4.partial' -> 'example_from_array.zarr\\zarr.json'

ubuntu-latest: This passes because the Ubuntu runners keep temp on the checkout's disk so it's the same filesystem on those runners and it works. Ubuntu moved to using tmpfs by default on Ubuntu 24.10, prior to that /tmp was on the same filesystem as the store. The github runner is using Ubuntu 24.04.4, so it predates this change. Note that tmpfs is just a default. Users can customize how they set up their /tmp filesystem, so even Ubuntu versions prior to 24.10 may experience this issue (such as RTD Ubuntu 22.04 above).

Therefore, any setup where a store isn't on the temp dir's filesystem (e.g., a tmpfs /tmp directory, a separate data mount/drive, HPC, etc.) will see this. I think the fix would be to change the default to go to the store's filesystem either as a reserved subdirectory under the store root (would need list methods to skip it), or as a sibling to the store root (won't be in the store so listing methods are fine, but writes to an actual parent dir that the user might not expect us to use. Also doesn't fully eliminate the issues above, but does make them rarer). Happy to do whichever is preferred or implement other suggestions.

@d-v-b
d-v-b requested a review from mkitti July 22, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LocalStore listing lists .partial temp files

1 participant