@@ -819,8 +819,7 @@ async def open_store_async(
819819 """
820820 loop = asyncio .get_running_loop ()
821821
822- # _get_open_params calls zarr.open_group() → sync(). Safe on
823- # executor thread: IO loop is free (awaiting run_in_executor).
822+ # Run on executor to avoid reentrant sync() deadlock
824823 (
825824 zarr_group ,
826825 consolidate_on_close ,
@@ -843,12 +842,9 @@ async def open_store_async(
843842 ),
844843 )
845844
846- # Already async — no change needed
847845 group_paths = await _iter_zarr_groups_async (zarr_group , parent = group )
848846
849- # _build_group_members calls zarr_group[rel_path] → sync().
850- # _create_stores_from_members with cache_members calls _fetch_members → sync().
851- # Both safe on executor thread for the same reason.
847+ # Run on executor to avoid reentrant sync() deadlock
852848 def _build_and_create ():
853849 group_members = _build_group_members (zarr_group , group_paths , group )
854850 return cls ._create_stores_from_members (
@@ -1915,10 +1911,7 @@ def open_datatree(
19151911 if _zarr_v3 ():
19161912 from zarr .core .sync import sync as zarr_sync
19171913
1918- # Decompose open_store() to use async group discovery.
1919- # Each step must be a separate top-level call — we cannot nest
1920- # zarr_sync() calls (e.g., calling _get_open_params from within
1921- # an async function dispatched via zarr_sync would deadlock).
1914+ # Sync call — safe outside zarr's IO loop
19221915 (
19231916 zarr_group ,
19241917 consolidate_on_close ,
@@ -1938,7 +1931,6 @@ def open_datatree(
19381931 zarr_format = zarr_format ,
19391932 )
19401933
1941- # Async parallel group discovery (25x faster than sync recursive)
19421934 group_paths = zarr_sync (_iter_zarr_groups_async (zarr_group , parent = parent ))
19431935
19441936 return zarr_sync (
@@ -2016,21 +2008,7 @@ async def _open_datatree_from_stores_async(
20162008 decode_timedelta = None ,
20172009 max_concurrency : int | None = None ,
20182010 ) -> DataTree :
2019- """Async helper to open datatree groups concurrently.
2020-
2021- Builds group members and creates ZarrStore instances on executor threads
2022- where zarr_sync() calls are safe (the IO loop is free, awaiting
2023- run_in_executor). This avoids the deadlock that occurs when zarr_sync()
2024- is called from the main thread while the IO loop is already running.
2025-
2026- Uses a dedicated ThreadPoolExecutor instead of the event loop's default
2027- executor to prevent thread pool exhaustion deadlocks. The deadlock occurs
2028- because open_dataset() triggers eager data reads (e.g., for time decoding)
2029- which call zarr_sync() — blocking the thread while waiting for the zarr IO
2030- loop. The IO loop in turn needs the default executor for codec decompression.
2031- If all default executor threads are blocked waiting on the IO loop, neither
2032- can proceed. A separate pool breaks this circular dependency.
2033- """
2011+ """Open datatree groups concurrently using a dedicated executor."""
20342012 from xarray .backends .api import _maybe_create_default_indexes_async
20352013
20362014 if max_concurrency is None :
0 commit comments