Partir un rango puede cambiar el resultado
Una API de períodos puede devolver datos distintos según dónde cortes las consultas. Cómo detectar huecos, alinear ventanas y comprobar el total.
El gráfico parecía mostrar un salto de consumo. Un día estaba mucho más bajo que los siguientes y la alerta hacía exactamente lo que le habíamos pedido: comparar períodos y señalar el cambio.
Pero el salto no estaba en el consumo. Estaba en la forma de consultar la API.
El mismo rango devolvía un total cuando se pedía completo y otro cuando se dividía en ventanas más chicas. La optimización que debía acelerar la lectura había cambiado los datos.
Una fecha no define un período
Muchas APIs reciben parámetros llamados from y to. Es fácil leerlos como si significaran “desde el comienzo de este día hasta el final de aquel”. A veces no es así.
Un cargo, una métrica o un evento puede tener su propio período:
inicio del período ─────────────── fin del período
La API tiene que decidir en qué consulta incluye ese registro. Puede hacerlo por el inicio, por el final, por cualquier solapamiento o por una regla que no está del todo explícita en la documentación.
Supongamos que filtra por fin del período y usa el intervalo (from, to]: excluye lo que termina exactamente en from e incluye lo que termina en to. Si cada ventana pretende cubrir ciertos días según la fecha de inicio, pero la consulta termina a medianoche y los períodos reales cierran varias horas más tarde, el último período del bloque todavía no entra en la respuesta. La consulta siguiente puede recibirlo, pero el código lo descarta porque su fecha de inicio pertenece al bloque anterior.
El registro existe y hasta puede viajar en una respuesta. Ninguna de tus ventanas se lo queda.
El problema no aparece necesariamente con datos falsos, errores HTTP ni valores imposibles. Aparece como un hueco razonable. Por eso puede atravesar validaciones de tipos, tests unitarios y controles superficiales.
La optimización escondía una decisión de dominio
Consultar todo el rango de una vez era simple, pero la respuesta era grande y lenta. Dividirla permitía ejecutar varias lecturas en paralelo y procesar cada bloque sin acumular todo en memoria.
Había tres alternativas:
- Volver a una sola consulta. Conservaba el total, a costa de perder la mejora de tiempo y memoria.
- Solapar las ventanas y deduplicar. Reducía el riesgo de huecos, pero obligaba a definir una identidad estable para cada registro y podía ocultar un error de límites.
- Partir según los períodos reales del proveedor. Mantenía el paralelismo y hacía explícita la regla temporal.
La tercera opción era la más útil, pero exigía dejar de tratar la medianoche como una frontera universal.
La unidad correcta no era “día UTC” ni “día local”. Era el período de cargo que definía la fuente. Las ventanas se alinearon con su hora de cierre y cada registro se asignó según el instante en que terminaba:
ventana 1: (corte A, corte B]
ventana 2: (corte B, corte C]
Así, un registro que termina exactamente en corte B pertenece sólo a la primera ventana. No hay hueco ni doble conteo.
En pseudocódigo, la regla queda más clara que una colección de ajustes horarios:
const pertenece =
periodo.fin > ventana.desde &&
periodo.fin <= ventana.hasta
La comparación importa tanto como la hora. Usar >= en ambos extremos haría que el límite compartido apareciera dos veces. Usar < en ambos podría dejarlo afuera.
El control que encontró el error
Los tests existentes comprobaban transformaciones internas: agrupación, clasificación y alertas. Ninguno preguntaba si dividir una consulta conservaba el resultado.
La comprobación útil fue una propiedad más general:
Para un rango cerrado y estable, consultar el rango completo debe producir el mismo agregado que consultar sus partes.
El control siguió cuatro pasos:
- Elegir períodos ya cerrados para evitar valores todavía mutables.
- Obtener el rango completo en una sola consulta.
- Obtener el mismo rango dividido en ventanas y sumar los resultados.
- Comparar totales y desglose por categoría, no sólo cantidad de filas.
La cantidad de registros podía coincidir mientras los importes diferían, o al revés. Comparar ambos niveles ayudó a distinguir un problema de límites de uno de agrupación.
Después de alinear las ventanas, las categorías de uso coincidieron con la consulta completa y desaparecieron los huecos artificiales del gráfico. También dejó de dispararse la alerta causada por esos huecos.
Quedó una diferencia pequeña en un concepto recurrente. La API lo prorrateaba según el rango solicitado, así que pedir varias ventanas no era matemáticamente equivalente a pedir una sola. Esa diferencia no se corrigió fingiendo igualdad: se documentó y se separó de los conceptos cuyo agregado sí debía cerrar exactamente.
Qué conviene probar además del caso feliz
Una prueba con un rango cómodo puede pasar aunque los bordes sigan mal. Para revisar una integración temporal hacen falta casos alrededor de cada frontera:
- un período que termina exactamente al comenzar la ventana;
- otro que termina exactamente al cerrarla;
- uno que atraviesa la medianoche;
- uno que dura más que la ventana usada para consultar;
- el período actual, todavía parcial;
- el cambio de mes o de ciclo de facturación;
- un ajuste publicado después del cierre original.
También conviene registrar qué semántica usa cada fecha. Un campo llamado day puede representar el inicio del consumo, el final del cargo o apenas la etiqueta elegida para mostrarlo. Si esa diferencia queda implícita, vuelve a aparecer cuando alguien agrupa, pagina o cambia la zona horaria.
Lo que esta regla no resuelve
Alinear cortes evita huecos y duplicados sólo si entendiste bien cómo la API selecciona los registros. No corrige datos que el proveedor revisa más tarde, períodos actuales que siguen acumulando valor ni cargos cuya fórmula depende del rango pedido.
Tampoco alcanza con sumar dinero. Para eventos mutables puede hacer falta conservar versiones; para métricas acumulativas, restar lecturas; para datos paginados, respetar cursores en lugar de inventar ventanas de tiempo.
Las zonas horarias agregan otra trampa. Un corte fijo en UTC no siempre equivale a una hora local fija si la región usa horario de verano. La frontera debe expresarse en la convención que realmente gobierna al proveedor, no en la que resulte más cómoda para la interfaz.
La propiedad que tiene que sobrevivir
Partir un trabajo grande en bloques parece una decisión de rendimiento. En sistemas de datos también es una decisión semántica: cada bloque define quién entra, quién queda afuera y quién puede contarse dos veces.
Antes de paralelizar una consulta temporal, escribí la propiedad que la transformación debe conservar. En este caso era sencilla:
el total y su desglose no deben depender de cómo se partió el rango.
Después compará una lectura completa contra la suma de sus partes usando datos ya estabilizados. Si no coinciden, no ajustes la alerta para que deje de molestar. Revisá primero qué significa realmente cada borde.