-
Notifications
You must be signed in to change notification settings - Fork 834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve the stability of SocketModeClient implementations #1114
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,7 +144,7 @@ async def monitor_current_session(self) -> None: | |
t = time.time() | ||
if self.last_ping_pong_time is None: | ||
self.last_ping_pong_time = float(t) | ||
await self.current_session.ping(f"ping-pong:{t}") | ||
await self.current_session.ping(f"sdk-ping-pong:{t}") | ||
|
||
if self.auto_reconnect_enabled: | ||
should_reconnect = False | ||
|
@@ -226,7 +226,10 @@ async def receive_messages(self) -> None: | |
if message.data is not None: | ||
str_message_data = message.data.decode("utf-8") | ||
elements = str_message_data.split(":") | ||
if len(elements) == 2: | ||
if ( | ||
len(elements) == 2 | ||
and elements[0] == "sdk-ping-pong" | ||
): | ||
try: | ||
self.last_ping_pong_time = float(elements[1]) | ||
except Exception as e: | ||
|
@@ -296,7 +299,30 @@ async def disconnect(self): | |
async def send_message(self, message: str): | ||
if self.logger.level <= logging.DEBUG: | ||
self.logger.debug(f"Sending a message: {message}") | ||
await self.current_session.send_str(message) | ||
try: | ||
await self.current_session.send_str(message) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A race condition where the background job can disconnect the current session if it's not working any more. In this scenario, reconnection will be completed shortly by the background job. By acquiring the |
||
except ConnectionError as e: | ||
# We rarely get this exception while replacing the underlying WebSocket connections. | ||
# We can do one more try here as the self.current_session should be ready now. | ||
if self.logger.level <= logging.DEBUG: | ||
self.logger.debug( | ||
f"Failed to send a message (error: {e}, message: {message})" | ||
" as the underlying connection was replaced. Retrying the same request only one time..." | ||
) | ||
# Although acquiring self.connect_operation_lock also for the first method call is the safest way, | ||
# we avoid synchronizing a lot for better performance. That's why we are doing a retry here. | ||
try: | ||
await self.connect_operation_lock.acquire() | ||
if await self.is_connected(): | ||
await self.current_session.send_str(message) | ||
else: | ||
self.logger.warning( | ||
"The current session is no longer active. Failed to send a message" | ||
) | ||
raise e | ||
finally: | ||
if self.connect_operation_lock.locked() is True: | ||
self.connect_operation_lock.release() | ||
|
||
async def close(self): | ||
self.closed = True | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
from random import randint | ||
from threading import Thread | ||
|
||
from slack_sdk.errors import SlackClientConfigurationError | ||
from slack_sdk.errors import SlackClientConfigurationError, SlackClientNotConnectedError | ||
from slack_sdk.socket_mode.request import SocketModeRequest | ||
|
||
from slack_sdk.socket_mode.client import BaseSocketModeClient | ||
|
@@ -126,7 +126,45 @@ def socket_mode_request_handler( | |
self.logger.info(f"Passed with buffer size: {buffer_size}") | ||
|
||
finally: | ||
client.close() | ||
self.server.stop() | ||
self.server.close() | ||
|
||
self.logger.info(f"Passed with buffer size: {buffer_size_list}") | ||
|
||
def test_send_message_while_disconnection(self): | ||
if is_ci_unstable_test_skip_enabled(): | ||
return | ||
t = Thread(target=start_socket_mode_server(self, 3011)) | ||
t.daemon = True | ||
t.start() | ||
time.sleep(2) # wait for the server | ||
|
||
try: | ||
self.reset_sever_state() | ||
client = SocketModeClient( | ||
app_token="xapp-A111-222-xyz", | ||
web_client=self.web_client, | ||
auto_reconnect_enabled=False, | ||
trace_enabled=True, | ||
) | ||
client.wss_uri = "ws://0.0.0.0:3011/link" | ||
client.connect() | ||
time.sleep(1) # wait for the connection | ||
client.send_message("foo") | ||
|
||
client.disconnect() | ||
time.sleep(1) # wait for the connection | ||
try: | ||
client.send_message("foo") | ||
self.fail("SlackClientNotConnectedError is expected here") | ||
except SlackClientNotConnectedError as _: | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we should verify if there are expected logs in this test but I manually checked There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is hard for me to understand the intention of this test. The name of the test, "test sending messages", could be interpreted as validating a variety of behaviours that make up the overall message sending functionality. In context of this PR, I assume that this test checks for the single-retry-when-underlying-WS-connection-is-re-established behaviour, but perhaps a week or two from now I will no longer remember this (my memory is not what it used to be 👴 ). Two questions:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You are definitely right here. Perhaps, a better test name would be
Good point. we can add an explicit failure for the case where the exception is not propagated. I will update the tests and the typo in comments later on. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! |
||
|
||
client.connect() | ||
time.sleep(1) # wait for the connection | ||
client.send_message("foo") | ||
finally: | ||
client.close() | ||
self.server.stop() | ||
self.server.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a minor improvement for #1112