Skip to content

Commit

Permalink
Use clearer middleware pattern and consistent app export name
Browse files Browse the repository at this point in the history
  • Loading branch information
sarayourfriend committed Nov 17, 2023
1 parent 8f42022 commit d198c1b
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 33 deletions.
4 changes: 2 additions & 2 deletions api/api/utils/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import aiohttp

from conf.asgi import application
from conf.asgi import APPLICATION_LIFECYCLE


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -56,7 +56,7 @@ async def get_aiohttp_session() -> aiohttp.ClientSession:

if create_session:
session = aiohttp.ClientSession()
application.register_shutdown_handler(session.close)
APPLICATION_LIFECYCLE.register_shutdown_handler(session.close)
_SESSIONS[loop] = session

return _SESSIONS[loop]
13 changes: 4 additions & 9 deletions api/conf/asgi.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import os

import django
from django.conf import settings
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from django.core.asgi import get_asgi_application

from conf.asgi_handler import OpenverseASGIHandler
from conf.lifecycle_handler import ASGILifecycleHandler


os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings")


def get_asgi_application():
django.setup(set_prefix=False)
return OpenverseASGIHandler()


application = get_asgi_application()
application = APPLICATION_LIFECYCLE = ASGILifecycleHandler(get_asgi_application())


if settings.ENVIRONMENT == "local":
static_files_application = ASGIStaticFilesHandler(application)
application = ASGIStaticFilesHandler(application)


if settings.GC_DEBUG_LOGGING:
Expand Down
36 changes: 23 additions & 13 deletions api/conf/asgi_handler.py → api/conf/lifecycle_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,34 @@

from django.core.handlers.asgi import ASGIHandler

import sentry_sdk


parent_logger = logging.getLogger(__name__)


class OpenverseASGIHandler(ASGIHandler):
class ASGILifecycleHandler:
"""
Extend default ASGIHandler to implement lifetime hooks.
Handle ASGI lifecycle messages.
Django's ASGIHandler does not handle these messages,
so we have to implement it ourselves. Only shutdown handlers
are currently supported.
Handlers are registered by calling `register_shutdown_handler`.
The class maintains only weak references to handler functions
Register shutdown handlers using the `register_shutdown_handler`
method. The class maintains only weak references to handler functions
and methods to prevent memory leaks. This removes the need
for explicit deregistration of handlers if, for example, their associated
objects (if a method) or contexts are garbage collected.
contexts are garbage collected.
Asynchronous handlers are automatically supported via `async_to_sync`
and do not need special consideration at registration time.
"""

logger = parent_logger.getChild("OpenverseASGIHandler")
logger = parent_logger.getChild("ASGILifecycleHandler")

def __init__(self):
super().__init__()
def __init__(self, app: ASGIHandler):
self.app = app
self._on_shutdown: list[weakref.WeakMethod | weakref.ref] = []
self.has_shutdown = False

Expand Down Expand Up @@ -59,7 +65,7 @@ async def __call__(self, scope, receive, send):
await send({"type": "lifespan.shutdown.complete"})
return

await super().__call__(scope, receive, send)
await self.app(scope, receive, send)

async def shutdown(self):
live_handlers = 0
Expand All @@ -72,10 +78,14 @@ async def shutdown(self):

live_handlers += 1

if asyncio.iscoroutinefunction(handler):
await handler()
else:
handler()
try:
if asyncio.iscoroutinefunction(handler):
await handler()
else:
handler()
except Exception as exc:
sentry_sdk.capture_exception(exc)
self.logger.error(f"Handler {repr(handler)} raised exception.")

self.logger.info(f"Executed {live_handlers} handler(s) before shutdown.")
self.has_shutdown = True
4 changes: 1 addition & 3 deletions api/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
if __name__ == "__main__":
is_local = os.getenv("ENVIRONMENT") == "local"

app = "conf.asgi:static_files_application" if is_local else "conf.asgi:application"

uvicorn.run(
app,
"conf.asgi:application",
host="0.0.0.0",
port=8000,
workers=1,
Expand Down
4 changes: 2 additions & 2 deletions api/test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from asgiref.sync import async_to_sync

from conf.asgi import application
from conf.asgi import APPLICATION_LIFECYCLE


@pytest.fixture(scope="session", autouse=True)
Expand All @@ -16,4 +16,4 @@ def call_application_shutdown():
fine here, so it's not a problem.
"""
yield
async_to_sync(application.shutdown)()
async_to_sync(APPLICATION_LIFECYCLE.shutdown)()
8 changes: 4 additions & 4 deletions api/test/unit/utils/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from asgiref.sync import async_to_sync

from api.utils.aiohttp import get_aiohttp_session
from conf.asgi import application
from conf.asgi import APPLICATION_LIFECYCLE


@pytest.fixture(autouse=True)
Expand All @@ -18,7 +18,7 @@ def _get_new_loop():

yield _get_new_loop

async_to_sync(application.shutdown)()
async_to_sync(APPLICATION_LIFECYCLE.shutdown)()
for loop in loops:
loop.close()

Expand Down Expand Up @@ -56,9 +56,9 @@ def test_multiple_loops_reuse_separate_sessions(get_new_loop):


def test_registers_shutdown_cleanup(get_new_loop):
assert len(application._on_shutdown) == 0
assert len(APPLICATION_LIFECYCLE._on_shutdown) == 0

loop = get_new_loop()
loop.run_until_complete(get_aiohttp_session())

assert len(application._on_shutdown) == 1
assert len(APPLICATION_LIFECYCLE._on_shutdown) == 1

0 comments on commit d198c1b

Please sign in to comment.