Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(extension): Enable Async Gunicorn workers for Flask and Django e… #747

Merged
Merged
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
c0d3102
Feat(extension): Enable Async Gunicorn workers for Flask and Django e…
alithethird Nov 8, 2024
009fe4c
chore(lint): Ran static checkers
alithethird Nov 8, 2024
a6d156f
chore(): Fix flask cli unit test
alithethird Nov 8, 2024
408db1d
chore(docs): Docs lint
alithethird Nov 8, 2024
39671e1
chore(docs): Improved wordlist
alithethird Nov 8, 2024
4220cd7
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 15, 2024
8d88e8c
Chore(): Update docs, update cli init test, update `rockcraft.yaml` f…
alithethird Nov 19, 2024
fbd636c
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 19, 2024
3280aa6
Chore(): Try to make linter happy
alithethird Nov 19, 2024
c9fab57
Chore(docs): Lint docs
alithethird Nov 19, 2024
a1df1b5
Chore(docs): Update async doc link
alithethird Nov 20, 2024
8446f7e
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 20, 2024
0ea1838
Run CI
alithethird Nov 20, 2024
a8e14d2
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 20, 2024
99982f8
Chore(Docs): Small doc improvements
alithethird Nov 26, 2024
a645957
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 26, 2024
f35db9e
Chore(lint): Doc lint
alithethird Nov 26, 2024
b433599
XMerge branch 'flask-django-extention-async-workers' of github.com:al…
alithethird Nov 26, 2024
56979d4
Chore(docs): Add words to list
alithethird Nov 26, 2024
7c57358
Chore(): Undone import format
alithethird Nov 26, 2024
5b98290
Chore(): Addressed comments.
alithethird Nov 27, 2024
dbd88b3
Chore(): Consistent spacing
alithethird Nov 28, 2024
9338ad9
Chore(Format): Format imports
alithethird Nov 28, 2024
69fc1c1
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 28, 2024
7935d3b
Chore(Lint): Add word
alithethird Nov 28, 2024
8837f79
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 28, 2024
2665251
Chore(): Update docs, update gunicorn conf for async
alithethird Nov 28, 2024
415807d
Chore(lint): Format
alithethird Nov 28, 2024
f8e9488
Chore(tests): Fix tests
alithethird Nov 28, 2024
c67ea0b
Chore(docs): Fix refs
alithethird Nov 28, 2024
41e4ce9
Chore(docs): Update refs
alithethird Nov 28, 2024
da397aa
Chore(): Updated docs, async conf file
alithethird Nov 29, 2024
071fa2e
Chore(docs): Fix lint
alithethird Nov 29, 2024
b1e4835
Chore(): Add a spread test for async
alithethird Nov 29, 2024
e6b7083
Chore(test): Removed unnecessary fixture
alithethird Nov 29, 2024
7d5b613
Chore(): Format
alithethird Nov 29, 2024
dd95620
Merge main
alithethird Nov 29, 2024
7f35f80
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 29, 2024
1c2bf63
Chore(test): Simplify test and fix typo
alithethird Nov 29, 2024
e00dacd
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 29, 2024
b2086af
Chore(): Improve code
alithethird Nov 29, 2024
dad6563
Chore(lint): Make linter happy
alithethird Nov 29, 2024
69f740f
Chore(): Make linter happier
alithethird Nov 29, 2024
ab3a635
Merge branch 'main' into flask-django-extention-async-workers
alithethird Nov 29, 2024
b2cd5dc
Chore(test): Fix unit test
alithethird Nov 29, 2024
e82b75e
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Nov 29, 2024
aaf21a5
Chore(lint): Make Mypy happy
alithethird Nov 29, 2024
27ce02d
Chore(): Fix shell lint
alithethird Nov 29, 2024
5f58c54
Chore(test): Fix test comment
alithethird Dec 2, 2024
8e9fd49
Chore(test): Add async flask/django tests. Created test dir
alithethird Dec 3, 2024
6eef44a
Chore(lint): Run autoformat
alithethird Dec 3, 2024
0b2be19
Chore(test): Ignore import errors in django data
alithethird Dec 3, 2024
757defc
Chore(test): Fix cli test
alithethird Dec 3, 2024
47b4136
Chore(format): format
alithethird Dec 3, 2024
efb4e23
Chore(): Change async flask implementation
alithethird Dec 4, 2024
16cd05b
Chore(test): Fix spread test
alithethird Dec 4, 2024
c7dbf84
Chore(): Update docs and spread test
alithethird Dec 4, 2024
22d18ea
Chore(test): Removed unnecessary django files
alithethird Dec 4, 2024
a6aaacf
Chore(doc): Lint docs
alithethird Dec 4, 2024
9e0efd1
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
fc2caf1
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
dbade98
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
517532f
Update docs/reference/extensions/django-framework.rst
alithethird Dec 5, 2024
46f82f8
Chore(): Update gevent install check
alithethird Dec 5, 2024
3b4602f
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 5, 2024
6fb5bfd
Chore(docs): Style update
alithethird Dec 5, 2024
9a25b90
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 5, 2024
e6e6d8d
Chore(): Updated pip package check
alithethird Dec 5, 2024
722e5ff
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 5, 2024
eac7d06
Chore(Deps): Add `packaging` dependency
alithethird Dec 5, 2024
18b16fc
Chore(lint): Format code
alithethird Dec 5, 2024
6bc31bb
Chore(format): Format
alithethird Dec 5, 2024
9a6dc45
Chore(docs): Format doc
alithethird Dec 5, 2024
6956da0
Chore(): Fix spread test, change async check
alithethird Dec 5, 2024
537a19d
Chore(): Except specific exception
alithethird Dec 5, 2024
b4ae759
Chore(): Uncomment restore
alithethird Dec 5, 2024
6b6f00c
Update docs/reference/extensions/django-framework.rst
alithethird Dec 9, 2024
4dc5791
Update docs/reference/extensions/flask-framework.rst
alithethird Dec 9, 2024
7612690
Chore(): Addressed comments. Add comment to test_cli.py. fix doc issu…
alithethird Dec 9, 2024
b993abb
Chore(test): Add unit async gunicorn tests
alithethird Dec 9, 2024
508afc0
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 9, 2024
aeee726
Chore(lint): Format code
alithethird Dec 9, 2024
e6a8d44
Chore(): Changed copy_tree() to shutil.copytree()
alithethird Dec 10, 2024
ae05f6d
chore(doc): Fix doc format
alithethird Dec 10, 2024
cdd9134
chore(doc): Format
alithethird Dec 10, 2024
6aba111
chore(doc): Format
alithethird Dec 10, 2024
9846735
chore(doc): Format
alithethird Dec 10, 2024
5617d29
chore(doc): Format
alithethird Dec 10, 2024
b6550ef
chore(test): Update tests
alithethird Dec 10, 2024
3b214dc
chore(doc): Change testing port in doc
alithethird Dec 11, 2024
ead9f9c
chore(update): Update statsd-exporter version in flask/django plugin
alithethird Dec 13, 2024
8454a69
Merge branch 'flask-django-extention-async-workers' of github.com:ali…
alithethird Dec 13, 2024
97e8bcb
chore(): Revert version change in statsd
alithethird Dec 13, 2024
ec5db1e
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 16, 2024
d517d98
chore(doc): Update async docs
alithethird Dec 17, 2024
75f95c1
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 17, 2024
2b4a3bb
Update docs/reference/extensions/django-framework.rst
alithethird Dec 18, 2024
1321bd2
Update docs/reference/extensions/flask-framework.rst
alithethird Dec 18, 2024
cae3f7d
Merge branch 'main' into flask-django-extention-async-workers
alithethird Dec 18, 2024
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
4 changes: 4 additions & 0 deletions docs/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ AMD
amd
ARGS
ASGI
async
Autotools
autotools
boolean
Expand Down Expand Up @@ -44,10 +45,12 @@ filesystems
filetree
fs
gc
gevent
GiB
GID
github
GPG
gunicorn
Gunicorn
gzipped
hardcoded
Expand Down Expand Up @@ -144,6 +147,7 @@ triaged
ubuntu
unbuilt
UID
uncomment
usrmerge
Uvicorn
VENV
Expand Down
30 changes: 30 additions & 0 deletions docs/reference/extensions/django-framework.rst
alithethird marked this conversation as resolved.
Show resolved Hide resolved
alithethird marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ server metrics.
The Django extension is compatible with the ``bare``, ``[email protected]``
and ``[email protected]`` bases.

