Skip to content

Commit

Permalink
ASGI Support (#19)
Browse files Browse the repository at this point in the history
* Add uvicorn support for async applications

* Remove shellcheck issue

* Update tna-python README.md
  • Loading branch information
ahosgood authored Jan 2, 2024
1 parent 80f897c commit 1f8ba1a
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Initial release of `tna-python-dev` Docker image
- Added `uvicorn` support for async applications such as FastAPI

### Changed

- Update Poetry to [1.7.1](https://github.com/python-poetry/poetry/releases/tag/1.7.1)
- Updated Base Docker image from `python:3.11-slim` to `python:3.12-slim-bookworm`
- Install `libcurl4` version `7.88.1-10+deb12u4`

### Deprecated
### Removed
Expand Down
2 changes: 1 addition & 1 deletion docker/tna-python/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ RUN set -eux; \
\
apt-get update; \
apt-get -y upgrade; \
apt-get install -y --no-install-recommends curl=7.88.1-10+deb12u4 build-essential=12.9 libmagic-dev=1:5.44-3; \
apt-get install -y --no-install-recommends libcurl4=7.88.1-10+deb12u4 curl=7.88.1-10+deb12u4 build-essential=12.9 libmagic-dev=1:5.44-3; \
\
apt-get clean; \
apt-get autoremove -y --purge; \
Expand Down
21 changes: 19 additions & 2 deletions docker/tna-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ This image comes with:
- A version of Python approved by TNA
- [Poetry](https://python-poetry.org/) for dependency management
- [nvm](https://github.com/nvm-sh/nvm) for compiling assets such as CSS and JavaScript
- [Gunicorn](https://gunicorn.org/) for serving the Python application
- [Gunicorn](https://gunicorn.org/) for serving the Python application via a WSGI
- [Uvicorn](https://www.uvicorn.org/) for serving the Python application via a ASGI

This image requires you have the following files in the root of your project:

Expand Down Expand Up @@ -35,12 +36,16 @@ This image requires you have the following files in the root of your project:
[^6]: [Gunicorn docs - timeout](https://docs.gunicorn.org/en/stable/settings.html#timeout)
[^7]: [Gunicorn docs - keepalive](https://docs.gunicorn.org/en/stable/settings.html#keepalive)

### Secret key

A secret key (for `SECRET_KEY`) can be generated using:

```sh
python -c 'import secrets; print(secrets.token_hex())'
```

Alternatively, using the [`tna-dev` image](https://github.com/nationalarchives/docker/tree/main/docker/tna-python-dev), you can run `secret-key` to generate one.

## Commands for the Dockerfile

There are two commands to use within your `Dockerfile`:
Expand Down Expand Up @@ -76,10 +81,22 @@ tna-run my_app:app
1. Count the number of CPU cores, multiply it by 2 and add 1 to get a suggested worker and thread count
1. Start `gunicorn` with values appropriate to the environment taking into account any overrides

#### Asynchronous support

For frameworks that require or can use an ASGI rather than a WSGI you can use `tna-run` with a `-a` flag:

```sh
tna-run -a my_app:app
```

When working in a development environment (`ENVIRONMENT=production`) and using FastAPI, Uvicorn is used as the ASGI for `tna-run`.

When using FastAPI in production, `tna-run -a` should be explicitly specified so Gunicron can use the Uvicorn worker class.

## Using Node

To use Node to build your assets you need three files in your project:

- `package.json`
- `package-lock.json`
- `.nvmrc`
- `.nvmrc` containing the version of Node you would like to support (e.g. `lts/iron`)
1 change: 1 addition & 0 deletions docker/tna-python/bin/tna-build
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ then
fi

poetry add [email protected]
poetry add [email protected]
poetry install --sync --no-root
77 changes: 64 additions & 13 deletions docker/tna-python/bin/tna-run
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,28 @@

set -e

if [ -z "$1" ]
# Set flag defaults
ASYNC=false

# Parse flags
while getopts ":a" option; do
case "${option}" in
a)
ASYNC=true
shift 1
;;
?)
echo "Invalid option: -${OPTARG}"
exit 1
;;
esac
done

# Get the application
APPLICATION=$1

# Error if no application is passed
if [ -z "$APPLICATION" ]
then
echo -e "Error: application not specified in tna-run\n";
echo "PARAMETERS"
Expand All @@ -11,56 +32,86 @@ then
exit 1
fi

# Error if $SECRET_KEY is not defined
if [[ -z $SECRET_KEY ]]
then
echo -e "Error: Environment variable SECRET_KEY not set";
exit 1
fi

# Convert $ENVIRONMENT to lowercase
ENVIRONMENT=$(echo "$ENVIRONMENT" | tr '[:upper:]' '[:lower:]')

# If there is an $NPM_DEVELOP_COMMAND and we are running a development environment, run the development command now
if [ "$ENVIRONMENT" == 'develop' ] && [ -n "$NPM_DEVELOP_COMMAND" ]
then
tna-node "$NPM_DEVELOP_COMMAND"
fi

MAX_REQUESTS=$(python -c "import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)")
DEFAULT_WORKERS=$MAX_REQUESTS
DEFAULT_THREADS=1
# Set up the default number of workers and threads
DEFAULT_WORKERS=$(python -c "import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)")
DEFAULT_THREADS=$((DEFAULT_WORKERS * 2))

# Set the worker class
[[ "$ASYNC" = true ]] && WORKER_CLASS=uvicorn.workers.UvicornWorker || WORKER_CLASS=sync

# Use the number of workers and threads (if provided) else use defaults
[[ -z $WORKERS ]] && WORKERS=$DEFAULT_WORKERS
[[ -z $THREADS ]] && THREADS=$DEFAULT_THREADS

if [ "$ENVIRONMENT" == 'production' ]
then
# Production environment
[[ -z $LOG_LEVEL ]] && LOG_LEVEL=warn
[[ -z $WORKERS ]] && WORKERS=$DEFAULT_WORKERS
[[ -z $TIMEOUT ]] && TIMEOUT=30
[[ -z $KEEP_ALIVE ]] && KEEP_ALIVE=30
poetry run gunicorn "$1" --workers "$WORKERS" --threads "$THREADS" --log-level "$LOG_LEVEL" --timeout "$TIMEOUT" --keep-alive "$KEEP_ALIVE" --access-logfile - --bind 0.0.0.0:8080
elif [ "$ENVIRONMENT" == 'develop' ]
then
# Development environment
echo "ENVIRONMENT is develop"
[[ -z $LOG_LEVEL ]] && LOG_LEVEL=debug
[[ -z $WORKERS ]] && WORKERS=3
[[ -z $THREADS ]] && THREADS=1
[[ -z $TIMEOUT ]] && TIMEOUT=600
[[ -z $KEEP_ALIVE ]] && KEEP_ALIVE=5

# Try using the Django development server if the application uses Django
echo "Trying Django development server..."
if poetry show django ;
then
echo "Django found, starting server"
poetry run python /app/manage.py runserver 0.0.0.0:8080
fi
echo "Django not found"

# Try using the Flask development server if the application uses Flask
echo "Trying Flask development server..."
if poetry show flask ;
then
echo "Flask found, starting server"
poetry run flask run --debug --host 0.0.0.0 --port 8080
fi
echo "Flask not found"

# Use a uvicorn server if the application uses FastAPI
echo "Trying FastAPI server..."
if poetry show fastapi ;
then
echo "FastAPI found, starting server"
poetry run uvicorn "$APPLICATION" --workers "$WORKERS" --log-level "$LOG_LEVEL" --timeout-keep-alive "$KEEP_ALIVE" --host 0.0.0.0 --port 8080 --reload
fi
echo "FastAPI not found"

# Fall back to using Gunicorn
echo "No framework found, using Gunicorn to serve development application"
[[ -z $LOG_LEVEL ]] && LOG_LEVEL=debug
[[ -z $WORKERS ]] && WORKERS=3
[[ -z $TIMEOUT ]] && TIMEOUT=600
[[ -z $KEEP_ALIVE ]] && KEEP_ALIVE=5
poetry run gunicorn "$1" --workers "$WORKERS" --threads "$THREADS" --log-level "$LOG_LEVEL" --timeout "$TIMEOUT" --keep-alive "$KEEP_ALIVE" --reload --bind 0.0.0.0:8080
poetry run gunicorn "$APPLICATION" --workers "$WORKERS" --threads "$THREADS" --log-level "$LOG_LEVEL" --timeout "$TIMEOUT" --keep-alive "$KEEP_ALIVE" --bind 0.0.0.0:8080 --worker-class="$WORKER_CLASS" --reload
else
# All other environments
[[ -z $LOG_LEVEL ]] && LOG_LEVEL=info
[[ -z $WORKERS ]] && WORKERS=$DEFAULT_WORKERS
[[ -z $TIMEOUT ]] && TIMEOUT=30
[[ -z $KEEP_ALIVE ]] && KEEP_ALIVE=5
poetry run gunicorn "$1" --workers "$WORKERS" --threads "$THREADS" --log-level "$LOG_LEVEL" --timeout "$TIMEOUT" --keep-alive "$KEEP_ALIVE" --access-logfile - --bind 0.0.0.0:8080
fi

# Start the server
echo "Starting $ENVIRONMENT server"
poetry run gunicorn "$APPLICATION" --workers "$WORKERS" --threads "$THREADS" --log-level "$LOG_LEVEL" --timeout "$TIMEOUT" --keep-alive "$KEEP_ALIVE" --access-logfile - --bind 0.0.0.0:8080 --worker-class="$WORKER_CLASS"

0 comments on commit 1f8ba1a

Please sign in to comment.