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'area di lavoro root Cargo di Aurabase dichiara 18 membri espliciti: 11 servizi,
aura-cliCLI, 5 librerie condivise eaurabase-rsSDK — sottoresolver = "2"e un singoloCargo.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 listamembersè 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 comepanic = "unwind"si applica quindi a tutti gli undici servizi contemporaneamente.
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.
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/*.
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.
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.
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.
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à.
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.
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.
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.
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.
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.
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.
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.
[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.
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.
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.
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.
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 membri | membri 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 cassa | Localmente, 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:
- Confronta
[workspace.package].versionconversiondi ciascuna cassa: un valore diverso non è necessariamente un bug, ma vale la pena documentarlo. - Confronta
[workspace.dependencies]con le dipendenze dichiarate localmente da ciascun crate: individua i nomi presenti su entrambi i lati con versioni diverse. - Confronta l'elenco
membersnella rootCargo.tomlcon le effettive sottocartelle nel repository: una cartella mancante damembersnon è necessariamente una svista. - 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.