Skip to content

Sharded CSR writes issue a per-array metadata-op storm β€” upstream ask: a no-probe / batch array-creation path in zarr-pythonΒ #189

Description

@espg

πŸ€– 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)

  1. 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.
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions