1
use super::registry::{PoolRegistry, TENANT_MAX_CONNECTIONS, TenantPool};
2
use crate::config::system_config;
3
use crate::db::DBError;
4
use crate::user::connection::TenantConnection;
5
use sqlx::postgres::PgPoolOptions;
6
use sqlx::types::Uuid;
7
use std::sync::Arc;
8
use std::time::Duration;
9
use tokio::sync::OnceCell;
10

            
11
/// Default idle life of a tenant pool, overridable via the
12
/// `userregistrytimeout` config key.
13
const DEFAULT_IDLE_SECS: u64 = 300;
14

            
15
/// How stale a tenant's quarantine status may be before it is re-read.
16
///
17
/// The status lives in the global directory and is written by the fleet
18
/// migrator in *another process*, so a warm pool here would otherwise keep
19
/// serving a database that has just been declared unusable — the registry is
20
/// per-process and nothing can invalidate it from outside. Re-reading on every
21
/// acquire would double the load on the admin pool (whose whole budget is 5);
22
/// re-reading on a short clock bounds the staleness instead, at one query per
23
/// tenant per interval.
24
const QUARANTINE_RECHECK: Duration = Duration::from_secs(30);
25

            
26
2028
async fn acquire_connection(
27
2028
    registry: &PoolRegistry,
28
2028
    user_id: Uuid,
29
2028
) -> Result<TenantConnection, DBError> {
30
2028
    let slot = match registry.touch(user_id)? {
31
117
        Some(slot) => slot,
32
1911
        None => registry.install(user_id)?,
33
    };
34

            
35
2028
    if registry.claim_recheck(user_id, QUARANTINE_RECHECK)? {
36
        match routing(user_id).await {
37
            // A tenant quarantined mid-flight must stop being served, so this
38
            // drops the pool as well as refusing the request.
39
            Ok(route) if route.quarantined || route.disabled => {
40
                registry.forget(user_id, &slot)?;
41
                return Err(refusal(&route));
42
            }
43
            Ok(_) => {}
44
            Err(e) => {
45
                // The claim already advanced the clock; give it back so the
46
                // next caller retries rather than trusting a stale answer for
47
                // the rest of the interval.
48
                registry.release_recheck(user_id, QUARANTINE_RECHECK)?;
49
                return Err(e);
50
            }
51
        }
52
2028
    }
53

            
54
    // Cloning the handle pins the pool for the length of this request: an
55
    // eviction between here and `acquire` only un-registers it, so the caller
56
    // never races a close.
57
2028
    let tenant = match slot.get_or_try_init(|| build(registry, user_id)).await {
58
234
        Ok(tenant) => Arc::clone(tenant),
59
1794
        Err(e) => {
60
1794
            registry.forget(user_id, &slot)?;
61
1794
            return Err(e);
62
        }
63
    };
64

            
65
234
    let conn = tenant.pool().acquire().await.map_err(DBError::Sqlx)?;
66

            
67
    // The pool travels with the connection: releasing its permit here, while
68
    // the connection is still open, is what let the resident bound be exceeded.
69
234
    Ok(TenantConnection::tenant(conn, tenant))
70
2028
}
71

            
72
/// The directory stores a database name; the DSN is composed here.
73
///
74
/// The residency permit is taken before connecting and moved into the
75
/// [`TenantPool`], so a build that fails or is cancelled returns it by dropping
76
/// it rather than leaving the slot to be reclaimed later.
77
1911
async fn build(registry: &PoolRegistry, user_id: Uuid) -> Result<Arc<TenantPool>, DBError> {
78
1911
    let permit = registry.reserve().await?;
79
1911
    let route = routing(user_id).await?;
80

            
81
    // Also here, not only on recheck: a *fresh* pool would otherwise be built
82
    // for a disabled user and served until the first recheck.
83
117
    if route.quarantined || route.disabled {
84
        return Err(refusal(&route));
85
117
    }
86

            
87
117
    let url = crate::provision::dsn_for(&route.database_name)?;
88

            
89
117
    let pool = PgPoolOptions::new()
90
117
        .max_connections(TENANT_MAX_CONNECTIONS)
91
117
        .idle_timeout(registry.idle_timeout())
92
117
        .acquire_timeout(Duration::from_secs(10))
93
117
        .connect(&url)
94
117
        .await
95
117
        .map_err(DBError::Sqlx)?;
96

            
97
117
    Ok(Arc::new(TenantPool::new(pool, permit)))
98
1911
}
99

            
100
/// Where a tenant lives and whether it may currently be served.
101
struct Route {
102
    database_name: String,
103
    quarantined: bool,
104
    disabled: bool,
105
}
106

            
107
/// Not the same event, so not the same user-facing text.
108
fn refusal(route: &Route) -> DBError {
109
    if route.quarantined {
110
        DBError::TenantQuarantined
111
    } else {
112
        DBError::TenantDisabled
113
    }
114
}
115

            
116
1911
async fn routing(user_id: Uuid) -> Result<Route, DBError> {
117
1911
    let mut conn = crate::db::get_connection().await?;
118

            
119
1911
    let row = sqlx::query_file!("../server/sql/select/system/tenant_routing.sql", &user_id)
120
1911
        .fetch_one(&mut *conn)
121
1911
        .await
122
1911
        .map_err(DBError::Sqlx)?;
123

            
124
117
    Ok(Route {
125
117
        database_name: row.database_name,
126
117
        quarantined: row.quarantined_at.is_some(),
127
117
        disabled: !row.verified,
128
117
    })
129
1911
}
130

            
131
static POOL_REGISTRY: OnceCell<PoolRegistry> = OnceCell::const_new();
132

            
133
2028
async fn pool_registry() -> &'static PoolRegistry {
134
2028
    POOL_REGISTRY
135
2028
        .get_or_init(|| async move {
136
156
            let timeout = match system_config("userregistrytimeout").await {
137
                Ok(Some(value)) => value.to_string().parse().unwrap_or(DEFAULT_IDLE_SECS),
138
156
                _ => DEFAULT_IDLE_SECS,
139
            };
140
156
            PoolRegistry::new(Duration::from_secs(timeout))
141
312
        })
142
2028
        .await
143
2028
}
144

            
145
2028
pub(in crate::user) async fn get_connection(user_id: Uuid) -> Result<TenantConnection, DBError> {
146
2028
    acquire_connection(pool_registry().await, user_id).await
147
2028
}