Skip to content

Commit 974816e

Browse files
fix: make the function handler timeout 5 seconds (#1348)
Co-authored-by: Eden Zimbelman <[email protected]>
1 parent eb3a519 commit 974816e

File tree

9 files changed

+95
-7
lines changed

9 files changed

+95
-7
lines changed

slack_bolt/app/app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,9 @@ def reverse_string(ack: Ack, inputs: dict, complete: Complete, fail: Fail):
946946
def __call__(*args, **kwargs):
947947
functions = self._to_listener_functions(kwargs) if kwargs else list(args)
948948
primary_matcher = builtin_matchers.function_executed(callback_id=callback_id, base_logger=self._base_logger)
949-
return self._register_listener(functions, primary_matcher, matchers, middleware, auto_acknowledge)
949+
return self._register_listener(
950+
functions, primary_matcher, matchers, middleware, auto_acknowledge, acknowledgement_timeout=5
951+
)
950952

951953
return __call__
952954

@@ -1422,6 +1424,7 @@ def _register_listener(
14221424
matchers: Optional[Sequence[Callable[..., bool]]],
14231425
middleware: Optional[Sequence[Union[Callable, Middleware]]],
14241426
auto_acknowledgement: bool = False,
1427+
acknowledgement_timeout: int = 3,
14251428
) -> Optional[Callable[..., Optional[BoltResponse]]]:
14261429
value_to_return = None
14271430
if not isinstance(functions, list):
@@ -1452,6 +1455,7 @@ def _register_listener(
14521455
matchers=listener_matchers,
14531456
middleware=listener_middleware,
14541457
auto_acknowledgement=auto_acknowledgement,
1458+
acknowledgement_timeout=acknowledgement_timeout,
14551459
base_logger=self._base_logger,
14561460
)
14571461
)

slack_bolt/app/async_app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,9 @@ def __call__(*args, **kwargs):
976976
primary_matcher = builtin_matchers.function_executed(
977977
callback_id=callback_id, base_logger=self._base_logger, asyncio=True
978978
)
979-
return self._register_listener(functions, primary_matcher, matchers, middleware, auto_acknowledge)
979+
return self._register_listener(
980+
functions, primary_matcher, matchers, middleware, auto_acknowledge, acknowledgement_timeout=5
981+
)
980982

981983
return __call__
982984

@@ -1456,6 +1458,7 @@ def _register_listener(
14561458
matchers: Optional[Sequence[Callable[..., Awaitable[bool]]]],
14571459
middleware: Optional[Sequence[Union[Callable, AsyncMiddleware]]],
14581460
auto_acknowledgement: bool = False,
1461+
acknowledgement_timeout: int = 3,
14591462
) -> Optional[Callable[..., Awaitable[Optional[BoltResponse]]]]:
14601463
value_to_return = None
14611464
if not isinstance(functions, list):
@@ -1491,6 +1494,7 @@ def _register_listener(
14911494
matchers=listener_matchers,
14921495
middleware=listener_middleware,
14931496
auto_acknowledgement=auto_acknowledgement,
1497+
acknowledgement_timeout=acknowledgement_timeout,
14941498
base_logger=self._base_logger,
14951499
)
14961500
)

slack_bolt/listener/async_listener.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class AsyncListener(metaclass=ABCMeta):
1515
ack_function: Callable[..., Awaitable[BoltResponse]]
1616
lazy_functions: Sequence[Callable[..., Awaitable[None]]]
1717
auto_acknowledgement: bool
18+
acknowledgement_timeout: int
1819

