Skip to content

Commit 7f4df63

Browse files
authored
Merge pull request #11 from cellgeni/fix/zarr-attr-write-batching
Batch Zarr attribute writes: fixes the split hang in CI
2 parents 0c69116 + 7183235 commit 7f4df63

2 files changed

Lines changed: 42 additions & 5 deletions

File tree

‎src/adata/elements/spec.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from __future__ import annotations
1212

13-
from typing import Any, Optional, Tuple
13+
from typing import Any, Dict, Optional, Tuple
1414

1515
ANNDATA = "anndata"
1616
RAW = "raw"
@@ -95,6 +95,25 @@ def encoding_type(obj: Any) -> Optional[str]:
9595
return encoding_of(obj)[0]
9696

9797

98+
def set_attrs(obj: Any, values: Dict[str, Any]) -> None:
99+
"""Set several attributes in as few writes as the backend allows.
100+
101+
On Zarr each assignment persists the whole metadata document through its
102+
sync-over-async bridge, so setting attributes one at a time costs a round
103+
trip each. HDF5 has no such batch API and falls back to assignment.
104+
"""
105+
if not values:
106+
return
107+
108+
put = getattr(obj.attrs, "put", None)
109+
if callable(put):
110+
put({**dict(obj.attrs), **values})
111+
return
112+
113+
for key, value in values.items():
114+
obj.attrs[key] = value
115+
116+
98117
def set_encoding(obj: Any, enc_type: str, version: Optional[str] = None) -> None:
99118
"""Stamp ``encoding-type``/``encoding-version`` on a group or dataset.
100119
@@ -103,5 +122,4 @@ def set_encoding(obj: Any, enc_type: str, version: Optional[str] = None) -> None
103122
"""
104123
if version is None:
105124
version = CURRENT_VERSION[enc_type]
106-
obj.attrs["encoding-type"] = enc_type
107-
obj.attrs["encoding-version"] = version
125+
set_attrs(obj, {"encoding-type": enc_type, "encoding-version": version})

‎src/adata/storage/__init__.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,27 @@ def _normalize_attr_value(value: Any, target_backend: str) -> Any:
229229

230230

231231
def copy_attrs(src_attrs: Any, dst_attrs: Any, *, target_backend: str) -> None:
232-
for k, v in src_attrs.items():
233-
dst_attrs[k] = _normalize_attr_value(v, target_backend)
232+
"""Copy attributes across, normalising values for the target backend.
233+
234+
Written in one go on Zarr. Each `attrs[k] = v` there persists the whole
235+
metadata document through zarr's sync-over-async bridge, so setting them
236+
one at a time means a separate round trip per attribute -- slow, and the
237+
place a `split` over many groups was observed to wedge in CI.
238+
"""
239+
normalized = {
240+
str(k): _normalize_attr_value(v, target_backend)
241+
for k, v in src_attrs.items()
242+
}
243+
if not normalized:
244+
return
245+
246+
put = getattr(dst_attrs, "put", None)
247+
if target_backend == "zarr" and callable(put):
248+
put({**dict(dst_attrs), **normalized})
249+
return
250+
251+
for k, v in normalized.items():
252+
dst_attrs[k] = v
234253

235254

236255
def dataset_create_kwargs(

0 commit comments

Comments
 (0)