Dies ist kein pädagogisches Beispiel, das für diesen Anlass erfunden wurde. Jeder Codeausschnitt unten stammt aus dem Aurabase-Stammverzeichnis Cargo.toml und seinen Crate-Manifesten, wie sie heute im Repository vorhanden sind – einschließlich zweier Unstimmigkeiten, die wir beim erneuten Lesen für diesen Artikel festgestellt haben und die wir unverändert dokumentieren, anstatt sie stillschweigend vor der Veröffentlichung zu beheben.
- Der Aurabase-Root-Cargo-Arbeitsbereich deklariert 18 explizite Mitglieder: 11 Dienste, die
aura-cliCLI, 5 gemeinsam genutzte Bibliotheken und dasaurabase-rsSDK – unterresolver = "2"und einem einzelnenCargo.lock. [workspace.dependencies]zentralisiert gemeinsam genutzte Versionen; Jede Kiste erbt mit{ workspace = true }, anstatt ihre eigene Nummer festzulegen – außer wenn eine Kiste stillschweigend überschreibt.- Ein Ordner unter
services/ist nicht automatisch Mitglied des Arbeitsbereichs: Die Listemembersist explizit und kein Glob, gerade um Nicht-Rust-Code ausschließen zu können. [profile.release]gilt nur einmal für den gesamten Arbeitsbereich – eine Auswahl wiepanic = "unwind"gilt dann für alle elf Dienste gleichzeitig.
Ein Depot pro Service oder nur ein Cargo.lock?
Ein Workspace Cargo gruppiert mehrere Crates unter einem einzigen Cargo.lock und einem einzigen Zielverzeichnis – genau die Funktionalität, die der Paketmanager von Rust für diesen Fall bereitstellt. Elf separate Rust-Binärdateien, jede in ihrem eigenen Repository, wirken auf den ersten Blick unabhängiger. In der Praxis bedeutet dies elf verschiedene Cargo.lock, elf Versionsauflösungen, die im Laufe der Zeit abweichen können, und keine Garantie dafür, dass zwei Dienste dieselbe Version vonaxum oder sqlxkompilieren.
Ein Cargo-Arbeitsbereich löst dieses Problem auf der Ebene des Paketmanagers, nicht auf der Ebene der Teamdisziplin. Alle Mitglieder teilen sich ein einziges Cargo.lock im Stammverzeichnis: Eine gemeinsame Abhängigkeit wird einmal für das gesamte Diagramm in eine identische Version aufgelöst. Dadurch wird auch ein dienstübergreifender Refactor (z. B. Ändern einer Signatur in aura-core) für einen einzelnen cargo build --workspacesichtbar und nicht für jeden einzelnen Dienst in der Produktion erkannt. Dies ist eine der Entscheidungen, die unseren 100 % einheitlichen Rust-Kern von einem heterogenen -Stack unterscheidet, der von Dienst zu Dienstzusammengestellt wird.
Das Stammverzeichnis von Cargo.toml: Resolver und Mitglieder
Alles beginnt mit der Deklaration [workspace] und ihrer Liste members. Bei Aurabase ist diese Liste handgeschrieben und nach Rollen gruppiert – Dienste, Tools, Bibliotheken – und nicht nach einem generischen Muster wie services/*generiert.
resolver = "2" ist kein kosmetisches Detail. Der -Resolver v2 von Cargo isoliert die Funktionalität von build-dependencies und zielspezifischen Abhängigkeiten (z. B.target.'cfg(windows)') vom Rest des Diagramms – sie dringen nicht mehr in die endgültige Binärdatei ein. Es vereinheitlicht außerdem die Versionen derselben Abhängigkeit für alle Mitglieder des Arbeitsbereichs, die sie gemeinsam nutzen: ein einziges axum, nur einmal, nicht elf unabhängige Auflösungen.
workspace.package: eine Version, eine Edition, grundsätzlich geteilt
[workspace.package] deklariert einmal die gemeinsamen Felder – Version, Edition, Autoren, Lizenz – die jede Kiste mit version.workspace = true erben kann, anstatt sie zu kopieren.
Die elf Dienste von Aurabase folgen alle diesem Muster. Zwei Crates weichen ab, und dies ist die erste der beiden Diskrepanzen, die bei der Erstellung dieses Artikels festgestellt wurden: aura-cli deklariert version = "0.2.0" in gedruckter Form, und die Bibliothek libs/aura-migrations deklariert version = "0.1.0" – beide unterscheiden sich von 0.1.1 des Arbeitsbereichs.
version.workspace = true ist optional, Feld für Feld, Kiste für Kiste. Nichts hindert eine Kiste daran, ihre eigene Nummerierung beizubehalten – freiwillig (z. B. ein separat veröffentlichtes Tool) oder aus Vergesslichkeit. Eine Arbeitsbereichsprüfung sollte dieses Feld Kiste für Kiste überprüfen und keine Vererbung annehmen.
workspace.dependencies: eine Quelle der Wahrheit, es sei denn, eine Kiste umgeht sie
[workspace.dependencies] zentralisiert Abhängigkeiten, die von mehreren Crates gemeinsam genutzt werden. Jeder Dienst verweist mit { workspace = true } darauf, anstatt eine eigene Versionseinschränkung festzulegen.
Dies ist die zweite wirkliche Diskrepanz: Der Arbeitsbereich zentralisiert governor in Version 0.10, aber aura-gateway deklariert seine eigene Zeile governor = "0.8" neu, anstatt zu erben – das Gateway wendet seine Ratenbegrenzung mit der bloßen Kiste governor an, wenn aura-auth, aura-ai, aura-db und aura-functions passieren tower_governor in der Middleware. Ein Arbeitsbereich verhindert Divergenz nicht: Er macht sie nur dann sichtbar, wenn wir uns die Mühe machen, sie zu vergleichen.
Allerdings verdienen nicht alle Abhängigkeiten eine Zentralisierung. wasmtime erscheint nirgendwo in [workspace.dependencies]: Nur eine Kiste, aura-functions, verwendet es für seine WASM-Laufzeit, sodass es lokal deklariert bleibt. Die Regel, die wir anwenden: Erhöhen Sie eine Abhängigkeit auf die Arbeitsbereichsebene, sobald zwei oder mehr Crates sie gemeinsam nutzen, nicht vorher.
libs/ zu Services/: was davon abhängt, was
Die fünf gemeinsam genutzten Bibliotheken (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) werden als Pfadabhängigkeiten in [workspace.dependencies] – aura-core = { path = "libs/aura-core" } deklariert – dann wählt jeder Dienst aus, welche er tatsächlich benötigt.
Das resultierende Diagramm bleibt von einer Datei zur anderen lesbar. aura-gateway hängt nur von drei der fünf internen Bibliotheken ab – aura-core, aura-crypto und aura-telemetry. Genug zum Routing, Signieren eines JWT für den PostgREST-Sidecar und Exportieren seiner Traces, ohne aura-db-adapters oder aura-migrationszu berühren. aura-provisionerfügt aura-migrationshinzu: Es spielt die Schemamigrationen ab, die sich aus der Erstellung eines Projekts ergeben.
Der engste Fall ist aura-migrator, die dedizierte Binärdatei, die CI/CD-Migrationen durchführt. Unter den internen Aurabase-Bibliotheken listet die Produktion [dependencies] nuraura-migrations auf – aura-core erscheint zu Testzwecken nur in [dev-dependencies]. Die an die Produktion gelieferte Binärdatei enthält daher keinenaura-core-Code; es existiert nur während cargo test.
Eine Kiste, mehrere Binärdateien: Aura-Echtzeit-Aufteilung
Für einen Arbeitsbereich muss nicht jedes Mal ein neues Mitglied erstellt werden, wenn Sie einen neuen bereitstellbaren Prozess wünschen. aura-realtime bleibt ein einzelnes Mitglied des Arbeitsbereichs, aber sein Cargo.toml deklariert drei verschiedene [[bin]]-Tabellen um denselben gemeinsamen [lib].
Das Manifest selbst dokumentiert die Grenze zwischen beiden. cdc-worker erfasst Postgres-Änderungen durch Abfragen und veröffentlicht sie auf NATS im Pub/Sub-Kern (Client::publish_with_headers), ohne jemals einen WebSocket-Server zu öffnen – JetStream bedient in demselben Dienst ausschließlich die instanzübergreifende Präsenz-KV, nicht den CDC-Fan-Out. ws-front verbraucht nur NATS und hält WebSocket/SSE-Verbindungen, ohne jemals den CDC zu berühren – die vollständigen Details finden Sie in derEchtzeit-Engine-Dokumentation. Beide Binärdateien nutzen über [lib]denselben RLS-Filtercode, werden jedoch unabhängig voneinander in Kubernetes bereitgestellt und skaliert. Dies ist das richtige Signal, mehrere Binärdateien in einer Kiste anstelle eines neuen Arbeitsbereichsmitglieds auszuwählen: gleiche interne Logik, unterschiedliche Bereitstellungstopologien.
Eine Datei unter „services/“ ist nicht unbedingt ein Cargo-Mitglied
Der Ordner aura-edge-runtime ist im Aurabase-Repository vorhanden. Es enthält jedoch kein Cargo.toml – nur TypeScript (index.ts, envelope.ts), und es erscheint nirgendwo in der members-Liste des Root-Arbeitsbereichs.
Genau aus diesem Grund ist die members-Liste von Aurabase handgeschrieben und nicht durch ein generisches Muster wie services/*ersetzt. Ein Glob hätte versucht, diesen Nicht-Rust-Ordner in den Arbeitsbereich aufzunehmen, was zu einem Auflösungsfehler geführt hätte. Eine explizite Liste ermöglicht die Koexistenz von Ordnern, die nicht dieselbe Sprache sprechen, unter demselben übergeordneten Ordner services/.
Die Lektion verallgemeinert: Das Zählen der Dienste eines Rust-Backends durch Auflisten der Unterordner von services/ ergibt eine falsche Zahl. Nur der Stamm Cargo.toml ist maßgebend dafür, was tatsächlich im Arbeitsbereich kompiliert wird.
[profile.release]: eine einzige Einstellung für den gesamten Arbeitsbereich
Ein im Stammverzeichnis des Arbeitsbereichs deklariertes [profile.release] gilt für alle im Release-Modus kompilierten Mitglieder – nur eine Stelle zum Anpassen, nicht elf. Cargo dokumentiert alle verfügbaren Schlüssel in seiner -Kompilierungsprofilreferenz; Aurabase aktiviert nur fünf.
Die Wahl von panic = "unwind" anstelle von "abort" wird direkt im Dateikommentar dokumentiert: Eine Panik in einem Axum-Handler wird von tokio abgefangen, die Aufgabe gibt eine 500 zurück und der Prozess bedient weiterhin andere gleichzeitige Anfragen. Der Leistungsgewinn vonabort ist den Verlust der Isolation zwischen Abfragen nicht wert.
Frieren Sie die Toolchain und die wichtigen Befehle ein
Ein einheitlicher Arbeitsbereich legt Abhängigkeitsversionen fest, jedoch nicht die Version des Compilers selbst. rust-toolchain.tomlim Stammverzeichnis friert die Toolchain (heutechannel = "1.93") für jeden direkten Aufruf von cargo oder rustc im Repository ein.
Diese Datei existiert genau deshalb, weil ein stiller Drift aufgetreten ist: Eine CI-Aktion hat den stable-Kanal des Tages installiert, ohne rust-toolchain.tomlzu lesen, während das Release-Docker-Image mit der eingefrorenen Version kompiliert wurde. Ein Commit könnte alle Tests mit dem heutigen stabilen Rust bestehen und dann beim Image-Build abbrechen – entdeckt nach der Zusammenführung, nicht vorher. rustuprespektiert diese Datei für jeden direkten Befehl: Es war die einzige notwendige Korrektur, ohne die CI-Workflows zu beeinträchtigen.
Ein letztes Detail für diejenigen, die ein Manifest lesen, bevor sie es ausführen: Die Kiste aura-cli kompiliert eine Binärdatei, deren Tabelle [[bin]] sie aurabaseund nicht auranennt. Der veröffentlichte npm-Wrapper @aurabase/clistellt die beiden Befehle aura und aurabase bereit, die beide auf dasselbe Skript verweisen. Der Name einer [[bin]]-Fracht, der Name der Kiste und der von einem NPM-Wrapper offengelegte Name sind drei verschiedene Dinge; Aus den anderen beiden lässt sich keines erraten.
Checkliste: Wo was deklarieren?
Jedes Mal, wenn Sie eine Kiste zu einem Cargo-Arbeitsbereich hinzufügen, stehen fünf Entscheidungen an. Hier wird jedes deklariert, basierend auf dem oben behandelten Aurabase-Beispiel.
| Liste der Mitglieder | [Arbeitsbereich] Mitglieder | Root – explizite Liste, niemals ein Glob |
|---|---|---|
| Geteilte Version/Ausgabe | [workspace.package] | Root – version.workspace = true, nach Kiste, optional |
| Abhängigkeit, die von mehr als 2 Kisten geteilt wird | [workspace.dependencies] | Root – dann { workspace = true } in jeder Kiste |
| Abhängigkeit von einem einzelnen Verbraucher | [Abhängigkeiten] der Kiste | Vor Ort, ohne den Arbeitsbereich zu durchlaufen |
| Profil erstellen | [profile.release] | Nur Root – gilt für alle Mitglieder |
Um einen bestehenden Cargo-Arbeitsbereich zu prüfen – Ihren oder den eines Projekts, das Sie übernehmen – reichen vier Prüfungen aus, um die oben dokumentierten Unstimmigkeiten zu finden:
- Vergleichen Sie
[workspace.package].versionmitversionjeder Kiste – ein anderer Wert ist nicht unbedingt ein Fehler, aber es lohnt sich, ihn zu dokumentieren. - Vergleichen Sie
[workspace.dependencies]mit den lokal von jeder Kiste deklarierten Abhängigkeiten – erkennen Sie die auf beiden Seiten vorhandenen Namen mit unterschiedlichen Versionen. - Vergleichen Sie die
members-Liste im StammverzeichnisCargo.tomlmit den tatsächlichen Unterordnern im Repository – ein fehlender Ordner inmembersist nicht unbedingt ein Versehen. - Überprüfen Sie den tatsächlichen Namen jeder kompilierten Binärdatei (
[[bin]] name), anstatt anzunehmen, dass er mit dem Crate-Namen oder dem von einem möglichen NPM-Wrapper bereitgestellten Namen übereinstimmt.