Describe the bug
If LDClient construction fails partway through, everything already started keeps running, and the caller has no way to release it.
__start_up() (ldclient/client.py) creates the event processor at line 320 — starting a dispatcher thread, a pool of five flush workers, two repeating timer threads, and an HTTP connection pool — and only then calls __register_plugins() and self._data_system.start(). If the data system fails to start, the exception propagates out of the constructor, the caller never receives a client object, and there is no handle on which to call close().
Found while auditing the shutdown path after #493, a production incident where close() hung and left worker pods alive for hours to days. I have not observed this particular leak in our own production, but it reproduces reliably.
The threads are daemons, so this does not prevent process exit. It does leak steadily in any application that retries client construction, and postfork() re-runs this same path.
To reproduce
PR #500 adds a failing test (marked xfail(strict=True); run with --runxfail to see it fail). It configures an update_processor_class that raises in start() — a documented configuration hook — and reports:
AssertionError: construction failed but left these threads running:
['ldclient.events.context-flush.repeating', 'ldclient.events.flush.repeating',
'ldclient.events.processor', 'ldclient.flush.1', 'ldclient.flush.2',
'ldclient.flush.3', 'ldclient.flush.4', 'ldclient.flush.5']
Expected behavior
A constructor that raises should leave nothing running.
SDK version
9.16.1 / main at 5da1515
Language version, developer tools
Python 3.14
OS/platform
Linux
Additional context
Related, and not covered by the test: postfork() calls __start_up() again on an existing client, rebinding _event_processor, _data_system and __big_segment_store_manager to fresh objects without stopping the previous ones. In the child the old threads do not exist, but the old urllib3 pools' inherited socket file descriptors are never released. I read this rather than tested it, so please treat it as worth a look rather than an established defect.
No fix proposed — where the teardown belongs (a try/except in __start_up, or restructuring so the event processor starts last) is a design call.
Describe the bug
If
LDClientconstruction fails partway through, everything already started keeps running, and the caller has no way to release it.__start_up()(ldclient/client.py) creates the event processor at line 320 — starting a dispatcher thread, a pool of five flush workers, two repeating timer threads, and an HTTP connection pool — and only then calls__register_plugins()andself._data_system.start(). If the data system fails to start, the exception propagates out of the constructor, the caller never receives a client object, and there is no handle on which to callclose().Found while auditing the shutdown path after #493, a production incident where
close()hung and left worker pods alive for hours to days. I have not observed this particular leak in our own production, but it reproduces reliably.The threads are daemons, so this does not prevent process exit. It does leak steadily in any application that retries client construction, and
postfork()re-runs this same path.To reproduce
PR #500 adds a failing test (marked
xfail(strict=True); run with--runxfailto see it fail). It configures anupdate_processor_classthat raises instart()— a documented configuration hook — and reports:Expected behavior
A constructor that raises should leave nothing running.
SDK version
9.16.1 /
mainat 5da1515Language version, developer tools
Python 3.14
OS/platform
Linux
Additional context
Related, and not covered by the test:
postfork()calls__start_up()again on an existing client, rebinding_event_processor,_data_systemand__big_segment_store_managerto fresh objects without stopping the previous ones. In the child the old threads do not exist, but the old urllib3 pools' inherited socket file descriptors are never released. I read this rather than tested it, so please treat it as worth a look rather than an established defect.No fix proposed — where the teardown belongs (a try/except in
__start_up, or restructuring so the event processor starts last) is a design call.