backbone_payroll/request_pool.rs
1//! The composer-installed request pool, made visible to the verb services.
2//!
3//! ADR-0029's pool law: modules are tenant-agnostic and the composing
4//! service owns routing to the right database. The composer's tenant router
5//! already inserts the request's `PgPool` into the request extensions;
6//! [`bind_request_pool`] copies it into a task-local for the duration of the
7//! handler future, and the write services resolve their database through
8//! [`current`] with their composed pool as the fallback — so a mount without
9//! tenant routing behaves exactly as before, and a verb under a tenant mount
10//! writes to that tenant's database (host-wired ports ride along untouched).
11
12use axum::{body::Body, http::Request, middleware::Next, response::Response};
13use sqlx::PgPool;
14
15tokio::task_local! {
16 static REQUEST_POOL: PgPool;
17}
18
19/// The pool this request's tenant router installed, if any.
20pub fn current() -> Option<PgPool> {
21 REQUEST_POOL.try_with(|pool| pool.clone()).ok()
22}
23
24/// Middleware that binds the composer-inserted pool for the whole handler
25/// call. Requests without an inserted pool pass straight through.
26pub async fn bind_request_pool(req: Request<Body>, next: Next) -> Response {
27 match req.extensions().get::<PgPool>().cloned() {
28 Some(pool) => REQUEST_POOL.scope(pool, next.run(req)).await,
29 None => next.run(req).await,
30 }
31}
32
33/// Run a future with a pool bound as this module's request pool — the host
34/// relay's per-tenant consumers use this so module-side consumers resolve
35/// the tenant database through their rpool() (ADR-0029 pool law).
36pub async fn with_pool_scope<F: std::future::Future<Output = O>, O>(
37 pool: sqlx::PgPool,
38 fut: F,
39) -> O {
40 REQUEST_POOL.scope(pool, fut).await
41}