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): Added option to enable async workers in Flask and Django #1986

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
69c697e
Feat(extension): Added option to enable async workers in Flask and Dj…
alithethird Nov 12, 2024
3e26ed8
Merge branch 'main' into flask-async-worker
alithethird Nov 15, 2024
a4d82c0
Chore(doc): Add Spread test/tutorial doc
alithethird Dec 2, 2024
d9aa901
Merge branch 'flask-async-worker' of https://github.com/alithethird/c…
alithethird Dec 2, 2024
33c8796
Merge branch 'main' into flask-async-worker
alithethird Dec 2, 2024
7fa50ec
Chore(docs): Lint docs
alithethird Dec 2, 2024
9657bc3
Merge branch 'flask-async-worker' of https://github.com/alithethird/c…
alithethird Dec 2, 2024
364e3cc
Run CI
alithethird Dec 2, 2024
a5c872f
Chore(test): Fix spread test
alithethird Dec 3, 2024
5000827
Chore(test): Fix charmcraft version in spread test
alithethird Dec 3, 2024
5d7739e
Chore(docs): Changed tutorial to how-to. Updated spread test.
alithethird Dec 9, 2024
0a83b2a
Chore(): Fix spread test
alithethird Dec 9, 2024
d0176be
Merge branch 'main' into flask-async-worker
alithethird Dec 9, 2024
a69f8db
Chore(): Change microk8s version in spread test
alithethird Dec 9, 2024
b8e39a6
chore(doc): Applied comments
alithethird Dec 11, 2024
cee3cb4
Merge branch 'main' into flask-async-worker
alithethird Dec 12, 2024
812c906
Merge branch 'main' into flask-async-worker
alithethird Dec 17, 2024
e9e240f
Merge branch 'main' into flask-async-worker
alithethird Dec 18, 2024
5e7a762
Merge branch 'main' into flask-async-worker
alithethird Dec 19, 2024
40eaa15
chore(test): Update spread test to use rockcraft latest/edge
alithethird Dec 19, 2024
a78aa82
Merge branch 'flask-async-worker' of https://github.com/alithethird/c…
alithethird Dec 19, 2024
0852a04
chore(doc): Update howto
alithethird Dec 19, 2024
361d165
chore(doc): Update docs
alithethird Dec 20, 2024
12640ab
Merge branch 'main' into flask-async-worker
alithethird Dec 20, 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 charmcraft/extensions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ def get_image_name(self) -> str:
"type": "int",
"description": "The number of webserver worker processes for handling requests.",
},
"webserver-worker-class": {
"type": "string",
"description": "The method of webserver worker processes for handling requests. Can be either 'gevent' or 'sync'.",
},
}


Expand Down
19 changes: 19 additions & 0 deletions docs/howto/code/flask-async/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from time import sleep

import flask

app = flask.Flask(__name__)


@app.route("/")
def index():
return "Hello, world!\n"


@app.route("/io")
def pseudo_io():
sleep(2)
return "ok\n"

if __name__ == "__main__":
app.run()
2 changes: 2 additions & 0 deletions docs/howto/code/flask-async/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask
gevent
182 changes: 182 additions & 0 deletions docs/howto/code/flask-async/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
###########################################
# IMPORTANT
# Comments matter!
# The docs use the wrapping comments as
# markers for including said instructions
# as snippets in the docs.
###########################################
summary: How to create async Flask Charm

kill-timeout: 90m

environment:

execute: |
# Move everything to $HOME so that Juju deployment works
mv *.yaml *.py *.txt $HOME
cd $HOME

# Don't use the staging store for this test
unset CHARMCRAFT_STORE_API_URL
unset CHARMCRAFT_UPLOAD_URL
unset CHARMCRAFT_REGISTRY_URL

# Add setup instructions
snap install rockcraft --channel=latest/edge --classic

snap install microk8s --channel=1.31-strict/stable
snap install juju --channel=3/stable

mkdir -p ~/.local/share

