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

Configure gunicorn with 2 gevent workers #4

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 13 additions & 7 deletions app/gunicorn.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@

app_config = AppConfig()

bind = app_config.host + ':' + str(app_config.port)
# Calculates the number of usable cores and doubles it. Recommended number of workers per core is two.
# https://docs.gunicorn.org/en/latest/design.html#how-many-workers
# We use 'os.sched_getaffinity(pid)' not 'os.cpu_count()' because it returns only allowable CPUs.
# os.sched_getaffinity(pid): Return the set of CPUs the process with PID pid is restricted to.
# os.cpu_count(): Return the number of CPUs in the system.
workers = len(os.sched_getaffinity(0)) * 2
bind = app_config.host + ":" + str(app_config.port)
# Calculates the number of usable cores and doubles it. Recommended number of workers when running in a
# container is 2 (see https://pythonspeed.com/articles/gunicorn-in-docker/)
workers = 2
threads = 4

# Use gevent worker class which is most appropriate for web apps which may make block network requests
# (like database queries for example). Under the hood, this approach monkeypatches blocking I/O calls
# with compatible cooperative counterparts from gevent package that allow the worker to switch threads.
# Read more in the following resources:
# https://docs.gunicorn.org/en/latest/design.html,
# https://medium.com/@danieldng/use-gevent-when-your-gunicorn-worker-is-waiting-for-data-180efef96367,
# https://stackoverflow.com/questions/69372896/gunicorn-with-gevent-performance-gain
worker_class = "gevent"
159 changes: 158 additions & 1 deletion app/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pytz = "^2023.3.post1"
APIFlask = "^2.0.2"
marshmallow-dataclass = {extras = ["enum", "union"], version = "^8.5.8"}
marshmallow = "^3.20.1"
gunicorn = "^21.2.0"
gunicorn = {extras = ["gevent"], version = "^21.2.0"}
psycopg = {extras = ["binary"], version = "^3.1.10"}
pydantic-settings = "^2.0.3"

Expand Down
32 changes: 6 additions & 26 deletions docs/app/api-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,7 @@ See [Technical Overview](./technical-overview.md) for details on the technologie

Each endpoint is configured in the [openapi.generated.yml](/app/openapi.generated.yml) file which provides basic request validation. Each endpoint specifies an `operationId` that maps to a function defined in the code that will handle the request.

To make handling a request easier, an [ApiContext](/app/src/util/api_context.py) exists which will fetch the DB session, request body, and current user. This can be used like so:
```py
def example_post() -> flask.Response:
with api_context_manager() as api_context:
# Work with the request body
first_name = api_context.request_body["first_name"]

# Work with the user
current_user = api_context.current_user

# Work with the DB session
api_context.db_session.query(..)

# Return a response
return response_util.success_response(
message="Success",
data={"db_model_id": "1234"}, # Whatever the response object should be
status_code=201
)
```
For more complex usages, it is recommended you put the implementation into a separate handler file in order to keep the API entrypoints easier to read.

# Swagger
## Swagger

