Priprema za Rust intervju

Pitanja za Rust Backend Developer intervju

15 često postavljanih pitanja za Rust intervju. Pitanja pokrivaju različite nivoe, a odgovore možete vežbati naglas u našem treneru za intervjue.

Započni Rust AI intervjuKreditna kartica nije potrebna. Dostupna je 1 besplatna sesija.
Vežba tehničkog intervjua na engleskomRežim u kome kandidati kojima engleski nije maternji mogu da vežbaju tehničke intervjue.

Pitanja za početni nivo

1Objasnite Rust pravila vlasništva i kako ona oblikuju praktične izbore na serverskoj strani, kao što su kloniranje, pozajmljivanje i deljenje stanja aplikacije.

Rust dodeljuje svakoj vrednosti jednog vlasnika. Premeštanje vrednosti koja ne implementira `Copy` prenosi vlasništvo, a resursi vrednosti se oslobađaju (drop) kada njen vlasnik izađe iz opsega vidljivosti. Kod može ili nepromenljivo da pozajmi podatke putem deljenih referenci ili promenljivo putem ekskluzivne promenljive reference. U serverskom kodu, ovo određuje da li funkcija preuzima vlasništvo, privremeno pozajmljuje, klonira podatke kako bi dobila nezavisnu vrednost u svom vlasništvu, ili deli dugovečno stanje putem struktura kao što je `Arc`, uz sinhronizaciju ili druge konkurentne primitive kada je neophodna deljena izmena stanja.

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;
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

2Koja je razlika između pozajmljivanja pomoću &T i &mut T, i kako ova pravila utiču na potpise API-ja (Application Programming Interface) i funkcija za obradu zahteva?

`&T` je deljena referenca: omogućava pristup samo za čitanje i više deljenih referenci na istu vrednost može biti aktivno u isto vreme. `&mut T` je ekskluzivna promenljiva referenca: omogućava modifikaciju, ali dok je aktivna, nijedna druga deljena ili promenljiva referenca na istu vrednost ne može se koristiti. Ova pravila oblikuju API-je tako što funkcije koje samo čitaju podatke prihvataju `&T` ili uže pozajmljene tipove kao što je `&str`, dok funkcije koje menjaju podatke prihvataju `&mut T`, preuzimaju vlasništvo ili koriste unutrašnju promenljivost (interior mutability) i sinhronizaciju. Funkcije za obradu zahteva na serverskoj strani obično izbegavaju direktan `&mut` pristup deljenom stanju aplikacije kroz konkurentne zahteve i umesto toga koriste deljene reference uz sinhronizaciju ili druge obrasce vlasništva.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

3Kako funkcioniše Box i koje probleme rešava alokacija na hipu pomoću Box-a?

`Box<T>` je pametni pokazivač koji poseduje vrednost čiji se tip `T` čuva na hipu (heap), dok je sam `Box` rukovalac vrednost fiksne veličine nalik pokazivaču. Poseduje isključivo vlasništvo i oslobađa memoriju na hipu kada izađe iz opsega vidljivosti. `Box` omogućava indirekciju, što pomaže kod velikih vrednosti, rekurzivnih tipova koji zahtevaju poznatu veličinu, dinamički dimenzionisanih vrednosti i trait objekata kao što je `Box<dyn Trait>`. Premeštanje (move) `Box`-a prenosi vlasništvo nad alokacijom bez premeštanja ili kopiranja same vrednosti sačuvane na hipu.

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

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

4Opišite Result i Option i primarne idiomatske načine za rad sa odsustvom vrednosti i operacijama koje mogu da ne uspeju.

`Option<T>` predstavlja ili `Some(T)` ili `None` i koristi se za očekivano odsustvo vrednosti. `Result<T, E>` predstavlja ili `Ok(T)` ili `Err(E)` i koristi se za operacije koje mogu da ne uspeju kada su informacije o grešci važne. Idiomatska obrada uključuje `match`, `if let`/`let else`, kombinatore kao što su `map`, `and_then`, `ok_or`, `unwrap_or` i `unwrap_or_else`, kao i operator `?` za ranu propagaciju iz funkcija koje vraćaju kompatibilne `Option` ili `Result` tipove. `unwrap` i `expect` je najbolje rezervisati za invarijante, testove, prototipove ili nepopravljive situacije, a ne za uobičajene serverske greške koje se mogu obraditi.

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);
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

5Kako funkcioniše operator ? i šta omogućava konverziju između različitih tipova grešaka?

