Skip to content

Commit 62b41a8

Browse files
fix(socket_mode): shut down current_session_runner in close()
`SocketModeClient` starts three `IntervalRunner` threads in `__init__`: `current_session_runner` (interval 0.1 s), `current_app_monitor` (interval = `ping_interval`, default 5 s) and `message_processor` (interval 0.001 s). `close()` shut down two of them but not `current_session_runner`, so every instance leaked one thread running a 100 ms loop. For long-running watchers that recreate the client occasionally (e.g. after a transient disconnect detected via `is_connected()`), the leaked threads accumulate and combine with the live instance's 1 ms `message_processor` loop to drive CPU usage up. The same client instances also fail to release their threads under normal lifetime management. Adds a guarded `current_session_runner.shutdown()` call alongside the existing two, plus a regression test verifying that all three runners exit after `close()`. Closes #1873
1 parent beecde2 commit 62b41a8

2 files changed

Lines changed: 20 additions & 0 deletions

File tree

slack_sdk/socket_mode/builtin/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def close(self):
225225
self.closed = True
226226
self.auto_reconnect_enabled = False
227227
self.disconnect()
228+
if self.current_session_runner.is_alive():
229+
self.current_session_runner.shutdown()
228230
if self.current_app_monitor.is_alive():
229231
self.current_app_monitor.shutdown()
230232
if self.message_processor.is_alive():

tests/slack_sdk/socket_mode/test_builtin.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ def test_init_close(self):
4848
finally:
4949
client.close()
5050

51+
def test_close_shuts_down_all_runners(self):
52+
# Regression for #1873: close() must shut down current_session_runner
53+
# along with current_app_monitor and message_processor. Previously
54+
# current_session_runner was left running (100 ms loop), so each
55+
# init/close cycle leaked one thread.
56+
client = SocketModeClient(app_token="xapp-A111-222-xyz")
57+
# The first two runners are started inside __init__.
58+
self.assertTrue(client.current_session_runner.is_alive())
59+
self.assertTrue(client.message_processor.is_alive())
60+
client.close()
61+
# IntervalRunner.shutdown() joins the thread, so by the time
62+
# close() returns these are no longer alive.
63+
self.assertFalse(client.current_session_runner.is_alive())
64+
self.assertFalse(client.message_processor.is_alive())
65+
# current_app_monitor is only started in connect(); since this test
66+
# never connects, it should report not-alive both before and after.
67+
self.assertFalse(client.current_app_monitor.is_alive())
68+
5169
def test_issue_new_wss_url(self):
5270
client = SocketModeClient(
5371
app_token="xapp-A111-222-xyz",

0 commit comments

Comments
 (0)