Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion slack_sdk/socket_mode/builtin/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ def check_state(self) -> None:
try:
if self.sock is not None:
try:
self.ping(f"{self.session_id}:{time.time()}")
ping_time = time.time()
self.ping(f"{self.session_id}:{ping_time}")
# Seed on first ping so the stale check fires even if no pong ever arrives.
if self.last_ping_pong_time is None:
self.last_ping_pong_time = ping_time
Comment on lines +252 to +256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

last_ping_pong_time is public, and today only a pong ever sets it, so apps use it as a liveness signal. For example, a health check like pong is not None and time.time() - pong < N would report a never-ponged connection as healthy for ~20s with this change. Could we leave it pong-only and give the stale check its own baseline?

# __init__
self._first_ping_time: Optional[float] = None

# check_state
ping_time = time.time()
self.ping(f"{self.session_id}:{ping_time}")
if self._first_ping_time is None:
    self._first_ping_time = ping_time
...
stale_check_since = self.last_ping_pong_time if self.last_ping_pong_time is not None else self._first_ping_time
if stale_check_since is not None:
    disconnected_seconds = int(time.time() - stale_check_since)

Setting it after ping() means the clock only starts once a ping actually goes out, and it needs to be set once, not on every ping. Otherwise its age never exceeds one interval and the stale check never fires.

except ssl.SSLZeroReturnError as e:
self.logger.info(
"Unable to send a ping message. Closing the connection..."
Expand Down
19 changes: 18 additions & 1 deletion tests/slack_sdk/socket_mode/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import ssl
import time
import unittest
from unittest.mock import patch, sentinel
from unittest.mock import MagicMock, patch, sentinel
from threading import Thread

from slack_sdk import WebClient
Expand Down Expand Up @@ -126,6 +126,23 @@ def run():

self.assertIsNotNone(conn)

def test_check_state_seeds_last_ping_pong_time_on_first_ping(self):
conn = Connection(url="wss://example.com", logger=self.logger)
conn.sock = MagicMock()
self.assertIsNone(conn.last_ping_pong_time)
with patch.object(conn, "ping"):
conn.check_state()
self.assertIsNotNone(conn.last_ping_pong_time)

def test_check_state_does_not_overwrite_existing_last_ping_pong_time(self):
conn = Connection(url="wss://example.com", logger=self.logger)
conn.sock = MagicMock()
existing_time = time.time() - 1
conn.last_ping_pong_time = existing_time
with patch.object(conn, "ping"):
conn.check_state()
self.assertEqual(conn.last_ping_pong_time, existing_time)

# ----------------------------------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What do you think about adding a test that covers the bug in the issue: with no pong ever arriving, the stale check disconnects?
For example, patch slack_sdk.socket_mode.builtin.connection.time.time with an advancing side_effect, call check_state() until we're past ping_interval * 4, and assert conn.sock is None. It might also be worth asserting that the seeded value equals the timestamp sent in the ping payload, not just that it's not None.

# FrameHeader

Expand Down
Loading