3131from dataclasses import dataclass , field
3232from datetime import datetime
3333from functools import cache
34+ import gc
3435import logging
3536import os
3637from pathlib import Path
3738import subprocess
39+ import threading
3840from typing import IO , Any , cast
3941
4042from ..misctypes import Readable
@@ -76,6 +78,62 @@ def annex_initialized(directory: Path) -> bool:
7678 return r .returncode == 0 and bool (r .stdout .strip ())
7779
7880
81+ _gc_lock = threading .Lock ()
82+ _gc_pauses = 0
83+ _gc_was_enabled = False
84+
85+
86+ class _GCPausedFile :
87+ """
88+ A file object streamed by datalad-fuse, during whose lifetime automatic
89+ garbage collection is disabled
90+
91+ h5py holds a global lock while it reads from a Python file object, and the
92+ read waits for another thread (fsspec's I/O thread, or an HTTP server in
93+ the same process) to fetch the data. If a garbage collection ran in that
94+ thread meanwhile, finalizers of h5py objects (e.g., of an unreferenced
95+ ``NWBHDF5IO``) would wait for the same lock, deadlocking the process.
96+ """
97+
98+ def __init__ (self , fp : Any ) -> None :
99+ global _gc_pauses , _gc_was_enabled
100+ with _gc_lock :
101+ if _gc_pauses == 0 :
102+ # Collect pending garbage now, in this thread, which does not
103+ # hold h5py's lock yet
104+ gc .collect ()
105+ _gc_was_enabled = gc .isenabled ()
106+ gc .disable ()
107+ _gc_pauses += 1
108+ self ._fp = fp
109+ self ._paused = True
110+
111+ def __getattr__ (self , name : str ) -> Any :
112+ return getattr (self ._fp , name )
113+
114+ def __enter__ (self ) -> _GCPausedFile :
115+ return self
116+
117+ def __exit__ (self , * _exc : Any ) -> None :
118+ self .close ()
119+
120+ def __del__ (self ) -> None :
121+ if self ._paused :
122+ self .close ()
123+
124+ def close (self ) -> None :
125+ global _gc_pauses
126+ try :
127+ self ._fp .close ()
128+ finally :
129+ with _gc_lock :
130+ if self ._paused :
131+ self ._paused = False
132+ _gc_pauses -= 1
133+ if _gc_pauses == 0 and _gc_was_enabled :
134+ gc .enable ()
135+
136+
79137@dataclass
80138class AnnexedReadableFile (Readable ):
81139 """
@@ -95,7 +153,7 @@ class AnnexedReadableFile(Readable):
95153 adapter : Any = field (repr = False , compare = False )
96154
97155 def open (self ) -> IO [bytes ]:
98- return cast ("IO[bytes]" , self .adapter .open (self .filepath , "rb" ))
156+ return cast ("IO[bytes]" , _GCPausedFile ( self .adapter .open (self .filepath , "rb" ) ))
99157
100158 def get_size (self ) -> int :
101159 return self .size
0 commit comments