Operator `?` predstavlja skraćenicu za propagiranje grešaka ili neuspeha. Za tip `Result`, `expr?` vraća vrednost `Ok` i nastavlja izvršavanje, ili prevremeno prekida trenutnu funkciju i vraća `Err`; za tip `Option`, vraća vrednost `Some` ili izlazi iz funkcije sa `None`. Funkcija u kojoj se operator nalazi mora da vraća kompatibilan tip. Za `Result`, različiti tipovi grešaka mogu se kombinovati zato što se izvorna greška konvertuje u tip greške funkcije korišćenjem konverzije u stilu `From`/`Into`, što se obično implementira ručno ili automatski izvodi pomoću pomoćnih biblioteka (crates) kao što 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)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

6Objasnite uparivanje obrazaca nad enumima i kako iscrpno uparivanje poboljšava ispravnost domena i API-ja (Application Programming Interface).

Enum tipovi modeluju vrednost koja može biti jedna iz fiksnog skupa varijanti, a same varijante mogu nositi podatke. Ključna reč `match` grana tok izvršavanja po varijanti i može destruktuirati podatke unutar nje. Rust proverava da li je pokrivanje enum varijanti iscrpno (exhaustive), osim ako se ne koristi džoker (wildcard) ili opšti obrazac. Ovo poboljšava ispravnost domena i API-ja jer kada se doda novo stanje ili varijanta odgovora, prevodilac može naterati kôd koji vrši uparivanje da eksplicitno obradi novi slučaj umesto da ga tiho ignoriše.

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}"),
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

7Šta su „newtype” omotači (newtype wrappers) i kako oni poboljšavaju tipsku bezbednost za identifikatore (ID - Identifier), novčane iznose, tokene i tajne podatke?

„Newtype” omotač je zaseban Rust tip, najčešće struktura n-torke (tuple struct) sa jednim poljem, koja obavija postojeću reprezentaciju, kao što su `struct UserId(Uuid);` ili `struct AccessToken(String);`. On poboljšava tipsku bezbednost jer prevodilac (compiler) neće pobrkati semantički različite vrednosti koje dele isti osnovni tip, kao što su `UserId` i `OrderId`, centi i dolari, ili sirovi tekstualni nizovi i validirani tokeni. Newtype obrasci takođe mogu centralizovati pravila konstrukcije i validacije, kao i kontrolisati implementacije osobina (traits) kao što su `Display`, `Debug`, `Serialize`, ili ponašanje pri maskiranju osetljivih podataka (secrets redaction).

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Pitanja za srednji nivo

8Kako funkcioniše trait Drop, uključujući redosled uništavanja (drop order), RAII (Resource Acquisition Is Initialization) čišćenje i ograničenja vezana za asinhrono čišćenje?

`Drop` predstavlja Rust-ov mehanizam za determinističko oslobađanje resursa (cleanup hook). Kada se uništi vrednost koja ima vlasništvo (owned value), na primer kada izađe iz opsega vidljivosti, Rust automatski poziva njen metod `drop(&mut self)` ako ona implementira `Drop`, a zatim uništava njena polja. Ovo podržava RAII: resursi poput fajlova, soketa, katanaca (locks), transakcija, dozvola (permits) ili bafera vezani su za vrednosti/čuvare koji poseduju vlasništvo (guards) i oslobađaju se kada se te vrednosti unište. Lokalne promenljive se uništavaju obrnutim redosledom od redosleda kreiranja; za strukturu sa traitom `Drop`, metod `drop` te strukture se izvršava pre nego što se njena polja unište, a sama polja se uništavaju po redosledu deklarisanja. Metod `Drop::drop` je sinhron i ne može biti `async` niti se nad njim može pozvati `await`, pa graciozno asinhrono čišćenje obično zahteva eksplicitni asinhroni metod za zatvaranje/gašenje/ispiranje bafera (close/shutdown/flush) ili drugačiji dizajn; `Drop` može izvršiti samo sinhrono čišćenje ili čišćenje po principu najboljeg mogućeg pokušaja (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");
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

9Objasnite životne vekove i zašto su eksplicitne anotacije životnog veka ponekad neophodne u Rust API-jima (Application Programming Interface).

Životni vek opisuje koliko dugo je referenca validna, omogućavajući prevodiocu da odbaci viseće reference. Anotacije životnog veka su ograničenja u vreme prevođenja koja izražavaju odnose između referenci; one ne produžavaju životni vek osnovnih podataka niti dodaju ponašanje tokom izvršavanja. Mnogi jednostavni potpisi se rešavaju kroz eliziju životnog veka, ali eksplicitne anotacije su neophodne kada prevodilac ne može da zaključi kako su ulazne i izlazne reference povezane, kada tipovi skladište reference ili kada API treba da izrazi generička ograničenja pozajmljivanja.

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);
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

