feat(api): filtros y retención del historial (#103) - #105
Merged
Conversation
…tention policy in response
…nce benchmarks - Updated requirements in `docs/requisitos.md` to clarify filtering criteria for execution history in the Backend API. - Enhanced `ruff.toml` to include detection of blocking calls within async functions and added comments on limitations. - Introduced new benchmarking scripts in `spikes/bench_poda_convergencia.py` and `spikes/bench_poda_coste_e_indice.py` to evaluate pruning strategies and their costs. - Created `spikes/bench_poda_limite_fijo_y_count.py` to address design flaws in previous benchmarks and assess the impact of fixed limits on pruning performance. - Added `spikes/check_fechas_lexicograficas.py` to verify the correctness of date comparisons in SQLite, ensuring lexicographic order aligns with chronological order.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#103 · Historial: filtros y retención
Closes #103
La otra mitad de R9. El issue anterior dejaba algo usable —historial paginado y
en orden inverso— y éste añade los dos refinamientos que quedaban, más un
criterio que hubo que reinterpretar porque su premisa había cambiado.
Con esto se completa R9 y con él el hito H2 → release
v0.3.0.Qué incluye
backend/config/settings.py:history_max_entries(1000) yhistory_max_days(30), con0para desactivar cada uno. Configuración y noconstantes porque el criterio los propone como ejemplo, no como norma, y
porque en desarrollo interesa desactivarlos para no perder las pruebas propias.
backend/api/history.py: índice encreated_at, la poda dentro de latransacción del
INSERT, y elWHEREdinámico de los filtros.backend/api/app.pyyschemas.py: seis parámetros de filtrovalidados en la firma —así salen publicados en OpenAPI— y un campo
retentionen la respuesta.
docs/requisitos.md: ajuste de R9.4, justificado abajo.tests/api/test_history.py: 16 tests nuevos (5 de poda, 11 de filtros).spikes/: los cuatro bancos de prueba, con resultados y conclusiones en lacabecera — incluidos los dos que salieron mal, con la advertencia escrita.
R9.4 se escribió para un historial que no existe
«Filtrado por nombre de herramienta, intervalo de fechas y estado» se redactó
pensando en el historial de invocaciones, que en #102 se descartó a favor de
guardar análisis. Dos de los tres criterios no encajan tal cual:
«Nombre de herramienta» no aplica a un análisis, que invocó cinco señales y
no tiene una. Se resuelve con dos parámetros:
kindsepara análisis deejecuciones sueltas y
toolsólo casa con las segundas. Ambas columnas yaexistían. En la pantalla el desplegable de herramientas aparece únicamente dentro
de la pestaña «Herramientas», así que la restricción no se explica: se ve.
«Estado» tampoco es una sola cosa: un análisis puede tener tres señales bien
y una caída. Se desdobla en
verdict—qué concluyó, y es el que interesa enpantalla— y
status—si funcionó la maquinaria, operativo—. Es también el motivopor el que en #102
statusquedó como cadena y no como enum.Se descartó la lectura literal —guardar qué señales participaron en cada
análisis— porque pide tabla nueva y habilita una consulta de depuración, no de
usuario.
La poda: la formulación barata era la incorrecta
MINsobre subconsultaOFFSETsobre el índiceMAX(id) - NLa (c) es 45 veces más barata que la ganadora y está mal: borra
incondicionalmente, así que mantiene el tamaño de partida en vez de llevarlo al
límite — con 500 filas y techo de 1000 seguía borrando una por escritura. El
primer banco no lo detectó porque arrancaba justo en el límite, así que su
«quedan 1000 filas» salía por construcción.
De ahí el criterio de convergencia desde ambos lados. Desde abajo es
corrección: borrar por debajo del techo destruye lo que la política dice
conservar. Desde arriba es la ruta de actualización, y no es hipotética — el
historial lleva creciendo sin techo desde #102, así que al desplegar esto lo
primero que se encuentra es una tabla por encima del límite. Que (d) reduzca de
golpe, de 3000 a 1000 en una sentencia, es lo que evita escribir una migración.
MAX(id) - Nes exacta porque los ids son contiguos, y eso costó dosafirmaciones falsas antes de comprobarlo: los huecos no vienen de la poda (borra
por la cola) ni de inserciones revertidas (medido:
AUTOINCREMENTno quemael id al revertir). Lo que garantiza es que un id no se reutilice tras borrar.
El índice, y la pregunta que no se le hizo a WAL
Unas 2.400 veces más rápida a 1.000 filas, y constante en vez de lineal. Y no
se paga al escribir: 14,29 µs sin índice contra 13,91 µs con él, por debajo del
ruido. Esa segunda medición es la que faltó en #102 al evaluar WAL — mirar sólo
lo que una optimización acelera, sin mirar lo que encarece, es cómo se acaba
adoptando algo que sale más lento.
La retención no es sólo higiene de disco
SELECT COUNT(*)cuesta ~1 µs por fila y es lineal, porque SQLite no locachea sino que recorre: 886 µs sobre 1.000 filas, 50 465 µs sobre 50.000. Y
GET /historylo ejecuta en cada lectura desde #102, para devolver eltotal.La retención es lo que mantiene barata una lectura que ya estaba escrita.
Dos invariantes frágiles, reforzados sin que hubiera fallo
Ninguno era un bug: eran código correcto por razones que nadie había escrito.
WHEREcompuesto construía los filtros de igualdad conf"{columna} = ?". Seguro por dónde venía esa variable, no por cómo estabaescrita la línea: el día que alguien pase un nombre de campo desde la petición,
esa misma línea se convierte en una inyección sin dar señal. Ahora el fragmento
entero va en la tupla.
isoformat()en UTC. Comprobado que hoy acierta —incluso mezclando marcas conmicrosegundos y sin ellos, porque
.es 46 y+es 43— pero un sufijoZes90 y ordena después de cualquier desfase: mezclarlo rompería las comparaciones
en silencio. Todo pasa ahora por una única función.
Qué encontró revisar, frente a qué encontró medir
COUNT(*)lineal, el coste del índice, y cuatro afirmaciones propias desmentidasSe añade
ASYNCa ruff, que detecta llamadas bloqueantes dentro de funcionesasíncronas — con la advertencia de que no conoce
sqlite3, así que el casoconcreto de este issue se le habría escapado igual.
Notas
INSERT, que es lo que hace quese cuele en el
fsyncya pagado. El precio: si la poda falla se deshace tambiénel
INSERT, y comorecordse traga los errores el síntoma sería «el historialdejó de guardar» sin ruido. Se asume, cubierto con tests.
?since=...+00:00escrito a mano devuelve 422, porque en una cadena deconsulta
+significa espacio. No es un fallo de la API —cualquier cliente quecodifique sus parámetros funciona— y está avisado en la descripción del
parámetro, que es donde lo verá quien genere el cliente Angular.
offset. Es estable a este volumen y laretención le quita el problema de rendimiento, pero si la pantalla acaba siendo
scroll infinito conviene migrar a cursor antes de generar el cliente: cambia
el contrato.
kind,verdictostatus. Con techo de 1.000 filas unescaneo es ~1 ms; añadirlos sería optimizar sin medir.
ajuste de R9.4 de arriba.