Ola 2 · Sebas 2.11: Idempotency-Key y límite de tasa por actor - #15
Open
heysebitas wants to merge 1 commit into
Open
heysebitas wants to merge 1 commit into
heysebitas wants to merge 1 commit into
Conversation
Spec §0: "Reintentos por mala conectividad de la ambulancia son la norma, no
la excepcion." La idempotencia existia SOLO dentro de RoutingStore: un
POST /dispatch reintentado creaba dos handshakes.
- Interceptor global de `Idempotency-Key`: clave (la accion) + huella
(metodo+ruta+cuerpo canonico). Misma clave y misma huella devuelve el
resultado guardado con `Idempotency-Replayed: true`; misma clave con
cuerpo distinto es 409, no un 200 con la respuesta vieja.
- Un reintento que llega MIENTRAS corre la primera espera su resultado en
vez de recibir un error: es el doble toque del paramedico con mala señal.
- Un fallo NO se cachea. Guardar un 500 convierte un timeout de Mapbox en
un error permanente para esa clave.
- Sin cabecera, todo sigue igual: no se deriva la clave del cuerpo porque
dos pacientes con el mismo cuadro en la misma esquina son dos
emergencias, y colisionarlas borra la segunda en silencio.
- `/auth/*` queda exento: su efecto viaja en la cookie, no en el cuerpo, y
repetir el cuerpo daria un 200 con una sesion que no existe.
- Tabla `idempotencia` (migracion 0005) generalizando
`pulso_routing_idempotency`, con purga a 24 h. Sin URL, memoria — y se
dice en el log que un reintento en otra instancia no se reconoceria.
- Limite de tasa por actor Y por organizacion, con Retry-After y
`retryable: true`. **`/triage` tiene su propio cubo y nunca hace esperar
mas de 5 s**: un paramedico con un paciente critico reintentando no es un
abusador, y bloquearlo es el peor fallo posible del sistema.
- `PulsoError` admite estado HTTP (409, 429). Antes todo era 400 y la cola
offline no podia distinguir "no insistas" de "espera y vuelve".
- El front manda claves derivadas de la accion en dispatch, respond y
escalar; `/campo` genera una por dictado y la reusa al reintentar.
Verificado end-to-end sobre HTTP: reintento que replica, 409 por cuerpo
distinto, y rafaga simultanea con la misma clave que produce un solo efecto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 23, 2026
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.
Tarea 2.11 · va sobre #14.
El problema
Spec §0: "Reintentos por mala conectividad de la ambulancia son la norma, no la excepción." La idempotencia existía solo dentro de
RoutingStore: unPOST /dispatchreintentado creaba dos handshakes.Qué trae
Idempotency-Key. Clave = la acción (la manda el cliente), huella = método + ruta + cuerpo canónico (la calcula el servidor). Misma clave y misma huella → resultado guardado conIdempotency-Replayed: true. Misma clave con cuerpo distinto →409, no un 200 con la respuesta vieja: eso sería contestar a una pregunta que nadie hizo./auth/*exento — su efecto viaja en la cookie, no en el cuerpo; repetir el cuerpo daría un 200 con una sesión que no existe, y cachear/auth/refreshdesharía la detección de reuso de 1.3.idempotencia(migración0005) generalizandopulso_routing_idempotency, con purga a 24 h. Sin URL, memoria — y el log dice que un reintento en otra instancia no se reconocería.Retry-Afteryretryable: true. Sin el eje de organización, una IPS con 200 actores tumba el sistema sin que ninguno pase su propio límite.PulsoErroradmite estado HTTP (409, 429). Antes todo era 400 y la cola offline no podía distinguir "no insistas" de "espera y vuelve".dispatch,respondyescalar;/campogenera una por dictado y la reusa al reintentar — no unrandomUUID()por intento, que es justo el bug que esto cierra.La trampa que no pisé
/triagetiene su propio cubo y nunca hace esperar más de 5 s. Un paramédico con un paciente crítico reintentando no es un abusador: es el usuario haciendo lo que el sistema le pide en el peor momento de su turno, y bloquearlo es el peor fallo posible de PULSO — peor que caerse, porque caerse se ve.Hecho cuando
Retry-Aftery no rompe la UI/camporeintenta sin duplicar16 tests nuevos. Verificado end-to-end sobre HTTP: reintento que replica, 409 por cuerpo distinto, y ráfaga simultánea con la misma clave que produce un solo efecto (201 + 201 con
Idempotency-Replayed: trueen la segunda).LimiteTasaGuard.🤖 Generated with Claude Code