10Šta označava životni vek 'static i kako on interaguje sa pokrenutim zadacima (spawned tasks) i dizajnom serverskog API-ja (Application Programming Interface)?

`'static` na referenci znači da su referencirani podaci validni tokom celog trajanja programa, kao što je slučaj sa literalima niski ili `static` stavkama. Ograničenje poput `T: 'static` znači da vrednosti tipa `T` ne sadrže pozajmljene reference koje nisu `'static`, tako da se mogu zadržati tokom proizvoljnog životnog veka; to ne znači da sama vrednost mora živeti večno ili da ne može biti uništena (dropped). Pokrenuti zadaci često zahtevaju `'static` objekte tipa future zato što izvršavač (executor) može nastaviti da ih izvršava i nakon što se stek okvir koji ih je pokrenuo vratio. U serverskom kodu, ovo obično podrazumeva prenos vlasništva nad podacima u zadatak pomoću `async move`, korišćenje tipova u sopstvenom vlasništvu (owned types), ili kloniranje deljenih ručki kao što je `Arc`, umesto pozajmljivanja lokalnih promenljivih. API-ji bi trebalo da koriste `'static` ograničenja za sačuvane povratne pozive (callbacks), pozadinske poslove i dugovečno stanje, ali treba izbegavati nepotrebna `'static` ograničenja za kratkotrajne pozajmljene operacije.

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);
    });
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

11Opišite kako Rust koristi osobine (traits) za polimorfizam i uporedite `impl Trait`, generičke tipove i `dyn Trait`.

Rust koristi osobine (`traits`) za izražavanje zajedničkog ponašanja. Generička ograničenja poput `fn f<T: Trait>(x: T)` obično koriste statičko otpremanje (static dispatch) putem monomorfizacije. `impl Trait` na poziciji argumenta uglavnom je skraćeni zapis za anonimni generički parametar. `impl Trait` na poziciji povratne vrednosti skriva jedan konkretan povratni tip koji funkcija bira. `dyn Trait` je objekat osobine (`trait object`) koji se koristi iza pokazivača, kao što su `&dyn Trait` ili `Box<dyn Trait>`; on koristi dinamičko otpremanje (dynamic dispatch) putem tabele virtuelnih metoda (`vtable`) i podržava brisanje tipova u vreme izvršavanja / heterogene vrednosti u skladu sa pravilima bezbednosti objekata (`object safety`).

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())); }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

12Šta su pridruženi tipovi u okviru trait-ova i kada su pogodniji od generičkih parametara tipa?

Pridruženi tipovi (associated types) su imenovani čuvari mesta za tipove deklarisani unutar trait-a koje određuje svaka konkretna implementacija, na primer `Iterator::Item`. Oni su pogodniji kada je tip deo ugovora samog trait-a i svaki implementator ima jedan prirodan izbor za njega. Generički parametri tipa su pogodniji kada pozivalac treba da bira tip ili kada istom implementatoru može biti potrebno više implementacija za različite izbore tipova.

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)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Pitanja za napredni nivo

13Kako biste implementirali obrazac transakcionog sandučeta (transactional outbox pattern) u Rust servisu?

Da biste implementirali obrazac transakcionog sandučeta u Rust servisu, upisujte ažuriranja domenskih podataka i tela odlaznih događaja atomično unutar jedne baze podataka u okviru transakcije (npr. koristeći `sqlx::Transaction`). Tabela `outbox` čuva odredišnu temu, teret poruke (payload) i status ili vremensku oznaku kreiranja. Pozadinski asinhroni radnik (ili proces promene podataka poput alata Debezium) zatim povlači ili strimuje događaje na čekanju i objavljuje ih brokeru poruka (kao što su Kafka, RabbitMQ ili NATS). Kada se u Rustu koristi radnik koji periodično proverava stanje (npr. unutar `tokio::spawn` petlje), upotreba `SELECT ... FOR UPDATE SKIP LOCKED` omogućava većem broju instanci servisa da konkurentno dohvataju i šalju redove iz sandučeta bez konflikata oko zaključavanja. Nakon što broker potvrdi prijem, radnik ažurira status zapisa u tabeli ili briše red. Pošto mrežni ponovni pokušaji mogu dovesti do isporuke barem jednom (at-least-once delivery), nizvodni potrošači moraju biti projektovani tako da idempotentno obrađuju poruke.

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;
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

