fix(socket_mode): shut down current_session_runner in built-in close() - #1962
Open
WilliamBergamin wants to merge 1 commit into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1962 +/- ##
=======================================
Coverage 84.13% 84.13%
=======================================
Files 118 118
Lines 13553 13556 +3
=======================================
+ Hits 11403 11406 +3
Misses 2150 2150 ☔ View full report in Codecov by Harness. |
The built-in SocketModeClient starts three IntervalRunner daemon threads in __init__ (current_session_runner, current_app_monitor, message_processor) but close() only shut down two of them, leaking current_session_runner on every closed instance. Long-lived processes that open and close many clients accumulate one idle thread per closed instance (issue slackapi#1873). The sync sibling websocket_client.close() already shuts its current_session_runner down, and the async backends cancel all their futures. The leak was built-in only. Adding the shutdown alone is not enough: current_session_runner runs run_until_completion, whose loop exits only when the connection state is terminated. disconnect() does not set that flag, and shutdown() joins with no timeout, so the join would hang on a connected client. close() now sets current_session_state.terminated = True before disconnecting (mirroring connect()'s handling of a retired session), letting the loop return so the join completes. This is the CI hang the earlier slackapi#1874 attempt ran into. Adds a regression test on the connected path (connect then close) asserting all three runners are reaped; it hangs under pytest-timeout if terminated is not set, so it guards both the leak and the deadlock. Refs slackapi#1873. Supersedes slackapi#1874. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
WilliamBergamin
force-pushed
the
fix/1873-socketmode-close-session-runner-leak
branch
from
September 8, 2026 16:26
1de0a11 to
6ee2433
Compare
WilliamBergamin
marked this pull request as ready for review
September 8, 2026 16:31
zimeg
approved these changes
Sep 8, 2026
zimeg
left a comment
Member
There was a problem hiding this comment.
@WilliamBergamin 💌 Praises to these patches! Thanks for keeping code health in mind.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The built-in
SocketModeClientstarts threeIntervalRunnerdaemon threads in__init__current_session_runnercurrent_app_monitormessage_processor Butclose()only shut down two of them.current_session_runnersurvivedclose()`, so a long-lived process that opens and closes many client instances leaks one idle daemon thread per closed instance (issue SocketModeClient.close() leaks current_session_runner thread (built-in client) #1873).For comparison, the sync sibling
websocket_client'sclose()already shuts downcurrent_session_runner, and the async backends cancel all their futures.Why
Adding
current_session_runner.shutdown()call alone deadlocksclose()on any client that actually connected. That runner runs_run_current_session->Connection.run_until_completion, whose loop exits only when the connection state isterminated.disconnect()nulls the socket but does not setterminated, andIntervalRunner.shutdown()joins the thread with no timeout, sojoin()blocks forever.So
close()setscurrent_session_state.terminated = Truebefore disconnecting, mirroring whatconnect()already does when it retires an old session. The loop returns, the join completes, and all three runners are reaped. This is the same CI hang the earlier #1874 ran into; settingterminatedis what resolves it.Tests
Adds
test_close_reaps_current_session_runnertotests/slack_sdk/socket_mode/test_interactions_builtin.py. It connects using the existing mock socket-mode server, callsclose(), and asserts all three runner threads are no longer alive. Because it exercises the connected path, it hangs underpytest-timeoutifterminatedis not set. It guards both the leak and the deadlock. The existingtest_interactionssuite (connect/close over many buffer sizes) still passes.Notes
websocket_clientclients; they already shut this runner down.Closes #1873
🤖 Generated with Claude Code