Skip to content

Commit 3128c62

Browse files
authored
feat(extension): Add option to enable async workers in Flask and Django (canonical#1986)
Related to [Rockcraft PR](canonical/rockcraft#747) and [Paas Charm PR](canonical/paas-charm#11). Adds charm option to enable Async Gunicorn Workers.
1 parent 33519b4 commit 3128c62

File tree

7 files changed

+280
-1
lines changed

7 files changed

+280
-1
lines changed

charmcraft/extensions/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ def get_image_name(self) -> str:
202202
"type": "int",
203203
"description": "The number of webserver worker processes for handling requests.",
204204
},
205+
"webserver-worker-class": {
206+
"type": "string",
207+
"description": "The webserver worker process class for handling requests. Can be either 'gevent' or 'sync'.",
208+
},
205209
}
206210

207211

docs/howto/code/flask-async/app.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from time import sleep
2+
3+
import flask
4+
5+
app = flask.Flask(__name__)
6+
7+
8+
@app.route("/")
9+
def index():
10+
return "Hello, world!\n"
11+
12+
13+
@app.route("/io")
14+
def pseudo_io():
15+
sleep(2)
16+
return "ok\n"
17+
18+
if __name__ == "__main__":
19+
app.run()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Flask
2+
gevent

docs/howto/code/flask-async/task.yaml

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
###########################################
2+
# IMPORTANT
3+
# Comments matter!
4+
# The docs use the wrapping comments as
5+
# markers for including said instructions
6+
# as snippets in the docs.
7+
###########################################
8+
summary: How to create async Flask Charm
9+
10+
kill-timeout: 90m
11+
12+
environment:
13+
14+
execute: |
15+
# Move everything to $HOME so that Juju deployment works
16+
mv *.yaml *.py *.txt $HOME
17+
cd $HOME
18+
19+
# Don't use the staging store for this test
20+
unset CHARMCRAFT_STORE_API_URL
21+
unset CHARMCRAFT_UPLOAD_URL
22+
unset CHARMCRAFT_REGISTRY_URL
23+
24+
# Add setup instructions
25+
snap install rockcraft --channel=latest/edge --classic
26+
27+
snap install microk8s --channel=1.31-strict/stable
28+
snap install juju --channel=3/stable
29+
30+
mkdir -p ~/.local/share
31+
32+
# MicroK8s config setup
33+
microk8s status --wait-ready
34+
microk8s enable hostpath-storage
35+
microk8s enable registry
36+
microk8s enable ingress
37+
38+
# Bootstrap controller
39+
juju bootstrap microk8s dev-controller
40+
41+
cd $HOME
42+
# [docs:create-venv]
43+
sudo apt-get update && sudo apt-get install python3-venv -y
44+
python3 -m venv .venv
45+
source .venv/bin/activate
46+
pip install -r requirements.txt
47+
# [docs:create-venv-end]
48+
49+
flask run -p 8000 &
50+
retry -n 5 --wait 2 curl --fail localhost:8000
51+
52+
# [docs:curl-flask]
53+
curl localhost:8000
54+
# [docs:curl-flask-end]
55+
56+
# [docs:curl-flask-async-app]
57+
curl localhost:8000/io
58+
# [docs:curl-flask-async-app-end]
59+
60+
kill $!
61+
62+
# [docs:create-rockcraft-yaml]
63+
rockcraft init --profile flask-framework
64+
# [docs:create-rockcraft-yaml-end]
65+
66+
sed -i "s/name: .*/name: flask-async-app/g" rockcraft.yaml
67+
sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml
68+
69+
# [docs:pack]
70+
rockcraft pack
71+
# [docs:pack-end]
72+
73+
# [docs:ls-rock]
74+
ls *.rock -l
75+
# [docs:ls-rock-end]
76+
77+
# [docs:skopeo-copy]
78+
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
79+
oci-archive:flask-async-app_0.1_$(dpkg --print-architecture).rock \
80+
docker://localhost:32000/flask-async-app:0.1
81+
# [docs:skopeo-copy-end]
82+
83+
# [docs:create-charm-dir]
84+
mkdir charm
85+
cd charm
86+
# [docs:create-charm-dir-end]
87+
88+
# [docs:charm-init]
89+
charmcraft init --profile flask-framework --name flask-async-app
90+
# [docs:charm-init-end]
91+
92+
sed -i "s/paas-charm.*/https:\/\/github.com\/canonical\/paas-charm\/archive\/async-workers.tar.gz/g" requirements.txt
93+
94+
# [docs:charm-pack]
95+
charmcraft pack
96+
# [docs:charm-pack-end]
97+
98+
# [docs:ls-charm]
99+
ls *.charm -l
100+
# [docs:ls-charm-end]
101+
102+
# [docs:add-juju-model]
103+
juju add-model flask-async-app
104+
# [docs:add-juju-model-end]
105+
106+
juju set-model-constraints -m flask-async-app arch=$(dpkg --print-architecture)
107+
108+
# [docs:deploy-juju-model]
109+
juju deploy ./flask-async-app_ubuntu-22.04-$(dpkg --print-architecture).charm \
110+
flask-async-app --resource \
111+
flask-app-image=localhost:32000/flask-async-app:0.1
112+
# [docs:deploy-juju-model-end]
113+
114+
# [docs:deploy-nginx]
115+
juju deploy nginx-ingress-integrator --channel=latest/edge --base [email protected]
116+
juju integrate nginx-ingress-integrator flask-async-app
117+
# [docs:deploy-nginx-end]
118+
119+
# [docs:config-nginx]
120+
juju config nginx-ingress-integrator \
121+
service-hostname=flask-async-app path-routes=/
122+
# [docs:config-nginx-end]
123+
124+
# give Juju some time to deploy the apps
125+
juju wait-for application flask-async-app --query='status=="active"' --timeout 10m
126+
juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m
127+
128+
# [docs:curl-init-deployment]
129+
curl http://flask-async-app --resolve flask-async-app:80:127.0.0.1
130+
# [docs:curl-init-deployment-end]
131+
132+
# [docs:config-async]
133+
juju config flask-async-app webserver-worker-class=gevent
134+
# [docs:config-async-end]
135+
136+
juju wait-for application flask-async-app --query='status=="active"' --timeout 10m
137+
138+
# test the async flask service
139+
NUM_REQUESTS=15
140+
ASYNC_RESULT='TRUE'
141+
142+
echo "Firing $NUM_REQUESTS requests to http://flask-async-app/io..."
143+
144+
overall_start_time=$(date +%s)
145+
146+
for i in $(seq 1 $NUM_REQUESTS); do
147+
(
148+
start_time=$(date +%s)
149+
echo "Request $i start time: $start_time"
150+
151+
curl -s http://flask-async-app/io --resolve flask-async-app:80:127.0.0.1
152+
153+
end_time=$(date +%s)
154+
pass_time=$((end_time - start_time))
155+
echo "Request $i end time: $end_time == $pass_time"
156+
) &
157+
done
158+
159+
wait
160+
end_time=$(date +%s)
161+
overall_passtime=$((end_time - overall_start_time))
162+
echo "Total pass time: $overall_passtime"
163+
if [ $((3 < overall_passtime)) -eq 1 ]; then
164+
echo "Error!"
165+
ASYNC_RESULT='FALSE'
166+
exit 2
167+
fi
168+
[ "$ASYNC_RESULT" == 'TRUE' ]
169+
170+
# Back out to main directory for clean-up
171+
cd ..
172+
173+
# [docs:clean-environment]
174+
# exit and delete the virtual environment
175+
deactivate
176+
rm -rf charm .venv __pycache__
177+
# delete all the files created during the tutorial
178+
rm flask-async-app_0.1_$(dpkg --print-architecture).rock rockcraft.yaml app.py \
179+
requirements.txt migrate.py
180+
# Remove the juju model
181+
juju destroy-model flask-async-app --destroy-storage --no-prompt --force
182+
# [docs:clean-environment-end]