14Kako biste projektovali višefaznu operaciju koja ažurira bazu podataka i poziva eksterni API (Application Programming Interface)?

Projektovanje višefazne operacije koja obuhvata upis u bazu podataka i poziv eksternog API-ja zahteva upravljanje delimičnim neuspesima i mrežnim particionisanjem bez oslanjanja na dvofazno potvrđivanje (2PC, two-phase commit). 1. **Prvo trajno sačuvajte stanje / nameru**: Zabeležite početno stanje (kao što je `PENDING` ili `INITIATED`) i potreban teret (payload) unutar lokalne transakcije baze podataka. Potvrdite (commit) ovu transakciju pre pokretanja eksternog mrežnog poziva kako biste izbegli držanje zaključavanja u bazi i zauzimanje resursa u skupu konekcija tokom sporog mrežnog I/O-a. 2. **Idempotentan eksterni zahtev**: Pozovite eksterni API koristeći deterministički ključ idempotentnosti (npr. izveden iz UUID-a same operacije). Ovo osigurava da ponovljeni pokušaji ne izazovu duple neželjene efekte u stvarnom svetu (poput dvostruke naplate). 3. **Promena stanja i obrada isteka vremena (timeout)**: Ako API poziv vrati uspešan odgovor, ažurirajte zapis u bazi na `COMPLETED`. Ako trajno ne uspe, označite ga kao `FAILED` i izvršite kompenzacione akcije. Ako poziv istekne ili vrati mrežnu grešku, označite zapis kao `INDETERMINATE` / `REQUIRES_RECONCILIATION` i prepustite pozadinskom radniku (background worker) da uskladi stanje upitom ka statusnoj krajnjoj tački provajdera ili ponovnim pokušajem sa istim ključem idempotentnosti.

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(())
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

15Kako biste projektovali višekorisnički (multi-tenant) Rust backend sa striktnom izolacijom podataka i dinamičkim rutiranjem?

Projektovanje višekorisničkog (multi-tenant) Rust backend-a obuhvata tri ključne oblasti: identifikaciju zakupca (tenant-a), strategiju izolacije podataka i dinamičko rutiranje. 1. **Ekstrakcija zakupca i propagacija konteksta**: HTTP posredni softver (middleware, npr. u Axum ili Actix-web radnim okvirima) prepoznaje identitet zakupca iz JWT tvrdnji (claims), poddomena ili zaglavlja. Zatim konstruiše strogo tipizirani `TenantContext` koji se čuva u ekstenzijama zahteva. Funkcije za obradu zahteva koriste Rust ekstraktore tipova kako bi osigurale da se poslovna logika ne može izvršiti bez validnog konteksta zakupca. 2. **Strategija izolacije podataka**: - *Zajednička baza podataka sa bezbednošću na nivou redova/kolona (Row-Level Security - RLS)*: Svi zakupci dele tabele sa kolonom `tenant_id`. Izolacija se sprovodi preko PostgreSQL RLS-a korišćenjem promenljivih sesije na nivou konekcije (npr. `SET LOCAL app.tenant_id = $1`) ili omotača za građenje upita (query builder). - *Zasebna šema po zakupcu (Schema-per-Tenant)*: Zakupci dele instancu baze podataka, ali imaju izolovane šeme; rutiranje prilagođava putanju pretrage (`search_path`) po konekciji. - *Zasebna baza po zakupcu (Database-per-Tenant)*: Zakupci imaju odvojene baze podataka. Servis održava registar bazena konekcija (npr. `Arc<DashMap<TenantId, PgPool>>` ili LRU keš za bazene) radi dinamičkog razrešavanja bazena konekcija. 3. **Sprečavanje curenja podataka**: Da bi se izbegla kontaminacija podataka između zakupaca u asinhronim Tokio aplikacijama, kontekst zakupca se mora eksplicitno prosleđivati kroz granice zadataka (tasks), umesto da se čuva u globalnom stanju ili lokalnom skladištu niti (`thread_local!`), što otkazuje prilikom migracije asinhronih poslova između radnih niti.

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 })
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom