Since the last update, I published a post on the Canonical blog about the Flask support in Charmcraft and Rockcraft!
In terms of the roadmap, we have delivered support for asynchronous workers for Flask and Django. You can read more about the feature in the Gunicorn documentation. Async workers can help increase the scale for I/O-bound applications. The default synchronous worker will handle one request at a time and scale is managed by increasing the number of workers. For async workers, the scale is further enhanced by Gunicorn patching I/O-bound operations with asynchronous functions. An example are database interactions. This means there is no need to have async def
functions in your code to take advantage of async workers.
To illustrate the benefits of async workers, let’s look at an example.
Flask Async Example
Let’s start with creating a Multipass VM:
multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04
multipass shell charm-dev
Then we need to install dependencies and set things up:
sudo snap install rockcraft --classic --channel latest/edge
sudo snap install charmcraft --classic --channel latest/edge
sudo snap install lxd
lxd init --auto
sudo snap install microk8s --channel 1.31-strict/stable
sudo adduser $USER snap_microk8s
newgrp snap_microk8s
sudo microk8s enable hostpath-storage
sudo microk8s enable registry
sudo microk8s enable ingress
sudo snap install juju --channel 3.5/stable
mkdir -p ~/.local/share
juju bootstrap microk8s dev-controller
mkdir flask-async
cd flask-async
Next we need to create the Flask application. We will have one root endpoint which will sleep for 2 seconds. This will simulate an I/O-bound task such as a slow database call. Create the requirements.txt
file with the following contents:
Flask
gunicorn[gevent]
Note that gunicorn[gevent]
is required for async workers. Create the app.py
file with the following contents:
from time import sleep
import flask
app = flask.Flask(__name__)
@app.route("/")
def index():
sleep(2)
return "Hello, world!\n"
if __name__ == "__main__":
app.run()
Now let’s create the charm and rock and deploy them:
rockcraft init --profile flask-framework
rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:flask-async_0.1_amd64.rock \
docker://localhost:32000/flask-async:0.1
mkdir charm
cd charm
charmcraft init --profile flask-framework --name flask-async
charmcraft pack
juju add-model flask-async
juju deploy ./flask-async_ubuntu-22.04-amd64.charm \
flask-async --resource \
flask-app-image=localhost:32000/flask-async:0.1
Use:
juju status --watch 2s
To wait for the application to be active. You should see the following output from juju status
:
Model Controller Cloud/Region Version SLA Timestamp
flask-async dev-controller microk8s/localhost 3.5.5 unsupported 23:39:22+11:00
App Version Status Scale Charm Channel Rev Address Exposed Message
flask-async active 1 flask-async 0 10.152.183.51 no
Unit Workload Agent Address Ports Message
flask-async/0* active idle 10.1.157.80
We can test everything is working by sending a curl request to the IP address listed for the unit (above 10.1.157.80
, yours may differ):
curl 10.1.157.80:8000
This should return Hello, world!
after 2 seconds. Run
juju config flask-async
Which will return all of the configurations of the charm. There is a new configuration option webserver-worker-class
(for both Flask and Django charms) which can be used to enable async mode. First, let’s check that the app is running in sync mode. Let’s create a shell script test-async.sh
with the following contents:
for i in $(seq 1 5); do
(
start_time=$(date +%s)
echo "Request $i start time: $start_time"
curl 10.1.157.80:8000
end_time=$(date +%s)
pass_time=$((end_time - start_time))
echo "Request $i end time: $end_time == $pass_time"
echo "Request $i end time: $end_time == $pass_time"
) &
done
Note that you will need to update the IP address based on your juju status
output. Make the shell script executable and run it using
chmod +x test-async.sh
./test-async.sh
You will notice that the total time the script takes to execute is roughly 10 seconds. Let’s change to the gevent
worker class which will make the app async:
juju config flask-async webserver-worker-class=gevent
Give the app a moment to restart, get the new IP address from juju status
and update it in the script and then run the test script again:
./test-async.sh
All of the requests will now return within 2 seconds.
Thats the end of the example! We saw the new webserver-worker-class
config option which changes the app to use async workers when it is set to gevent
. This meant that simulated I/O-bound application could respond to several requests at the same time rather than be blocked.