Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FIX Callback transport don't store connections forever - #34870

Open
jeremiedbb wants to merge 4 commits into
scikit-learn:mainfrom
jeremiedbb:fix-callback-transport-fd-2
Open

FIX Callback transport don't store connections forever#34870
jeremiedbb wants to merge 4 commits into
scikit-learn:mainfrom
jeremiedbb:fix-callback-transport-fd-2

Conversation

@jeremiedbb

@jeremiedbb jeremiedbb commented Sep 2, 2026

Copy link
Copy Markdown
Member

Fixes #34734

This PR fixes the second (and main) source of leaking threads and file descriptors. It ensures that we don't keep open connections forever, even when they're not useful anymore.
(I'd rather have #34856 merged first to have easier merge conflicts but the 2 PRs fix 2 independent problems and can be reviewed independently)

Currently, we store every connection in a process-wide dict (_worker_connections) and never remove them. So they stay open until the worker is shut down (which can take a while since loky can reuse workers for subsequent tasks). Making it a lru cache mitigates the issue a bit, but not entirely since it still allows to grow the number of open connections far beyond what's actually needed.

This PR proposes to refactor the transport module a bit to add a new object (Channel) that abstracts both end of the communication channel (listener in the main process and clients in the workers). This way the connection made to send messages is tied to the channel and dies with it. The connection alone can also be closed to immediately free the resources instead of waiting for the callback to be collected (i.e. at teardown for scoring monitor).

Maybe it could have been fixed with a bit less refactoring but I actually find it cleaner and simpler. Now the only public object from transport is Channel (at least when we make transport public :) ). The callbacks create and store a channel and then call methods of the channel.

Comment on lines +128 to +131
# Nothing more will be sent for this fit. The channel itself outlives the fit,
# since this callback keeps logging across fits, but the connection a worker
# copy holds is released here rather than whenever that copy is collected.
self._channel.disconnect()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That part is new and useful to stop the leak because callbacks are held a reference cycles and thus are not collected immediately. Adding the fact that loky reuses worker makes the callbacks and thus the channels and thus the connections alive for a very long time, which gives time for the number of file descriptors to grow too much.
So ideally the better fix would be to avoid that reference cycle (it comes from the context tree holding references to both its parent and children), which I'm planning to investigate as a follow-up work. In the meantime the disconnect fix is acceptable.

Comment on lines +166 to 176
def test_channel_state_is_not_walked_by_the_pickler(factory, monkeypatch):
"""Check that pickling a callback never traverses state its channels write to.

An estimator carrying a callback can be pickled by a background thread, e.g. loky's
queue feeder dispatching a task to a worker, while the listener thread of that same
queue feeder dispatching a task to a worker, while a channel thread of that same
callback mutates the callback's state as messages come in. Pickling a container that
another thread mutates breaks the dump, which joblib reports as "Could not pickle
the task to send it to the workers".

The check runs from a hook, i.e. while the listeners are up, which is when such a
The check runs from a hook, i.e. while the channels are up, which is when such a
dispatch would happen.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to rewrite this test because it used to inspect the global _message_consumers dict which is no longer there. Instead I monkeypatch Channel.init to record the message_consumer. The intent of the test is still the same and it still fails if we remove the fix from #34821

Comment on lines +1 to +3
- Fixed a leak of file descriptors and background threads by
:class:`callback.ScoringMonitor` and :class:`callback.ProgressBar`.
By :user:`Jérémie du Boisberranger <jeremiedbb>`. :pr:`34856`,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks weird but the idea is to reference this PR and #34856 in a single changelog entry. Look at the generated changelog, it looks as expected.

@jeremiedbb jeremiedbb added this to Labs Sep 2, 2026
@jeremiedbb jeremiedbb moved this to PR waiting for reviews in Labs Sep 2, 2026
@jeremiedbb

jeremiedbb commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

The missing coverage is for a line that was already uncovered before the refactoring (see https://app.codecov.io/gh/scikit-learn/scikit-learn/pull/34870). I'd rather ignore it for this PR and open a follow up PR to write a test to cover this line (unrelated to this PR).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Callback's _worker_connections cache grows too big

1 participant