From d8889cdedd7ba937b949238ea0ec9236259d41f3 Mon Sep 17 00:00:00 2001 From: Tomas Sanchez Date: Fri, 12 Jun 2026 21:10:57 -0300 Subject: [PATCH] docs(template): emphasize async-first and recommend Asyncer's asyncify Make async the explicit first choice for new adapters/handlers/routes in AGENTS.md and ADR 0017, and recommend wrapping unavoidable blocking calls with asyncify() from tiangolo's Asyncer (await asyncify(fn)(arg); uv add asyncer) instead of blocking the event loop. Co-Authored-By: Claude Fable 5 --- template/AGENTS.md | 17 ++++++++++------- .../adr/0017-async-persistence-by-default.md | 13 ++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/template/AGENTS.md b/template/AGENTS.md index 2f60b48..08272ae 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -31,14 +31,17 @@ an ADR, run `make adr-check`. ## Async and Persistence -- The application is **async end to end** - ([ADR 0017](docs/adr/0017-async-persistence-by-default.md)). Routes, handlers, - the message bus, repositories, query readers, and the unit of work are `async`; - persistence uses `create_async_engine` and `AsyncSession`. Keep **domain objects - synchronous** — business rules perform no I/O. +- **Async is the default — reach for it first.** The application is async end to + end ([ADR 0017](docs/adr/0017-async-persistence-by-default.md)): routes, + handlers, the message bus, repositories, query readers, and the unit of work + are `async`, and persistence uses `create_async_engine` and `AsyncSession`. + Keep **domain objects synchronous** — business rules perform no I/O. - Never block the event loop inside `async def` (no synchronous DB driver, no - blocking call). `await` database work; offload genuinely blocking work to a - thread only with a documented reason. + blocking call). `await` database work. When a dependency is unavoidably + synchronous, wrap it with `asyncify()` from + [Asyncer](https://asyncer.tiangolo.com/) (by FastAPI's author) — + `await asyncify(blocking_fn)(arg)` runs it in a worker thread + (`uv add asyncer`) — rather than leaving the blocking call inline. - The default database is **PostgreSQL** via `asyncpg`; **SQLite** via `aiosqlite` is the optional alternative ([ADR 0018](docs/adr/0018-postgresql-default-with-pgvector.md)). Select the diff --git a/template/docs/adr/0017-async-persistence-by-default.md b/template/docs/adr/0017-async-persistence-by-default.md index 0ced804..eb59f5a 100644 --- a/template/docs/adr/0017-async-persistence-by-default.md +++ b/template/docs/adr/0017-async-persistence-by-default.md @@ -49,9 +49,15 @@ semantics. The domain layer is unaffected and remains trivially unit-testable. ## Agent Guidance -- Make every adapter, service-layer, and entrypoint I/O path `async`/`await`. -- Never call blocking I/O inside an `async def`; offload to a thread only with a - documented reason. +- Reach for async first: a new adapter, handler, route, or client is `async` by + default. Make every adapter, service-layer, and entrypoint I/O path + `async`/`await`. +- Never call blocking I/O inside an `async def`. When a dependency is + unavoidably synchronous, wrap the call with `asyncify()` from + [Asyncer](https://asyncer.tiangolo.com/) (by FastAPI's author) — e.g. + `await asyncify(blocking_fn)(arg)` — which runs it in a worker thread instead + of blocking the event loop (`uv add asyncer`). Do not paper over a blocking + call by leaving it inline. - Keep domain methods synchronous and free of I/O. - Use `AsyncSession` and `await` commits/queries; do not mix a sync `Session` into the request path. @@ -61,5 +67,6 @@ semantics. The domain layer is unaffected and remains trivially unit-testable. - [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) - [FastAPI: async and await](https://fastapi.tiangolo.com/async/) +- [Asyncer: `asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/) - [pytest-asyncio](https://pytest-asyncio.readthedocs.io/) - [ADR 0006: Async Is an Explicit End-to-End Choice](0006-async-is-an-explicit-end-to-end-choice.md)