Skip to content

Commit fc7013b

Browse files
authored
Introduce ListenerStartHandler in the listener runner for better managing Django DB connections (#514)
1 parent cf3951e commit fc7013b

File tree

10 files changed

+195
-11
lines changed

10 files changed

+195
-11
lines changed

slack_bolt/adapter/django/handler.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from slack_bolt.error import BoltError
1010
from slack_bolt.lazy_listener import ThreadLazyListenerRunner
1111
from slack_bolt.lazy_listener.internals import build_runnable_function
12+
from slack_bolt.listener.listener_start_handler import (
13+
ListenerStartHandler,
14+
DefaultListenerStartHandler,
15+
)
1216
from slack_bolt.listener.listener_completion_handler import (
1317
ListenerCompletionHandler,
1418
DefaultListenerCompletionHandler,
@@ -67,6 +71,16 @@ def release_thread_local_connections(logger: Logger, execution_timing: str):
6771
)
6872

6973

74+
class DjangoListenerStartHandler(ListenerStartHandler):
75+
"""Django sets DB connections as a thread-local variable per thread.
76+
If the thread is not managed on the Django app side, the connections won't be released by Django.
77+
This handler releases the connections every time a ThreadListenerRunner execution completes.
78+
"""
79+
80+
def handle(self, request: BoltRequest, response: Optional[BoltResponse]) -> None:
81+
release_thread_local_connections(request.context.logger, "listener-start")
82+
83+
7084
class DjangoListenerCompletionHandler(ListenerCompletionHandler):
7185
"""Django sets DB connections as a thread-local variable per thread.
7286
If the thread is not managed on the Django app side, the connections won't be released by Django.
@@ -86,6 +100,9 @@ def start(self, function: Callable[..., None], request: BoltRequest) -> None:
86100
)
87101

88102
def wrapped_func():
103+
release_thread_local_connections(
104+
request.context.logger, "before-lazy-listener"
105+
)
89106
try:
90107
func()
91108
finally:
@@ -117,6 +134,29 @@ def __init__(self, app: App): # type: ignore
117134
self.app.logger.debug("App.process_before_response is set to True")
118135
return
119136

137+
current_start_handler = listener_runner.listener_start_handler
138+
if current_start_handler is not None and not isinstance(
139+
current_start_handler, DefaultListenerStartHandler
140+
):
141+
# As we run release_thread_local_connections() before listener executions,
142+
# it's okay to skip calling the same connection clean-up method at the listener completion.
143+
message = """As you've already set app.listener_runner.listener_start_handler to your own one,
144+
Bolt skipped to set it to slack_sdk.adapter.django.DjangoListenerStartHandler.
145+
146+
If you go with your own handler here, we highly recommend having the following lines of code
147+
in your handle() method to clean up unmanaged stale/old database connections:
148+
149+
from django.db import close_old_connections
150+
close_old_connections()
151+
"""
152+
self.app.logger.info(message)
153+
else:
154+
# for proper management of thread-local Django DB connections
155+
self.app.listener_runner.listener_start_handler = (
156+
DjangoListenerStartHandler()
157+
)
158+
self.app.logger.debug("DjangoListenerStartHandler has been enabled")
159+
120160
current_completion_handler = listener_runner.listener_completion_handler
121161
if current_completion_handler is not None and not isinstance(
122162
current_completion_handler, DefaultListenerCompletionHandler
@@ -145,13 +185,6 @@ def handle(self, req: HttpRequest) -> HttpResponse:
145185
bolt_resp = oauth_flow.handle_callback(to_bolt_request(req))
146186
return to_django_response(bolt_resp)
147187
elif req.method == "POST":
148-
# As bolt-python utilizes threads for async `ack()` method execution,
149-
# we have to manually clean old/stale Django ORM connections bound to the "unmanaged" threads
150-
# Refer to https://github.com/slackapi/bolt-python/issues/280 for more details.
151-
release_thread_local_connections(
152-
self.app.logger, "before-listener-invocation"
153-
)
154-
# And then, run the App listener/lazy listener here
155188
bolt_resp: BoltResponse = self.app.dispatch(to_bolt_request(req))
156189
return to_django_response(bolt_resp)
157190

slack_bolt/app/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from slack_bolt.listener.builtins import TokenRevocationListeners
2424
from slack_bolt.listener.custom_listener import CustomListener
2525
from slack_bolt.listener.listener import Listener
26+
from slack_bolt.listener.listener_start_handler import DefaultListenerStartHandler
2627
from slack_bolt.listener.listener_completion_handler import (
2728
DefaultListenerCompletionHandler,
2829
)
@@ -317,6 +318,9 @@ def message_hello(message, say):
317318
listener_error_handler=DefaultListenerErrorHandler(
318319
logger=self._framework_logger
319320
),
321+
listener_start_handler=DefaultListenerStartHandler(
322+
logger=self._framework_logger
323+
),
320324
listener_completion_handler=DefaultListenerCompletionHandler(
321325
logger=self._framework_logger
322326
),

slack_bolt/app/async_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from slack_bolt.app.async_server import AsyncSlackAppServer
1010
from slack_bolt.listener.async_builtins import AsyncTokenRevocationListeners
11+
from slack_bolt.listener.async_listener_start_handler import (
12+
AsyncDefaultListenerStartHandler,
13+
)
1114
from slack_bolt.listener.async_listener_completion_handler import (
1215
AsyncDefaultListenerCompletionHandler,
1316
)
@@ -337,6 +340,9 @@ async def message_hello(message, say): # async function
337340
listener_error_handler=AsyncDefaultListenerErrorHandler(
338341
logger=self._framework_logger
339342
),
343+
listener_start_handler=AsyncDefaultListenerStartHandler(
344+
logger=self._framework_logger
345+
),
340346
listener_completion_handler=AsyncDefaultListenerCompletionHandler(
341347
logger=self._framework_logger
342348
),

slack_bolt/listener/async_listener_completion_handler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ async def handle(
1515
request: AsyncBoltRequest,
1616
response: Optional[BoltResponse],
1717
) -> None:
18-
"""Handles an unhandled exception.
18+
"""Do something extra after the listener execution
1919
2020
Args:
21-
error: The raised exception.
2221
request: The request.
2322
response: The response.
2423
"""
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import inspect
2+
from abc import ABCMeta, abstractmethod
3+
from logging import Logger
4+
from typing import Callable, Dict, Any, Awaitable, Optional
5+
6+
from slack_bolt.kwargs_injection.async_utils import build_async_required_kwargs
7+
from slack_bolt.request.async_request import AsyncBoltRequest
8+
from slack_bolt.response import BoltResponse
9+
10+
11+
class AsyncListenerStartHandler(metaclass=ABCMeta):
12+
@abstractmethod
13+
async def handle(
14+
self,
15+
request: AsyncBoltRequest,
16+
response: Optional[BoltResponse],
17+
) -> None:
18+
"""Do something extra before the listener execution
19+
20+
Args:
21+
request: The request.
22+
response: The response.
23+
"""
24+
raise NotImplementedError()
25+
26+
27+
class AsyncCustomListenerStartHandler(AsyncListenerStartHandler):
28+
def __init__(self, logger: Logger, func: Callable[..., Awaitable[None]]):
29+
self.func = func
30+
self.logger = logger
31+
self.arg_names = inspect.getfullargspec(func).args
32+
33+
async def handle(
34+
self,
35+
request: AsyncBoltRequest,
36+
response: Optional[BoltResponse],
37+
) -> None:
38+
kwargs: Dict[str, Any] = build_async_required_kwargs(
39+
required_arg_names=self.arg_names,
40+
logger=self.logger,
41+
request=request,
42+
response=response,
43+
next_keys_required=False,
44+
)
45+
await self.func(**kwargs)
46+
47+
48+
class AsyncDefaultListenerStartHandler(AsyncListenerStartHandler):
49+
def __init__(self, logger: Logger):
50+
self.logger = logger
51+
52+
async def handle(
53+
self,
54+
request: AsyncBoltRequest,
55+
response: Optional[BoltResponse],
56+
):
57+
pass

slack_bolt/listener/asyncio_runner.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from slack_bolt.context.ack.async_ack import AsyncAck
88
from slack_bolt.lazy_listener.async_runner import AsyncLazyListenerRunner
99
from slack_bolt.listener.async_listener import AsyncListener
10+
from slack_bolt.listener.async_listener_start_handler import (
11+
AsyncListenerStartHandler,
12+
)
1013
from slack_bolt.listener.async_listener_completion_handler import (
1114
AsyncListenerCompletionHandler,
1215
)
@@ -25,6 +28,7 @@ class AsyncioListenerRunner:
2528
logger: Logger
2629
process_before_response: bool
2730
listener_error_handler: AsyncListenerErrorHandler
31+
listener_start_handler: AsyncListenerStartHandler
2832
listener_completion_handler: AsyncListenerCompletionHandler
2933
lazy_listener_runner: AsyncLazyListenerRunner
3034

@@ -33,12 +37,14 @@ def __init__(
3337
logger: Logger,
3438
process_before_response: bool,
3539
listener_error_handler: AsyncListenerErrorHandler,
40+
listener_start_handler: AsyncListenerStartHandler,
3641
listener_completion_handler: AsyncListenerCompletionHandler,
3742
lazy_listener_runner: AsyncLazyListenerRunner,
3843
):
3944
self.logger = logger
4045
self.process_before_response = process_before_response
4146
self.listener_error_handler = listener_error_handler
47+
self.listener_start_handler = listener_start_handler
4248
self.listener_completion_handler = listener_completion_handler
4349
self.lazy_listener_runner = lazy_listener_runner
4450

@@ -55,6 +61,9 @@ async def run(
5561
if self.process_before_response:
5662
if not request.lazy_only:
5763
try:
64+
await self.listener_start_handler.handle(
65+
request=request, response=response
66+
)
5867
returned_value = await listener.run_ack_function(
5968
request=request, response=response
6069
)
@@ -113,6 +122,9 @@ async def run_ack_function_asynchronously(
113122
response: BoltResponse,
114123
):
115124
try:
125+
await self.listener_start_handler.handle(
126+
request=request, response=response
127+
)
116128
await listener.run_ack_function(
117129
request=request, response=response
118130
)

slack_bolt/listener/listener_completion_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def handle(
1515
request: BoltRequest,
1616
response: Optional[BoltResponse],
1717
) -> None:
18-
"""Handles an unhandled exception.
18+
"""Do something extra after the listener execution
1919
2020
Args:
2121
request: The request.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import inspect
2+
from abc import ABCMeta, abstractmethod
3+
from logging import Logger
4+
from typing import Callable, Dict, Any, Optional
5+
6+
from slack_bolt.kwargs_injection import build_required_kwargs
7+
from slack_bolt.request.request import BoltRequest
8+
from slack_bolt.response.response import BoltResponse
9+
10+
11+
class ListenerStartHandler(metaclass=ABCMeta):
12+
@abstractmethod
13+
def handle(
14+
self,
15+
request: BoltRequest,
16+
response: Optional[BoltResponse],
17+
) -> None:
18+
"""Do something extra before the listener execution.
19+
20+
This handler is useful if a developer needs to maintain/clean up
21+
thread-local resources such as Django ORM database connections
22+
before a listener execution starts.
23+
24+
Args:
25+
request: The request.
26+
response: The response.
27+
"""
28+
raise NotImplementedError()
29+
30+
31+
class CustomListenerStartHandler(ListenerStartHandler):
32+
def __init__(self, logger: Logger, func: Callable[..., None]):
33+
self.func = func
34+
self.logger = logger
35+
self.arg_names = inspect.getfullargspec(func).args
36+
37+
def handle(
38+
self,
39+
request: BoltRequest,
40+
response: Optional[BoltResponse],
41+
):
42+
kwargs: Dict[str, Any] = build_required_kwargs(
43+
required_arg_names=self.arg_names,
44+
logger=self.logger,
45+
request=request,
46+
response=response,
47+
next_keys_required=False,
48+
)
49+
self.func(**kwargs)
50+
51+
52+
class DefaultListenerStartHandler(ListenerStartHandler):
53+
def __init__(self, logger: Logger):
54+
self.logger = logger
55+
56+
def handle(
57+
self,
58+
request: BoltRequest,
59+
response: Optional[BoltResponse],
60+
):
61+
pass

slack_bolt/listener/thread_runner.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from slack_bolt.lazy_listener import LazyListenerRunner
77
from slack_bolt.listener import Listener
8+
from slack_bolt.listener.listener_start_handler import ListenerStartHandler
89
from slack_bolt.listener.listener_completion_handler import ListenerCompletionHandler
910
from slack_bolt.listener.listener_error_handler import ListenerErrorHandler
1011
from slack_bolt.logger.messages import (
@@ -21,6 +22,7 @@ class ThreadListenerRunner:
2122
logger: Logger
2223
process_before_response: bool
2324
listener_error_handler: ListenerErrorHandler
25+
listener_start_handler: ListenerStartHandler
2426
listener_completion_handler: ListenerCompletionHandler
2527
listener_executor: Executor
2628
lazy_listener_runner: LazyListenerRunner
@@ -30,13 +32,15 @@ def __init__(
3032
logger: Logger,
3133
process_before_response: bool,
3234
listener_error_handler: ListenerErrorHandler,
35+
listener_start_handler: ListenerStartHandler,
3336
listener_completion_handler: ListenerCompletionHandler,
3437
listener_executor: Executor,
3538
lazy_listener_runner: LazyListenerRunner,
3639
):
3740
self.logger = logger
3841
self.process_before_response = process_before_response
3942
self.listener_error_handler = listener_error_handler
43+
self.listener_start_handler = listener_start_handler
4044
self.listener_completion_handler = listener_completion_handler
4145
self.listener_executor = listener_executor
4246
self.lazy_listener_runner = lazy_listener_runner
@@ -54,6 +58,10 @@ def run( # type: ignore
5458
if self.process_before_response:
5559
if not request.lazy_only:
5660
try:
61+
self.listener_start_handler.handle(
62+
request=request,
63+
response=response,
64+
)
5765
returned_value = listener.run_ack_function(
5866
request=request, response=response
5967
)
@@ -109,6 +117,10 @@ def run( # type: ignore
109117
def run_ack_function_asynchronously():
110118
nonlocal ack, request, response
111119
try:
120+
self.listener_start_handler.handle(
121+
request=request,
122+
response=response,
123+
)
112124
listener.run_ack_function(request=request, response=response)
113125
except Exception as e:
114126
# The default response status code is 500 in this case.

slack_bolt/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Check the latest version at https://pypi.org/project/slack-bolt/"""
2-
__version__ = "1.9.4"
2+
__version__ = "1.10.0a"

0 commit comments

Comments
 (0)