Skip to content

Failed LDClient construction leaks the event processor's threads and HTTP pool #497

Description

@aviadr1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions