Skip to content
Merged
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
47 changes: 40 additions & 7 deletions slack_bolt/adapter/django/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
from slack_bolt.error import BoltError
from slack_bolt.lazy_listener import ThreadLazyListenerRunner
from slack_bolt.lazy_listener.internals import build_runnable_function
from slack_bolt.listener.listener_start_handler import (
ListenerStartHandler,
DefaultListenerStartHandler,
)
from slack_bolt.listener.listener_completion_handler import (
ListenerCompletionHandler,
DefaultListenerCompletionHandler,
Expand Down Expand Up @@ -67,6 +71,16 @@ def release_thread_local_connections(logger: Logger, execution_timing: str):
)


class DjangoListenerStartHandler(ListenerStartHandler):
"""Django sets DB connections as a thread-local variable per thread.
If the thread is not managed on the Django app side, the connections won't be released by Django.
This handler releases the connections every time a ThreadListenerRunner execution completes.
"""

def handle(self, request: BoltRequest, response: Optional[BoltResponse]) -> None:
release_thread_local_connections(request.context.logger, "listener-start")


class DjangoListenerCompletionHandler(ListenerCompletionHandler):
"""Django sets DB connections as a thread-local variable per thread.
If the thread is not managed on the Django app side, the connections won't be released by Django.
Expand All @@ -86,6 +100,9 @@ def start(self, function: Callable[..., None], request: BoltRequest) -> None:
)

def wrapped_func():
release_thread_local_connections(
request.context.logger, "before-lazy-listener"
)
try:
func()
finally:
Expand Down Expand Up @@ -117,6 +134,29 @@ def __init__(self, app: App): # type: ignore
self.app.logger.debug("App.process_before_response is set to True")
return

current_start_handler = listener_runner.listener_start_handler
if current_start_handler is not None and not isinstance(
current_start_handler, DefaultListenerStartHandler
):
# As we run release_thread_local_connections() before listener executions,
# it's okay to skip calling the same connection clean-up method at the listener completion.
message = """As you've already set app.listener_runner.listener_start_handler to your own one,
Bolt skipped to set it to slack_sdk.adapter.django.DjangoListenerStartHandler.

If you go with your own handler here, we highly recommend having the following lines of code
in your handle() method to clean up unmanaged stale/old database connections:

from django.db import close_old_connections
close_old_connections()
"""
self.app.logger.info(message)
else:
# for proper management of thread-local Django DB connections
self.app.listener_runner.listener_start_handler = (
DjangoListenerStartHandler()
)
self.app.logger.debug("DjangoListenerStartHandler has been enabled")

current_completion_handler = listener_runner.listener_completion_handler
if current_completion_handler is not None and not isinstance(
current_completion_handler, DefaultListenerCompletionHandler
Expand Down Expand Up @@ -145,13 +185,6 @@ def handle(self, req: HttpRequest) -> HttpResponse:
bolt_resp = oauth_flow.handle_callback(to_bolt_request(req))
return to_django_response(bolt_resp)
elif req.method == "POST":
# As bolt-python utilizes threads for async `ack()` method execution,
# we have to manually clean old/stale Django ORM connections bound to the "unmanaged" threads
# Refer to https://github.com/slackapi/bolt-python/issues/280 for more details.
release_thread_local_connections(
self.app.logger, "before-listener-invocation"
)
# And then, run the App listener/lazy listener here
bolt_resp: BoltResponse = self.app.dispatch(to_bolt_request(req))
return to_django_response(bolt_resp)

Expand Down
4 changes: 4 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from slack_bolt.listener.builtins import TokenRevocationListeners
from slack_bolt.listener.custom_listener import CustomListener
from slack_bolt.listener.listener import Listener
from slack_bolt.listener.listener_start_handler import DefaultListenerStartHandler
from slack_bolt.listener.listener_completion_handler import (
DefaultListenerCompletionHandler,
)
Expand Down Expand Up @@ -317,6 +318,9 @@ def message_hello(message, say):
listener_error_handler=DefaultListenerErrorHandler(
logger=self._framework_logger
),
listener_start_handler=DefaultListenerStartHandler(
logger=self._framework_logger
),
listener_completion_handler=DefaultListenerCompletionHandler(
logger=self._framework_logger
),
Expand Down
6 changes: 6 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

from slack_bolt.app.async_server import AsyncSlackAppServer
from slack_bolt.listener.async_builtins import AsyncTokenRevocationListeners
from slack_bolt.listener.async_listener_start_handler import (
AsyncDefaultListenerStartHandler,
)
from slack_bolt.listener.async_listener_completion_handler import (
AsyncDefaultListenerCompletionHandler,
)
Expand Down Expand Up @@ -337,6 +340,9 @@ async def message_hello(message, say): # async function
listener_error_handler=AsyncDefaultListenerErrorHandler(
logger=self._framework_logger
),
listener_start_handler=AsyncDefaultListenerStartHandler(
logger=self._framework_logger
),
listener_completion_handler=AsyncDefaultListenerCompletionHandler(
logger=self._framework_logger
),
Expand Down
3 changes: 1 addition & 2 deletions slack_bolt/listener/async_listener_completion_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ async def handle(
request: AsyncBoltRequest,
response: Optional[BoltResponse],
) -> None:
"""Handles an unhandled exception.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This had been wrong 🤦

"""Do something extra after the listener execution

Args:
error: The raised exception.
request: The request.
response: The response.
"""
Expand Down
57 changes: 57 additions & 0 deletions slack_bolt/listener/async_listener_start_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import inspect
from abc import ABCMeta, abstractmethod
from logging import Logger
from typing import Callable, Dict, Any, Awaitable, Optional

from slack_bolt.kwargs_injection.async_utils import build_async_required_kwargs
from slack_bolt.request.async_request import AsyncBoltRequest
from slack_bolt.response import BoltResponse


class AsyncListenerStartHandler(metaclass=ABCMeta):
@abstractmethod
async def handle(
self,
request: AsyncBoltRequest,
response: Optional[BoltResponse],
) -> None:
"""Do something extra before the listener execution

Args:
request: The request.
response: The response.
"""
raise NotImplementedError()


class AsyncCustomListenerStartHandler(AsyncListenerStartHandler):
def __init__(self, logger: Logger, func: Callable[..., Awaitable[None]]):
self.func = func
self.logger = logger
self.arg_names = inspect.getfullargspec(func).args

async def handle(
self,
request: AsyncBoltRequest,
response: Optional[BoltResponse],
) -> None:
kwargs: Dict[str, Any] = build_async_required_kwargs(
required_arg_names=self.arg_names,
logger=self.logger,
request=request,
response=response,
next_keys_required=False,
)
await self.func(**kwargs)


class AsyncDefaultListenerStartHandler(AsyncListenerStartHandler):
def __init__(self, logger: Logger):
self.logger = logger

async def handle(
self,
request: AsyncBoltRequest,
response: Optional[BoltResponse],
):
pass
12 changes: 12 additions & 0 deletions slack_bolt/listener/asyncio_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from slack_bolt.context.ack.async_ack import AsyncAck
from slack_bolt.lazy_listener.async_runner import AsyncLazyListenerRunner
from slack_bolt.listener.async_listener import AsyncListener
from slack_bolt.listener.async_listener_start_handler import (
AsyncListenerStartHandler,
)
from slack_bolt.listener.async_listener_completion_handler import (
AsyncListenerCompletionHandler,
)
Expand All @@ -25,6 +28,7 @@ class AsyncioListenerRunner:
logger: Logger
process_before_response: bool
listener_error_handler: AsyncListenerErrorHandler
listener_start_handler: AsyncListenerStartHandler
listener_completion_handler: AsyncListenerCompletionHandler
lazy_listener_runner: AsyncLazyListenerRunner

Expand All @@ -33,12 +37,14 @@ def __init__(
logger: Logger,
process_before_response: bool,
listener_error_handler: AsyncListenerErrorHandler,
listener_start_handler: AsyncListenerStartHandler,
listener_completion_handler: AsyncListenerCompletionHandler,
lazy_listener_runner: AsyncLazyListenerRunner,
):
self.logger = logger
self.process_before_response = process_before_response
self.listener_error_handler = listener_error_handler
self.listener_start_handler = listener_start_handler
self.listener_completion_handler = listener_completion_handler
self.lazy_listener_runner = lazy_listener_runner