The Django extension supports both synchronous and asynchronous
Gunicorn workers. If you want asynchronous workers, you have to add
the ``gevent`` package to the ``requirements.txt`` file.
Read more :ref:`Using asynchronous Gunicorn workers <django-async-gunicorn-workers>`.

Project requirements
====================

Expand All @@ -29,6 +34,7 @@ There are 2 requirements to be able to use the ``django-framework`` extension:
``./<Rock name with - replaced by _>/<Rock name with - replaced by _>/manage.py``
relative to the ``rockcraft.yaml`` file.


``parts`` > ``django-framework/dependencies:`` > ``stage-packages``
===================================================================

Expand All @@ -43,6 +49,30 @@ application. In the following example we use it to specify ``libpq-dev``:
# list required packages or slices for your Django application below.
- libpq-dev

.. _django-async-gunicorn-workers:

Using asynchronous Gunicorn workers
===================================

If you want to use asynchronous workers, you have to add the ``gevent`` package
to the ``requirements.txt`` file. Rockcraft automatically detects this and
updates the Pebble plan to use the asynchronous workers. If you have ``gevent``
installed in your rock but decided to use ``sync`` workers instead you can do
so by using the ``--args`` parameter of ``docker run``:

.. code-block:: shell
:caption: Use sync workers instead of gevent
$ docker run \
--name django-container \
-d -p 8138:8000 \
django-image:1.0 \
--args django sync


