π€ from Claude
Summary
The sharded ragged (CSR) write path generates far more S3 requests than its payload requires, because zarr-python's array-creation API re-probes and re-ensures the entire ancestor hierarchy on every open_array(mode="w") call. This was the request-rate fuel behind the 503 SlowDown failures root-caused on #186. PR #188 fixed the failure mode itself (paced retry backoff β decided as the (b) option on PR #188: retry-only, raise the op storm upstream). This issue tracks the op storm and the upstream ask.
Measurements (zarr 3.2.1, op-counting spy store)
One CSR subgroup write (write_csr: 3 arrays β values/offsets/cell_ids) is 33 store ops:
| op |
count |
notes |
| GET |
15 |
5 per open_array β target existence + a probe of every ancestor (zarr.json, {group}/zarr.json, {group}/{field}/zarr.json, {group}/{field}/{key}/zarr.json) |
conditional PUT (set_if_not_exists) |
12 |
4 per open_array β implicit parent group docs re-ensured per array, not per subgroup |
| PUT |
5 |
3 array metadata docs + 2 chunk objects |
| DELETE |
1 |
write_empty_chunks=False: an all-fill chunk (e.g. cell_ids == [0]) deletes instead of writing |
18 of the 33 ops hit the same three shared hot keys (zarr.json, {group}/zarr.json, {group}/{field}/zarr.json). The o9 NEON shard that failed 3/3 on #186 has 256 subgroups: ~8,400 ops per shard, ~4,600 of them on those three keys β per worker, and the #186 reproduction ran multiple concurrent arms. A 4th array (locations, issue #87 located fields) raises all per-array counts proportionally.
What doesn't work on zarr's public API (both measured)
- Caching an opened parent group and creating arrays through the handle (
group.create_array(...)): identical per-create ancestor probing (same 4 GETs/array), and open_group(mode="a") additionally PUTs a group doc. There is nothing to cache β the probes are inside the per-create path.
zarr.create_hierarchy batches metadata properly (one probe pass + concurrent PUTs), but it writes explicit group docs for all implicit parents including the root β with overwrite=True it would clobber the template's root metadata written by emit_template. Layout-changing, not a drop-in.
What does work (prototyped, not landed β decided against for now)
Building ArrayV3Metadata locally from an in-memory template and PUTting metadata + chunk directly: 2 PUTs per array, zero GETs, byte-identical array docs and chunk objects. But it requires zarr.core internals (AsyncArray-from-metadata, StorePath, zarr.core.sync.sync) with no stability guarantee, skips zarr's ancestor-conflict checks (they become an unchecked invariant of zagg's layout), and takes ownership of subtle semantics (write_empty_chunks DELETE behavior, parent-doc creation). Per the decision on PR #188, zagg stays on the public API unless fleet evidence forces the escalation.
The upstream ask (zarr-python)
A public way to create many arrays without per-create ancestor traffic. Any of:
- an
ensure_parents=False / check_ancestors=False flag on create_array (caller asserts the hierarchy exists β zagg writes into a pre-templated store);
- a
create_hierarchy mode that creates only the named nodes, leaving implicit parents implicit (no root/parent group-doc writes);
- honoring an already-open parent
Group handle as the hierarchy authority, so group.create_array skips re-probing the ancestors the handle already proved.
Filing this needs a maintainer call on framing (feature request vs. perf issue) and is a human-owned outbound step β parking the drafted context here so it can be lifted into a zarr-python issue verbatim.
Escalation trigger
If fleet runs still hit SlowDown exhaustion with PR #188's paced retries in place (the failure is loud: ragged (CSR) write failed ... 503 SlowDown after ~180 s of paced retries), revisit the internals-based writer (option (a) on PR #188) β the prototype and its byte-identity test approach are described there.
Refs #186 (root cause), PR #188 (retry fix + decision), #147 (parallel CSR writes), #108/#133 (ShardingCodec / sharded layout), #87 (located fields' 4th array).
π€ from Claude
Summary
The sharded ragged (CSR) write path generates far more S3 requests than its payload requires, because zarr-python's array-creation API re-probes and re-ensures the entire ancestor hierarchy on every
open_array(mode="w")call. This was the request-rate fuel behind the 503 SlowDown failures root-caused on #186. PR #188 fixed the failure mode itself (paced retry backoff β decided as the (b) option on PR #188: retry-only, raise the op storm upstream). This issue tracks the op storm and the upstream ask.Measurements (zarr 3.2.1, op-counting spy store)
One CSR subgroup write (
write_csr: 3 arrays βvalues/offsets/cell_ids) is 33 store ops:open_arrayβ target existence + a probe of every ancestor (zarr.json,{group}/zarr.json,{group}/{field}/zarr.json,{group}/{field}/{key}/zarr.json)set_if_not_exists)open_arrayβ implicit parent group docs re-ensured per array, not per subgroupwrite_empty_chunks=False: an all-fill chunk (e.g.cell_ids == [0]) deletes instead of writing18 of the 33 ops hit the same three shared hot keys (
zarr.json,{group}/zarr.json,{group}/{field}/zarr.json). The o9 NEON shard that failed 3/3 on #186 has 256 subgroups: ~8,400 ops per shard, ~4,600 of them on those three keys β per worker, and the #186 reproduction ran multiple concurrent arms. A 4th array (locations, issue #87 located fields) raises all per-array counts proportionally.What doesn't work on zarr's public API (both measured)
group.create_array(...)): identical per-create ancestor probing (same 4 GETs/array), andopen_group(mode="a")additionally PUTs a group doc. There is nothing to cache β the probes are inside the per-create path.zarr.create_hierarchybatches metadata properly (one probe pass + concurrent PUTs), but it writes explicit group docs for all implicit parents including the root β withoverwrite=Trueit would clobber the template's root metadata written byemit_template. Layout-changing, not a drop-in.What does work (prototyped, not landed β decided against for now)
Building
ArrayV3Metadatalocally from an in-memory template and PUTting metadata + chunk directly: 2 PUTs per array, zero GETs, byte-identical array docs and chunk objects. But it requireszarr.coreinternals (AsyncArray-from-metadata,StorePath,zarr.core.sync.sync) with no stability guarantee, skips zarr's ancestor-conflict checks (they become an unchecked invariant of zagg's layout), and takes ownership of subtle semantics (write_empty_chunksDELETE behavior, parent-doc creation). Per the decision on PR #188, zagg stays on the public API unless fleet evidence forces the escalation.The upstream ask (zarr-python)
A public way to create many arrays without per-create ancestor traffic. Any of:
ensure_parents=False/check_ancestors=Falseflag oncreate_array(caller asserts the hierarchy exists β zagg writes into a pre-templated store);create_hierarchymode that creates only the named nodes, leaving implicit parents implicit (no root/parent group-doc writes);Grouphandle as the hierarchy authority, sogroup.create_arrayskips re-probing the ancestors the handle already proved.Filing this needs a maintainer call on framing (feature request vs. perf issue) and is a human-owned outbound step β parking the drafted context here so it can be lifted into a zarr-python issue verbatim.
Escalation trigger
If fleet runs still hit
SlowDownexhaustion with PR #188's paced retries in place (the failure is loud:ragged (CSR) write failed ... 503 SlowDownafter ~180 s of paced retries), revisit the internals-based writer (option (a) on PR #188) β the prototype and its byte-identity test approach are described there.Refs #186 (root cause), PR #188 (retry fix + decision), #147 (parallel CSR writes), #108/#133 (ShardingCodec / sharded layout), #87 (located fields' 4th array).