PRODPiattaforma BaaS europea sovranaApri Dashboard →

Ingegneria · 10 lettura minima

Aree di lavoro Cargo per un backend multiservizio Rust

Affane Daylami · Fondateur · 12 agosto 2026

Torniamo al blog

Un backend multiservizio in Rust solleva rapidamente la stessa domanda: un deposito per servizio o un singolo spazio di lavoro Cargo? Aurabase ha deciso per la seconda opzione. Undici servizi, una CLI e cinque librerie condivise risiedono in un'unica root Cargo.toml, con un unico Cargo.lock per tutti. Ecco come è costruito questo spazio di lavoro, leggendolo riga per riga.

Questo testo inglese è stato generato automaticamente dall'originale francese e non è stato ancora rivisto.
Questa pagina è stata tradotta automaticamente. Fa fede la versione inglese.

Non si tratta di un esempio didattico inventato per l'occasione. Ogni frammento di codice riportato di seguito proviene dalla root Aurabase Cargo.toml e dai suoi manifest crate, così come esistono oggi nel repository, incluse due discrepanze che abbiamo riscontrato durante la rilettura per questo articolo e che stiamo documentando così come sono invece di risolverle silenziosamente prima della pubblicazione.

L'essenziale
  • L'area di lavoro root Cargo di Aurabase dichiara 18 membri espliciti: 11 servizi, aura-cliCLI, 5 librerie condivise e aurabase-rs SDK — sotto resolver = "2" e un singolo Cargo.lock.
  • [workspace.dependencies] centralizza le versioni condivise; ogni cassa eredita con { workspace = true } anziché impostare il proprio numero, tranne quando una cassa prevale, in silenzio.
  • Una cartella sotto services/ non è automaticamente membro del workspace: la lista members è esplicita, non un glob, proprio per poter escludere codice non Rust.
  • [profile.release] si applica solo una volta all'intero spazio di lavoro: una scelta come panic = "unwind" si applica quindi a tutti gli undici servizi contemporaneamente.
#
Perché uno spazio di lavoro

Un deposito per servizio o solo un Cargo.lock?

Uno spazio di lavoro Cargo raggruppa più crate in un singolo Cargo.lock e in un'unica directory di destinazione: la stessa funzionalità fornita dal gestore pacchetti di Rust per questo caso. Undici binari Rust separati, ciascuno nel proprio repository, sembrano più indipendenti a prima vista. In pratica, ciò significa undici Cargo.lockdistinti, undici risoluzioni di versione che possono divergere nel tempo e nessuna garanzia che due servizi compilino la stessa versione diaxum o sqlx.

Uno spazio di lavoro Cargo risolve questo problema a livello di gestore dei pacchetti, non a livello di disciplina del team. Tutti i membri condividono un singolo Cargo.lock alla radice: una dipendenza comune viene risolta una volta, in una versione identica, per l'intero grafico. Questo è anche ciò che rende un refactoring multiservizio (ad esempio, la modifica di una firma in aura-core) visibile a un singolo cargo build --workspace, anziché un servizio scoperto per servizio in produzione. Questa è una delle scelte che distingue il nostro core Rust unificato al 100% da un servizio eterogeneo assemblato in stack per servizio.

#
Anatomia

La radice Cargo.toml: risolutore e membri