# MicroK8s config setup
microk8s status --wait-ready
microk8s enable hostpath-storage
microk8s enable registry
microk8s enable ingress

# Bootstrap controller
juju bootstrap microk8s dev-controller

cd $HOME
# [docs:create-venv]
sudo apt-get update && sudo apt-get install python3-venv -y
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# [docs:create-venv-end]

flask run -p 8000 &
retry -n 5 --wait 2 curl --fail localhost:8000

# [docs:curl-flask]
curl localhost:8000
# [docs:curl-flask-end]

# [docs:curl-flask-async-app]
curl localhost:8000/io
# [docs:curl-flask-async-app-end]

kill $!

# [docs:create-rockcraft-yaml]
rockcraft init --profile flask-framework
# [docs:create-rockcraft-yaml-end]

sed -i "s/name: .*/name: flask-async-app/g" rockcraft.yaml
sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml

# [docs:pack]
rockcraft pack
# [docs:pack-end]

# [docs:ls-rock]
ls *.rock -l
# [docs:ls-rock-end]

# [docs:skopeo-copy]
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:flask-async-app_0.1_$(dpkg --print-architecture).rock \
docker://localhost:32000/flask-async-app:0.1
# [docs:skopeo-copy-end]

# [docs:create-charm-dir]
mkdir charm
cd charm
# [docs:create-charm-dir-end]

# [docs:charm-init]
charmcraft init --profile flask-framework --name flask-async-app
# [docs:charm-init-end]

sed -i "s/paas-charm.*/https:\/\/github.com\/canonical\/paas-charm\/archive\/async-workers.tar.gz/g" requirements.txt

# [docs:charm-pack]
charmcraft pack
# [docs:charm-pack-end]

# [docs:ls-charm]
ls *.charm -l
# [docs:ls-charm-end]

# [docs:add-juju-model]
juju add-model flask-async-app
# [docs:add-juju-model-end]

juju set-model-constraints -m flask-async-app arch=$(dpkg --print-architecture)

# [docs:deploy-juju-model]
juju deploy ./flask-async-app_ubuntu-22.04-$(dpkg --print-architecture).charm \
flask-async-app --resource \
flask-app-image=localhost:32000/flask-async-app:0.1
# [docs:deploy-juju-model-end]

# [docs:deploy-nginx]
juju deploy nginx-ingress-integrator --channel=latest/edge --base [email protected]
juju integrate nginx-ingress-integrator flask-async-app
# [docs:deploy-nginx-end]

# [docs:config-nginx]
juju config nginx-ingress-integrator \
service-hostname=flask-async-app path-routes=/
# [docs:config-nginx-end]

# give Juju some time to deploy the apps
juju wait-for application flask-async-app --query='status=="active"' --timeout 10m
juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m

# [docs:curl-init-deployment]
curl http://flask-async-app --resolve flask-async-app:80:127.0.0.1
# [docs:curl-init-deployment-end]

# [docs:config-async]
juju config flask-async-app webserver-worker-class=gevent
# [docs:config-async-end]

juju wait-for application flask-async-app --query='status=="active"' --timeout 10m

# test the async flask service
NUM_REQUESTS=15
ASYNC_RESULT='TRUE'

echo "Firing $NUM_REQUESTS requests to http://flask-async-app/io..."

overall_start_time=$(date +%s)

for i in $(seq 1 $NUM_REQUESTS); do
(
start_time=$(date +%s)
echo "Request $i start time: $start_time"

curl -s http://flask-async-app/io --resolve flask-async-app:80:127.0.0.1

end_time=$(date +%s)
pass_time=$((end_time - start_time))
echo "Request $i end time: $end_time == $pass_time"
) &
done

wait
end_time=$(date +%s)
overall_passtime=$((end_time - overall_start_time))
echo "Total pass time: $overall_passtime"
if [ $((3 < overall_passtime)) -eq 1 ]; then
echo "Error!"
ASYNC_RESULT='FALSE'
exit 2
fi
[ "$ASYNC_RESULT" == 'TRUE' ]

