JUANCHI
InicioCVBlogLabContacto
Saltar al contenido
JUANCHI
InicioCVBlogLabContacto
S
Volver al blog
produccionsistemasrustconcurrenciamutexdeadlocksurelock

Surelock y deadlocks en Rust: lo intenté, me quemé, y ahora entiendo por qué esto tiene 214 puntos

Tenía código Rust en producción con mutexes. Me dio un deadlock a las 2am. Cuando vi Surelock en Hacker News con 214 puntos, abrí el repo y entendí por qué el compilador de Rust te da falsa confianza con la concurrencia.

12 de abril de 20269 min de lectura8 visualizacionesVer en Dev.to

Contenido

Contenido

Hay una creencia instalada en la comunidad dev sobre Rust que está, con todo respeto, bastante equivocada: que si compiló, es seguro. No. El compilador de Rust te protege de memory safety. De deadlocks, no te protege nadie. Y eso lo aprendí de la peor manera.

Era un martes a las 2am. Tenía un servicio en producción — Next.js en el frontend, pero el worker que procesaba las tareas pesadas estaba escrito en Rust. El servicio dejó de responder. Sin panic, sin error, sin nada. Solo silencio. Un deadlock clásico, en producción, en Rust. En el lenguaje que se supone que "te hace escribir código correcto".

Cuando vi Surelock aparecer en Hacker News con 214 puntos, lo primero que hice fue abrir el repo. Y lo segundo fue sentir una mezcla de alivio y bronca.

Surelock Rust deadlock mutex: el problema que nadie te explica bien

Rust te da Mutex<T> de la librería estándar. Y ahí empieza el problema conceptual: mucha gente (yo incluido, en su momento) asume que Mutex en Rust es intrínsecamente más seguro que en otros lenguajes. Y lo es, pero no de la manera que importa.

Lo que Rust garantiza con su sistema de ownership:

  • No podés acceder al dato sin lockear el mutex
  • El lock se libera automáticamente cuando el MutexGuard sale de scope
  • No hay data races

Lo que Rust NO garantiza:

  • Que dos threads no se esperen mutuamente para siempre
  • Orden de adquisición de locks
  • Deadlocks entre más de un mutex

Este fue mi código en producción. Simplificado, pero la lógica es exactamente esta:

use std::sync::{Arc, Mutex};
use std::thread;

// Dos recursos compartidos — parecía razonable en ese momento
let recurso_a = Arc::new(Mutex::new(vec!["dato_a"]));
let recurso_b = Arc::new(Mutex::new(vec!["dato_b"]));

let a1 = Arc::clone(&recurso_a);
let b1 = Arc::clone(&recurso_b);

// Thread 1: lockea A, después lockea B
let handle1 = thread::spawn(move || {
    let _lock_a = a1.lock().unwrap(); // Agarra A
    thread::sleep(std::time::Duration::from_millis(10)); // El timing exacto del infierno
    let _lock_b = b1.lock().unwrap(); // Espera B... que nunca llega
    println!("Thread 1 terminó");
});

let a2 = Arc::clone(&recurso_a);
let b2 = Arc::clone(&recurso_b);

// Thread 2: lockea B, después lockea A — y acá está el problema
let handle2 = thread::spawn(move || {
    let _lock_b = b2.lock().unwrap(); // Agarra B
    thread::sleep(std::time::Duration::from_millis(10)); // Suficiente para que explote
    let _lock_a = a2.lock().unwrap(); // Espera A... que tampoco llega
    println!("Thread 2 terminó");
});

// Esto nunca ejecuta
handle1.join().unwrap();
handle2.join().unwrap();

El compilador lo acepta sin chistar. Cero warnings. Cero errores. Y en producción, con el timing justo, deadlock.

Lo más frustrante es que yo sabía que era un problema potencial. Lo había visto en teoría. Análisis II en la UBA lo aprobé al cuarto intento, pero los algoritmos de detección de deadlocks los entendí bien. El problema es la brecha entre entender el concepto y aplicarlo cuando estás escribiendo código rápido a las 11pm después de un día largo.

Lo que intenté hacer a mano (y por qué no alcanzó)

Antes de encontrar Surelock, intenté resolver esto de manera artesanal. La solución clásica es establecer un orden global de adquisición de locks. Si siempre lockeás A antes que B, nunca hay deadlock.

use std::sync::{Arc, Mutex};

// Intento manual de orden de locks — frágil por definición
struct RecursosOrdenados {
    // Convención: siempre lockear en orden de id
    recursos: Vec<Arc<Mutex<Vec<String>>>>,
}

impl RecursosOrdenados {
    fn lock_en_orden(&self, indices: &mut [usize]) -> Vec<std::sync::MutexGuard<Vec<String>>> {
        // Ordenar índices para garantizar orden consistente
        indices.sort();
        indices
            .iter()
            .map(|&i| self.recursos[i].lock().unwrap())
            .collect()
    }
}

El problema con esto: es una convención. No hay nada en el compilador que te obligue a usarla. Un nuevo dev en el equipo, o yo mismo tres meses después con el contexto perdido, puede simplemente no seguirla. Y puf, deadlock de vuelta.

Esto es exactamente el tipo de problema que me pasó revisando PRs esta semana — el código parece correcto, sigue patterns razonables, y el error está en una suposición implícita que nadie documentó. Lo mencioné en el post sobre code review y seguridad: los bugs más peligrosos son los que el linter no ve.

Surelock: lo que hace diferente

Surelock toma un approach distinto. En vez de ser una convención, codifica el orden de los locks en el tipo. El sistema de tipos de Rust hace el trabajo.

La idea central: cada mutex tiene un nivel. Solo podés lockear un mutex de nivel N si no tenés ningún lock de nivel N o superior. El compilador verifica esto en compile time.

// Con Surelock — esto es lo que el compilador ahora puede verificar
use surelock::{new_lock_hierarchy, HierarchicalMutex};

// Definir la jerarquía — nivel 0 es el más externo
new_lock_hierarchy! {
    pub struct Level0; // Nivel más alto de la jerarquía
    pub struct Level1; // Solo se puede lockear después de Level0
}

// Mutex tipado con su nivel
let recurso_a: HierarchicalMutex<Vec<String>, Level0> = 
    HierarchicalMutex::new(vec!["dato_a".to_string()]);
let recurso_b: HierarchicalMutex<Vec<String>, Level1> = 
    HierarchicalMutex::new(vec!["dato_b".to_string()]);

// Esto compila — orden correcto
{
    let guard_a = recurso_a.lock();
    let guard_b = recurso_b.lock_after(&guard_a); // OK: Level1 después de Level0
    // trabajar con los datos...
}

// Esto NO compila — el compilador lo rechaza
{
    let guard_b = recurso_b.lock();
    // ¡Error en compile time!
    // let guard_a = recurso_a.lock_after(&guard_b); // Level0 no puede ir después de Level1
}

Eso es el poder real. No es una herramienta de runtime que detecta deadlocks cuando ya pasaron. Es una herramienta de compile time que los hace imposibles por construcción.

El approach me recuerda a algo que estuve pensando con los agentes de IA — la diferencia entre detectar errores y hacer que los errores sean inexpresables. Lo cubrí un poco en el post sobre Research-Driven Agents: hay una diferencia enorme entre un sistema que te avisa que hiciste algo mal y un sistema que no te deja hacerlo.

Los gotchas que Surelock no resuelve (y hay que saber)

Surelock es brillante, pero no es magia. Hay escenarios donde no alcanza y hay que saberlos:

1. Locks que no podés jerarquizar fácilmente

Si tenés un grafo de recursos donde el orden de acceso depende de datos en runtime, no podés expresarlo en compile time. En esos casos, necesitás otras estrategias: try_lock con backoff, timeouts, o rediseñar la estructura de datos.

// Cuando el orden depende de runtime — Surelock no te salva acá
fn procesar_transaccion(from_id: u64, to_id: u64) {
    // ¿Lockeás from primero o to primero?
    // Depende de los IDs — esto requiere otra estrategia
    let (primero, segundo) = if from_id < to_id {
        (from_id, to_id)
    } else {
        (to_id, from_id)
    };
    // Acá sí podés lockear en orden, pero Surelock no lo verifica automáticamente
}

2. Async Rust es otro mundo

Surelock trabaja con std::sync::Mutex. Si usás tokio::sync::Mutex (que probablemente usás si hacés async Rust), la integración no es directa. Los deadlocks en async son más raros pero no imposibles — especialmente si mezclás sync mutexes dentro de async code.

