@@ -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
174335def cleanup_lock_files (root_path ):
175336 """Remove lock files associated with a path or an entire tree.
0 commit comments