Veamos algunas técnicas de depuración para encontrar problemáticas en un sistema (troubleshooting).
Depuración (debugging)
En términos generales, la depuración de un componente nos ayudará a entender su funcionamiento a bajo nivel. Esto puede tener distintos propósitos, comúnmente suelen ser:
- Introducir cambios de requerimientos a una pieza de software existente.
- Encontrar y/o reparar un problema con la implementación actual.
- Evaluar el funcionamiento actual para sugerir mejoras (rendimiento, diseño, etc.).
Técnicas de depuración
Dependiendo del nivel de los componentes, distintas prácticas pueden ser aplicadas, pero las ideas tienden a ser similares.
Entender el caso de uso
Puede que sea bastante obvio, pero en mi experiencia, he notado que tendemos a omitir (o damos por entendido) el caso de uso que está generando un problema. Lo que lleva a pasar por alto alguna posible validación de negocio, por ejemplo:
- Validaciones explícitas en el código de la aplicación.
- Validaciones implícitas en la definición del esquema de la BD (o el tipo de dato al que estamos convirtiendo).
Es importante tomarse el tiempo para entender cuales son las suposiciones con las que funciona nuestro caso de negocio, los pasos y los límites definidos para este escenario.
Supongamos un ejemplo donde el cálculo de un carrito en un comercio electrónico está presentando errores.
La definición de negocio nos ayudará a saber cuáles son las suposiciones a confirmar y las funciones involucradas en este proceso.
Romper el problema en sus partes fundamentales
Ahora que sabemos cuales son los parámetros iniciales y el proceso de negocio, podemos mirar el código para confirmar que la definición de negocio se satisface en la implementación.
Para esto, tendremos que leer el código y comenzar a entender qué es lo que hace desde la capa más alta hasta la más baja, esto nos permitirá remover/descartar suposiciones y reducir variables.
Aplicado a nuestro ejemplo, podemos comenzar a hacer la revisión de los siguientes elementos:
Consistencia de datos
- ¿Todos los productos tienen precios válidos?
- ¿Es posible que tengamos inconsistencia de datos en la base de datos?
Integraciones
- ¿La conexión con el origen de datos desde la aplicación es exitosa?
Implementación
- ¿El cálculo puede fallar por datos? (por ejemplo, valores nulos).
Mensajes en la bitácora (log)
Si contamos con un log de actividades y errores de nuestra aplicación, será mejor asegurarse de ingresar actividades relevantes del usuario para entender cómo llegamos al problema, por ejemplo:
public Double getDiscounts(final User user, final Cart cart) {
LOG.debug("about to apply discounts to user {}", user.getEmail());
// el resto del código
}
public void calculateCartForUser(final User user, final Cart cart) {
LOG.trace("about to get prices");
final var prices = getPrices(user, cart);
LOG.trace("about to get discounts");
final var discounts = getDiscounts(user, cart);
LOG.trace("about to calculate the totals");
// calculo de precios - descuentos
}
Este punto también requiere entender la configuración de los niveles de información de la bitácora para evitar llenar de mensajes no deseados los distintos ambientes con los que contamos.
Revisión del manejo de errores
Es posible que el manejo actual de errores no sea el adecuado, por ejemplo, podríamos estar delegando el control de errores a las capas superiores, y a su vez, omitir atrapar un caso en particular, por ejemplo:
public Double getDiscounts(final User user, final Cart cart) {
LOG.debug("about to apply discounts to user {}", user.getEmail());
final var isCartEligible = isCartForDiscounts(cart);
if (!isCartEligible) {
throw new RuntimeException("Cart is not eligible for discounts");
}
// el resto del código
}
public void calculateCartForUser(final User user, final Cart cart) {
final var prices = getPrices(user, cart);
final var discounts = getDiscounts(user, cart);
// calculo de precios - descuentos
}
En el segmento anterior, getDiscounts
levanta una excepción, sin embargo, el componente superior calculateCartForUser
, no implementa el manejo del error, delegando nuevamente esta responsabilidad a una capa superior. Esto podría ocasionar que terminemos con errores inesperados en el sistema.
Delimitación del problema
Esto consiste básicamente en ir eliminando componentes para saber qué sección está generando el problema. Y aunque esto parece ser muy desesperado y salvaje, podemos encontrarnos en un escenario en el cual simplemente no sea claro el mensaje del error o aún ignoremos la totalidad de las variables involucradas, así que esto podría ayudarnos a limitar el problema a un único segmento de código.
Cabe mencionar que esto puede ser muy tedioso, ya que sería necesario ir cambiando algunas cosas ya sea desde la compilación o en tiempo de ejecución (dependiendo de las herramientas que utilicemos), por ejemplo:
public Double getDiscounts(final User user, final Cart cart) {
LOG.debug("about to apply discounts to user {}", user.getEmail());
final var isCartEligible = isCartForDiscounts(cart);
if (!isCartEligible) {
throw new RuntimeException("Cart is not eligible for discounts");
}
// el resto del código
}
public void calculateCartForUser(final User user, final Cart cart) {
// final var prices = getPrices(user, cart);
final var prices = 100;
final var discounts = getDiscounts(user, cart);
// calculo de precios - descuentos
}
Esto hace que efectivamente sólo ejecutemos getDiscounts
, reduciendo así los elementos involucrados en las pruebas y permite descartar que la generación del problema sea del código comentado.
Esta estrategia también puede ayudar a identificar y generar las pruebas unitarias que hicieron falta en la aplicación.
Conclusiones
Para poder encontrar un segmento de código que está genera un error, es necesario contar con una definición clara del negocio.
Debido a que la cantidad de puntos de fallo en una aplicación es muy variada, elementos como pruebas unitarias y mensajes a bitácora son muy importantes para el troubleshooting.
Encontrar problemas en una aplicación es muy complejo, así que la integración de la de elementos, prácticas y estrategias para poder mejorar la observabilidad y compatibilidad de nuestros cambios son cruciales para recuperarnos de los errores.