Příprava na pohovor Rust

Dotazy k pohovoru pro Rust backend vývojáře

15 vybraných dotazů k pohovoru pro Rust backend, seskupených podle úrovně seniority. Použijte je k zopakování základů, praktických kompromisů a uvažování na seniorské úrovni pro produkční prostředí.

Spustit AI pohovor RustNení vyžadována platební karta. K dispozici je 1 bezplatná relace.
Příprava na technické pohovory v angličtiněRežim pro lidi, pro které angličtina není rodným jazykem, určený k procvičování technických pohovorů.

Otázky pro juniory

1Vysvětlete pravidla vlastnictví (ownership) v Rustu a jak ovlivňují praktická rozhodnutí při vývoji backendu, jako je klonování, půjčování (borrowing) a sdílení stavu aplikace.

Rust přiřazuje každé hodnotě jediného vlastníka. Přesunutí hodnoty, která není typu `Copy`, přenáší vlastnictví, a hodnota je zrušena, když její vlastník opustí rozsah platnosti. Kód si může data půjčit (borrow) buď neměnně prostřednictvím sdílených referencí, nebo měnně prostřednictvím výhradní měnitelné reference. V kódu backendu to ovlivňuje, zda si funkce převezme vlastnictví, půjčí si data dočasně, klonuje je, aby získala nezávislou vlastní hodnotu, nebo sdílí dlouhodobě žijící stav prostřednictvím úchytů, jako je `Arc`, se synchronizací nebo jinými primitivy pro souběžnost, pokud je potřeba sdílená mutace.

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;
}
Zkusit odpovědět na tuto otázku s trenérem AI

2Jaký je rozdíl mezi vypůjčováním s `&T` a `&mut T` a jak tato pravidla ovlivňují API (Application Programming Interface) a podpisy handlerů?

`&T` je sdílená reference: umožňuje přístup pouze pro čtení a mnoho sdílených referencí na stejnou hodnotu může být aktivních najednou. `&mut T` je exkluzivní mutabilní reference: umožňuje mutaci, ale dokud je aktivní, nelze použít žádné jiné sdílené ani mutabilní reference na stejnou hodnotu. Tato pravidla utvářejí API (Application Programming Interface) tím, že funkce pouze pro čtení přijímají `&T` nebo užší vypůjčené typy, jako je `&str`, zatímco mutující funkce přijímají `&mut T`, přebírají vlastnictví nebo používají vnitřní mutabilitu/synchronizaci. Backendové handlery obvykle zabraňují přímému `&mut` přístupu ke sdílenému aplikačnímu stavu napříč souběžnými požadavky a místo toho používají sdílené úchyty plus synchronizaci nebo jiné vzorce vlastnictví.

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
}
Zkusit odpovědět na tuto otázku s trenérem AI

3Jak funguje `Box` a jaké problémy řeší alokace na haldě pomocí `Box`?

`Box<T>` je vlastnící chytrý ukazatel, jehož `T` je uloženo na haldě, zatímco identifikátor `Box` je hodnota s pevnou velikostí podobná ukazateli. Má jediné vlastnictví a uvolní alokaci na haldě, když vyjde z rozsahu platnosti. `Box` poskytuje nepřímé adresování, což pomáhá u velkých hodnot, rekurzivních typů, které potřebují známou velikost, dynamicky dimenzovaných hodnot a objektů vlastností (trait objects), jako například `Box<dyn Trait>`. Přesunutí `Box` přenáší vlastnictví alokace bez přesouvání nebo kopírování samotné hodnoty uložené na haldě.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Zkusit odpovědět na tuto otázku s trenérem AI

4Popište `Result` a `Option` a primární idiomatické způsoby práce s nepřítomností hodnoty a operacemi, které mohou selhat.

`Option<T>` představuje buď `Some(T)` nebo `None` a používá se pro očekávanou nepřítomnost hodnoty. `Result<T, E>` představuje buď `Ok(T)` nebo `Err(E)` a používá se pro operace, které mohou selhat a kde jsou důležité informace o chybě. Idiomatické zpracování zahrnuje `match`, `if let`/`let else`, kombinátory jako `map`, `and_then`, `ok_or`, `unwrap_or`, a `unwrap_or_else`, a operátor `?` pro časné šíření chyb z funkcí vracejících kompatibilní typy `Option` nebo `Result`. Metody `unwrap` a `expect` jsou nejlépe vyhrazeny pro invarianty, testy, prototypy nebo neobnovitelné situace, nikoliv pro běžné obnovitelné chyby na backendu.

fn parse_port(s: Option<&str>) -> u16 {
    s.and_then(|v| v.parse::<u16>().ok())
     .unwrap_or(8080)
}

assert_eq!(parse_port(Some("3000")), 3000);
assert_eq!(parse_port(None), 8080);
Zkusit odpovědět na tuto otázku s trenérem AI

