Expediente 001 · Ingeniería de software

El bug que desaparece cuando intentas observarlo

En producción falla. En tu máquina funciona. Añades logs y deja de fallar. No es magia: suele ser una pista sobre las condiciones que hacen posible el error.

Hay errores que parecen tener voluntad propia: aparecen una vez, dejan una traza incompleta y luego no vuelven a presentarse durante horas. El impulso natural es cambiar código hasta que el síntoma desaparezca. El problema es que eliminar un síntoma sin identificar su causa puede esconder el defecto en lugar de resolverlo.

La pregunta útil no es «¿por qué mi código falla solo cuando quiere?», sino «¿qué condición está presente cuando falla y ausente cuando funciona?». Esa condición puede ser el entorno, el orden de ejecución, los datos o simplemente el tiempo.

1. Tu entorno no es idéntico al de producción

Dos equipos pueden ejecutar la misma versión del código y producir resultados diferentes si cambian las versiones del runtime, las variables de entorno, los permisos, la configuración regional, el sistema operativo, las dependencias o los datos de entrada. Incluso un archivo puede comportarse distinto por mayúsculas y minúsculas según el sistema de archivos.

Antes de tocar la lógica, compara lo que realmente se ejecuta: versión desplegada, configuración efectiva, dependencias y características del entorno. Un contenedor reduce algunas diferencias, pero no elimina todas las diferencias de red, almacenamiento, permisos o recursos.

2. El tiempo puede ser parte del bug

Los errores de concurrencia son especialmente difíciles de reproducir. Si dos tareas leen y modifican el mismo estado, el resultado puede depender de cuál llega primero. Añadir una pausa, un log o un depurador cambia el calendario de ejecución y puede hacer que la secuencia problemática deje de ocurrir.

Una condición de carrera no se demuestra solo porque el fallo sea intermitente. Busca evidencia: operaciones concurrentes sobre estado compartido, falta de sincronización, invariantes rotas o registros que muestren un orden inesperado.

3. Los logs pueden alterar la observación

La instrumentación puede cambiar tiempos, consumo de recursos y orden de ejecución. También puede ocultar la señal si registra demasiado. Esto no significa que debas depurar a ciegas: significa que debes elegir observabilidad que ayude a distinguir hipótesis sin convertir el sistema en otro experimento completamente distinto.

Experimento: una carrera de datos en C#

Podemos observar el problema con un contador compartido. El siguiente programa lanza muchas operaciones que incrementan la misma variable. La operación contador++ parece una sola instrucción, pero conceptualmente implica leer el valor, calcular el siguiente y escribirlo. Si varias tareas intercalan esos pasos, algunas actualizaciones pueden perderse.

using System;
using System.Threading.Tasks;

const int operaciones = 100_000;
int contador = 0;

Parallel.For(0, operaciones, _ =>
{
    contador++;
});

Console.WriteLine($"Esperado: {operaciones}");
Console.WriteLine($"Obtenido: {contador}");

Qué observar: ejecuta el programa varias veces en una aplicación de consola .NET. En determinadas condiciones el valor final puede ser inferior al esperado, aunque el resultado depende del entorno y de la planificación de tareas. Que una ejecución dé el resultado correcto no demuestra que el código sea seguro; la carrera puede no manifestarse en cada intento.

Ahora sustituye el incremento por una operación atómica:

using System;
using System.Threading;
using System.Threading.Tasks;

const int operaciones = 100_000;
int contador = 0;

Parallel.For(0, operaciones, _ =>
{
    Interlocked.Increment(ref contador);
});

Console.WriteLine($"Esperado: {operaciones}");
Console.WriteLine($"Obtenido: {contador}");

Interlocked.Increment realiza el incremento como una operación atómica. En este ejemplo, el valor final debe coincidir con el número de operaciones. En código real, la solución correcta depende de la invariante que necesitas proteger: puede ser Interlocked, un bloqueo con lock, una estructura concurrente u otro diseño. No añadas sincronización a ciegas sin entender qué estado comparten las tareas.

Límite del experimento: este ejemplo ilustra una carrera de datos; no prueba que todos los errores intermitentes tengan esa causa ni reproduce necesariamente un incidente de producción concreto.

Un método para investigar sin adivinar

  1. Define el síntoma: anota qué debería ocurrir, qué ocurre realmente y cómo se reconoce el fallo.
  2. Conserva evidencia: registra timestamp con zona horaria, versión, identificador de correlación y contexto no sensible.
  3. Busca diferencias: compara configuración, entradas, carga, dependencias y permisos entre ejecuciones correctas e incorrectas.
  4. Formula hipótesis separadas: por ejemplo, diferencia de configuración frente a condición de carrera. No las mezcles en un único cambio.
  5. Diseña una prueba: intenta variar una condición cada vez y conserva el caso mínimo que reproduce el fallo.
  6. Verifica la corrección: añade una prueba de regresión y observa si el arreglo mantiene las invariantes bajo condiciones similares.

Qué información conviene registrar

Para un servicio web, una traza útil suele incluir un identificador de petición, versión de la aplicación, duración, dependencia involucrada y resultado. Evita guardar contraseñas, tokens, datos personales innecesarios o cuerpos completos de peticiones sin una justificación y controles adecuados.

Una hipótesis que explica el fallo es un comienzo. Una prueba que distingue esa hipótesis de las alternativas es progreso.

Una pequeña tabla de diagnóstico

La conclusión

Los bugs intermitentes son difíciles porque el programa no se ejecuta en el vacío: depende de estados, tiempos, recursos y entornos. El camino más fiable es preservar evidencia, reducir el problema, probar hipótesis y convertir el hallazgo en una prueba de regresión. Cambiar código al azar puede dar alivio momentáneo, pero no conocimiento.

Fuentes y lecturas:

Este artículo describe patrones generales de depuración, no un incidente particular. El ejemplo es ilustrativo y debe ejecutarse en el entorno del lector; los síntomas pueden tener varias causas y el diagnóstico debe apoyarse en evidencia del sistema investigado.

Sigue investigando

← Volver a todas las investigaciones