1920
async def async_matches(
2021
self,
@@ -87,6 +88,7 @@ class AsyncCustomListener(AsyncListener):
8788
matchers: Sequence[AsyncListenerMatcher]
8889
middleware: Sequence[AsyncMiddleware]
8990
auto_acknowledgement: bool
91+
acknowledgement_timeout: int
9092
arg_names: MutableSequence[str]
9193
logger: Logger
9294

@@ -99,6 +101,7 @@ def __init__(
99101
matchers: Sequence[AsyncListenerMatcher],
100102
middleware: Sequence[AsyncMiddleware],
101103
auto_acknowledgement: bool = False,
104+
acknowledgement_timeout: int = 3,
102105
base_logger: Optional[Logger] = None,
103106
):
104107
self.app_name = app_name
@@ -107,6 +110,7 @@ def __init__(
107110
self.matchers = matchers
108111
self.middleware = middleware
109112
self.auto_acknowledgement = auto_acknowledgement
113+
self.acknowledgement_timeout = acknowledgement_timeout
110114
self.arg_names = get_arg_names_of_callable(ack_function)
111115
self.logger = get_bolt_app_logger(app_name, self.ack_function, base_logger)
112116

slack_bolt/listener/asyncio_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ async def run_ack_function_asynchronously(
149149
self._start_lazy_function(lazy_func, request)
150150

151151
# await for the completion of ack() in the async listener execution
152-
while ack.response is None and time.time() - starting_time <= 3:
152+
while ack.response is None and time.time() - starting_time <= listener.acknowledgement_timeout:
153153
await asyncio.sleep(0.01)
154154

155155
if response is None and ack.response is None:

slack_bolt/listener/custom_listener.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class CustomListener(Listener):
1818
matchers: Sequence[ListenerMatcher]
1919
middleware: Sequence[Middleware]
2020
auto_acknowledgement: bool
21+
acknowledgement_timeout: int = 3
2122
arg_names: MutableSequence[str]
2223
logger: Logger
2324

@@ -30,6 +31,7 @@ def __init__(
3031
matchers: Sequence[ListenerMatcher],
3132
middleware: Sequence[Middleware],
3233
auto_acknowledgement: bool = False,
34+
acknowledgement_timeout: int = 3,
3335
base_logger: Optional[Logger] = None,
3436
):
3537
self.app_name = app_name
@@ -38,6 +40,7 @@ def __init__(
3840
self.matchers = matchers
3941
self.middleware = middleware
4042
self.auto_acknowledgement = auto_acknowledgement
43+
self.acknowledgement_timeout = acknowledgement_timeout
4144
self.arg_names = get_arg_names_of_callable(ack_function)
4245
self.logger = get_bolt_app_logger(app_name, self.ack_function, base_logger)
4346

slack_bolt/listener/listener.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Listener(metaclass=ABCMeta):
1313
ack_function: Callable[..., BoltResponse]
1414
lazy_functions: Sequence[Callable[..., None]]
1515
auto_acknowledgement: bool
16+
acknowledgement_timeout: int = 3
1617

1718
def matches(
1819
self,

slack_bolt/listener/thread_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def run_ack_function_asynchronously():
160160
self._start_lazy_function(lazy_func, request)
161161

162162
# await for the completion of ack() in the async listener execution
163-
while ack.response is None and time.time() - starting_time <= 3:
163+
while ack.response is None and time.time() - starting_time <= listener.acknowledgement_timeout:
164164
time.sleep(0.01)
165165

166166
if response is None and ack.response is None:

tests/scenario_tests/test_function.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import time
33
import pytest
4+
from unittest.mock import Mock
45

56
from slack_sdk.signature import SignatureVerifier
67
from slack_sdk.web import WebClient
@@ -51,6 +52,10 @@ def build_request_from_body(self, message_body: dict) -> BoltRequest:
5152
timestamp, body = str(int(time.time())), json.dumps(message_body)
5253
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
5354

55+
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock: Mock, sleep_mock: Mock):
56+
monkeypatch.setattr(time, "time", time_mock)
57+
monkeypatch.setattr(time, "sleep", sleep_mock)
58+
5459
def test_valid_callback_id_success(self):
5560
app = App(
5661
client=self.web_client,
@@ -124,20 +129,48 @@ def test_auto_acknowledge_false_with_acknowledging(self):
124129
assert response.status == 200
125130
assert_auth_test_count(self, 1)
126131

127-
def test_auto_acknowledge_false_without_acknowledging(self, caplog):
132+
def test_auto_acknowledge_false_without_acknowledging(self, caplog, monkeypatch):
128133
app = App(
129134
client=self.web_client,
130135
signing_secret=self.signing_secret,
131136
)
132137
app.function("reverse", auto_acknowledge=False)(just_no_ack)
133138

134139
request = self.build_request_from_body(function_body)
140+
self.setup_time_mocks(
141+
monkeypatch=monkeypatch,
142+
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
143+
sleep_mock=Mock(),
144+
)
135145
response = app.dispatch(request)
136146

137147
assert response.status == 404
138148
assert_auth_test_count(self, 1)
139149
assert f"WARNING {just_no_ack.__name__} didn't call ack()" in caplog.text
140150

151+
def test_function_handler_timeout(self, monkeypatch):
152+
app = App(
153+
client=self.web_client,
154+
signing_secret=self.signing_secret,
155+
)
156+
app.function("reverse", auto_acknowledge=False)(just_no_ack)
157+
request = self.build_request_from_body(function_body)
158+
159+
sleep_mock = Mock()
160+
self.setup_time_mocks(
161+
monkeypatch=monkeypatch,
162+
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
163+
sleep_mock=sleep_mock,
164+
)
165+
166+
response = app.dispatch(request)
167+
168+
assert response.status == 404
169+
assert_auth_test_count(self, 1)
170+
assert (
171+
sleep_mock.call_count == 5
172+
), f"Expected handler to time out after calling time.sleep 5 times, but it was called {sleep_mock.call_count} times"
173+
141174

142175
function_body = {
143176
"token": "verification_token",

tests/scenario_tests_async/test_function.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44

55
import pytest
6+
from unittest.mock import Mock, MagicMock
67
from slack_sdk.signature import SignatureVerifier
78
from slack_sdk.web.async_client import AsyncWebClient
89

@@ -17,6 +18,10 @@
1718
from tests.utils import remove_os_env_temporarily, restore_os_env
1819

1920

21+
async def fake_sleep(seconds):
22+
pass
23+
24+
2025
class TestAsyncFunction:
2126
signing_secret = "secret"
2227
valid_token = "xoxb-valid"
@@ -56,6 +61,10 @@ def build_request_from_body(self, message_body: dict) -> AsyncBoltRequest:
5661
timestamp, body = str(int(time.time())), json.dumps(message_body)
5762
return AsyncBoltRequest(body=body, headers=self.build_headers(timestamp, body))
5863

64+
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock: Mock, sleep_mock: MagicMock):
65+
monkeypatch.setattr(time, "time", time_mock)
66+
monkeypatch.setattr(asyncio, "sleep", sleep_mock)
67+
5968
@pytest.mark.asyncio
6069
async def test_mock_server_is_running(self):
6170
resp = await self.web_client.api_test()
@@ -130,19 +139,49 @@ async def test_auto_acknowledge_false_with_acknowledging(self):
130139
await assert_auth_test_count_async(self, 1)
131140

132141
@pytest.mark.asyncio
133-
async def test_auto_acknowledge_false_without_acknowledging(self, caplog):
142+
async def test_auto_acknowledge_false_without_acknowledging(self, caplog, monkeypatch):
134143
app = AsyncApp(
135144
client=self.web_client,
136145
signing_secret=self.signing_secret,
137146
)
138147
app.function("reverse", auto_acknowledge=False)(just_no_ack)
139-
140148
request = self.build_request_from_body(function_body)
149+
150+
self.setup_time_mocks(
151+
monkeypatch=monkeypatch,
152+
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
153+
sleep_mock=MagicMock(side_effect=fake_sleep),
154+
)
155+
141156
response = await app.async_dispatch(request)
142157
assert response.status == 404
143158
await assert_auth_test_count_async(self, 1)
144159
assert f"WARNING {just_no_ack.__name__} didn't call ack()" in caplog.text
145160

161+
@pytest.mark.asyncio
162+
async def test_function_handler_timeout(self, monkeypatch):
163+
app = AsyncApp(
164+
client=self.web_client,
165+
signing_secret=self.signing_secret,
166+
)
167+
app.function("reverse", auto_acknowledge=False)(just_no_ack)
168+
request = self.build_request_from_body(function_body)
169+
170+
sleep_mock = MagicMock(side_effect=fake_sleep)
171+
self.setup_time_mocks(
172+
monkeypatch=monkeypatch,
173+
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
174+
sleep_mock=sleep_mock,
175+
)
176+
177+
response = await app.async_dispatch(request)
178+
179+
assert response.status == 404
180+
await assert_auth_test_count_async(self, 1)
181+
assert (
182+
sleep_mock.call_count == 5
183+
), f"Expected handler to time out after calling time.sleep 5 times, but it was called {sleep_mock.call_count} times"
184+
146185

147186
function_body = {
148187
"token": "verification_token",

0 commit comments

Comments
 (0)