3. La jerarquía tiene que estar bien diseñada desde el principio

Si asignás mal los niveles, terminás con una jerarquía que compila pero que te fuerza a hacer refactors dolorosos más adelante. El diseño inicial importa. Mucho.

4. Código legacy

Si tenés código Rust existente con std::sync::Mutex, migrar a Surelock no es trivial. Tenés que auditar todos los puntos de lock y asignarles niveles consistentes. Es trabajo manual, y en el proceso podés descubrir que tu diseño actual no tiene una jerarquía clara — lo que en sí mismo es información valiosa.

Este tipo de deuda técnica es análoga a lo que vi en el debate sobre migrar infraestructura pública a Linux: la migración en sí te revela problemas que ya existían pero estaban ocultos.

FAQ: surelock rust deadlock mutex

¿Surelock reemplaza completamente a std::sync::Mutex?

No exactamente. Surelock envuelve mutexes y agrega información de jerarquía en el tipo. Para casos simples con un solo mutex, std::sync::Mutex está perfectamente bien — los deadlocks con un solo mutex no existen (excepto si lockeás el mismo mutex dos veces en el mismo thread, lo que Rust detecta con LockResult). Surelock brilla cuando tenés múltiples mutexes que se adquieren en distintos órdenes.

¿Esto funciona en Rust stable o necesito nightly?

Surelock usa features del sistema de tipos de Rust que están disponibles en stable. No necesitás nightly. Eso es parte de lo que lo hace práctico para proyectos reales — no es un experimento de research, es algo que podés usar hoy.

¿Por qué el compilador de Rust no detecta deadlocks nativo?

Detectar deadlocks statically en el caso general es un problema indecidible — es equivalente al halting problem. Lo que Rust puede garantizar es memory safety (ownership, borrow checker) pero la liveness (que el programa eventualmente termina, que no se bloquea para siempre) es mucho más difícil de verificar estáticamente. Surelock no resuelve el caso general — resuelve el caso específico de adquisición de locks en orden inconsistente, que es el caso más común.

¿Qué pasa si tengo un deadlock en async Rust con tokio::sync::Mutex?

En async Rust, los deadlocks son más sutiles. El runtime de Tokio puede detectar algunos casos (si lockeás un mutex y el future se suspende sin liberarlo), pero no todos. Para async, las herramientas de detección en runtime como tokio-console son más útiles que Surelock. La arquitectura también ayuda: si podés diseñar para evitar locks compartidos entre tasks (usando channels en su lugar), eliminás el problema de raíz.

¿Surelock tiene overhead de performance?

El overhead es mínimo. La verificación de jerarquía es puramente en compile time — no hay código extra ejecutándose en runtime. El HierarchicalMutex en runtime es básicamente el mismo que std::sync::Mutex. Si en el futuro necesitás optimizar la contención de locks, podés hacerlo con las mismas técnicas de siempre: reducir el scope de los guards, usar RwLock donde corresponda, o rediseñar para evitar locks.

¿Existe algo similar para otros lenguajes?

Sí, pero no con el mismo nivel de garantías en compile time. En Java, hay herramientas de análisis estático como FindBugs que detectan algunos patrones de deadlock. En Go, el race detector de runtime ayuda con data races pero no con deadlocks. El caso de Rust es especial porque el sistema de tipos es suficientemente expresivo como para encodear la jerarquía de locks como información de tipo — algo que en Java o Go simplemente no podés hacer de la misma manera. Es parte de por qué el ecosistema de Rust sigue produciendo cosas interesantes, similar a cómo los agentes de IA están cambiando cómo contribuimos a proyectos como el kernel de Linux.

Conclusión: el compilador de Rust es tu aliado, no tu cobertura

Rust te da herramientas extraordinarias. Pero hay una trampa psicológica: cuando algo compila en Rust, sentís una confianza que no siempre está justificada. El borrow checker elimina toda una clase de bugs. Y eso es tanto una garantía real como una invitación a bajar la guardia en las clases de bugs que Rust no garantiza.

Los deadlocks son una de esas clases. Y la solución de Surelock — encodear el orden de adquisición en el sistema de tipos — es exactamente el tipo de thinking que hace a Rust interesante: en vez de detectar el error cuando ocurre, hacés que el error sea inexpresable.