# Back out to main directory for clean-up
cd ..

# [docs:clean-environment]
# exit and delete the virtual environment
deactivate
rm -rf charm .venv __pycache__
# delete all the files created during the tutorial
rm flask-async-app_0.1_$(dpkg --print-architecture).rock rockcraft.yaml app.py \
requirements.txt migrate.py
# Remove the juju model
juju destroy-model flask-async-app --destroy-storage --no-prompt --force
# [docs:clean-environment-end]
54 changes: 54 additions & 0 deletions docs/howto/flask-async.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
======================================================
How to write a Kubernetes charm for an Async Flask app
alithethird marked this conversation as resolved.
Show resolved Hide resolved
======================================================

In this how-to guide we will configure a 12-factor Flask
alithethird marked this conversation as resolved.
Show resolved Hide resolved
application to use asynchronous Gunicorn workers to be
able to serve to multiple users easily.

Make the rock async
===================

Before packing the rock make sure to put the following in ``requirements.txt``
alithethird marked this conversation as resolved.
Show resolved Hide resolved
file:

.. literalinclude:: code/flask-async/requirements.txt

Configure the async application
alithethird marked this conversation as resolved.
Show resolved Hide resolved
===============================
alithethird marked this conversation as resolved.
Show resolved Hide resolved

Now let's enable async Gunicorn workers using a configuration option. We will
alithethird marked this conversation as resolved.
Show resolved Hide resolved
expect this configuration option to be available in the Flask app configuration
under the keyword ``webserver-worker-class``. Verify that the new configuration
alithethird marked this conversation as resolved.
Show resolved Hide resolved
has been added using
``juju config flask-async-app | grep -A 6 webserver-worker-class:`` which should
show the configuration option.
alithethird marked this conversation as resolved.
Show resolved Hide resolved

The worker class can be changed using Juju:

.. literalinclude:: code/flask-async/task.yaml
:language: bash
:start-after: [docs:config-async]
:end-before: [docs:config-async-end]
:dedent: 2

alithethird marked this conversation as resolved.
Show resolved Hide resolved
Now you can run
``curl --parallel --parallel-immediate --resolve flask-async-app:80:127.0.0.1 \
http://flask-async-app/io http://flask-async-app/io http://flask-async-app/io \
http://flask-async-app/io http://flask-async-app/io``
and they will all return at the same time.
alithethird marked this conversation as resolved.
Show resolved Hide resolved

Output will be similar to following:

.. code-block:: bash
alithethird marked this conversation as resolved.
Show resolved Hide resolved

ok
ok
ok
ok
ok

.. note::

When the configuration is done the charm will be in ``active`` state.
It might take a short time for the configuration to take effect.
alithethird marked this conversation as resolved.
Show resolved Hide resolved
alithethird marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ How-To
charm-to-poetry
charm-to-python
shared-cache
flask-async
8 changes: 7 additions & 1 deletion spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ backends:
system=$(echo "${SPREAD_SYSTEM}" | tr . -)
instance_name="spread-${SPREAD_BACKEND}-${instance_num}-${system}"

multipass launch --cpus 4 --disk 40G --memory 4G --name "${instance_name}" "${multipass_image}"
multipass launch --cpus 4 --disk 40G --memory 8G --name "${instance_name}" "${multipass_image}"

# Enable PasswordAuthentication for root over SSH.
multipass exec "$instance_name" -- \
Expand Down Expand Up @@ -82,6 +82,8 @@ backends:
workers: 1
- ubuntu-22.04-64:
workers: 4
- ubuntu-24.04-64:
workers: 4
prepare: |
set -e

Expand Down Expand Up @@ -129,6 +131,10 @@ prepare: |
install_charmcraft

suites:
docs/howto/code/:
summary: tests howto from the docs
systems:
- ubuntu-24.04-64
docs/tutorial/code/:
summary: tests tutorial from the docs
systems:
Expand Down
Loading