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 14 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
194 changes: 194 additions & 0 deletions docs/howto/code/flask-async/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
###########################################
# 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
# (Ran into issues in prepare section)
# snap install rockcraft --channel=latest/edge --classic

# Install the latest rockcraft snap
# (This can be removed after the Rockcraft PR is merged)
# The PR: https://github.com/canonical/rockcraft/pull/747
snap install snapcraft --channel=latest/edge --classic
# Download rockcraft async-workers branch and alithethird fork
git clone -b flask-django-extention-async-workers https://github.com/alithethird/rockcraft
cd rockcraft
snapcraft pack
snap install --dangerous --classic rockcraft_*

# snap refresh charmcraft --channel=latest/edge --amend
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 --revision 122
alithethird marked this conversation as resolved.
Show resolved Hide resolved
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]
48 changes: 48 additions & 0 deletions docs/howto/flask-async.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
======================================================
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 we will configure 12 Factor Flask application
to use asynchronous Gunicorn workers to be able to serve
to multiple users easily.
alithethird marked this conversation as resolved.
Show resolved Hide resolved

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

.. note::

The ``grep`` command extracts a portion of the configuration to make
it easier to check whether the configuration option has been added.
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``
in they will all return at the same time.
alithethird marked this conversation as resolved.
Show resolved Hide resolved
alithethird marked this conversation as resolved.
Show resolved Hide resolved

.. note::

It might take a short time for the configuration to take effect.
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 2 --disk 20G --memory 2G --name "${instance_name}" "${multipass_image}"
multipass launch --cpus 4 --disk 50G --memory 8G --name "${instance_name}" "${multipass_image}"
alithethird marked this conversation as resolved.
Show resolved Hide resolved

# 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
tests/spread/commands/:
summary: simple charmcraft commands
tests/spread/charms/:
Expand Down
Loading