15 valittua Rust-taustaohjelmistokehityksen haastattelukysymystä ryhmiteltynä kokemustason mukaan. Käytä niitä perusasioiden, käytännön kompromissien ja senioritason tuotantopäätösten kertaamiseen.
1Selitä Rustin omistajuussäännöt ja miten ne muokkaavat käytännön taustajärjestelmän valintoja, kuten kloonausta, lainaamista ja sovelluksen tilan jakamista.
Rust antaa jokaiselle arvolle yhden omistajan. Ei-`Copy`-arvon siirtäminen siirtää omistajuuden, ja arvo tuhoutuu, kun sen omistaja poistuu näkyvyysalueelta. Koodi voi joko lainata tietoa muuttamattomana jaettujen viittausten (shared references) kautta tai muokattavasti yksinomaisen muokattavan viittauksen (exclusive mutable reference) kautta. Taustajärjestelmäkoodissa tämä vaikuttaa siihen, ottaako funktio omistajuuden, lainaako se tilapäisesti, kloonaako se itsenäisen omistetun arvon saamiseksi, vai jakaako se pitkäikäistä tilaa kahvojen, kuten `Arc`:in, avulla, synkronoinnin tai muiden samanaikaisuuden primitiivien kanssa, kun jaettua mutaatiota tarvitaan.
use std::sync::Arc;
struct AppState {
service_name: String,
}
async fn handler(state: Arc<AppState>, request_id: String) -> String {
// Borrow: no ownership transfer of the String inside state.
let name: &str = &state.service_name;
// Clone only if an owned independent value is needed.
let owned_name = name.to_owned();
format!("{owned_name}:{request_id}")
}
fn configure(state: Arc<AppState>) {
// Cloning Arc increments the reference count; it does not clone AppState.
let state_for_route = Arc::clone(&state);
let _ = state_for_route;
}
2Mitä eroa on lainaamisella (`borrowing`) `&T`:llä ja `&mut T`:llä, ja miten nämä säännöt vaikuttavat ohjelmointirajapintojen (API) ja käsittelijöiden (handler) allekirjoituksiin?
`&T` on jaettu viite: se sallii vain lukuoikeuden, ja monta jaettua viitettä samaan arvoon voi olla aktiivisena yhtä aikaa. `&mut T` on yksinomainen muuttuva viite: se sallii mutaation, mutta sen ollessa aktiivisena mitään muita jaettuja tai muuttuvia viitteitä samaan arvoon ei saa käyttää. Nämä säännöt muokkaavat ohjelmointirajapintoja siten, että vain lukuoikeuden omaavat funktiot hyväksyvät `&T`:n tai kapeampia lainattuja tyyppejä kuten `&str`, kun taas muuttavat funktiot hyväksyvät `&mut T`:n, ottavat omistajuuden tai käyttävät sisäistä muuttuvuutta/synkronointia. Taustajärjestelmän käsittelijät yleensä välttävät suoraa `&mut`-pääsyä jaettuun sovellustilaan samanaikaisten pyyntöjen kesken ja käyttävät sen sijaan jaettuja viitteitä sekä synkronointia tai muita omistajuusmalleja.
struct User {
name: String,
login_count: u64,
}
fn display_name(user: &User) -> &str {
&user.name
}
fn record_login(user: &mut User) {
user.login_count += 1;
}
fn main() {
let mut user = User { name: "Ada".into(), login_count: 0 };
let name = display_name(&user); // shared borrow for reading
println!("{name}");
record_login(&mut user); // exclusive mutable borrow for mutation
}
3Miten `Box` toimii, ja mitä ongelmia keon varaaminen `Box`:in avulla ratkaisee?
`Box<T>` on omistava älyosoitin, jonka `T` tallennetaan kekoon, kun taas `Box`-käsittelijä on kiinteänkokoinen osoitintyyppinen arvo. Sillä on yksinomistus, ja se pudottaa/vapauttaa keon varauksen, kun se poistuu näkyvyysalueelta. `Box` tarjoaa epäsuoran osoituksen, mikä auttaa suurten arvojen, rekursiivisten tyyppien, jotka tarvitsevat tunnetun koon, dynaamisesti mitoitettujen arvojen ja trait-olioiden, kuten `Box<dyn Trait>`, kanssa. `Box`:in siirtäminen siirtää varauksen omistajuuden siirtämättä tai kopioimatta itse kekoon tallennettua arvoa.
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
4Kuvaile `Result`- ja `Option`-tyypit sekä ensisijaiset idiomaattiset tavat käsitellä arvon puuttumista ja virheitä tuottavia operaatioita.
`Option<T>` edustaa joko `Some(T)`:tä tai `None`:a ja sitä käytetään odotettuun arvon puuttumiseen. `Result<T, E>` edustaa joko `Ok(T)`:tä tai `Err(E)`:tä ja sitä käytetään virheitä tuottaviin operaatioihin, joissa virhetiedolla on merkitystä. Idiomaattiseen käsittelyyn kuuluvat `match`, `if let`/`let else`, yhdistelijät (combinators) kuten `map`, `and_then`, `ok_or`, `unwrap_or` ja `unwrap_or_else`, sekä `?`-operaattori nopeaan virheen/puuttumisen propagointiin (välittämiseen) funktioista, jotka palauttavat yhteensopivia `Option`- tai `Result`-tyyppejä. `unwrap`:ia ja `expect`:iä tulisi käyttää ainoastaan invarianttien (muuttumattomuusehtojen), testien, prototyyppien tai korjaamattomien tilanteiden yhteydessä, ei normaaleihin palautettavissa oleviin taustajärjestelmän virheisiin.
5Miten `?`-operaattori toimii, ja mikä mahdollistaa muunnoksen eri virhetyyppien välillä?
`?`-operaattori on lyhenne virheiden eteenpäin välittämiseen. `Result`-tyypin osalta `expr?` antaa `Ok`-arvon ja jatkaa suoritusta, tai palautuu aikaisin nykyisestä funktiosta `Err`-arvolla; `Option`-tyypin osalta se antaa `Some`-arvon tai palauttaa `None`. Kutsuvan funktion on palautettava yhteensopiva tyyppi. `Result`-tyypin osalta eri virhetyypit voivat yhdistyä, koska lähdevirhe muunnetaan funktion virhetyypiksi käyttämällä `From`/`Into`-tyyppistä muunnoksen, joka on usein toteutettu manuaalisesti tai johdettu apupaketeilla kuten `thiserror`.
use std::{fs, io, num::ParseIntError};
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn read_number(path: &str) -> Result<i32, AppError> {
let s = fs::read_to_string(path)?; // io::Error -> AppError
let n = s.trim().parse::<i32>()?; // ParseIntError -> AppError
Ok(n)
}
6Selitä kuviohaku (pattern matching) enum-tyypeillä ja miten kattava kuviohaku (exhaustive matching) parantaa sovellusalueen ja API (Application Programming Interface) -rajapinnan korrektiutta.
Enum-tyypit mallintavat arvoa, joka voi olla yksi kiinteästä varianttijoukosta, ja variantit voivat sisältää dataa. `match`-lause haarautuu variantin mukaan ja voi purkaa sen sisältämää dataa. Rust tarkistaa, että enum-tyypin kuviohaku on kattava (exhaustive), ellei käytetä jokerimerkkiä (wildcard) tai kaiken kattavaa kuviota (catch-all pattern). Tämä parantaa sovellusalueen ja API-rajapinnan korrektiutta, koska kun uusi tila- tai vastausvariantti lisätään, kääntäjä voi pakottaa sitä vastaavan koodin päättämään, miten uusi tapaus käsitellään, sen sijaan, että se ohitettaisiin hiljaa.
7Mitä ovat newtype-kääreet, ja miten ne parantavat tyyppiturvallisuutta ID:lle, rahoille, tokeneille ja salaisuuksille?
Newtype-kääre on erillinen Rust-tyyppi, usein yhden kentän tuple-rakenne, joka käärii olemassa olevan esityksen, kuten `struct UserId(Uuid);` tai `struct AccessToken(String);`. Se parantaa tyyppiturvallisuutta, koska kääntäjä ei sekoita semanttisesti erilaisia arvoja, jotka jakavat saman taustalla olevan tyypin, kuten `UserId` vs. `OrderId`, sentit vs. dollarit, tai raakamerkkijonot vs. validoidut tokenit. Newtype-kääreet voivat myös keskittää luonti- ja validointisäännöt sekä hallita traitteja, kuten `Display`, `Debug`, `Serialize`, tai salaisuuksien sensurointikäyttäytymistä.
use uuid::Uuid;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct UserId(Uuid);
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct OrderId(Uuid);
struct SecretToken(String);
impl std::fmt::Debug for SecretToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SecretToken(**redacted**)")
}
}
fn load_user(id: UserId) {
// query by user id
}
fn example(user_id: UserId, order_id: OrderId) {
load_user(user_id);
// load_user(order_id); // does not compile: expected UserId, found OrderId
}
8Miten `Drop`-piirre (trait) toimii, mukaan lukien pudotusjärjestys, RAII (Resource Acquisition Is Initialization) -siivous ja rajoitukset asynkronisen siivouksen ympärillä?
`Drop` on Rustin deterministinen siivouskoukku. Kun omistettu arvo tuhotaan, esimerkiksi kun se menee näkyvyysalueen ulkopuolelle, Rust kutsuu automaattisesti sen `drop(&mut self)`-metodia, jos se toteuttaa `Drop`-piirteen, ja pudottaa sitten sen kentät. Tämä tukee RAII-periaatetta: resurssit kuten tiedostot, socketit, lukot, transaktiot, luvat tai puskurit sidotaan omistaviin arvoihin/suojiin ja vapautetaan, kun nämä arvot pudotetaan. Paikalliset muuttujat pudotetaan luomisjärjestyksen käänteisessä järjestyksessä; `Drop`-piirteen omaavan rakenteen (struct) `drop`-metodi suoritetaan ennen sen kenttien pudottamista, ja kentät pudotetaan määrittelyjärjestyksessä. `Drop::drop` on synkroninen eikä voi olla `async` tai odotettavissa, joten sulava asynkroninen siivous vaatii yleensä eksplisiittisen asynkronisen sulkemis-/sammutus-/huuhtelumenetelmän tai toisen suunnittelun; `Drop` voi suorittaa vain synkronista tai parhaan yrityksen siivousta.
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Guard,
second: Guard,
}
impl Drop for Pair {
fn drop(&mut self) {
println!("drop Pair");
}
}
fn main() {
let _a = Guard("a");
let _pair = Pair {
first: Guard("first"),
second: Guard("second"),
};
let _b = Guard("b");
}
9Selitä eliniät (lifetimes) ja miksi eksplisiittisiä elinikämerkintöjä (lifetime annotations) tarvitaan joskus Rustin API-rajapinnoissa.
Elinikä (lifetime) kuvaa, kuinka kauan viite (reference) on voimassa, mikä sallii kääntäjän hylätä roikkuvat viitteet (dangling references). Elinikämerkinnät (lifetime annotations) ovat käännösaikaisia rajoitteita, jotka ilmaisevat viitteiden välisiä suhteita; ne eivät pidennä taustalla olevan datan elinikää tai lisää ajonaikaista toimintaa. Monet yksinkertaiset allekirjoitukset käsitellään eliniän elisiolla (lifetime elision), mutta eksplisiittisiä merkintöjä tarvitaan, kun kääntäjä ei voi päätellä, miten syöte- ja tulosviitteet liittyvät toisiinsa, kun tyypit tallentavat viitteitä tai kun API-rajapinta tarvitsee ilmaista yleisiä lainausrajoituksia (generic borrowing constraints).
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() { left } else { right }
}
struct UserView<'a> {
name: &'a str,
}
fn main() {
let a = String::from("short");
let b = String::from("longer");
let result = longest(&a, &b);
let view = UserView { name: result };
println!("{}", view.name);
}
10Mitä `'static`-elinkaari tarkoittaa, ja miten se vuorovaikuttaa käynnistettyjen tehtävien ja taustajärjestelmän API-rajapinnan suunnittelun kanssa?
`'static` viittauksessa tarkoittaa, että viitattu data on voimassa koko ohjelman ajan, kuten merkkijonoliteraalien tai `static`-kohteiden tapauksessa. Rajoitus kuten `T: 'static` tarkoittaa, että tyypin `T` arvot eivät sisällä ei-`'static`-lainaamista viittauksia, joten niitä voidaan pitää minkä tahansa elinkaaren ajan; se ei tarkoita, että arvon itsensä täytyy elää ikuisesti tai sitä ei voi pudottaa (drop). Käynnistetyt tehtävät vaativat usein `'static`-futureja, koska suoritin (executor) saattaa jatkaa niiden suorittamista sen jälkeen, kun käynnistävä pinokehys on palannut. Taustajärjestelmäkoodissa tämä tarkoittaa yleensä omistetun datan siirtämistä tehtävään `async move`:lla, omistettujen tyyppien käyttämistä tai jaettujen kahvojen, kuten `Arc`, kloonaamista sen sijaan, että lainattaisiin paikallisia muuttujia. API-rajapintojen tulisi käyttää `'static`-rajoituksia tallennetuille takaisinkutsuille, taustatehtäville ja pitkäikäiselle tilalle, mutta välttää tarpeettomia `'static`-rajoituksia lyhytikäisille lainatuille operaatioille.
use std::sync::Arc;
struct AppState {
name: String,
}
fn spawn_background(state: Arc<AppState>, user_id: String) {
tokio::spawn(async move {
// The task owns an Arc handle and a String, so it does not borrow this function's stack.
println!("{}:{user_id}", state.name);
});
}
11Kuvaile, miten Rust käyttää `trait`-ominaisuuksia polymorfismiin, ja vertaa `impl Trait`, `generics` ja `dyn Trait`.
Rust käyttää `trait`-ominaisuuksia ilmaisemaan jaettua käyttäytymistä. Geneeriset rajat, kuten `fn f<T: Trait>(x: T)`, käyttävät yleensä staattista lähetystä (static dispatch) monomorfisoinnin kautta. Argumenttipositiossa oleva `impl Trait` on yleensä lyhenne anonyymistä geneerisestä parametrista. Palautuspositiossa oleva `impl Trait` piilottaa yhden konkreettisen palautustyypin, jonka funktio on valinnut. `dyn Trait` on `trait`-olio (trait object), jota käytetään osoittimen, kuten `&dyn Trait` tai `Box<dyn Trait>`, takana; se käyttää dynaamista lähetystä (dynamic dispatch) virtuaalitaulukon (vtable) kautta ja tukee ajonaikaista tyyppien poistamista (runtime type erasure) / heterogeenisiä arvoja (heterogeneous values) `object-safety`-rajoitusten mukaisesti.
12Mitä ovat liitetyt tyypit (associated types) piirteissä (traits), ja milloin ne ovat suositeltavampia kuin geneeriset tyyppiparametrit?
Liitetyt tyypit ovat piirteen sisällä määriteltyjä nimettyjä tyyppipaikkoja, jotka jokainen toteutus määrittää, esimerkiksi `Iterator::Item`. Ne ovat parempia, kun tyyppi on osa piirteen sopimusta ja jokaisella toteuttajalla on sille yksi luonnollinen valinta. Geneeriset tyyppiparametrit ovat parempia, kun kutsujan tulisi valita tyyppi tai kun sama toteuttaja saattaa tarvita useita toteutuksia eri tyyppivalinnoille.
Transaktiollisen outbox-mallin toteuttamiseksi Rust-palvelussa, kirjoita toimialueen (domain) tiedonpäivitykset ja lähtevät tapahtumasanomat (event payloads) atomisesti yhden tietokantatransaktion sisällä (esim. käyttäen `sqlx::Transaction`). `outbox`-taulukko tallentaa kohdeaiheen, sanoman sisällön (payload) ja tilan tai luontihetken aikaleiman. Taustalla ajava asynkroninen työsäie (tai Change Data Capture (CDC) -prosessi kuten Debezium) kysyy tai striimaa odottavia tapahtumia ja julkaisee ne sanomajonopalveluun (esim. Kafka, RabbitMQ tai NATS). Kun Rustissa käytetään kyselyyn perustuvaa työsäiettä (esim. `tokio::spawn`-silmukassa), `SELECT ... FOR UPDATE SKIP LOCKED` -lauseen käyttäminen mahdollistaa useiden palveluesiintymien hakevan ja lähettävän `outbox`-rivejä samanaikaisesti ilman lukitusristiriitoja (lock contention). Kun sanomajonopalvelun toimitus on vahvistettu, työsäie päivittää `outbox`-tietueen tilan tai poistaa rivin. Koska verkon uudelleenyritykset voivat johtaa ainakin kerran -toimitukseen (at-least-once delivery), vastaanottajien (downstream consumers) on oltava suunniteltu käsittelemään viestejä idempotentisti.
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;
pub async fn create_user_with_outbox(
mut tx: Transaction<'_, Postgres>,
username: &str,
email: &str,
) -> Result<(), sqlx::Error> {
let user_id = Uuid::new_v4();
sqlx::query!(
"INSERT INTO users (id, username, email) VALUES ($1, $2, $3)",
user_id, username, email
)
.execute(&mut *tx)
.await?;
let payload = serde_json::json!({"event": "UserCreated", "user_id": user_id, "email": email});
sqlx::query!(
"INSERT INTO outbox_events (id, topic, payload, status) VALUES ($1, $2, $3, 'PENDING')",
Uuid::new_v4(),
"user_events",
payload
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
pub async fn run_outbox_relay(pool: PgPool) {
loop {
let mut tx = match pool.begin().await { Ok(t) => t, Err(_) => continue };
let rows = sqlx::query!(
"SELECT id, topic, payload FROM outbox_events WHERE status = 'PENDING' LIMIT 50 FOR UPDATE SKIP LOCKED"
)
.fetch_all(&mut *tx)
.await;
if let Ok(events) = rows {
for event in events {
// publish_to_broker(&event.topic, &event.payload).await;
let _ = sqlx::query!("UPDATE outbox_events SET status = 'PROCESSED' WHERE id = $1", event.id)
.execute(&mut *tx)
.await;
}
let _ = tx.commit().await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
}
14Miten suunnittelisit monivaiheisen operaation, joka päivittää tietokannan ja kutsuu ulkoista APIa (Application Programming Interface)?
Monivaiheisen operaation suunnittelu, joka sisältää tietokantakirjoituksen ja ulkoisen API-kutsun (Application Programming Interface), edellyttää osittaisten vikojen ja verkon ositusten hallintaa turvautumatta kaksivaiheiseen sitoutumiseen (2PC - Two-Phase Commit):
1. **Tilan / tarkoituksen tallentaminen ensin:** Tallenna alustava tila (kuten `PENDING` tai `INITIATED`) ja tarvittava hyötykuorma paikalliseen tietokantatransaktioon. Sitoudu tähän transaktioon ennen ulkoisen verkkokutsun käynnistämistä välttääksesi tietokantalukkojen ja yhteyspoolin paikkojen pitämisen hitaiden verkon I/O-operaatioiden aikana.
2. **Idempotentti ulkoinen pyyntö:** Kutsu ulkoista APIa käyttäen determinististä idempotenssiavainta (esim. johdettu operaation UUID:stä). Tämä varmistaa, etteivät toistuvat yritykset aiheuta päällekkäisiä todellisia sivuvaikutuksia (kuten kaksoisveloitusta).
3. **Tilanmuutos ja aikakatkaisujen käsittely:** Jos API-kutsu palauttaa onnistuneen vastauksen, päivitä tietokantatietue tilaksi `COMPLETED`. Jos se epäonnistuu pysyvästi, merkitse se tilaksi `FAILED` ja suorita korjaavat toimenpiteet. Jos kutsu aikakatkaistaan tai se palauttaa verkon virheen, merkitse tietue tilaksi `INDETERMINATE` / `REQUIRES_RECONCILIATION` ja anna taustatyöntekijän täsmäyttää tila kysymällä palveluntarjoajan tilapäätepistettä tai yrittämällä uudelleen samalla idempotenssiavaimella.
use uuid::Uuid;
pub async fn process_external_charge(pool: &sqlx::PgPool, user_id: Uuid, amount: i64) -> Result<(), String> {
let payment_id = Uuid::new_v4();
let idempotency_key = format!("pay_{}", payment_id);
// 1. Commit intent locally before making external network calls
sqlx::query!(
"INSERT INTO payments (id, user_id, amount, status, idempotency_key) VALUES ($1, $2, $3, 'PENDING', $4)",
payment_id, user_id, amount, idempotency_key
)
.execute(pool)
.await
.map_err(|e| e.to_string())?;
// 2. Call external API with timeout and idempotency key
let client = reqwest::Client::new();
let api_result = client.post("https://api.payment.com/v1/charges")
.header("Idempotency-Key", &idempotency_key)
.json(&serde_json::json!({ "amount": amount }))
.timeout(std::time::Duration::from_secs(5))
.send()
.await;
// 3. Update state based on outcome; handle timeouts via reconciliation
match api_result {
Ok(resp) if resp.status().is_success() => {
sqlx::query!("UPDATE payments SET status = 'SUCCESS' WHERE id = $1", payment_id)
.execute(pool)
.await
.map_err(|e| e.to_string())?;
}
Ok(_) => {
sqlx::query!("UPDATE payments SET status = 'FAILED' WHERE id = $1", payment_id)
.execute(pool)
.await
.map_err(|e| e.to_string())?;
}
Err(_) => {
sqlx::query!("UPDATE payments SET status = 'REQUIRES_RECONCILIATION' WHERE id = $1", payment_id)
.execute(pool)
.await
.map_err(|e| e.to_string())?;
}
}
Ok(())
}
15Miten suunnittelisit monivuokralaisen (multi-tenant) Rust-taustaosan tiukalla tietojen eristyksellä ja dynaamisella reitityksellä?
Monivuokralaisen Rust-taustaosan suunnitteluun liittyy kolme avainaluetta: vuokralaisen tunnistus, tietojen eristysstrategia ja dynaaminen reititys.
1. **Vuokralaisen poiminta ja kontekstin levitys (Tenant Extraction & Context Propagation)**: HTTP-väliohjelmisto (esim. Axumissa tai Actix-webissä) tunnistaa vuokralaisen identiteetin JWT (JSON Web Token) -vaateista, aliverkkotunnuksista tai otsakkeista. Se luo vahvasti tyypitetyn `TenantContext`-olion, joka tallennetaan pyynnön laajennuksiin. Käsittelijät käyttävät Rustin tyyppiekstraktoreita varmistaakseen, ettei liiketoimintalogiikka voi suorittaa ilman kelvollista vuokralaiskontekstia.
2. **Tietojen eristysstrategia (Data Isolation Strategy)**:
* ***Jaettu tietokanta sarake-/rivi-tason turvallisuudella (RLS)***: Kaikki vuokralaiset jakavat taulukot, joissa on `tenant_id`-sarake. Eristys toteutetaan PostgreSQL:n RLS:n avulla käyttäen yhteyskohtaisia istuntomuuttujia (esim. `SET LOCAL app.tenant_id = $1`) tai kyselyrakentajan kääreitä.
* ***Skeema vuokralaista kohti***: Vuokralaiset jakavat tietokantaesiintymän, mutta heillä on eristetyt skeemat; reititys säätää hakupolkua jokaista yhteyttä kohden.
* ***Tietokanta vuokralaista kohti***: Vuokralaisilla on erilliset tietokannat. Palvelu ylläpitää poolirekisteriä (esim. `Arc<DashMap<TenantId, PgPool>>` tai LRU-poolikätkö), jotta yhteyspoolit voidaan ratkaista dynaamisesti.
3. **Vuotojen estäminen (Leak Prevention)**: Vuokralaisten välisen kontaminaation välttämiseksi asynkronisissa Tokio-sovelluksissa vuokralaiskonteksti on välitettävä eksplisiittisesti tehtävien rajojen yli, eikä sitä saa tallentaa globaaliin tilaan tai säietason muistiin (`thread_local!`), joka rikkoutuu asynkronisten työsäikeiden siirtojen yhteydessä.
use axum::{extract::{FromRequestParts, State}, http::request::Parts, async_trait};
use std::sync::Arc;
use dashmap::DashMap;
use sqlx::PgPool;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TenantId(pub String);
#[derive(Clone)]
pub struct AppState {
pub pool_registry: Arc<DashMap<TenantId, PgPool>>,
}
#[derive(Clone)]
pub struct TenantContext {
pub tenant_id: TenantId,
pub pool: PgPool,
}
#[async_trait]
impl FromRequestParts<AppState> for TenantContext {
type Rejection = (axum::http::StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
let tenant_header = parts.headers.get("X-Tenant-ID")
.and_then(|v| v.to_str().ok())
.ok_or((axum::http::StatusCode::BAD_REQUEST, "Missing tenant ID"))?;
let tenant_id = TenantId(tenant_header.to_string());
let pool = match state.pool_registry.get(&tenant_id) {
Some(p) => p.value().clone(),
None => return Err((axum::http::StatusCode::NOT_FOUND, "Tenant database pool not found")),
};
Ok(TenantContext { tenant_id, pool })
}
}