5Jak funguje operátor `?` a co umožňuje konverzi mezi různými typy chyb?

Operátor `?` je zkratka pro propagování chyb. Pro `Result` operátor `expr?` vrátí hodnotu `Ok` a pokračuje, nebo předčasně vrátí `Err` z aktuální funkce; pro `Option` vrátí hodnotu `Some` nebo vrátí `None`. Obklopující funkce musí vracet kompatibilní typ. Pro `Result` se různé typy chyb mohou skládat, protože zdrojová chyba je konvertována na typ chyby funkce pomocí konverze ve stylu `From`/`Into`, často implementované ručně nebo odvozené pomocí pomocných `crate`ů, jako je `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)
}
Zkusit odpovědět na tuto otázku s trenérem AI

6Vysvětlete porovnávání vzorů (pattern matching) u výčtových typů (enums) a jak vyčerpávající porovnávání zlepšuje správnost domény a API.

Výčtové typy (enums) modelují hodnotu, která může být jednou z pevné sady variant, a varianty mohou nést data. Konstrukce `match` rozvětvuje podle varianty a může destrukturovat data uvnitř. Rust kontroluje, zda jsou shody výčtových typů vyčerpávající, pokud není použit zástupný nebo všeobjímající vzor. To zlepšuje správnost domény a API (Application Programming Interface), protože když je přidán nový stav nebo varianta odpovědi, kompilátor může vynutit, aby kód, který na ni shody provádí, rozhodl, jak s novým případem naložit, místo aby jej tiše ignoroval.

enum PaymentStatus {
    Pending,
    Paid { receipt_id: String },
    Failed(String),
}

fn message(status: PaymentStatus) -> String {
    match status {
        PaymentStatus::Pending => "Payment is pending".to_string(),
        PaymentStatus::Paid { receipt_id } => format!("Paid, receipt {receipt_id}"),
        PaymentStatus::Failed(reason) => format!("Payment failed: {reason}"),
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

7Co jsou to newtype obaly (newtype wrappers) a jak zlepšují typovou bezpečnost pro ID, peníze, tokeny a tajemství?

Newtype obal je v Rustu odlišný typ, často struktura s jedním polem typu tuple (one-field tuple struct), obalující existující reprezentaci, například `struct UserId(Uuid);` nebo `struct AccessToken(String);`. Zlepšuje typovou bezpečnost, protože kompilátor si nesplete sémanticky odlišné hodnoty, které sdílejí stejný podkladový typ, jako je `UserId` versus `OrderId`, centy oproti dolarům, nebo čisté řetězce oproti validovaným tokenům. Newtype obaly mohou také centralizovat pravidla pro konstrukci a validaci a řídit traity jako `Display`, `Debug`, `Serialize` nebo chování pro začernění tajemství.

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
}
Zkusit odpovědět na tuto otázku s trenérem AI

Otázky pro středně pokročilé

8Jak funguje trait `Drop` včetně pořadí rušení, úklidu pomocí RAII (Resource Acquisition Is Initialization) a omezení týkajících se asynchronního úklidu?

`Drop` je deterministický háček pro úklid (cleanup hook) v Rustu. Když je vlastněná hodnota zničena, například když opustí svůj rozsah (goes out of scope), Rust automaticky volá její metodu `drop(&mut self)`, pokud implementuje `Drop`, a poté ruší její pole. To podporuje RAII (Resource Acquisition Is Initialization): prostředky jako soubory, sokety, zámky, transakce, oprávnění nebo buffery jsou vázány na vlastnící hodnoty/stráže (guards) a jsou uvolněny, když jsou tyto hodnoty zrušeny. Lokální proměnné jsou rušeny v opačném pořadí jejich vytvoření; pro strukturu s `Drop` se metoda `drop` struktury spustí před zrušením jejích polí a pole jsou rušena v pořadí deklarace. `Drop::drop` je synchronní a nemůže být `async` ani očekáván (awaited), takže elegantní asynchronní úklid (graceful async cleanup) obvykle vyžaduje explicitní asynchronní metodu pro zavření/vypnutí/vyprázdnění nebo jiný návrh; `Drop` může provádět pouze synchronní úklid nebo úklid typu „best-effort“.

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");
}
Zkusit odpovědět na tuto otázku s trenérem AI

9Vysvětlete životnost (lifetimes) a proč jsou explicitní anotace životnosti někdy vyžadovány v API v Rustu.

Životnost (lifetime) popisuje, jak dlouho je reference platná, což kompilátoru umožňuje odmítnout visící reference (dangling references). Anotace životnosti jsou omezení v době kompilace, která vyjadřují vztahy mezi referencemi; neprodlužují životnost podkladových dat ani nepřidávají chování za běhu. Mnoho jednoduchých signatur je zpracováno elizí životnosti (lifetime elision), ale explicitní anotace jsou potřeba, když kompilátor nemůže odvodit, jak souvisí vstupní a výstupní reference, když typy ukládají reference, nebo když API potřebuje vyjádřit generická omezení vypůjčování (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);
}
Zkusit odpovědět na tuto otázku s trenérem AI

10Co znamená životnost `'static` a jak interaguje se spouštěnými úlohami a návrhem backend API?

