You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Writing dask arrays to a ZipStore causes a corrupt zip file #3516
Via binder, with xarray's blank_template.ipynb documentation example
Description
When dask provides the data for a zarr array that is backed by a ZipStore, the ZipStore on disk becomes corrupt and cannot be read back in, throwing a BadZipFile exception. This corruption does not happen with a LocalStore. Nontrivial sharding/chunking is not required.
Steps to reproduce
# /// script# requires-python = ">=3.11"# dependencies = [# "zarr@git+https://github.com/zarr-developers/zarr-python.git@main",# ]# ///## This script automatically imports the development branch of zarr to check for issues# your reproducer codeimportdask.arrayasdaimportnumpyasnpimportzarrdask_array=da.zeros((1,),dtype=np.float32)
store=zarr.storage.ZipStore("bug.zarr.zip",mode='w',read_only=False)
# store = zarr.storage.LocalStore("bug.zarr",read_only=False) # Does not errorgroup=zarr.open_group(store=store, mode="w")
zarr_array=group.create_array(
name="data",
shape=dask_array.shape,
dtype=dask_array.dtype,
overwrite=True,
)
da.to_zarr(dask_array, zarr_array)
store.close()
store_read=zarr.storage.ZipStore("bug.zarr.zip",mode='r',read_only=True)
# store_read = zarr.storage.LocalStore("bug.zarr",read_only=True) # Does not errorgroup_read=zarr.open_group(store=store_read, mode="r")
# Error:## ---------------------------------------------------------------------------## BadZipFile Traceback (most recent call last)## [...]## BadZipFile: Bad magic number for file headerarray_read=group_read["data"]
array_read[0] # Should be array(0., dtype=float32)# zarr.print_debug_info()
I am not an expert in the zip archive format, but my guess is that it is not safe to write concurrently. I think if you want to create zipped zarr data via dask, the best approach is probably to use dask to concurrently write data to local storage, and then zip it in a separate step.
I'm not sure how we can guard against this corruption at runtime without implementing a distributed lock. Open to ideas here.
If it were a concurrency bug, then I'd expect it to also happen with non-dask writes to the ZipStore. In the maximal code where I discovered the bug (by way of xarray), a large-ish but in-memory dataset would write successfully to a ZipStore if the chunking was left at default values (delegating it to zarr), but specifying manual chunks via xarray/dask caused this corruption.
Additionally, this did work correctly in earlier versions of zarr, so I think there's a regression somewhere. In a colab notebook with zarr 2.16.1:
Additionally, this did work correctly in earlier versions of zarr, so I think there's a regression somewhere. In a colab notebook with zarr 2.16.1:
That's good to know — I hope we can restore this functionality. Now I have no intuition for where the data corruption could be coming from, so hopefully someone who does can figure this out!
I was hoping that the parallelism hint would work out, but the current version's ZipStore.set()takes a lock before calling _set() which performs the write, and 2.16.1's ZipStore.__setitem__does essentially the same thing:
Unfortunately, I'm getting lost inside dask more deeply than I have time for right now. I can confirm that this still happens with the 'synchronous' scheduler that should eliminate parallelsim, and I can also track it down into how dask.array.blockwise maps load_store_chunk onto the zarr array. However, I cannot get the bug to trigger by calling load_store_chunk myself (even via dask.delayed); easy replication seems to require calling .compute() on a generated (even if trivial) dask graph.
Edit to add: I'm definitely flummoxed. Even if I modify ZipStore to use a monkeypatched ZipFile that logs-to-console every call to _zf.writestr, I see an identical sequence of writes between da.to_zarr (which corrupts) and a direct slice assignment (which succeeds).
I re-ran the original da.to_zarr(dask_array, zarr_array) reproducer against current stable releases and the corruption still occurs.
Environment:
Python 3.14.2
zarr 3.2.1
dask 2026.7.1
numpy 2.5.1
macOS arm64
After store.close(), zipfile.ZipFile(path).testzip() reports zarr.json as the first bad member, and reopening through zarr.open_group(...) raises BadZipFile: Bad magic number for file header. The result is the same with Dask's default scheduler and scheduler="synchronous".
So the later Dask create_array work and the copy-store workaround do not appear to resolve this original write-to-an-existing-Zarr-array path. This issue remains reproducible on current releases.
Activity
d-v-b commentedon Oct 10, 2025
I am not an expert in the zip archive format, but my guess is that it is not safe to write concurrently. I think if you want to create zipped zarr data via dask, the best approach is probably to use dask to concurrently write data to local storage, and then zip it in a separate step.
I'm not sure how we can guard against this corruption at runtime without implementing a distributed lock. Open to ideas here.
csubich commentedon Oct 10, 2025
If it were a concurrency bug, then I'd expect it to also happen with non-dask writes to the ZipStore. In the maximal code where I discovered the bug (by way of xarray), a large-ish but in-memory dataset would write successfully to a ZipStore if the chunking was left at default values (delegating it to zarr), but specifying manual chunks via xarray/dask caused this corruption.
Additionally, this did work correctly in earlier versions of zarr, so I think there's a regression somewhere. In a colab notebook with zarr 2.16.1:
d-v-b commentedon Oct 10, 2025
That's good to know — I hope we can restore this functionality. Now I have no intuition for where the data corruption could be coming from, so hopefully someone who does can figure this out!
csubich commentedon Oct 10, 2025
I was hoping that the parallelism hint would work out, but the current version's
ZipStore.set()takes a lock before calling_set()which performs the write, and 2.16.1'sZipStore.__setitem__does essentially the same thing:Unfortunately, I'm getting lost inside dask more deeply than I have time for right now. I can confirm that this still happens with the 'synchronous' scheduler that should eliminate parallelsim, and I can also track it down into how
dask.array.blockwisemapsload_store_chunkonto the zarr array. However, I cannot get the bug to trigger by callingload_store_chunkmyself (even viadask.delayed); easy replication seems to require calling.compute()on a generated (even if trivial) dask graph.Edit to add: I'm definitely flummoxed. Even if I modify ZipStore to use a monkeypatched ZipFile that logs-to-console every call to
_zf.writestr, I see an identical sequence of writes betweenda.to_zarr(which corrupts) and a direct slice assignment (which succeeds).fligt commentedon Dec 11, 2025
Under the hood the
dask.array.to_zarr()method in the latest stable dask version '2025.11.0' uses the deprecatedzarr.create()function.Yesterday @melonora merged a fix to the dask codebase which implements the new
zarr.create_array()function that supports zarr sharding: Support zarr sharding through create_array #12153My workaround for now is to add dask arrays to a ZipStore by using
zarr.create_array()in my code:melonora commentedon Dec 11, 2025
FYI, the PR got merged in time for the monthly dask release which would happen either end of this week or start of next week.
melonora commentedon Dec 11, 2025
also FYI #3612
AMBRA7592 commentedon Jul 20, 2026
I re-ran the original
da.to_zarr(dask_array, zarr_array)reproducer against current stable releases and the corruption still occurs.Environment:
After
store.close(),zipfile.ZipFile(path).testzip()reportszarr.jsonas the first bad member, and reopening throughzarr.open_group(...)raisesBadZipFile: Bad magic number for file header. The result is the same with Dask's default scheduler andscheduler="synchronous".So the later Dask
create_arraywork and the copy-store workaround do not appear to resolve this original write-to-an-existing-Zarr-array path. This issue remains reproducible on current releases.tests: read a zipped OME-Zarr through a ZipStore
tests: read a zipped OME-Zarr through a ZipStore
d-v-b commentedon Oct 27, 2026
this should have been closed by #4450, we can re-open if it's still a problem