Skip to content

fix(socket_mode): shut down current_session_runner in built-in close() - #1962

Open
WilliamBergamin wants to merge 1 commit into
slackapi:mainfrom
WilliamBergamin:fix/1873-socketmode-close-session-runner-leak
Open

fix(socket_mode): shut down current_session_runner in built-in close()#1962
WilliamBergamin wants to merge 1 commit into
slackapi:mainfrom
WilliamBergamin:fix/1873-socketmode-close-session-runner-leak

Conversation

@WilliamBergamin

@WilliamBergamin WilliamBergamin commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

The built-in SocketModeClient starts three IntervalRunner daemon threads in __init__

For comparison, the sync sibling websocket_client's close() already shuts down current_session_runner, and the async backends cancel all their futures.

Why

Adding current_session_runner.shutdown() call alone deadlocks close() on any client that actually connected. That runner runs _run_current_session -> Connection.run_until_completion, whose loop exits only when the connection state is terminated. disconnect() nulls the socket but does not set terminated, and IntervalRunner.shutdown() joins the thread with no timeout, so join() blocks forever.

So close() sets current_session_state.terminated = True before disconnecting, mirroring what connect() 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; setting terminated is what resolves it.

Tests

Adds test_close_reaps_current_session_runner to tests/slack_sdk/socket_mode/test_interactions_builtin.py. It connects using the existing mock socket-mode server, calls close(), and asserts all three runner threads are no longer alive. Because it exercises the connected path, it hangs under pytest-timeout if terminated is not set. It guards both the leak and the deadlock. The existing test_interactions suite (connect/close over many buffer sizes) still passes.

Notes

Closes #1873

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.13%. Comparing base (c30a05d) to head (6ee2433).
✅ All tests successful. No failed tests found.

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.
📢 Have feedback on the report? Share it here.

@WilliamBergamin WilliamBergamin added this to the 3.next milestone Sep 8, 2026
@WilliamBergamin WilliamBergamin added bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented socket-mode area:async labels Sep 8, 2026
@WilliamBergamin WilliamBergamin self-assigned this Sep 8, 2026
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
WilliamBergamin force-pushed the fix/1873-socketmode-close-session-runner-leak branch from 1de0a11 to 6ee2433 Compare September 8, 2026 16:26
@WilliamBergamin
WilliamBergamin marked this pull request as ready for review September 8, 2026 16:31
@WilliamBergamin
WilliamBergamin requested a review from a team as a code owner September 8, 2026 16:31

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@WilliamBergamin 💌 Praises to these patches! Thanks for keeping code health in mind.

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

Labels

area:async bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented socket-mode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SocketModeClient.close() leaks current_session_runner thread (built-in client)

2 participants