Skip to content

Commit 44709bd

Browse files
ugly solution....
1 parent 7cb1566 commit 44709bd

File tree

1 file changed

+168
-7
lines changed

1 file changed

+168
-7
lines changed

mne_bids/_fileio.py

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,25 +151,186 @@ def _open_lock(path, *args, lock_timeout=None, **kwargs):
151151
yield None
152152
return
153153

154-
with _get_lock_context(
155-
canonical_path,
156-
timeout=lock_timeout,
157-
) as lock_context:
154+
# Increment multiprocess refcount before acquiring lock
155+
_increment_lock_refcount(canonical_path)
156+
157+
lock_path = canonical_path.with_name(f"{canonical_path.name}.lock")
158+
attempts = 0
159+
while True:
158160
try:
159-
with lock_context:
161+
with _get_lock_context(
162+
canonical_path,
163+
timeout=lock_timeout,
164+
) as lock_context:
165+
with lock_context:
166+
if args or kwargs:
167+
with open(canonical_path, *args, **kwargs) as fid:
168+
yield fid
169+
else:
170+
yield None
171+
break
172+
except FileNotFoundError:
173+
attempts += 1
174+
Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
175+
if attempts > 3:
176+
warn("Could not create lock. Proceeding without a lock.")
160177
if args or kwargs:
161178
with open(canonical_path, *args, **kwargs) as fid:
162179
yield fid
163180
else:
164181
yield None
165-
finally:
166-
cleanup_lock_files(canonical_path)
182+
break
167183
finally:
168184
with _ACTIVE_LOCKS_GUARD:
169185
_ACTIVE_LOCKS[lock_key] -= 1
170186
if _ACTIVE_LOCKS[lock_key] == 0:
171187
del _ACTIVE_LOCKS[lock_key]
172188

189+
# Clean up lock files safely using reference counting across processes.
190+
# Only clean up when this was the outermost lock (not re-entrant) and
191+
# when the lock depth was 0 (meaning we actually acquired the lock).
192+
if not is_reentrant and lock_depth == 0:
193+
_decrement_and_cleanup_lock_file(canonical_path)
194+
195+
196+
def _increment_lock_refcount(file_path):
197+
"""Increment the multiprocess reference count for a lock file.
198+
199+
Parameters
200+
----------
201+
file_path : Path
202+
The original file path (not the lock file path).
203+
"""
204+
file_path = Path(file_path)
205+
refcount_file = file_path.parent / f"{file_path.name}.lock.refcount"
206+
207+
try:
208+
from filelock import FileLock
209+
210+
# Use the refcount file as a lock for atomic refcount operations
211+
refcount_lock = FileLock(f"{refcount_file}.lock", timeout=5.0)
212+
213+
try:
214+
with refcount_lock:
215+
# Read current refcount
216+
try:
217+
if refcount_file.exists():
218+
count = int(refcount_file.read_text().strip())
219+
else:
220+
count = 0
221+
except (ValueError, OSError):
222+
count = 0
223+
224+
# Increment refcount
225+
count += 1
226+
227+
# Write back incremented count
228+
try:
229+
refcount_file.write_text(str(count))
230+
except OSError:
231+
pass
232+
except TimeoutError:
233+
# Another process is updating refcount, that's OK
234+
pass
235+
finally:
236+
# Clean up the refcount lock file
237+
try:
238+
refcount_lock_file = Path(f"{refcount_file}.lock")
239+
if refcount_lock_file.exists():
240+
refcount_lock_file.unlink()
241+
except OSError:
242+
pass
243+
244+
except ImportError:
245+
# filelock not available, skip refcounting
246+
pass
247+
except Exception:
248+
# Any other error, skip refcounting
249+
pass
250+
251+
252+
def _decrement_and_cleanup_lock_file(file_path):
253+
"""Safely remove a lock file using multiprocess reference counting.
254+
255+
Maintains a reference count in a .refcount file to track how many processes
256+
are currently using or waiting for the lock. Only deletes the lock file when
257+
the reference count reaches zero.
258+
259+
Parameters
260+
----------
261+
file_path : Path
262+
The original file path (not the lock file path).
263+
"""
264+
file_path = Path(file_path)
265+
lock_file = file_path.parent / f"{file_path.name}.lock"
266+
refcount_file = file_path.parent / f"{file_path.name}.lock.refcount"
267+
268+
# Don't try to cleanup if the lock file doesn't exist
269+
if not lock_file.exists():
270+
# Clean up refcount file if it exists but lock doesn't
271+
try:
272+
if refcount_file.exists():
273+
refcount_file.unlink()
274+
except OSError:
275+
pass
276+
return
277+
278+
try:
279+
from filelock import FileLock
280+
281+
# Use the refcount file as a lock for atomic refcount operations
282+
refcount_lock = FileLock(f"{refcount_file}.lock", timeout=5.0)
283+
284+
try:
285+
with refcount_lock:
286+
# Read current refcount
287+
try:
288+
if refcount_file.exists():
289+
count = int(refcount_file.read_text().strip())
290+
else:
291+
count = 0
292+
except (ValueError, OSError):
293+
count = 0
294+
295+
# Decrement refcount
296+
count = max(0, count - 1)
297+
298+
if count == 0:
299+
# No more processes using this lock, safe to delete
300+
try:
301+
lock_file.unlink()
302+
except OSError:
303+
pass
304+
try:
305+
if refcount_file.exists():
306+
refcount_file.unlink()
307+
except OSError:
308+
pass
309+
else:
310+
# Write back decremented count
311+
try:
312+
refcount_file.write_text(str(count))
313+
except OSError:
314+
pass
315+
except TimeoutError:
316+
# Another process is updating refcount, skip cleanup
317+
pass
318+
finally:
319+
# Clean up the refcount lock file
320+
try:
321+
refcount_lock_file = Path(f"{refcount_file}.lock")
322+
if refcount_lock_file.exists():
323+
refcount_lock_file.unlink()
324+
except OSError:
325+
pass
326+
327+
except ImportError:
328+
# filelock not available, skip cleanup to be safe
329+
pass
330+
except Exception:
331+
# Any other error, skip cleanup
332+
pass
333+
173334

174335
def cleanup_lock_files(root_path):
175336
"""Remove lock files associated with a path or an entire tree.

0 commit comments

Comments
 (0)