The Swagger UI can be reached locally at [http://localhost:8080/docs](http://localhost:8080/docs) when running the API. The UI is based on the [openapi.generated.yml](/app/openapi.generated.yml) file.
![Swagger UI](/docs/app/images/swagger-ui.png)
Expand All @@ -36,12 +14,14 @@ Each of the endpoints you've described in your openapi.generated.yml file will a

All model schemas defined can be found at the bottom of the UI.

# Routes
## Routes

## Health Check
[GET /v1/healthcheck](/app/src/route/healthcheck.py) is an endpoint for checking the health of the service. It verifies that the database is reachable, and that the API service itself is up and running.
### Health Check

[GET /v1/healthcheck](/app/src/api/healthcheck.py) is an endpoint for checking the health of the service. It verifies that the database is reachable, and that the API service itself is up and running.

Note this endpoint explicitly does not require authorization so it can be integrated with any automated monitoring you may build.

### Example Response

![Example Response](/docs/app/images/healthcheck-response.png)
2 changes: 1 addition & 1 deletion docs/app/database/database-access-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document describes the best practices and patterns for how the application

## Client Initialization and Configuration

The database client is initialized when the application starts (see [src/\_\_main\_\_.py](../../../app/src/app.py). The database engine that is used to create acquire connections to the database is initialized using the database configuration defined in [db_config.py](../../../app/src/db/db_config.py), which is configured through environment variables. The initialized database client is then stored on the Flask app's [\`extensions\` dictionary](https://flask.palletsprojects.com/en/2.2.x/src/#flask.Flask.extensions) to be used throughout the lifetime of the application.
The database client is initialized when the application starts (see [src/\_\_main\_\_.py](../../../app/src/app.py). The database engine that is used to create acquire connections to the database is initialized using the database configuration defined in [postgres_config.py](/app/src/adapters/db/clients/postgres_config.py), which is configured through environment variables. The initialized database client is then stored on the Flask app's [\`extensions\` dictionary](https://flask.palletsprojects.com/en/2.3.x/api/#flask.Flask.extensions) to be used throughout the lifetime of the application.

## Session Management

Expand Down
18 changes: 9 additions & 9 deletions docs/app/technical-overview.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Technical Overview

- [Key Technologies](#key-technologies)
- [Request operations](#request-operations)
- [Authentication](#authentication)
- [Authorization](#authorization)
- [Running In Hosted Environments](#running-in-hosted-environments)
- [ECS](#ecs)
- [Technical Overview](#technical-overview)
- [Key Technologies](#key-technologies)
- [Request operations](#request-operations)
- [Authentication](#authentication)
- [Authorization](#authorization)
- [Database diagram](#database-diagram)

## Key Technologies

Expand All @@ -21,12 +21,11 @@ generally preferred.
- [OpenAPI Specification][oas-docs]
- [API Flask][apiflask-home] ([source code][apiflask-src])
- [SQLAlchemy][sqlalchemy-home] ([source code][sqlalchemy-src])
- [Alembic][alembic-home] ([source code](alembic-src))
- [Alembic][alembic-home] ([source code][alembic-src])
- [pydantic][pydantic-home] ([source code][pydantic-src])
- [poetry](https://python-poetry.org/docs/) - Python dependency management

[oas-docs]: http://spec.openapis.org/oas/v3.0.3
[oas-swagger-docs]: https://swagger.io/docs/specification/about/

[apiflask-home]: https://apiflask.com/
[apiflask-src]: https://github.com/apiflask/apiflask
Expand All @@ -38,6 +37,7 @@ generally preferred.
[sqlalchemy-src]: https://github.com/sqlalchemy/sqlalchemy

[alembic-home]: https://alembic.sqlalchemy.org/en/latest/
[alembic-src]: https://github.com/sqlalchemy/alembic

## Request operations

Expand All @@ -58,4 +58,4 @@ function that is run to do the authentication.
n/a - Specific user authorization is not yet implemented for this API.

### Database diagram
n/a - Database diagrams are not yet available for this application.
n/a - Database diagrams are not yet available for this application.
2 changes: 1 addition & 1 deletion docs/infra/set-up-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Before creating migrations that create tables, first create a migration that inc
ALTER DEFAULT PRIVILEGES GRANT ALL ON TABLES TO app
```

This will cause all future tables created by the `migrator` user to automatically be accessible by the `app` user. See the [Postgres docs on ALTER DEFAULT PRIVILEGES](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html) for more info. As an example see the example app's migrations file [migrations.sql](/app/migrations.sql).
This will cause all future tables created by the `migrator` user to automatically be accessible by the `app` user. See the [Postgres docs on ALTER DEFAULT PRIVILEGES](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html) for more info. As an example see the example app's migrations file [migrations.sql](https://github.com/navapbc/template-infra/blob/main/app/migrations.sql).

Why is this needed? The reason is because the `migrator` role will be used by the migration task to run database migrations (creating tables, altering tables, etc.), while the `app` role will be used by the web service to access the database. Moreover, in Postgres, new tables won't automatically be accessible by roles other than the creator unless specifically granted, even if those other roles have usage access to the schema that the tables are created in. In other words if the `migrator` user created a new table `foo` in the `app` schema, the `app` user will not have automatically be able to access it by default.

Expand Down
Loading