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

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 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
a7be9a8
Merge branch 'main' into flask-async-worker
alithethird Jan 6, 2025
b0899b2
Merge branch 'main' into flask-async-worker
alithethird Jan 7, 2025
8d2a695
chore(): Change option description.
alithethird Jan 7, 2025
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'.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not thrilled about having the name here include class but the description say method - is this standard language? If not, can we come up with something more standardised?

(Non-blocker, except that if we change the name we'd need to change it before release)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good catch! Thank you. In the description by method I meant the way of handling requests. I can see how this language can be confusing. I will update the description to make it clearer.

},
}


Expand Down
19 changes: 19 additions & 0 deletions docs/tutorial/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()
1 change: 1 addition & 0 deletions docs/tutorial/code/flask-async/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask
229 changes: 229 additions & 0 deletions docs/tutorial/code/flask-async/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
###########################################
# IMPORTANT
# Comments matter!
# The docs use the wrapping comments as
# markers for including said instructions
# as snippets in the docs.
###########################################
summary: Getting started with Flask tutorial

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_*
alithethird marked this conversation as resolved.
Show resolved Hide resolved
snap install lxd
lxd init --auto
# snap refresh charmcraft --channel=latest/edge --amend
alithethird marked this conversation as resolved.
Show resolved Hide resolved
snap install microk8s --channel=1.31-strict/stable
snap install juju --channel=3.5/stable
alithethird marked this conversation as resolved.
Show resolved Hide resolved
# Juju config setup
lxc network set lxdbr0 ipv6.address none
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
pip install Flask
alithethird marked this conversation as resolved.
Show resolved Hide resolved
# [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
# uncomment the parts main section
sed -i "s/# parts:/parts:/g" rockcraft.yaml
# uncomment the async-dependencies part
awk -i inplace -v block_key="flask-framework/async-dependencies" '
BEGIN {
in_block = 0;
comment_pattern = "^#[[:space:]]";
uncommented_line = "";
}
/^#[[:space:]]/ {
# Check if the line contains the block key
if (in_block == 0 && $0 ~ block_key) {
in_block = 1;
}
}
{
# If in_block is active, uncomment lines
if (in_block == 1) {
uncommented_line = gensub(comment_pattern, "", 1, $0);
if (uncommented_line == $0) {
in_block = 0;
}
print uncommented_line;
} else {
print $0;
}
}' 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
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]
alithethird marked this conversation as resolved.
Show resolved Hide resolved
# test the async flask service
NUM_REQUESTS=15
ASYNC_RESULT='TRUE'
echo "Firing $NUM_REQUESTS requests to $URL..."
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 flask-async-app_0.2_$(dpkg --print-architecture).rock \
flask-async-app_0.3_$(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]
Loading
Loading