docs/howto/flask-async.rst

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.. _write-a-kubernetes-charm-for-an-async-flask-app:
2+
3+
How to write a Kubernetes charm for an async Flask app
4+
======================================================
5+
6+
In this how-to guide you will configure a 12-factor Flask
7+
application to use asynchronous Gunicorn workers to be
8+
able to serve to multiple users easily.
9+
10+
Make the rock async
11+
===================
12+
13+
To make the rock async, make sure to put the following in its ``requirements.txt``
14+
file:
15+
16+
.. literalinclude:: code/flask-async/requirements.txt
17+
18+
Pack the rock using ``rockcraft pack`` and redeploy the charm with the new rock using
19+
[``juju refresh``](https://juju.is/docs/juju/juju-refresh).
20+
21+
Configure the async application
22+
-------------------------------
23+
24+
Now let's enable async Gunicorn workers. We will
25+
expect this configuration option to be available in the Flask app configuration
26+
under the ``webserver-worker-class`` key. Verify that the new configuration
27+
has been added by running:
28+
29+
.. code:: bash
30+
31+
juju config flask-async-app | grep -A 6 webserver-worker-class:
32+
33+
The result should contain the key.
34+
35+
The worker class can be changed using Juju:
36+
37+
.. literalinclude:: code/flask-async/task.yaml
38+
:language: bash
39+
:start-after: [docs:config-async]
40+
:end-before: [docs:config-async-end]
41+
:dedent: 2
42+
43+
Test that the workers are operating in parallel by sending multiple
44+
simultaneous requests with curl:
45+
46+
.. code:: bash
47+
48+
curl --parallel --parallel-immediate --resolve flask-async-app:80:127.0.0.1 \
49+
http://flask-async-app/io http://flask-async-app/io http://flask-async-app/io \
50+
http://flask-async-app/io http://flask-async-app/io
51+
52+
and they will all return at the same time.
53+
54+
The results should arrive simultaneously and contain five instances of ``ok``:
55+
56+
.. terminal::
57+
58+
ok
59+
ok
60+
ok
61+
ok
62+
ok
63+
64+
It can take up to a minute for the configuration to take effect. When the
65+
configuration changes, the charm will re-enter the active state.

docs/howto/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ How-To
99
charm-to-poetry
1010
charm-to-python
1111
shared-cache
12+
flask-async

spread.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ backends:
5252
system=$(echo "${SPREAD_SYSTEM}" | tr . -)
5353
instance_name="spread-${SPREAD_BACKEND}-${instance_num}-${system}"
5454
55-
multipass launch --cpus 4 --disk 40G --memory 4G --name "${instance_name}" "${multipass_image}"
55+
multipass launch --cpus 4 --disk 40G --memory 8G --name "${instance_name}" "${multipass_image}"
5656
5757
# Enable PasswordAuthentication for root over SSH.
5858
multipass exec "$instance_name" -- \
@@ -82,6 +82,8 @@ backends:
8282
workers: 1
8383
- ubuntu-22.04-64:
8484
workers: 4
85+
- ubuntu-24.04-64:
86+
workers: 4
8587
prepare: |
8688
set -e
8789
@@ -129,6 +131,10 @@ prepare: |
129131
install_charmcraft
130132
131133
suites:
134+
docs/howto/code/:
135+
summary: tests howto from the docs
136+
systems:
137+
- ubuntu-24.04-64
132138
docs/tutorial/code/:
133139
summary: tests tutorial from the docs
134140
systems:

0 commit comments

Comments
 (0)