.. note::
The Django extension is compatible with the ``bare``, ``[email protected]`` and
``[email protected]`` bases.
alithethird marked this conversation as resolved.
Show resolved Hide resolved

Useful links
============

Expand Down
25 changes: 25 additions & 0 deletions docs/reference/extensions/flask-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ server metrics.
The Flask extension is compatible with the ``bare``, ``[email protected]``
and ``[email protected]`` bases.

The Flask extension supports both synchronous and asynchronous
Gunicorn workers. If you want asynchronous workers, you have to add
``gevent`` package to the ``requirements.txt`` file.
Read more :ref:`Using asynchronous Gunicorn workers <flask-async-gunicorn-workers>`.

Project requirements
====================

Expand All @@ -41,6 +46,26 @@ application. In the following example we use it to specify ``libpq-dev``:
# list required packages or slices for your flask app below.
- libpq-dev

.. _flask-async-gunicorn-workers:

Using asynchronous Gunicorn workers
===================================

If you want to use asynchronous workers, you have to add ``gevent`` package to
the ``requirements.txt`` file. Rockcraft automatically detects this and updates
the pebble plan to use the asynchronous workers. If you have ``gevent``
installed in your rock but decided to use ``sync`` workers instead you can use
the ``--args`` parameter of ``docker run`` to use ``sync`` workers instead of the
default ``gevent``:

.. code-block:: shell
:caption: Use sync workers instead of gevent
$ docker run \
--name flask-container \
-d -p 8138:8000 \
flask-image:1.0 \
--args flask sync

``parts`` > ``flask-framework/install-app`` > ``prime``
=======================================================

Expand Down
16 changes: 15 additions & 1 deletion rockcraft/extensions/gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Any

from overrides import override
from packaging.requirements import InvalidRequirement, Requirement

from ..errors import ExtensionError
from ._python_utils import has_global_variable
Expand Down Expand Up @@ -112,6 +113,19 @@ def _gen_parts(self) -> dict:
}
return parts

def _check_async(self) -> str:
"""Check if gevent package installed in requirements.txt."""
requirements_file = self.project_root / "requirements.txt"
requirements_text = requirements_file.read_text()
for line in requirements_text.splitlines():
try:
req = Requirement(line)
if req.name == "gevent":
return "gevent"
except InvalidRequirement:
pass
return "sync"