Tutto inizia con la dichiarazione [workspace] e la sua lista members. In Aurabase, questo elenco è scritto a mano, raggruppato per ruolo (servizi, strumenti, librerie) anziché generato da un modello generico come services/*.

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    # Servizi
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # … 8 altri servizi (aura-realtime, aura-storage, aura-ai, …)
    # Strumenti
    "aura-cli",
    # Librerie
    "libs/aura-core",
    # …4 altre librerie
    "aurabase-rs",
]

resolver = "2" non è un dettaglio estetico. Il risolutore v2 di Cargo isola la funzionalità di build-dependencies e le dipendenze specifiche del target (target.'cfg(windows)', ad esempio) dal resto del grafico: non si perdono più nel file binario finale. Unifica inoltre le versioni della stessa dipendenza tra tutti i membri dell'area di lavoro che la condividono: un singolo axum, solo una volta, non undici risoluzioni indipendenti.

#
Eredità

workspace.package: una versione, un'edizione, in linea di principio condivisa

[workspace.package] dichiara una volta i campi comuni - versione, edizione, autori, licenza - che ogni crate può ereditare con version.workspace = true invece di copiarli.

Cargo.toml (radice)toml
[workspace.package]
version = "0.1.1"
edition = "2021"

# services/aura-gateway/Cargo.toml
[package]
name = "aura-gateway"
version.workspace = true

Gli undici servizi di Aurabase seguono tutti questo schema. Due casse si discostano e questa è la prima delle due discrepanze trovate durante la preparazione di questo articolo: aura-cli dichiara version = "0.2.0" in copia cartacea e la lib libs/aura-migrations dichiara version = "0.1.0" — entrambe diverse da 0.1.1 dell'area di lavoro.

Cosa significa

version.workspace = true è facoltativo, campo per campo, cassa per cassa. Nulla impedisce che una cassa mantenga la propria numerazione, volontariamente (uno strumento pubblicato separatamente, ad esempio) o per dimenticanza. Un controllo dell'area di lavoro dovrebbe controllare questo campo cassa per cassa, non presupporre l'ereditarietà.

#
Deduplicazione

workspace.dependencies: una fonte di verità, a meno che una cassa non la aggiri

[workspace.dependencies] centralizza le dipendenze condivise da diversi crate. Ogni servizio fa riferimento ad esso con { workspace = true } invece di impostare il proprio vincolo di versione.

Cargo.toml (radice)toml
[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }
sqlx = { version = "0.8", default-features = false, features = […] }
tower_governor = { version = "0.8", features = ["axum"] }
governor = "0.10"

# services/aura-gateway/Cargo.toml — NON eredita il governatore dello spazio di lavoro
governor = "0.8"  # versione locale, diversa

Questa è la seconda vera discrepanza: lo spazio di lavoro centralizza governor nella versione 0.10, ma aura-gateway dichiara nuovamente la propria linea governor = "0.8" invece di ereditare: il gateway applica la limitazione della velocità con il crate nudo governor, quando aura-auth, aura-ai, aura-db e aura-functions passano attraverso tower_governor nel middleware. Uno spazio di lavoro non impedisce la divergenza: la rende solo visibile, se ci prendiamo la briga di confrontare.

Tuttavia, non tutte le dipendenze meritano di essere centralizzate. wasmtime non appare da nessuna parte in [workspace.dependencies]: solo un crate, aura-functions, lo utilizza per il suo runtime WASM, quindi rimane dichiarato localmente. La regola che applichiamo: elevare una dipendenza al livello di spazio di lavoro dal momento in cui due o più casse la condividono, non prima.

#
Grafico interno

libs/ to services/: che dipende da cosa

Le cinque librerie condivise (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) sono dichiarate come dipendenze del percorso in [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — quindi ciascun servizio sceglie di quali ha effettivamente bisogno.

Il grafico risultante rimane leggibile da un file all'altro. aura-gateway dipende solo da tre delle cinque librerie interne: aura-core, aura-crypto e aura-telemetry. Basta instradare, firmare un JWT per il sidecar PostgREST ed esportare le sue tracce, senza toccare aura-db-adapters né aura-migrations. aura-provisioneraggiunge aura-migrations: riproduce le migrazioni dello schema risultanti dalla creazione di un progetto.

Il caso più ristretto è aura-migrator, il file binario dedicato che esegue le migrazioni CI/CD. Tra le librerie interne di Aurabase, la sua produzione [dependencies] elenca soloaura-migrations — aura-core appare solo nel suo [dev-dependencies], per i test. Il binario consegnato alla produzione quindi non include alcun codiceaura-core; esiste solo durante cargo test.

#
Caso concreto

Una cassa, diversi binari: divisione aura-tempo reale

Uno spazio di lavoro non richiede la creazione di un nuovo membro ogni volta che si desidera un nuovo processo distribuibile. aura-realtime rimane un singolo membro dello spazio di lavoro, ma il suo Cargo.toml dichiara tre tabelle [[bin]] distinte attorno alla stessa [lib]condivisa.

Cargo.tomltoml
[lib]
name = "aura_realtime"

[[bin]]
name = "aura-realtime"

[[bin]]
name = "aura-realtime-cdc-worker"
path = "src/bin/cdc_worker.rs"

[[bin]]
name = "aura-realtime-ws-front"
path = "src/bin/ws_front.rs"

Il manifesto stesso documenta il confine tra i due. cdc-worker acquisisce le modifiche Postgres tramite polling e pubblica su NATS in pub/sub core (Client::publish_with_headers), senza mai aprire un server WebSocket: JetStream, in questo stesso servizio, serve esclusivamente la presenza tra istanze KV, non il fan-out CDC. ws-front consuma solo NATS e mantiene le connessioni WebSocket/SSE, senza mai toccare il CDC: i dettagli completi sono nella documentazione del motore in tempo reale . Entrambi i file binari condividono lo stesso codice di filtro RLS tramite [lib], ma vengono distribuiti e scalati in modo indipendente in Kubernetes. Questo è il segnale giusto per scegliere diversi binari in un crate piuttosto che un nuovo membro dell'area di lavoro: stessa logica interna, diverse topologie di distribuzione.

#
Trappola da evitare

Un file in services/ non è necessariamente un membro Cargo

La cartella aura-edge-runtime esiste nel repository Aurabase. Tuttavia, non contiene Cargo.toml — solo TypeScript (index.ts, envelope.ts) e non viene visualizzato da nessuna parte nell'elenco members dell'area di lavoro root.

Astuce

Questo è esattamente il motivo per cui l'elenco members di Aurabase è scritto a mano anziché sostituito da un modello generico come services/*. Un glob avrebbe tentato di includere questa cartella non Rust nell'area di lavoro, con un risultato negativo nella risoluzione. Un elenco esplicito consente alle cartelle che non parlano la stessa lingua di coesistere, sotto lo stesso genitore services/.

La lezione generalizza: contando i servizi di un backend Rust elencando le sottocartelle di services/ si ottiene un numero falso. Solo la radice Cargo.toml è autorevole su ciò che viene effettivamente compilato nell'area di lavoro.

#
Compilazione

[profile.release]: un'unica impostazione per l'intero spazio di lavoro

Un [profile.release] dichiarato nella radice dello spazio di lavoro si applica a tutti i membri compilati in modalità di rilascio: solo un posto da modificare, non undici. Cargo documenta tutte le chiavi disponibili nel suo riferimento profilo di compilazione ; Aurabase ne attiva solo cinque.

Cargo.toml (radice)toml
[profile.release]
lto = "thin"          # Casse incrociate LTO, equilibrio prestazioni/tempi di costruzione
codegen-units = 1     # il miglior inlining complessivo
panic = "unwind"   # un panico isolato da tokio/axum → 500, non un crollo globale
strip = "symbols"
opt-level = 3

La scelta di panic = "unwind" anziché "abort" è documentata direttamente nel commento del file: un panico in un gestore axum viene intercettato da tokio, l'attività restituisce un 500 e il processo continua a servire altre richieste simultanee. Il miglioramento delle prestazioni diabort non vale la perdita dell'isolamento tra query.

#
In pratica

Congela la toolchain e i comandi che contano

Un'area di lavoro unificata imposta le versioni delle dipendenze, ma non la versione del compilatore stesso. rust-toolchain.toml, alla radice, congela la toolchain (channel = "1.93" oggi) per qualsiasi chiamata diretta a cargo o rustc nel repository.

Questo file esiste proprio perché si è verificata una deriva silenziosa: un'azione CI ha installato il canale stable del giorno senza leggere rust-toolchain.toml, mentre l'immagine Docker del rilascio è stata compilata con la versione congelata. Un commit potrebbe superare tutti i test con l'attuale Rust stabile, per poi interrompere la creazione dell'immagine, scoperto dopo la fusione, non prima. rustuprispetta questo file per qualsiasi comando diretto: era l'unica correzione necessaria, senza influenzare i flussi di lavoro CI.

terminalbash
# Compila l'intero spazio di lavoro
cargo build --workspace

# Testare una singola cassa (non l'intera area di lavoro)
cargo test -p aura-auth

# Lanugine rigorose sull'intera area di lavoro, avvisi = errori
cargo clippy --workspace --all-targets -- -D warnings

# Formattazione
cargo fmt --all

Un ultimo dettaglio, per chi legge un manifest prima di eseguirlo: il crate aura-cli compila un binario la cui tabella [[bin]] lo chiama aurabase, non aura. Il wrapper npm pubblicato, @aurabase/cli, espone i due comandi: aura e aurabase puntano entrambi allo stesso script. Il nome di un [[bin]] Cargo, il nome della cassa e il nome esposto da un wrapper npm sono tre cose separate; nessuno può essere indovinato dagli altri due.

#
Riepilogo

Checklist: dove dichiarare cosa

Ogni volta che aggiungi una cassa a un'area di lavoro Cargo vengono prese cinque decisioni. Qui è dove ciascuno viene dichiarato, in base all'esempio Aurabase trattato sopra.

Elenco dei membrimembri dell'[area di lavoro].Root: elenco esplicito, mai glob
Versione/edizione condivisa[area di lavoro.pacchetto]Root: versione.workspace = true, per cassa, facoltativo
Dipendenza condivisa da 2+ casse[area di lavoro.dipendenze]Root, quindi { workspace = true } in ogni cassa
Dipendenza da un unico consumatore[dipendenze] della cassaLocalmente, senza passare dallo spazio di lavoro
Costruisci profilo[profilo.rilascio]Solo root: si applica a tutti i membri

Per verificare un'area di lavoro Cargo esistente, la tua o quella di un progetto che stai assumendo, sono sufficienti quattro controlli per trovare il tipo di discrepanze documentate sopra:

  1. Confronta [workspace.package].version con version di ciascuna cassa: un valore diverso non è necessariamente un bug, ma vale la pena documentarlo.
  2. Confronta [workspace.dependencies] con le dipendenze dichiarate localmente da ciascun crate: individua i nomi presenti su entrambi i lati con versioni diverse.
  3. Confronta l'elenco members nella root Cargo.toml con le effettive sottocartelle nel repository: una cartella mancante da members non è necessariamente una svista.
  4. Controllare il nome effettivo di ciascun binario compilato ([[bin]] name) anziché dare per scontato che corrisponda al nome della cassa o al nome esposto da un possibile wrapper npm.

PRONTO PER L'IMPLEMENTAZIONE?

Il tuo backend in cinque minuti.

Nessuna carta di credito richiesta · 500 MB gratuiti · 50.000 MAU