`'static` na referenci znamená, že referencovaná data jsou platná po celou dobu běhu programu, podobně jako u řetězcových literálů nebo `static` položek. Omezení typu `T: 'static` znamená, že hodnoty typu `T` neobsahují vypůjčené reference, které nejsou `'static`, takže mohou být drženy po libovolně dlouhou dobu; neznamená to, že hodnota samotná musí žít věčně nebo že nemůže být uvolněna. Spouštěné úlohy (spawned tasks) často vyžadují `'static` futures, protože exekutor je může nadále spouštět i poté, co se vrátí zásobníkový rámec spouštění. V backend kódu to obvykle znamená přesun vlastněných dat do úlohy pomocí `async move`, použití vlastněných typů nebo klonování sdílených úchytů, jako je `Arc`, spíše než vypůjčování lokálních proměnných. API by měly používat `'static` omezení pro uložená zpětná volání (callbacks), úlohy na pozadí a dlouhodobý stav, ale vyhnout se zbytečným `'static` omezením pro krátkodobé vypůjčené operace.

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);
    });
}
Zkusit odpovědět na tuto otázku s trenérem AI

11Popište, jak Rust využívá traity pro polymorfismus, a porovnejte `impl Trait`, generika a `dyn Trait`.

Rust využívá traity (vlastnosti) k vyjádření sdíleného chování. Generické hranice, jako `fn f<T: Trait>(x: T)`, obvykle používají statickou dispečerizaci (static dispatch) prostřednictvím monomorfizace. `impl Trait` v pozici argumentu je většinou zkratkou pro anonymní generický parametr. `impl Trait` v pozici návratové hodnoty skrývá jeden konkrétní návratový typ zvolený funkcí. `dyn Trait` je trait objekt používaný za ukazatelem, jako například `&dyn Trait` nebo `Box<dyn Trait>`; využívá dynamickou dispečerizaci (dynamic dispatch) prostřednictvím vtable a podporuje mazání typů za běhu (runtime type erasure) / heterogenní hodnoty s výhradou omezení bezpečnosti objektů (object-safety restrictions).

trait Render { fn render(&self) -> String; }

struct Html;
struct Json;

impl Render for Html { fn render(&self) -> String { "html".into() } }
impl Render for Json { fn render(&self) -> String { "json".into() } }

fn render_generic<T: Render>(x: &T) -> String { x.render() } // static dispatch
fn render_impl_arg(x: &impl Render) -> String { x.render() } // anonymous generic
fn make_renderer() -> impl Render { Html } // one hidden concrete type
fn render_dyn(x: &dyn Render) -> String { x.render() } // dynamic dispatch

fn main() {
    let items: Vec<Box<dyn Render>> = vec![Box::new(Html), Box::new(Json)];
    for item in items { println!("{}", render_dyn(item.as_ref())); }
}
Zkusit odpovědět na tuto otázku s trenérem AI

12Co jsou to asociované typy v traitech a kdy jsou vhodnější než generické typové parametry?

Asociované typy jsou pojmenované zástupné symboly typů (type placeholders) deklarované uvnitř traitů a specifikované každou implementací, například `Iterator::Item`. Jsou vhodnější, když je typ součástí kontraktu traitu a každý implementátor pro něj má jednu přirozenou volbu. Generické typové parametry jsou vhodnější, když má volající vybrat typ, nebo když stejný implementátor může potřebovat více implementací pro různé typové volby.

trait Repository {
    type Entity;
    fn get(&self, id: u64) -> Option<Self::Entity>;
}

struct User;
struct UserRepo;

impl Repository for UserRepo {
    type Entity = User;
    fn get(&self, _id: u64) -> Option<Self::Entity> { Some(User) }
}

fn load<R: Repository>(repo: &R) -> Option<R::Entity> {
    repo.get(1)
}
Zkusit odpovědět na tuto otázku s trenérem AI

Otázky pro seniory

13Jak byste implementovali vzor transakční schránky (transactional outbox pattern) ve službě v Rustu?

