Skip to content

Commit

Permalink
Run the app as ASGI
Browse files Browse the repository at this point in the history
  • Loading branch information
sarayourfriend committed Nov 2, 2023
1 parent 374a93e commit 9e0e893
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 98 deletions.
3 changes: 1 addition & 2 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@ EXPOSE 8000
# Wait for ES to accept connections
ENTRYPOINT ["./run.sh"]

# gunicorn configuration in gunicorn.conf.py
CMD ["gunicorn"]
CMD ["python", "run.py"]

#########
# NGINX #
Expand Down
3 changes: 1 addition & 2 deletions api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@ drf-spectacular = "*"
elasticsearch = "==8.10.1"
elasticsearch-dsl = "~=8.9"
future = "~=0.18"
gunicorn = "~=21.2"
limit = "~=0.2"
Pillow = "~=10.1.0"
python-decouple = "~=3.8"
python-xmp-toolkit = "~=2.0"
sentry-sdk = "~=1.30"
django-split-settings = "*"
uvloop = "~=0.17"
psycopg = "~=3.1"
uvicorn = {extras = ["standard"], version = "~=0.23"}

[requires]
python_version = "3.11"
297 changes: 252 additions & 45 deletions api/Pipfile.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions api/conf/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os

import django

from conf.asgi_handler import OpenverseASGIHandler


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


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


application = get_asgi_application()
75 changes: 75 additions & 0 deletions api/conf/asgi_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import asyncio
import inspect
import logging
import weakref
from collections.abc import Callable

from django.core.handlers.asgi import ASGIHandler


parent_logger = logging.getLogger(__name__)


class OpenverseASGIHandler(ASGIHandler):
"""
Extend default ASGIHandler to implement lifetime hooks.
Handlers are registered by calling `register_shutdown_handler`.
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.
Asynchronous handlers are automatically supported via `async_to_sync`
and do not need special consideration at registration time.
"""

logger = parent_logger.getChild("OpenverseASGIHandler")

def __init__(self):
super().__init__()
self._on_shutdown: list[weakref.WeakMethod | weakref.ref] = []

def _clean_ref(self, ref):
self.logger.info("Cleaning up a ref")
self._on_shutdown.remove(ref)

def register_shutdown_handler(self, handler: Callable[[], None]):
"""Register an individual shutdown handler."""
if inspect.ismethod(handler):
ref = weakref.WeakMethod(handler, self._clean_ref)
else:
ref = weakref.ref(handler, self._clean_ref)

self._on_shutdown.append(ref)

async def __call__(self, scope, receive, send):
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})

elif message["type"] == "lifespan.shutdown":
await self.shutdown()
await send({"type": "lifespan.shutdown.complete"})
return

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

async def shutdown(self):
live_handlers = 0

for handler_ref in self._on_shutdown:
if not (handler := handler_ref()):
self.logger.debug("Reference dead, skipping handler")
continue

live_handlers += 1

if asyncio.iscoroutinefunction(handler):
await handler()
else:
handler()

self.logger.info(f"Executed {live_handlers} handler(s) before shutdown.")
2 changes: 0 additions & 2 deletions api/conf/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@
},
]

WSGI_APPLICATION = "conf.wsgi.application"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

Expand Down
46 changes: 0 additions & 46 deletions api/gunicorn.conf.py

This file was deleted.

21 changes: 21 additions & 0 deletions api/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

import uvicorn


if __name__ == "__main__":
uvicorn.run(
"conf.asgi:application",
host="0.0.0.0",
port=8000,
reload=os.getenv("ENVIRONMENT") == "local",
log_level="debug",
log_config={
"version": 1,
"formatters": {
"generic": {
"format": "[%(asctime)s - %(name)s - %(lineno)3d][%(levelname)s] %(message)s", # noqa: E501
},
},
},
)
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,6 @@ services:
- api/.env
stdin_open: true
tty: true
command: gunicorn --reload -w 1

ingestion_server:
profiles:
Expand Down

0 comments on commit 9e0e893

Please sign in to comment.