@override
def get_root_snippet(self) -> dict[str, Any]:
"""Fill in some default root components.
Expand All @@ -130,7 +144,7 @@ def get_root_snippet(self) -> dict[str, Any]:
self.framework: {
"override": "replace",
"startup": "enabled",
"command": f"/bin/python3 -m gunicorn -c /{self.framework}/gunicorn.conf.py {self.wsgi_path}",
"command": f"/bin/python3 -m gunicorn -c /{self.framework}/gunicorn.conf.py {self.wsgi_path} -k [ {self._check_async()} ]",
"after": ["statsd-exporter"],
"user": "_daemon_",
},
Expand Down
2 changes: 1 addition & 1 deletion rockcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ def load_project(filename: Path) -> dict[str, Any]:
msg = err.strerror or "unknown"
if err.filename:
msg = f"{msg}: {err.filename!r}."
raise ProjectLoadError(msg) from err
raise ProjectLoadError(str(msg)) from err

return transform_yaml(filename.parent, yaml_data)

Expand Down
43 changes: 43 additions & 0 deletions tests/data/django/expected_rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: test-name
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Django application
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 79 char long summary
description: |
This is test-name's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:

# to ensure the django-framework extension functions properly, your Django project
# should have a structure similar to the following with ./test_name/test_name/wsgi.py
# being the WSGI entry point and contain an application object.
# +-- test_name
# | |-- test_name
# | | |-- wsgi.py
# | | +-- ...
# | |-- manage.py
# | |-- migrate.sh
# | +-- some_app
# | |-- views.py
# | +-- ...
# |-- requirements.txt
# +-- rockcraft.yaml

extensions:
- django-framework

# uncomment the sections you need and adjust according to your requirements.
# parts:
# django-framework/dependencies:
# stage-packages:
# # list required packages or slices for your Django application below.
# - libpq-dev
1 change: 1 addition & 0 deletions tests/data/django/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Django
7 changes: 7 additions & 0 deletions tests/data/django/test_name/test_name/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os

# Rockcraft repo doesn't need Django installed so ignore module-not-found mypy error
from django.core.wsgi import get_wsgi_application # type: ignore

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_app.settings")
application = get_wsgi_application()
1 change: 1 addition & 0 deletions tests/data/flask/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app = object()
71 changes: 71 additions & 0 deletions tests/data/flask/expected_rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: test-name
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Flask application
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your Flask application # 79 char long summary
description: |
This is test-name's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:

# to ensure the flask-framework extension works properly, your Flask application
# should have an `app.py` file with an `app` object as the WSGI entrypoint.
# a `requirements.txt` file with at least the flask package should also exist.
# see https://documentation.ubuntu.com/rockcraft/en/latest/reference/extensions/flask-framework
# for more information.
extensions:
- flask-framework

# uncomment the sections you need and adjust according to your requirements.
# parts: # you need to uncomment this line to add or update any part.

# flask-framework/install-app:
# prime:
# # by default, only the files in app/, templates/, static/, migrate, migrate.sh,
# # migrate.py and app.py are copied into the image. You can modify the list
# # below to override the default list and include or exclude specific
# # files/directories in your project.
# # note: prefix each entry with "flask/app/" followed by the local path.
# - flask/app/.env
# - flask/app/app.py
# - flask/app/webapp
# - flask/app/templates
# - flask/app/static

# you may need Ubuntu packages to build a python dependency. Add them here if necessary.
# flask-framework/dependencies:
# build-packages:
# # for example, if you need pkg-config and libxmlsec1-dev to build one
# # of your packages:
# - pkg-config
# - libxmlsec1-dev

# you can add package slices or Debian packages to the image.
# package slices are subsets of Debian packages, which result
# in smaller and more secure images.
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/chisel/

# add this part if you want to add packages slices to your image.
# you can find a list of packages slices at https://github.com/canonical/chisel-releases
# runtime-slices:
# plugin: nil
# stage-packages:
# # list the required package slices for your flask application below.
# # for example, for the slice libs of libpq5:
# - libpq5_libs

# if you want to add a Debian package to your image, add the next part
# runtime-debs:
# plugin: nil
# stage-packages:
# # list required Debian packages for your flask application below.
# - libpq5
1 change: 1 addition & 0 deletions tests/data/flask/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask
1 change: 1 addition & 0 deletions tests/spread/rockcraft/extension-flask-async/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
16 changes: 16 additions & 0 deletions tests/spread/rockcraft/extension-flask-async/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from time import sleep

from flask import Flask # pyright: ignore[reportMissingImports]

app = Flask(__name__)


@app.route("/")
def ok():
return "ok"


@app.route("/io")
def pseudo_io():
sleep(2)
return "ok"
Empty file.
2 changes: 2 additions & 0 deletions tests/spread/rockcraft/extension-flask-async/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask
gevent
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("hello")
Loading
Loading