Expand All @@ -55,6 +61,9 @@ async def run(
if self.process_before_response:
if not request.lazy_only:
try:
await self.listener_start_handler.handle(
request=request, response=response
)
returned_value = await listener.run_ack_function(
request=request, response=response
)
Expand Down Expand Up @@ -113,6 +122,9 @@ async def run_ack_function_asynchronously(
response: BoltResponse,
):
try:
await self.listener_start_handler.handle(
request=request, response=response
)
await listener.run_ack_function(
request=request, response=response
)
Expand Down
2 changes: 1 addition & 1 deletion slack_bolt/listener/listener_completion_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def handle(
request: BoltRequest,
response: Optional[BoltResponse],
) -> None:
"""Handles an unhandled exception.
"""Do something extra after the listener execution
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This had been wrong 🤦


Args:
request: The request.
Expand Down
61 changes: 61 additions & 0 deletions slack_bolt/listener/listener_start_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import inspect
from abc import ABCMeta, abstractmethod
from logging import Logger
from typing import Callable, Dict, Any, Optional

from slack_bolt.kwargs_injection import build_required_kwargs
from slack_bolt.request.request import BoltRequest
from slack_bolt.response.response import BoltResponse


class ListenerStartHandler(metaclass=ABCMeta):
@abstractmethod
def handle(
self,
request: BoltRequest,
response: Optional[BoltResponse],
) -> None:
"""Do something extra before the listener execution.

This handler is useful if a developer needs to maintain/clean up
thread-local resources such as Django ORM database connections
before a listener execution starts.

Args:
request: The request.
response: The response.
"""
raise NotImplementedError()


class CustomListenerStartHandler(ListenerStartHandler):
def __init__(self, logger: Logger, func: Callable[..., None]):
self.func = func
self.logger = logger
self.arg_names = inspect.getfullargspec(func).args

def handle(
self,
request: BoltRequest,
response: Optional[BoltResponse],
):
kwargs: Dict[str, Any] = build_required_kwargs(
required_arg_names=self.arg_names,
logger=self.logger,
request=request,
response=response,
next_keys_required=False,
)
self.func(**kwargs)


class DefaultListenerStartHandler(ListenerStartHandler):
def __init__(self, logger: Logger):
self.logger = logger

def handle(
self,
request: BoltRequest,
response: Optional[BoltResponse],
):
pass
12 changes: 12 additions & 0 deletions slack_bolt/listener/thread_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from slack_bolt.lazy_listener import LazyListenerRunner
from slack_bolt.listener import Listener
from slack_bolt.listener.listener_start_handler import ListenerStartHandler
from slack_bolt.listener.listener_completion_handler import ListenerCompletionHandler
from slack_bolt.listener.listener_error_handler import ListenerErrorHandler
from slack_bolt.logger.messages import (
Expand All @@ -21,6 +22,7 @@ class ThreadListenerRunner:
logger: Logger
process_before_response: bool
listener_error_handler: ListenerErrorHandler
listener_start_handler: ListenerStartHandler
listener_completion_handler: ListenerCompletionHandler
listener_executor: Executor
lazy_listener_runner: LazyListenerRunner
Expand All @@ -30,13 +32,15 @@ def __init__(
logger: Logger,
process_before_response: bool,
listener_error_handler: ListenerErrorHandler,
listener_start_handler: ListenerStartHandler,
listener_completion_handler: ListenerCompletionHandler,
listener_executor: Executor,
lazy_listener_runner: LazyListenerRunner,
):
self.logger = logger
self.process_before_response = process_before_response
self.listener_error_handler = listener_error_handler
self.listener_start_handler = listener_start_handler
self.listener_completion_handler = listener_completion_handler
self.listener_executor = listener_executor
self.lazy_listener_runner = lazy_listener_runner
Expand All @@ -54,6 +58,10 @@ def run( # type: ignore
if self.process_before_response:
if not request.lazy_only:
try:
self.listener_start_handler.handle(
request=request,
response=response,
)
returned_value = listener.run_ack_function(
request=request, response=response
)
Expand Down Expand Up @@ -109,6 +117,10 @@ def run( # type: ignore
def run_ack_function_asynchronously():
nonlocal ack, request, response
try:
self.listener_start_handler.handle(
request=request,
response=response,
)
listener.run_ack_function(request=request, response=response)
except Exception as e:
# The default response status code is 500 in this case.
Expand Down
2 changes: 1 addition & 1 deletion slack_bolt/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Check the latest version at https://pypi.org/project/slack-bolt/"""
__version__ = "1.9.4"
__version__ = "1.10.0a"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll bump the minor version as this pull request introduces new functionalities