Repository navigation
Size Granian to the pool and return 503 on pool timeouts - #100
Merged
Merged
Conversation
Add APP_WORKERS, DB_POOL_TIMEOUT and DB_POOL_RECYCLE. Set Granian backpressure to pool size plus overflow so excess requests wait in the socket backlog instead of on the pool, and require a positive pool size and non-negative overflow so that sum stays meaningful. A pool checkout timeout, raw or wrapped by advanced-alchemy in RepositoryError, now returns 503 instead of 500.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sizes Granian to the database pool and turns pool exhaustion into a 503. Before this, Granian accepted up to 1024 requests per worker against a pool of 5. Extra requests waited 30 s on pool checkout and then failed with a 500.
Changes
APP_WORKERS(default 1) is passed to Granian'sworkers. Each worker has its own pool.app/__main__.pysets Granian'sbackpressuretoDB_POOL_SIZE + DB_MAX_OVERFLOW. Granian's default isbacklog // workers(granian/server/common.py), and its CLI help says the value is per worker. Requests beyond the pool now wait in the socket backlog instead of holding an event-loop task while they queue on the pool.DB_POOL_SIZEmust be positive andDB_MAX_OVERFLOWnon-negative. SQLAlchemy readsmax_overflow=-1as unlimited, which would give a backpressure below the pool size.pool_size=0would give a backpressure of 0, and Granian treats 0 as unset and falls back to 1024.DB_POOL_TIMEOUT(default 30 s, as before) is passed to the engine. A checkout timeout now returns 503 throughexceptions.database_unavailable_handler. advanced-alchemy wraps the timeout inRepositoryErrorwhen it happens inside a repository call, and the autocommit transaction raises it raw. So the handler is registered for bothsqlalchemy.exc.TimeoutErrorandRepositoryError, and it checks__cause__. In FastAPI the handler re-raises anything else, so Starlette'sServerErrorMiddlewarestill logs it and returns 500, as before. The more specificDuplicateKeyError,NotFoundErrorandForeignKeyErrorhandlers still win.postgres_retrydoesn't retry a pool timeout, because it isn't a DBAPI error.DB_POOL_RECYCLEis exposed with SQLAlchemy's default of-1. The template can't know the network, so AGENTS.md says to set it below any proxy or load balancer idle timeout. That avoids the 1-2 s retry backoff on the first request after an idle period.The Dockerfile (missing
CMD, dev dependencies in the image) is left for a separate change. The image is shared withjust test, so dropping dev dependencies needs its own build target.Tests
test_mainchecks the Granian kwargs:workersfromAPP_WORKERS, andbackpressureequal to pool size plus overflow.tests/test_pool.pyholds the only connection of a one-connection pool with a 0.1 s timeout. It checks that GET deck (the repository path) and POST deck (the autocommit path) both return 503. A second test pins that an unrelated repository error, a NUL byte that Postgres rejects, keeps its previous behaviour.test_settingsrejectsDB_POOL_SIZE=0andDB_MAX_OVERFLOW=-1.just testpasses 85 tests at 100% coverage,just test-migrationspasses, andjust lintis clean.