Skip to content

fix: FusedCodecPipeline falls back to async path for sharded arrays with async-only inner codecs#237

Open
d-v-b wants to merge 5 commits into
mainfrom
fix/fused-pipeline-async-inner-codecs
Open

fix: FusedCodecPipeline falls back to async path for sharded arrays with async-only inner codecs#237
d-v-b wants to merge 5 commits into
mainfrom
fix/fused-pipeline-async-inner-codecs

Conversation

@d-v-b

@d-v-b d-v-b commented Jul 22, 2026

Copy link
Copy Markdown
Owner

🤖 AI text below 🤖

Problem

Under the opt-in FusedCodecPipeline (zarr.config.set({"codec_pipeline.path": "zarr.core.codec_pipeline.FusedCodecPipeline"})), a sharded array whose inner codec chain contains a third-party codec implementing only the async codec interface (_encode_single/_decode_single, no SupportsSyncCodec) raised on both write and read:

TypeError: All codecs must implement SupportsSyncCodec. The following do not: <name>

The pipeline's existing guard handles async-only codecs at the top level of the chain (evolve_from_array_spec catches the TypeError and falls back to the async path), but ShardingCodec structurally satisfies SupportsSyncCodec, so the pipeline committed to the sync fast path and crashed inside the sharding codec's inner ChunkTransform. The default BatchedCodecPipeline handles the same configuration correctly. Found in the 3.3.0 pre-release audit.

Fix

Sync capability is now a dynamic query, _codec_supports_sync (src/zarr/abc/codec.py): structural protocol membership plus an optional _sync_capable opt-out.

  • ShardingCodec._sync_capable reports False when its inner or index codec chain is not fully sync-capable (recursive, so nested sharding propagates).
  • ChunkTransform.__post_init__ consults the query, so construction raises for such chains and the pipeline's existing top-level guard sets sync_transform=None — declining the sync fast path and routing reads through the async partial shard decode (byte-range coalesced, matching BatchedCodecPipeline) and writes through the async fallback.

All-sync chains still build the sync transform and keep the fast path, byte-identical. BatchedCodecPipeline behavior is unchanged. No towncrier fragment: FusedCodecPipeline is new in unreleased 3.3.0.

Tests

  • New regression test: sharded array with an async-only inner codec round-trips (full write, partial write, full read, partial read) under FusedCodecPipeline, asserts the codec actually ran (invocation counters), and re-reads the store under the default pipeline to prove the bytes are valid cross-pipeline.
  • tests/test_fused_pipeline.py tests/test_pipeline_parity.py tests/test_fastpath_equivalence.py tests/test_codecs/test_sharding.py tests/test_codec_pipeline.py: 504 passed, 6 skipped (pre-existing skips).
  • Instrumented check: async-only inner chain → partial reads go through ShardingCodec._decode_partial_single (no whole-shard over-read); all-sync chain → read_sync fast path still fires.
  • mypy and ruff clean on all touched files.

🤖 Generated with Claude Code

d-v-b added 2 commits July 20, 2026 16:39
…arr-developers#4165)