Si tenés código Rust con múltiples mutexes, te recomiendo hacer este ejercicio: intentá dibujar el grafo de adquisición de locks en tu sistema. Si no podés asignar un orden consistente a todos los nodos, ya tenés un deadlock latente esperando el timing justo para manifestarse. Con Surelock o sin él, esa auditoría es valiosa.

Yo voy a migrar el worker que me explotó a las 2am. No esta semana — tengo otros fuegos — pero está en el backlog con prioridad alta. Esas 2am te dejan marca.

¿Usás mutexes en Rust en producción? ¿Tuviste algún deadlock que tardaste en diagnosticar? Me interesa saber cómo lo resolviste — respondeme por acá o encontrá mis datos en el sitio.

Compartir:

Comentarios (0)

Deja un comentario

No hay comentarios aún. ¡Sé el primero en opinar!

Artículos Relacionados

Cómo Linux ejecuta un binario: lo entendí a los 33 años de programar y me da vergüenza
Historiadevopslinux

Cómo Linux ejecuta un binario: lo entendí a los 33 años de programar y me da vergüenza

33 años con computadoras y recién ahora entiendo qué pasa entre que escribís `./mi-programa` y corre el código. ELF, dynamic linking, ld-linux — el agujero negro que siempre esquivé.

9 min134
Docker para desarrolladores Node.js: de cero a producción sin morir en el intento
TutorialesTutorialTypeScript

Docker para desarrolladores Node.js: de cero a producción sin morir en el intento

Me llevó tres Dockerfiles rotos, dos servidores caídos en producción y una noche sin dormir entender cómo funciona Docker con Node.js de verdad. Acá te cuento todo lo que aprendí para que vos no pases por lo mismo.

9 min225

Categorías

Experimentos10Historia3Opinión9Reflexiones5Tecnología5Tutoriales4

Etiquetas

#TypeScript#nextjs#devops#linux#seguridad#LLM#devtools#React#open source#ia#javascript#Full Stack#Gemma#WebGPU#desarrollo web#infraestructura#server-components#node.js#postgresql#git#code review#claude code#productividad#anthropic#vscode#frontend#inteligencia-artificial#lighthouse#desarrollo de software#agentes-ia#produccion#coding-agents#sistemas#accesibilidad#historia programador argentino#workflow#Tutorial#TLS#AI agents#vibe-coding#desarrollo#AI#JWT#desarrollo-software#Browser#Edge#Inferencia Local#codebase visualization#github#análisis de código#developer tools#repomix#elf#dynamic-linking#bajo-nivel#strace#haproxy#lsp#homelab#neurociencia#Experimentos#Portfolio#WebDev#Performance#pnpm#npm#yarn#bun#package manager#monorepo#tooling#docker#backend#docker-compose#app-router#web-development#tailwind#railway#postmortem#Patrones de diseño#Programación#stack tecnologico 2025#drizzle orm#optimizacion#web-performance#autobiografía tech#railway deploy#nativo digital#ai tools#reflexión técnica#WebAssembly#inferencia en browser#WebLLM#edge inferencia#IA local#freestyle#sandboxes#quantum computing#criptografía#seguridad web#post-quantum cryptography#deadlock#surelock#bci#als#brainwaves#interfaz cerebro computadora#openbci#eeg#python#axe#aria#wcag#orquestacion#google-deepmind#scion#multi-agente#2025#ssl#x509#certificados#pki#node-forge#machine learning#LLMs#entrenamiento de modelos#GPU#deep learning#MegaTrain#full precision training#AI infraestructura#firewall#opensnitch#ebpf#networking#supply-chain#ci-cd#dependencias#sbom#ai-coding#APIs#vendor lock-in#producción#arquitectura#billing#análisis de datos#linux kernel#pgit#historia de commits#herramientas de desarrollo#sql#openrouter#zed-editor#ia-herramientas#costos-ia#investigacion#ai-engineering#SynthID#watermark IA#Gemini#edge computing#detección IA#reverse engineering#Google#modelos locales#filesystem#fuse#storage#control de versiones#agentes de ia#jujutsu#version control#software engineering#programacion#criptografia#migracion#gobierno#windows#argentina#automatización#YC S25#Twill.ai#PRs#kernel#contribuciones#hacker news#secretos#aws#benchmarks#swe-bench#evaluacion#robustez#research-driven-agents#rust#concurrencia#mutex#Web Vitals#Herramientas#Next.js#Best Practices#Opinión#Stack#Reflexiones#Carrera#Historia

