🦀 Rust#
Versión fijada:
1.83· Imagen base:rust:1.83-alpine· Hub::8700· Casos operativos: 20 / 20
⬅️ Volver a los perfiles de lenguaje · 🗺️ Mapa de stacks · 🔄 Protocolo de actualización
🪪 Identidad#
Rust es un lenguaje compilado y de tipado estático sin recolector de basura, que garantiza seguridad de memoria y de concurrencia en tiempo de compilación mediante un sistema de propiedad (ownership) verificado por el borrow checker. La consigna del proyecto es que un programa que compila no tiene fugas de memoria ni condiciones de carrera en código seguro.
Para qué se usa en la industria: sistemas donde el costo de un fallo de memoria es alto o donde no hay presupuesto para un GC — motores de navegador, kernels, sistemas embebidos, bases de datos, herramientas de línea de comandos de alto rendimiento y, cada vez más, servicios de red. Firefox, el kernel de Linux, Dropbox, Discord y buena parte de la infraestructura de Cloudflare tienen componentes en Rust.
Por qué está en este laboratorio: porque desplaza errores de runtime a errores de compilación, y eso se puede demostrar. En el caso 12 omitir el brazo None de un match no compila. En el caso 03 el compilador impide que el contexto del request sobreviva al request. Son las dos únicas garantías estructurales del set completo de siete stacks.
⚙️ Modelo de ejecución#
Threads del sistema operativo, sin recolector de basura, con liberación determinista por Drop.
El laboratorio usa Rust sin runtime asincrónico: std::thread y un thread por conexión. Es una decisión deliberada, y tiene consecuencias visibles:
| Consecuencia | Dónde se nota |
|---|---|
| La liberación es determinista y contable | Un impl Drop propio descuenta bytes vivos y cuenta liberaciones. dropped_total es contabilidad real del destructor, no una estimación del GC — caso 05 |
| No hay cierre que escribir | La Connection se cierra al salir de scope. Ni try-with-resources, ni using, ni defer, ni finally — caso 01 |
| Thread por conexión 1:1 no escala como goroutines | Es el costo de no usar tokio. El caso 11 lo documenta explícitamente en vez de esconderlo — caso 11 |
std no trae HTTP, JSON ni async | La capa HTTP del laboratorio está escrita a mano sobre TcpListener. Es la contrapartida honesta de una stdlib mínima — transversal |
🧰 Primitivas que usa el laboratorio#
| Caso | Primitiva central | Por qué esta y no otra |
|---|---|---|
| 01 · API lenta | rusqlite (feature bundled) + Ownership/Drop | SQLite compilado dentro del binario: no depende del libsqlite3 del sistema |
| 02 · N+1 | query_map(...).collect::<Result<Vec<_>>>() | Materializa las filas propagando el error de cualquiera de ellas. Ignorar un fallo parcial es imposible |
| 03 · Observabilidad | struct RequestCtx prestado por & + lifetimes | Una referencia al contexto no puede almacenarse en una estructura de vida más larga. La fuga de contexto no compila |
| 04 · Timeouts | mpsc::channel + recv_timeout | Deadline del lado del llamador. Limitación documentada: corta la espera, no el trabajo |
| 05 · Memoria | impl Drop for Tracked + LazyLock<Mutex<Lru>> | Destructor propio que hace observable la liberación. HashMap::new() no es const, de ahí LazyLock |
| 06 · Pipeline | enum DeployOutcome + match exhaustivo | Agregar Canary mañana rompe la compilación hasta contemplarlo en todos lados |
| 07 · Monolito | Box<dyn Fn(&Request) -> Response + Send + Sync> | El compilador verifica la thread-safety en el punto de registro, no en el primer request concurrente |
| 08 · Extracción | mpsc::channel (multi-producer, single-consumer) | El Receiver no implementa Clone: no puede haber dos consumidores. La restricción está en el tipo |
| 09 · Integración externa | Mutex<i64> con decremento condicional | Menos expresivo que el canal de Go, pero el guard libera en todos los caminos |
| 10 · Sobre-arquitectura | String::with_capacity + LazyLock<HashMap> | Sin realocaciones intermedias; el mapa de solo lectura no necesita lock |
| 11 · Reportes | Mutex<usize> + Condvar | El que no consigue slot duerme hasta ser despertado: espera pasiva, sin busy-wait |
| 12 · Punto único | Option<T> + operador ? + match exhaustivo | Omitir el brazo None no compila. El ? propaga la ausencia sin escribir un solo if |
| 13 · Cache stampede | Arc<Flight> con Mutex + Condvar | La std no trae Future ejecutable; el Arc obliga a que el vuelo sobreviva al mapa |
| 14 · Pool de conexiones | impl Drop sobre Lease | No hay línea que olvidar; fugar exige llamar a mem::forget por su nombre |
| 15 · Backpressure | mpsc::sync_channel(N) | El límite está en el tipo, y TrySendError::Full(T) devuelve el mensaje rechazado |
| 16 · Idempotencia | HashMap::entry + match exhaustivo | El único donde ignorar el resultado de la reserva no compila |
| 17 · Migración sin downtime | RwLock con spin acotado | El único caso del lab donde la respuesta de Rust es peor; los guards salvan el unlock |
| 18 · Arranque en frío | AOT sin runtime + OnceLock<T> | La curva más plana (1,00x), y el estado «no lista» inalcanzable por tipos |
| 19 · Deriva del índice | #[must_use] + HashSet | El único con las dos piezas: el bug no compila, y el diff no se escribe a mano |
| 20 · DLQ olvidada | enum + match exhaustivo | Una clase de error nueva no compila; y panic! no es un Result |
💡 El patrón que solo se ve mirando la columna entera: en los casos 03, 06, 07, 08 y 12 la corrección no la impone la disciplina del programador sino el compilador. Son cinco categorías de bug que en los otros seis stacks se evitan acordándose.
📈 Rendimiento: qué mide el laboratorio y cómo reproducirlo#
⚠️ Este repositorio no publica benchmarks entre lenguajes. Lo que se mide es la pendiente dentro de cada stack: legacy contra optimized, mismo runtime, misma máquina.
Rust es el único stack donde la medición de memoria es contabilidad exacta en vez de estimación: no hay GC que difiera la liberación, así que live_bytes refleja el estado real en el instante de la consulta.
| Señal | De dónde sale | Qué caso la expone |
|---|---|---|
live_bytes · live_mb | contador propio en impl Drop | 05 |
dropped_total | incremento dentro del destructor | 05 |
avg_ms · p95_ms · p99_ms | AtomicI64 + Mutex<Vec<f64>> | 01, 02, 10 |
| procesadores lógicos | thread::available_parallelism() | 11 |
IN_FLIGHT | AtomicI64 | 11 |
Reproducir la medición del caso 05 (liberación determinista):
docker compose -f compose.rust.yml up -d --build
curl -s localhost:8700/05/state
for i in $(seq 1 40); do curl -s "localhost:8700/05/batch-legacy?size_kb=64" > /dev/null; done
curl -s localhost:8700/05/state # live_bytes y retained_count crecen juntos
curl -s localhost:8700/05/reset-lab # el Drop libera en el acto, no "cuando pase el GC"
curl -s localhost:8700/05/state # dropped_total refleja exactamente lo liberadoEspecificación de rendimiento que este stack verifica y ningún otro puede: entre el reset-lab y la consulta siguiente no hay ventana de incertidumbre. En Java, .NET, Go, Node y Python la memoria se libera en algún momento después; acá se libera al salir de scope. Esa diferencia es el argumento completo del caso 05.
🚧 Límites, problemas sin solución y desafíos#
| Límite | Por qué importa | Dónde se ve |
|---|---|---|
recv_timeout corta la espera, no el trabajo | Exactamente la misma limitación de CompletableFuture.orTimeout en Java. El thread proveedor sigue dormido. tokio lo resuelve; std no | caso 04 |
std no trae HTTP, JSON ni runtime asincrónico | La capa HTTP del lab está escrita a mano. En producción se usaría axum + tokio + serde, tres dependencias que otros stacks tienen en la caja | transversal |
std::thread es 1:1 con el SO | Sin tokio, mil conexiones son mil threads. Go multiplexa; Rust sin runtime async, no | caso 11 |
| Curva de aprendizaje del borrow checker | Es el costo real de las garantías. Un equipo que no lo conoce paga semanas antes de ser productivo | transversal |
| Tiempos de compilación | El ciclo editar-compilar-probar es el más lento del laboratorio, con diferencia | transversal |
.unwrap() sigue estando ahí | El lenguaje impide ignorar la ausencia, pero no impide convertirla en panic de una línea | caso 12 |
Desafío abierto del stack en este laboratorio: el caso 04 es el punto flojo, y no por una mala decisión de implementación sino por una limitación real de std. Documentarlo como quinto puesto —en vez de esconderlo detrás de tokio— es lo que hace comparable el caso. Si una versión futura de std incorporara cancelación cooperativa, esa limitación dejaría de ser cierta y el caso 04 tendría que reescribirse. Está anotado como disparador explícito en el protocolo de actualización.
🏆 Dónde gana y dónde pierde en el laboratorio#
Agregado de los veredictos de las 19 comparativas que rankean: 10 primeros puestos, media 2.1 — mas oros y mejor promedio que ningun otro stack.
- 🥇 Gana en 02, 03, 05, 06, 07, 12, 14 y 16 — en el 14 por lo que el lenguaje impide:
impl Drophace que fugar una conexión no se pueda escribir por descuido — todos los casos donde el sistema de tipos o elDropdeterminista convierten un error de runtime en un error de compilación. - 🥈 Segundo en 08, 09 y 15 — pierde el primer puesto contra Go por expresividad, no por corrección.
- 🥉 Tercero en 01 y 11 — el thread-per-connection 1:1 le cuesta el podio.
- 4º en 13 — hay que construir el single-flight entero con
Condvar: el compilador protege del use-after-remove, pero no regala la primitiva. - 5º en 04 — la limitación de
recv_timeout, documentada arriba. - 🥇 Gana en 20 — el
enumconmatchexhaustivo es la primitiva exacta de un caso que trata de clasificar: una clase de error nueva no compila hasta que alguien decida qué hacer con ella. Ypanic!como canal separado deResultimpide estructuralmente que un bug del consumidor termine en la DLQ disfrazado de dato malo. - 🥇 Gana en 20 — el
enumconmatchexhaustivo es la primitiva exacta de un caso que trata de clasificar: una clase de error nueva no compila hasta que alguien decida qué hacer con ella. Ypanic!como canal separado deResultimpide estructuralmente que un bug del consumidor termine en la DLQ disfrazado de dato malo. - 🥇 Gana en 19 — el único stack con las dos piezas del caso:
#[must_use]sobreResulthace que ignorar la escritura fallida no compile sin escribirlo a propósito, yHashSetda el diff de tres caras sin recorrer a mano. Y la defensa está en la biblioteca estándar, no en una herramienta externa. - 🥈 Segundo en 18 — la curva más plana medida (1,00x) y la única garantía de tipo del lab contra servir desde una instancia no inicializada: con
OnceLock, el estado «no lista» es inalcanzable. El reverso exacto del caso 17. - 6º en 17 — el único caso donde su respuesta es peor que la de los otros seis: la
stdno ofreceRwLockcon deadline, así que la única opción es un spin que consume CPU.
Lectura honesta: Rust tiene el mayor rango del laboratorio. Es primero siete veces y quinto una vez. Go promedia mejor porque nunca baja del tercer puesto; Rust brilla donde el compilador aporta y queda al descubierto donde std no llega.
🔄 Ciclo de versiones#
| Versión fijada hoy | 1.83 (rust:1.83-alpine) |
| Cadencia upstream | Una release menor cada 6 semanas |
| Política de soporte | Solo la última versión estable; el compromiso es de estabilidad hacia atrás por edición, no soporte de versiones viejas |
| Producto en endoflife.date | rust |
Qué revisar en el próximo salto:
- 🚨 Cancelación en
std— sistdincorpora algo equivalente a un timeout cancelable o a un semáforo, las limitaciones documentadas en los casos 04 y 09 dejan de ser ciertas. Es el disparador de revisión más importante de este stack: no es un bump, es una reescritura de la narrativa del caso. LazyLock— estabilizado en 1.80 y usado en los casos 05, 07, 08 y 10. Cualquier cambio de API los toca a todos.- Nueva edición (2024 en adelante) — cambia reglas del lenguaje, no solo la versión del compilador. Requiere revisar los siete casos con código
unsafe-adyacente o con inferencia sensible. asyncenstd— si llegara a estabilizarse un runtime mínimo, la decisión de "sin runtime asincrónico" pasaría de honesta a anticuada.
El detalle del procedimiento está en docs/language-upgrade-protocol.md.
🚀 Levantar el stack#
docker compose -f compose.rust.yml up -d --buildLos 20 casos quedan servidos en http://localhost:8700/NN/. La primera compilación es notablemente más lenta que la de los otros seis stacks — es esperable, no es un fallo del build.