Describe the bug
ldclient.set_config() holds the global write lock across both the construction of the replacement client and old_client.close(), so every concurrent ldclient.get() blocks for the entire duration.
ldclient/__init__.py:38-47:
with __lock.write():
try:
if __client:
new_client = LDClient(config=config, start_wait=start_wait) # blocks up to start_wait
old_client = __client
__client = new_client
old_client.close() # blocks on network shutdown
finally:
__config = config
ReadWriteLock.lock() (ldclient/impl/rwlock.py) acquires the underlying mutex and holds it for the whole write section, and rlock() must acquire that same mutex. So while set_config() runs, every ldclient.get() — which is on the hot path of every flag evaluation in an application using the singleton API — is blocked.
This couples a network-dependent shutdown to a lock that every evaluating thread needs. A slow close() stalls the whole application, not just the thread that called set_config().
Found while investigating #493, a production incident where close() hung and left worker pods alive for hours to days. This is what makes that class of hang so damaging under the singleton API: before a shutdown timeout existed, a stalled close() inside set_config() would block every get() in the process indefinitely. I have not observed this specific path in our own production — we do not call set_config() after startup — but the coupling is real and reproduces reliably.
Even with a bounded close(), the window is start_wait (default 5s) plus the shutdown timeout, so a reconfigure can stall all evaluating threads for ~10 seconds.
To reproduce
PR #499 adds a failing test (marked xfail(strict=True); run with --runxfail to see it fail). It drives a 3 second close() and measures a concurrent get():
AssertionError: ldclient.get() blocked for 2.8s while set_config() was closing the previous client
Expected behavior
Reconfiguring should not stall threads that are only reading the shared client. The write lock should cover the pointer swap, not the construction and teardown around it.
SDK version
9.16.1 / main at 5da1515
Language version, developer tools
Python 3.14
OS/platform
Linux
Additional context
I have not proposed a fix. Moving construction and close() outside the lock is the obvious direction, but doing it without introducing a lost-update race between concurrent set_config() callers is a design decision I would rather leave to you.
Describe the bug
ldclient.set_config()holds the global write lock across both the construction of the replacement client andold_client.close(), so every concurrentldclient.get()blocks for the entire duration.ldclient/__init__.py:38-47:ReadWriteLock.lock()(ldclient/impl/rwlock.py) acquires the underlying mutex and holds it for the whole write section, andrlock()must acquire that same mutex. So whileset_config()runs, everyldclient.get()— which is on the hot path of every flag evaluation in an application using the singleton API — is blocked.This couples a network-dependent shutdown to a lock that every evaluating thread needs. A slow
close()stalls the whole application, not just the thread that calledset_config().Found while investigating #493, a production incident where
close()hung and left worker pods alive for hours to days. This is what makes that class of hang so damaging under the singleton API: before a shutdown timeout existed, a stalledclose()insideset_config()would block everyget()in the process indefinitely. I have not observed this specific path in our own production — we do not callset_config()after startup — but the coupling is real and reproduces reliably.Even with a bounded
close(), the window isstart_wait(default 5s) plus the shutdown timeout, so a reconfigure can stall all evaluating threads for ~10 seconds.To reproduce
PR #499 adds a failing test (marked
xfail(strict=True); run with--runxfailto see it fail). It drives a 3 secondclose()and measures a concurrentget():Expected behavior
Reconfiguring should not stall threads that are only reading the shared client. The write lock should cover the pointer swap, not the construction and teardown around it.
SDK version
9.16.1 /
mainat 5da1515Language version, developer tools
Python 3.14
OS/platform
Linux
Additional context
I have not proposed a fix. Moving construction and
close()outside the lock is the obvious direction, but doing it without introducing a lost-update race between concurrentset_config()callers is a design decision I would rather leave to you.