Más Leídos

  • 01

    pnpm vs npm vs yarn vs bun: la comparativa definitiva que nadie te va a dar en 2025

    264 views
  • 02

    Next.js App Router: la guía que me hubiera gustado tener cuando migré de Pages Router

    238 views
  • 03

    TypeScript: los patrones que realmente uso todos los días

    236 views
  • 04

    De DOS a Cloud: mi viaje de 33 años con la tecnología — desde una Amiga en 1994 hasta deployar en Railway con Next.js

    234 views
  • 05

    Docker para desarrolladores Node.js: de cero a producción sin morir en el intento

    225 views

Newsletter

Recibe los últimos artículos directamente en tu inbox.

Categorías

Experimentos10Historia3Opinión9Reflexiones5Tecnología5Tutoriales4

Etiquetas

#TypeScript#nextjs#devops#linux#seguridad#LLM#devtools#React#open source#ia#javascript#Full Stack#Gemma#WebGPU#desarrollo web#infraestructura#server-components#node.js#postgresql#git#code review#claude code#productividad#anthropic#vscode#frontend#inteligencia-artificial#lighthouse#desarrollo de software#agentes-ia#produccion#coding-agents#sistemas#accesibilidad#historia programador argentino#workflow#Tutorial#TLS#AI agents#vibe-coding#desarrollo#AI#JWT#desarrollo-software#Browser#Edge#Inferencia Local#codebase visualization#github#análisis de código#developer tools#repomix#elf#dynamic-linking#bajo-nivel#strace#haproxy#lsp#homelab#neurociencia#Experimentos#Portfolio#WebDev#Performance#pnpm#npm#yarn#bun#package manager#monorepo#tooling#docker#backend#docker-compose#app-router#web-development#tailwind#railway#postmortem#Patrones de diseño#Programación#stack tecnologico 2025#drizzle orm#optimizacion#web-performance#autobiografía tech#railway deploy#nativo digital#ai tools#reflexión técnica#WebAssembly#inferencia en browser#WebLLM#edge inferencia#IA local#freestyle#sandboxes#quantum computing#criptografía#seguridad web#post-quantum cryptography#deadlock#surelock#bci#als#brainwaves#interfaz cerebro computadora#openbci#eeg#python#axe#aria#wcag#orquestacion#google-deepmind#scion#multi-agente#2025#ssl#x509#certificados#pki#node-forge#machine learning#LLMs#entrenamiento de modelos#GPU#deep learning#MegaTrain#full precision training#AI infraestructura#firewall#opensnitch#ebpf#networking#supply-chain#ci-cd#dependencias#sbom#ai-coding#APIs#vendor lock-in#producción#arquitectura#billing#análisis de datos#linux kernel#pgit#historia de commits#herramientas de desarrollo#sql#openrouter#zed-editor#ia-herramientas#costos-ia#investigacion#ai-engineering#SynthID#watermark IA#Gemini#edge computing#detección IA#reverse engineering#Google#modelos locales#filesystem#fuse#storage#control de versiones#agentes de ia#jujutsu#version control#software engineering#programacion#criptografia#migracion#gobierno#windows#argentina#automatización#YC S25#Twill.ai#PRs#kernel#contribuciones#hacker news#secretos#aws#benchmarks#swe-bench#evaluacion#robustez#research-driven-agents#rust#concurrencia#mutex#Web Vitals#Herramientas#Next.js#Best Practices#Opinión#Stack#Reflexiones#Carrera#Historia

Más Leídos

  • 01

    pnpm vs npm vs yarn vs bun: la comparativa definitiva que nadie te va a dar en 2025

    264 views
  • 02

    Next.js App Router: la guía que me hubiera gustado tener cuando migré de Pages Router

    238 views
  • 03

    TypeScript: los patrones que realmente uso todos los días

    236 views
  • 04

    De DOS a Cloud: mi viaje de 33 años con la tecnología — desde una Amiga en 1994 hasta deployar en Railway con Next.js

    234 views
  • 05

    Docker para desarrolladores Node.js: de cero a producción sin morir en el intento

    225 views

Newsletter

Recibe los últimos artículos directamente en tu inbox.