- ConfigStore.read takes the commit lock, so no read handle is open while config.json is replaced (Windows refuses that replace). - os.replace is retried briefly on a Windows PermissionError (antivirus or indexer handles) before the save is reported as not written. - The temp file is written beside realpath(config.json), so a symlinked config keeps its link and a bind mount fails truthfully. - commit() takes a publish callback that runs under the lock after the write; settings and LoRA saves publish there, so two concurrent saves reach the runtime in write order and the runtime ends on the last one. - The concurrency tests now wait until the second caller is blocked on the lock (tests/lock_probe.py) before releasing the first.
36 lines
944 B
Python
36 lines
944 B
Python
"""A lock stand-in that reports when a second caller has to wait for it.
|
|
|
|
Concurrency tests patch it over a module's lock so they can wait until the
|
|
second caller is really blocked on the lock, instead of hoping a thread
|
|
start or one event-loop tick got it there.
|
|
"""
|
|
|
|
import threading
|
|
|
|
|
|
class ProbeLock:
|
|
"""A re-entrant lock whose ``contended`` event is set when a caller blocks."""
|
|
|
|
def __init__(self):
|
|
self._lock = threading.RLock()
|
|
self.contended = threading.Event()
|
|
|
|
def acquire(self, blocking=True, timeout=-1):
|
|
if self._lock.acquire(blocking=False):
|
|
return True
|
|
self.contended.set()
|
|
if not blocking:
|
|
return False
|
|
return self._lock.acquire(timeout=timeout)
|
|
|
|
def release(self):
|
|
self._lock.release()
|
|
|
|
def __enter__(self):
|
|
self.acquire()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.release()
|
|
return False
|