Este no es un ejemplo educativo inventado para la ocasión. Cada fragmento de código a continuación proviene de la raíz de Aurabase Cargo.toml y sus manifiestos de caja, tal como existen en el repositorio hoy, incluidas dos discrepancias que encontramos al volver a leerlas para este artículo, y que estamos documentando tal como están en lugar de corregirlas silenciosamente antes de su publicación.
- El espacio de trabajo raíz de Aurabase Cargo declara 18 miembros explícitos: 11 servicios, la CLI
aura-cli, 5 bibliotecas compartidas y el SDKaurabase-rs, bajoresolver = "2"y un únicoCargo.lock. [workspace.dependencies]centraliza las versiones compartidas; cada caja hereda con{ workspace = true }en lugar de establecer su propio número, excepto cuando una caja se anula, de forma silenciosa.- Una carpeta bajo
services/no es automáticamente miembro del espacio de trabajo: la listamemberses explícita, no global, precisamente para poder excluir código que no sea de Rust. [profile.release]se aplica solo una vez a todo el espacio de trabajo; una elección comopanic = "unwind"se aplica a los once servicios a la vez.
¿Un depósito por servicio o solo un Cargo.lock?
Un espacio de trabajo Cargo agrupa varias cajas bajo un único Cargo.lock y un único directorio de destino: la misma funcionalidad que el administrador de paquetes de Rust proporciona para este caso. Once binarios de Rust separados, cada uno en su propio repositorio, parecen más independientes a primera vista. En la práctica, esto significa once Cargo.lockdistintos, once resoluciones de versión que pueden divergir con el tiempo y no hay garantía de que dos servicios compilen la misma versión deaxum o sqlx.
Un espacio de trabajo de Cargo resuelve esto a nivel de administrador de paquetes, no a nivel de disciplina del equipo. Todos los miembros comparten un único Cargo.lock en la raíz: una dependencia común se resuelve una vez, en una versión idéntica, para todo el gráfico. Esto también es lo que hace que una refactorización entre servicios (cambiar una firma en aura-core, por ejemplo) sea visible para un solo cargo build --workspace, en lugar de ser descubierto servicio por servicio en producción. Esta es una de las opciones que distingue nuestro núcleo Rust 100% unificado de una pila heterogénea ensamblada servicio por servicio.
La raíz de Cargo.toml: solucionador y miembros
Todo comienza con la declaración [workspace] y su lista members. En Aurabase, esta lista está escrita a mano, agrupada por función (servicios, herramientas, bibliotecas) en lugar de generarse mediante un patrón genérico como services/*.
resolver = "2" no es un detalle cosmético. El solucionador v2 de Cargo aísla la funcionalidad de build-dependencies y las dependencias específicas del objetivo (target.'cfg(windows)', por ejemplo) del resto del gráfico; ya no se filtran al binario final. También unifica las versiones de la misma dependencia entre todos los miembros del espacio de trabajo que la comparten: un único axum, solo una vez, no once resoluciones independientes.
workspace.package: una versión, una edición, en principio compartida
[workspace.package] declara una vez los campos comunes (versión, edición, autores, licencia) que cada caja puede heredar con version.workspace = true en lugar de copiarlos.
Los once servicios de Aurabase siguen este patrón. Dos cajas se desvían, y esta es la primera de las dos discrepancias encontradas al preparar este artículo: aura-cli declara version = "0.2.0" en copia impresa, y la biblioteca libs/aura-migrations declara version = "0.1.0", ambas diferentes de la 0.1.1 del espacio de trabajo.
version.workspace = true es opcional, campo por campo, caja por caja. Nada impide que una caja mantenga su propia numeración, ya sea voluntariamente (una herramienta publicada por separado, por ejemplo) o por olvido. Una auditoría del espacio de trabajo debe verificar este campo caja por caja, no asumir herencia.
workspace.dependencies: una fuente de verdad, a menos que una caja la pase por alto
[workspace.dependencies] centraliza las dependencias compartidas por varias cajas. Cada servicio hace referencia a él con { workspace = true } en lugar de establecer su propia restricción de versión.
Esta es la segunda discrepancia real: el espacio de trabajo centraliza governor en la versión 0.10, pero aura-gateway vuelve a declarar su propia línea governor = "0.8" en lugar de heredar; la puerta de enlace aplica su limitación de velocidad con la caja governor desnuda, cuando aura-auth, aura-ai, aura-db y aura-functions pasan tower_governor en software intermedio. Un espacio de trabajo no impide la divergencia: sólo la hace visible si nos tomamos la molestia de comparar.
Sin embargo, no todas las dependencias merecen estar centralizadas. wasmtime no aparece en ninguna parte de [workspace.dependencies]: solo una caja, aura-functions, la usa para su tiempo de ejecución WASM, por lo que permanece declarada localmente. La regla que aplicamos: elevar una dependencia al nivel del espacio de trabajo desde el momento en que dos o más cajas la comparten, no antes.
libs/ a servicios/: que depende de qué
Las cinco bibliotecas compartidas (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) se declaran como dependencias de ruta en [workspace.dependencies] - aura-core = { path = "libs/aura-core" } - luego cada servicio elige cuáles necesita realmente.
El gráfico resultante sigue siendo legible de un archivo a otro. aura-gateway solo depende de tres de las cinco bibliotecas internas: aura-core, aura-crypto y aura-telemetry. Basta con enrutar, firmar un JWT para el sidecar PostgREST y exportar sus trazas, sin tocar aura-db-adapters ni aura-migrations. aura-provisioneragrega aura-migrations: reproduce las migraciones de esquema resultantes de la creación de un proyecto.
El caso más limitado es aura-migrator, el binario dedicado que realiza migraciones CI/CD. Entre las bibliotecas internas de Aurabase, su producción [dependencies] solo enumeraaura-migrations - aura-core solo aparece en su [dev-dependencies], para pruebas. Por lo tanto, el binario entregado a producción no incluye ningún códigoaura-core; solo existe durante cargo test.
Una caja, varios binarios: división del aura en tiempo real
Un espacio de trabajo no requiere la creación de un nuevo miembro cada vez que desee un nuevo proceso implementable. aura-realtime sigue siendo un único miembro del espacio de trabajo, pero su Cargo.toml declara tres tablas [[bin]] distintas alrededor del mismo [lib]compartido.
El propio manifiesto documenta la frontera entre ambos. cdc-worker captura los cambios de Postgres mediante sondeo y los publica en NATS en pub/sub core (Client::publish_with_headers), sin siquiera abrir un servidor WebSocket; JetStream, en este mismo servicio, sirve exclusivamente la presencia entre instancias KV, no la distribución de CDC. ws-front solo consume NATS y mantiene conexiones WebSocket/SSE, sin siquiera tocar el CDC; los detalles completos se encuentran en la documentación del motor en tiempo real. Ambos binarios comparten el mismo código de filtrado RLS a través de [lib], pero se implementan y escalan de forma independiente en Kubernetes. Esta es la señal correcta para elegir varios archivos binarios en una caja en lugar de un nuevo miembro del espacio de trabajo: misma lógica interna, diferentes topologías de implementación.
Un archivo bajo servicios/ no es necesariamente un miembro de Cargo
La carpeta aura-edge-runtime existe en el repositorio de Aurabase. Sin embargo, no contiene ningún Cargo.toml, solo TypeScript (index.ts, envelope.ts), y no aparece en ninguna parte de la lista members del espacio de trabajo raíz.
Esta es exactamente la razón por la que la lista members de Aurabase está escrita a mano en lugar de ser reemplazada por un patrón genérico como services/*. Un globo habría intentado incluir esta carpeta que no es de Rust en el espacio de trabajo, con un error de resolución como resultado. Una lista explícita permite que coexistan carpetas que no hablan el mismo idioma, bajo el mismo padre services/.
La lección se generaliza: contar los servicios de un backend de Rust enumerando las subcarpetas de services/ da un número falso. Sólo la raíz Cargo.toml tiene autoridad sobre lo que realmente se compila en el espacio de trabajo.
[profile.release]: una configuración única para todo el espacio de trabajo
Un [profile.release] declarado en la raíz del espacio de trabajo se aplica a todos los miembros compilados en modo de lanzamiento: solo un lugar para ajustar, no once. Cargo documenta todas las claves disponibles en su referencia de perfil de compilación ; Aurabase sólo activa cinco.
La elección de panic = "unwind" en lugar de "abort" está documentada directamente en el comentario del archivo: tokio intercepta un pánico en un controlador de axum, la tarea devuelve un 500 y el proceso continúa atendiendo otras solicitudes simultáneas. La ganancia de rendimiento deabort no justifica la pérdida del aislamiento entre consultas.
Congele la cadena de herramientas y los comandos importantes
Un espacio de trabajo unificado establece versiones de dependencia, pero no la versión del compilador en sí. rust-toolchain.toml, en la raíz, congela la cadena de herramientas (channel = "1.93" hoy) para cualquier llamada directa a cargo o rustc en el repositorio.
Este archivo existe precisamente porque se produjo una deriva silenciosa: una acción de CI instaló el canal stable del día sin leer rust-toolchain.toml, mientras que la imagen de lanzamiento de Docker se compiló con la versión congelada. Una confirmación podría pasar todas las pruebas con el Rust estable actual y luego fallar en la compilación de la imagen, descubierta después de la fusión, no antes. rustuprespeta este archivo para cualquier comando directo: era la única solución necesaria, sin afectar los flujos de trabajo de CI.
Un último detalle, para quienes leen un manifiesto antes de ejecutarlo: la caja aura-cli compila un binario cuya tabla [[bin]] lo nombra aurabase, no aura. El contenedor npm publicado, @aurabase/cli, expone los dos comandos: aura y aurabase ambos apuntan al mismo script. El nombre de una carga [[bin]], el nombre de la caja y el nombre expuesto por un contenedor npm son tres cosas separadas; ninguno se puede adivinar a partir de los otros dos.
Lista de verificación: dónde declarar qué
Surgen cinco decisiones cada vez que agrega una caja a un espacio de trabajo de Cargo. Aquí es donde se declara cada uno, según el ejemplo de Aurabase mencionado anteriormente.
| Lista de miembros | miembros [espacio de trabajo] | Raíz: lista explícita, nunca global |
|---|---|---|
| Versión/edición compartida | [espacio de trabajo.paquete] | Raíz: version.workspace = true, por caja, opcional |
| Dependencia compartida por más de 2 cajas | [espacio de trabajo.dependencias] | Raíz: luego {workspace = true} en cada caja |
| Dependencia de un solo consumidor | [dependencias] de la caja | De forma local, sin pasar por el espacio de trabajo |
| Crear perfil | [perfil.lanzamiento] | Solo raíz: se aplica a todos los miembros |
Para auditar un espacio de trabajo de Cargo existente (el suyo o el de un proyecto que está asumiendo) cuatro comprobaciones son suficientes para encontrar el tipo de discrepancias documentadas anteriormente:
- Compare
[workspace.package].versioncon elversionde cada caja; un valor diferente no es necesariamente un error, pero vale la pena documentarlo. - Compare
[workspace.dependencies]con las dependencias declaradas localmente por cada caja; identifique los nombres presentes en ambos lados con diferentes versiones. - Compare la lista
membersen la raízCargo.tomlcon las subcarpetas reales en el repositorio; una carpeta que falta enmembersno es necesariamente un descuido. - Verifique el nombre real de cada binario compilado (
[[bin]] name) en lugar de asumir que coincide con el nombre de la caja o el nombre expuesto por un posible contenedor npm.