Pro implementaci vzoru transakční schránky ve službě v Rustu je třeba zapsat aktualizace doménových dat a odchozí události (payloads) atomicky v rámci jedné databázové transakce (např. pomocí `sqlx::Transaction`). Tabulka `outbox` ukládá cílové téma, data události (payload), a stav nebo čas vytvoření. Asynchronní pracovník na pozadí (nebo proces snímání změn dat (Change Data Capture - CDC), jako je Debezium) poté dotazuje nebo streamuje čekající události a publikuje je do message brokeru (například Kafka, RabbitMQ nebo NATS). Při použití dotazujícího pracovníka (polling worker) v Rustu (např. ve smyčce `tokio::spawn`) umožňuje použití `SELECT ... FOR UPDATE SKIP LOCKED` více instancím služby paralelně načítat a odesílat řádky z outboxu bez kolize zámků. Jakmile je doručení brokerem potvrzeno, pracovník aktualizuje stav záznamu v outboxu nebo řádek smaže. Protože opakované pokusy o síťové doručení mohou vést k doručení minimálně jednou (at-least-once delivery), následní konzumenti (downstream consumers) musí být navrženi tak, aby zprávy zpracovávali idempotentně.

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;
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

14Jak byste navrhli vícestupňovou operaci, která aktualizuje databázi a volá externí API (Application Programming Interface)?

Návrh vícestupňové operace, která zahrnuje zápis do databáze a volání externího API, vyžaduje správu částečných selhání a síťových oddílů (network partitions) bez spoléhání se na dvoufázové potvrzení (2PC – Two-Phase Commit). 1. **Nejprve perzistujte stav / záměr**: Zaznamenejte počáteční stav (např. `PENDING` nebo `INITIATED`) a požadované datové zatížení uvnitř lokální databázové transakce. Potvrďte tuto transakci před iniciováním externího síťového volání, abyste se vyhnuli držení databázových zámků a slotů v poolu připojení během pomalého síťového I/O (Input/Output). 2. **Idempotentní externí požadavek**: Volejte externí API pomocí deterministického klíče idempotence (např. odvozeného z UUID operace). Tím se zajistí, že opakované pokusy nezpůsobí duplicitní reálné vedlejší efekty (např. dvojité účtování). 3. **Přechod stavu a zpracování vypršení časového limitu**: Pokud volání API vrátí úspěšnou odpověď, aktualizujte záznam v databázi na `COMPLETED`. Pokud trvale selže, označte jej jako `FAILED` a proveďte kompenzační akce. Pokud volání vyprší časový limit nebo vrátí síťovou chybu, označte záznam jako `INDETERMINATE` / `REQUIRES_RECONCILIATION` a nechte úlohu na pozadí (background worker) sladit stav dotazováním se na stavový koncový bod poskytovatele nebo opakovaným pokusem se stejným klíčem idempotence.

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(())
}
Zkusit odpovědět na tuto otázku s trenérem AI

15Jak byste navrhli víceuživatelský (multi-tenant) Rust backend s přísnou izolací dat a dynamickým směrováním?

Návrh víceuživatelského (multi-tenant) Rust backendu zahrnuje tři klíčové oblasti: identifikaci uživatele (tenanta), strategii izolace dat a dynamické směrování. 1. **Extrakce uživatele a šíření kontextu**: HTTP middleware (např. v Axum nebo Actix-web) rozpozná identitu uživatele z JWT (JSON Web Token) tokenů, subdomén nebo hlaviček. Vytvoří silně typovaný `TenantContext` uložený v rozšířeních požadavku (request extensions). Handlery používají Rust typové extraktory k vynucení, aby obchodní logika nemohla být spuštěna bez platného kontextu uživatele. 2. **Strategie izolace dat**: * **Sdílená databáze s bezpečností na úrovni sloupců / řádků (Column / Row-Level Security – RLS)**: Všichni uživatelé sdílejí tabulky se sloupcem `tenant_id`. Izolace je vynucena prostřednictvím PostgreSQL RLS s použitím proměnných relace na úrovni připojení (např. `SET LOCAL app.tenant_id = $1`) nebo obalů dotazovacích nástrojů (query builder wrappers). * **Schéma na uživatele (Schema-per-Tenant)**: Uživatelé sdílejí instanci databáze, ale mají izolovaná schémata; směrování upravuje cestu vyhledávání (search path) pro každé připojení. * **Databáze na uživatele (Database-per-Tenant)**: Uživatelé mají oddělené databáze. Služba udržuje registr fondů připojení (pool registry) (např. `Arc<DashMap<TenantId, PgPool>>` nebo LRU (Least Recently Used) cache fondů) pro dynamické rozlišení fondů připojení. 3. **Prevence úniků**: Aby se zabránilo kontaminaci mezi uživateli v asynchronních Tokio aplikacích, musí být kontext uživatele explicitně předáván přes hranice úloh (task boundaries), spíše než aby byl uložen v globálním stavu nebo lokálním úložišti vlákna (`thread_local!`), což by se porušilo při migracích asynchronních pracovních vláken.

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 })
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI