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

ASGI Support #19

Merged
merged 3 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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"