* fix: byte-order handling for structured dtypes in the bytes codec (#220)

* fix: byte-order handling for structured dtypes in the bytes codec

The bytes codec neither byte-swapped structured-dtype fields to its
configured endian on encode (numpy reports byteorder '|' for void
dtypes, so the top-level byteorder comparison never detected a
mismatch) nor honored its endian when decoding, silently corrupting
any structured data whose field byte order differed from the stored
one (e.g. virtual references to external big-endian data).

Encode now detects byte-order mismatches by comparing full dtypes via
newbyteorder, and decode reinterprets raw bytes in the stored byte
order before converting to the data type's declared byte order, so the
stored layout (codec state) and the in-memory layout (array data type)
are independent.

Closes zarr-developers#4141

Assisted-by: ClaudeCode:claude-fable-5

* test: fold structured byte-order cases into existing bytes codec tests

Extend test_endian's parametrization with structured dtypes and
test_bytes_codec_sync_roundtrip with endian/dtype parametrization plus
stored-layout and decoded-dtype assertions, instead of adding parallel
test functions for the same properties.

Assisted-by: ClaudeCode:claude-fable-5

* refactor: rename stored_dtype to view_dtype in BytesCodec decode

The variable is the dtype used to view the raw chunk bytes (byte order
from the codec's endian configuration), not a property of the stored
data or of the returned buffer, which always carries the array's
declared dtype.

Assisted-by: ClaudeCode:claude-fable-5

* docs: note that the decode-side byte-order conversion copies the chunk

Assisted-by: ClaudeCode:claude-fable-5

* fix(store): FsspecStore.close() no longer closes the filesystem

FsspecStore.close() closed the underlying filesystem's session, on the
premise that a store built by from_url "owns" the filesystem it created.
That premise does not hold: fsspec caches and shares filesystem
instances across callers (its instance cache keys on storage options,
not path), and users can hand one filesystem to many stores directly.
Closing one store therefore killed the session that sibling stores were
still using, and left the dead filesystem in fsspec's cache for later
callers.

Determining whether a filesystem is actually shared requires reaching
into fsspec's private instance cache (_cache, _fs_token, cachable) and
walking wrapper chains for caching/proxy filesystems — an implementation
detail that leaks upward and that we would have to keep in sync with
fsspec forever, getting it subtly wrong in between. The wrapper case
alone (simplecache::/dir://) already slipped through a cache-membership
check.

The filesystem's lifecycle is simply not the store's to manage. This
removes the ownership model added in the unreleased zarr-developersgh-4003: no _owns_fs,
no _close_fs, no ownership transfer in with_read_only, and close() just
marks the store not-open. The only thing given up is suppressing an
"Unclosed client session" ResourceWarning, which was true anyway — the
session belongs to a cached filesystem that outlives the store.

Since zarr-developersgh-4003 never shipped (latest release is v3.2.1), its changelog
fragment is removed rather than superseded.

Assisted-by: ClaudeCode:claude-opus-4.8

* test: skip with_read_only fs test when AsyncFileSystemWrapper is absent

test_with_read_only_shares_filesystem replaced an ownership test that
carried a guard for fsspec < 2024.12.0, and the guard was dropped in the
rewrite. The test still opens a file:// URL, which needs
AsyncFileSystemWrapper, so it failed the min_deps job.

Assisted-by: ClaudeCode:claude-opus-4.8

* docs: correct changelog claim about zarr-developersgh-4003 release status

The fragment said zarr-developersgh-4003 was unreleased with no net change for
released versions. Its text is already in the staged 3.3.0 release
notes, so the revert is a real behavior change for anyone relying on
close() releasing the session.

Assisted-by: ClaudeCode:claude-opus-4.8

* docs: remove changelog entry for unreleased versions
…async-only inner codecs

Under the opt-in FusedCodecPipeline, a sharded array whose inner codec
chain contained a codec implementing only the async interface (no
SupportsSyncCodec) raised TypeError on both read and write:
ShardingCodec structurally satisfies SupportsSyncCodec, so the pipeline
built a top-level sync transform and took the sync fast path, which dove
into the sharding codec's sync shard paths and crashed constructing the
inner ChunkTransform. The default BatchedCodecPipeline handled the same
configuration fine.

Fix: sync capability is now a dynamic query (_codec_supports_sync) —
structural SupportsSyncCodec membership plus an optional _sync_capable
opt-out. ShardingCodec reports _sync_capable=False when its inner or
index codec chain is not fully sync-capable (recursively, so nested
sharding propagates). ChunkTransform consults the query, so its
construction raises for such chains and the pipeline's existing
top-level guard (evolve_from_array_spec -> sync_transform=None) now
declines the sync fast path, routing reads through the async partial
shard decode and writes through the async fallback — the same graceful
degradation already used for async-only top-level codecs and non-sync
stores. All-sync chains still build the sync transform and keep the
fast path.

Assisted-by: ClaudeCode:claude-fable-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant