From eb0e7609283023f4f25fd56a4a7bc3a482e2bee5 Mon Sep 17 00:00:00 2001 From: Jonathan Moss Date: Wed, 15 Jan 2025 15:38:36 +1100 Subject: [PATCH] Remove Gunicorn Uvicorn now has process management, so we can use that directly and remove the need for Gunicorn. I also took the opportunity to update the default logging so it pretty closely matches the default logging format used by uvicorn. This prevents the jarring changed when the Django logging is configured. --- docs/decisions/0002-gunicorn.md | 2 +- docs/decisions/0004-no-gunicorn.md | 36 +++++++++ template/Dockerfile | 3 - template/poetry.lock | 90 +++++++++++++++------- template/pyproject.toml | 4 +- template/src/entrypoint.sh | 20 ++++- template/src/gunicorn.conf.py | 13 ---- template/src/project_name/main/settings.py | 12 ++- 8 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 docs/decisions/0004-no-gunicorn.md delete mode 100644 template/src/gunicorn.conf.py diff --git a/docs/decisions/0002-gunicorn.md b/docs/decisions/0002-gunicorn.md index db38acb..6b55610 100644 --- a/docs/decisions/0002-gunicorn.md +++ b/docs/decisions/0002-gunicorn.md @@ -2,7 +2,7 @@ - Date: 2024-02-09 - Author(s): [Jonathan Moss][jmoss] -- Status: `Active` +- Status: `Obsolete` ## Decision diff --git a/docs/decisions/0004-no-gunicorn.md b/docs/decisions/0004-no-gunicorn.md new file mode 100644 index 0000000..2fd9ffe --- /dev/null +++ b/docs/decisions/0004-no-gunicorn.md @@ -0,0 +1,36 @@ +# 0002 Remove Gunicorn as a Process Manager + +- Date: 2025-01-015 +- Author(s): [Jonathan Moss][jmoss] +- Status: `Active` + +## Decision + +[Uvicorn][uvicorn] now has built in support for process management, so we no longer need +[Gunicorn][gunicorn] as well. + +## Context + +An ASGI runtime is needed to support asyncio operations in Django. We currently use +Uvicorn for this. Previously Uvicorn ran as a single process and it was recommended that +Gunicorn was used as a process manager, configured to use Uvicorn purely for the +workers. This is no longer the case, as of release [0.30.0][release] Uvicorn now has +built in process management support. + +## Implications + +This removes one of the moving part of our stack, reducing the over all number of +packages we need to understand and keep up to date. + +Whilst some benchmarking has been performed - it is not exhaustive. The general results +however show a comparable level of throughput to what we had with the previous gunicorn +setup. + + +[jmoss]: mailto:jonathan.moss@ackama.com +[uvicorn]: https://www.uvicorn.org/ +[gunicorn]: https://gunicorn.org/ +[release]: https://www.uvicorn.org/release-notes/#0300-2024-05-28 + + +*[ASGI]: Asynchronous Server Gateway Interface diff --git a/template/Dockerfile b/template/Dockerfile index 00bf629..c1fa404 100644 --- a/template/Dockerfile +++ b/template/Dockerfile @@ -60,9 +60,6 @@ RUN apt-get update && apt-get install -y \ COPY --from=builder /venv /venv -# Copy Gunicorn configs -COPY --chown=runtime:runtime src/gunicorn.conf.py /gunicorn.conf.py - # _activate_ the virtual environment ENV VIRTUAL_ENV=/venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" diff --git a/template/poetry.lock b/template/poetry.lock index 8c44917..1e3948a 100644 --- a/template/poetry.lock +++ b/template/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "asgiref" @@ -1000,27 +1000,6 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] -[[package]] -name = "gunicorn" -version = "22.0.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.7" -files = [ - {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, - {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - [[package]] name = "h11" version = "0.14.0" @@ -1032,6 +1011,61 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + [[package]] name = "idna" version = "3.10" @@ -2776,13 +2810,13 @@ dev = ["black", "pytest"] [[package]] name = "uvicorn" -version = "0.24.0.post1" +version = "0.34.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, - {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, ] [package.dependencies] @@ -2790,7 +2824,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -2985,4 +3019,4 @@ test = ["pytest"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e281e184efa4844315c151b2df395258a6e300a5e82450c9c28f5a5f4c958809" +content-hash = "caba351a2b358dae99e5a365af30caf898f209c544ff55c4c279af15413f59b3" diff --git a/template/pyproject.toml b/template/pyproject.toml index 7a86677..4d40917 100644 --- a/template/pyproject.toml +++ b/template/pyproject.toml @@ -14,10 +14,10 @@ django = "^5.1" django-environ = "^0.11.2" psycopg = "^3.1.12" python = "^3.12" -uvicorn = "^0.24.0" +uvicorn = "^0.34.0" uvloop = { version = "^0.19.0", markers = "sys_platform != 'win32'" } -gunicorn = "^22.0.0" sentry-sdk = "^2.15.0" +httptools = "^0.6.4" [tool.poetry.group.dev.dependencies] bandit = "^1.7.4" diff --git a/template/src/entrypoint.sh b/template/src/entrypoint.sh index 67aa1c8..b23b35d 100644 --- a/template/src/entrypoint.sh +++ b/template/src/entrypoint.sh @@ -1,13 +1,29 @@ #! /bin/bash set -e +UVICORN_LOG_LEVEL="${UVICORN_LOG_LEVEL:-info}" +UVICORN_WORKERS="${UVICORN_WORKERS:-4}" + +serve () { + exec uvicorn \ + --workers="$UVICORN_WORKERS" \ + --lifespan=off \ + --host=0.0.0.0 \ + --port=8000 \ + --ws=none \ + --no-use-colors \ + --access-log \ + --log-level="$UVICORN_LOG_LEVEL" \ + {{ project_name }}.main.asgi:application +} + if [ -z "$1" ] # nothing specified so we bootstrap the service itself then manage migrate --noinput - exec gunicorn -c gunicorn.conf.py {{ project_name }}.main.asgi:application + serve elif [ "$1" = "run" ] # run the service only then - exec gunicorn -c gunicorn.conf.py {{ project_name }}.main.asgi:application + serve elif [ "$1" = "migrate" ] # run database migrations then exec manage migrate --noinput "${@:2}" diff --git a/template/src/gunicorn.conf.py b/template/src/gunicorn.conf.py deleted file mode 100644 index ebb91d6..0000000 --- a/template/src/gunicorn.conf.py +++ /dev/null @@ -1,13 +0,0 @@ -GUNICORN_LOG_LEVEL = "info" - -# Server Socket -bind = "0.0.0.0:8000" - -# Worker Processes -workers = 4 -worker_class = "uvicorn.workers.UvicornWorker" - -# Logging -loglevel = GUNICORN_LOG_LEVEL -accesslog = "-" # Log to stdout -errorlog = "-" # Log to stderr diff --git a/template/src/project_name/main/settings.py b/template/src/project_name/main/settings.py index 3901866..785f6ea 100644 --- a/template/src/project_name/main/settings.py +++ b/template/src/project_name/main/settings.py @@ -132,17 +132,23 @@ LOGGING = { "version": 1, "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "{levelname:<9s} {name} {asctime} {message}", + "style": "{", + }, + }, "handlers": { "console": { - "level": LOG_LEVEL, "class": "logging.StreamHandler", + "formatter": "standard", }, }, "loggers": { - "django": { + "{{ project_name }}": { "handlers": ["console"], "level": LOG_LEVEL, - "propagate": True, + "propagate": False, }, }, }