Skip to content

Commit 82756a8

Browse files
authored
Use async tasks instead of blocking sync for store operations (#338)
The widget message handler was using zarr's `_sync()` to block the Jupyter kernel event loop while waiting on store reads. This freezes the kernel during slow operations like remote store access. Since Jupyter already runs an asyncio event loop and zarr v3 stores expose a native async API, we can schedule store operations as async tasks directly via `asyncio.create_task()` rather than blocking. A `_pending_tasks` set prevents in-flight tasks from being garbage collected, following the same pattern used by lonboard.
1 parent 91fef61 commit 82756a8

1 file changed

Lines changed: 9 additions & 3 deletions

File tree

python/src/vizarr/_widget.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Vizarr: an anywidget for viewing Zarr-based images."""
22

3+
import asyncio
34
import pathlib
45
from typing import Literal
56

@@ -11,7 +12,6 @@
1112
import zarr.storage
1213
from zarr.abc.store import Store
1314
from zarr.core.buffer import default_buffer_prototype
14-
from zarr.core.sync import sync as _sync
1515

1616
__all__ = ["Viewer"]
1717

@@ -86,6 +86,7 @@ class Viewer(anywidget.AnyWidget):
8686
def __init__(self, **kwargs: object) -> None:
8787
super().__init__(**kwargs)
8888
self._store_paths: list[tuple[Store, str]] = []
89+
self._pending_tasks: set[asyncio.Task[None]] = set()
8990
self.on_msg(self._handle_custom_message)
9091

9192
def _handle_custom_message(
@@ -94,19 +95,24 @@ def _handle_custom_message(
9495
msg: object,
9596
_buffers: list[object],
9697
) -> None:
98+
task = asyncio.create_task(self._handle_store_request(msg))
99+
self._pending_tasks.add(task)
100+
task.add_done_callback(self._pending_tasks.discard)
101+
102+
async def _handle_store_request(self, msg: object) -> None:
97103
message = msgspec.convert(msg, type=Message[StoreOperation])
98104
store_id, path = message.payload.target
99105
store, key_prefix = self._store_paths[store_id]
100106
key = key_prefix + path.lstrip("/")
101107

102108
if message.payload.method == "has":
103-
success = _sync(store.exists(key))
109+
success = await store.exists(key)
104110
reply = Message(message.uuid, StoreResult(success))
105111
self.send(msgspec.to_builtins(reply))
106112
return
107113

108114
if message.payload.method == "get":
109-
buf = _sync(store.get(key, prototype=default_buffer_prototype()))
115+
buf = await store.get(key, prototype=default_buffer_prototype())
110116
if buf is not None:
111117
reply = Message(message.uuid, StoreResult(success=True))
112118
self.send(msgspec.to_builtins(reply), [buf.to_bytes()])

0 commit comments

Comments
 (0)