¿El orden de las llamadas en la cola de aprobaciones sobrevive a una carrera?
El orden de las llamadas en una cola de aprobaciones determina si una aprobación humana sigue teniendo sentido cuando llegan juntas dos escrituras de agentes. Reproduce la carrera y revisa las tarjetas, la ejecución y los diarios.

Una aprobación humana sirve de poco si una llamada posterior del agente puede saltarse una anterior porque su tarjeta se pulsó primero. Cuando dos escrituras compiten por el mismo estado remoto, la puerta de enlace debe asignarles un orden antes de que alguien toque cualquiera de las dos tarjetas y conservarlo durante el envío y hasta el registro de auditoría.
Es fácil pasar esto por alto porque el camino normal tiene una tarjeta, un clic y una respuesta correcta. El defecto aparece cuando dos procesos de agente envían solicitudes en el mismo instante, un revisor aprueba las tarjetas visibles en una secuencia poco conveniente y el destino acepta la solicitud que llega primero. He visto equipos llamar a esto concurrencia. En realidad, es una decisión sin definir que se toma después de que la persona creía tener el control.
El orden comienza cuando la puerta de enlace acepta la llamada
El orden de las llamadas es la secuencia en la que la puerta de enlace de acciones acepta solicitudes en un carril para el mismo recurso en conflicto. No es el orden en que se muestran las tarjetas, el orden en que una persona hace clic, el orden en que se abre una conexión de red ni el orden en que regresan las respuestas remotas.
Asigna a cada llamada aceptada un ticket inmutable, como 42, 43 y 44. Guarda el ticket junto con la descripción de la solicitud, la identidad del proceso que llama, el destino y el identificador del carril. El ticket debe existir antes de que la puerta de enlace solicite la aprobación. De lo contrario, la interfaz solo puede informar qué solicitud vio primero el revisor, y eso no basta para reconstruir una carrera.
Un carril es el ámbito en el que cambiar el orden puede cambiar el resultado. Dos solicitudes que sobrescriben el mismo entorno de despliegue deben pertenecer a un mismo carril. Dos llamadas que añaden datos al mismo registro remoto de incidencias también deben pertenecer a un mismo carril. Una solicitud para obtener metadatos de paquetes y otra para actualizar un servicio de pruebas independiente quizá no tengan que esperarse. Ordenar globalmente todas las acciones parece seguro, pero convierte una solicitud lenta y no relacionada en una interrupción para todos los agentes.
La parte difícil es elegir el carril con honestidad. El nombre de host del destino suele ser demasiado amplio. Una ruta de endpoint sin más suele ser demasiado específica. PATCH /documents/7 y POST /documents/7/publish afectan al mismo documento aunque sus rutas sean distintas. Si la puerta de enlace no tiene información suficiente para deducir esa relación, coloca esas acciones en el mismo carril configurado. No finjas que el servicio remoto reparará una promesa de orden que la puerta de enlace nunca hizo.
El registro de admisión debería tener una forma parecida a esta:
ticket=42 lane=release-prod event=accepted action=write-release caller=agent-a
ticket=43 lane=release-prod event=accepted action=write-release caller=agent-b
Esas líneas responden después a una pregunta precisa: ¿de qué llamada asumió responsabilidad primero la puerta de enlace? No afirman que el ticket 42 terminara primero. Un destino lento puede hacer que el ticket 43 termine después o antes, según el modelo de ejecución permitido. La puerta de enlace debe declarar ese modelo, en lugar de dejar que el diario invente uno a posteriori.
Una aprobación es una decisión, no un permiso para adelantarse
Que un revisor decida sobre la solicitud B antes que sobre la solicitud A no convierte a B en anterior. Solo significa que B ha cumplido una condición mientras espera su turno.
Esta distinción se difumina porque las interfaces de aprobación suelen tratar cada tarjeta como una solicitud aislada. Eso funciona en una acción sin consecuencias de orden. Falla con escrituras en conflicto. Si la interfaz permite que ambas tarjetas sigan disponibles, un clic en B debe cambiar B a un estado de aprobación en espera, no enviarla directamente al despachador. El despachador comprueba el ticket que encabeza el carril. Solo envía el ticket más antiguo que tenga una decisión de aprobación.
Una puerta de enlace tiene tres resultados razonables para el ticket que encabeza el carril:
- La aprobación permite enviar ese ticket.
- El rechazo registra una negativa terminal y después libera el siguiente ticket.
- La expiración o la cancelación explícita registra un resultado terminal y después libera el siguiente ticket.
Hay un cuarto comportamiento que causa problemas: una aprobación posterior se envía de inmediato porque la solicitud anterior aún no tiene una decisión. Parece práctico porque el revisor obtiene un resultado rápido. También cambia la secuencia remota según un instante de interacción en la interfaz. Una persona puede haber abierto A para revisar sus parámetros mientras aprobaba B como una tarea de mantenimiento inofensiva. El sistema envía entonces B primero, aunque presentaba el par como una cola.
La interfaz debe hacer visible el estado. La tarjeta que encabeza el carril puede ofrecer Aprobar y Rechazar. Las tarjetas posteriores pueden aceptar una decisión, pero su estado debe indicar que esperan detrás del ticket 42; otra opción es mantener sus controles bloqueados hasta que se resuelvan los tickets anteriores. Cualquiera de los dos enfoques puede conservar el orden. El primero ofrece más control al revisor; el segundo es más difícil de interpretar mal. Lo que la interfaz nunca debe dar a entender es que cada clic positivo provoca una ejecución inmediata.
Los controles de autorización por sesión y de aprobación por llamada de Sallyport hacen visible el límite de la solicitud, pero el orden todavía necesita un ticket asignado antes de la decisión de aprobación. Un revisor no puede evaluar una cola si el producto solo registra un conjunto sin estructura de tarjetas.
Reproduce la carrera con escrituras que dejen rastro
Una prueba útil envía dos procesos de agente independientes hacia un mismo carril de escritura y hace que el destino registre el orden de llegada. No uses dos lecturas, dos comprobaciones de estado idempotentes ni dos solicitudes que actualicen registros no relacionados. Esas pruebas pueden pasar aunque la cola permita adelantamientos.
Usa un endpoint HTTP desechable que acepte un cuerpo POST y añada el ticket recibido a un archivo o a una tabla de base de datos. Debe devolver el ticket que recibió. El destino no necesita autenticación para este ejercicio si se ejecuta solo en una dirección local de prueba; el objetivo es probar la ruta de aprobación y envío de la puerta de enlace, no la inyección de credenciales.
Este pequeño servidor de Node produce un registro de llegadas sencillo:
const fs = require("node:fs");
const http = require("node:http");
http.createServer((request, response) => {
let body = "";
request.on("data", chunk => { body += chunk; });
request.on("end", () => {
const item = JSON.parse(body);
fs.appendFileSync("arrival.log", `${item.ticket} ${item.value}\n`);
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ received: item.ticket }));
});
}).listen(8787);
Configura una acción aprobada que envíe POST /write a ese endpoint y lleve el ticket de la solicitud en su cuerpo JSON. Inicia dos procesos de agente nuevos en lugar de pedir a un solo proceso que haga dos llamadas en serie. Cada proceso envía la misma acción con un valor distinto, por ejemplo A y B. Haz que ambas llamadas requieran aprobación para que las tarjetas permanezcan pendientes al mismo tiempo.
La evidencia inicial esperada es sencilla y específica:
Sessions
42 accepted agent-a write A
43 accepted agent-b write B
Approval cards
42 write A
43 write B
arrival.log
(empty)
Si las tarjetas aparecen en orden inverso al de los tickets, detente e investiga. Un orden visual inverso no demuestra que la ejecución sea incorrecta, pero invita al revisor a actuar basándose en una historia falsa. Si solo aparece una tarjeta a la vez, registra si la segunda solicitud ya tiene un ticket y si el diario indica que espera detrás de la primera. Ocultar la segunda tarjeta puede estar bien. Ocultar que existe, no.
Ejecuta este ejercicio muchas veces. Inicia primero A y después B, y luego primero B. Inícialos con la menor separación posible según permita tu sistema de pruebas. Añade un breve retraso después de que uno de los procesos entre en la puerta de enlace para que la planificación dependa menos del azar. La prueba necesita solapamiento repetido porque una sola ejecución correcta suele reflejar una planificación favorable de los hilos, no una regla aplicada.
El orden de las tarjetas, los envíos y las finalizaciones es distinto
La prueba debe recoger tres órdenes, porque responden a preguntas diferentes y no deben reducirse a una sola columna.
El orden de las tarjetas es el orden en que el revisor puede ver las decisiones pendientes. Debe seguir el orden de los tickets dentro de un carril, aunque la aplicación dibuje las tarjetas en turnos distintos del bucle de eventos. Una tarjeta puede mostrar una marca de tiempo de llegada, pero el ticket es el campo decisivo.
El orden de ejecución es el orden en que la puerta de enlace libera las acciones aceptadas hacia el canal HTTP o SSH. En un carril de escritura estrictamente serializado, el destino debe observar el ticket 42 antes que el ticket 43. La puerta de enlace debe emitir un evento dispatched justo antes de entregar la solicitud al canal. No infieras el envío a partir de un evento de respuesta. Un destino puede recibir una escritura, cambiar el estado y después perder la conexión antes de enviar una respuesta.
El orden de finalización es el orden en que regresan las respuestas, las expiraciones o los errores de transporte. Puede diferir del orden de envío si el sistema permite llamadas superpuestas en carriles separados. Incluso en un solo carril, una implementación asíncrona puede registrar tarde la finalización porque vacía los registros después de limpiar la red. El orden de finalización es información operativa útil, pero no debe sobrescribir el orden causal.
Un registro de prueba compacto hace difícil ignorar la diferencia:
42 accepted
43 accepted
42 card-shown
43 card-shown
43 approved
42 approved
42 dispatched
42 succeeded
43 dispatched
43 succeeded
Que 43 approved aparezca antes que 42 approved es lo esperado en esta ejecución. Que 42 dispatched aparezca antes que 43 dispatched es el contrato. Si la aplicación solo registra los éxitos finales, desaparecen ambos datos y el investigador no puede saber si el usuario aprobó B primero, si el despachador adelantó A o si el destino reordenó dos solicitudes que ya habían sido liberadas.
RFC 9110 distingue los métodos HTTP seguros de los métodos que solicitan cambios de estado. Esa distinción importa aquí: una puerta de enlace puede tolerar un orden menos estricto para las observaciones, mientras que las escrituras necesitan un contrato de concurrencia explícito. RFC 9110 no proporciona un orden total útil entre dos conexiones independientes de clientes. La puerta de enlace toma esa decisión cuando coloca la aprobación delante de la llamada.
Aprueba primero la segunda tarjeta y mantenla en espera
La prueba básica de carrera más sólida aprueba deliberadamente B primero. Comprueba si la implementación trata la aprobación como una transición de estado o como un botón de envío directo.
Comienza con los tickets 42 y 43 pendientes en el mismo carril. Aprueba el ticket 43. La tarjeta o el diario deben cambiar a algo equivalente a approved, waiting for 42. El arrival.log del destino debe seguir vacío. Después aprueba el ticket 42. El destino debe recibir 42 A seguido de 43 B, y el registro de actividad debe mostrar ambos envíos en esa secuencia.
Repite la misma prueba rechazando el ticket 42. El registro esperado del destino contiene solo B. El diario conserva ambos tickets:
42 accepted
43 accepted
43 approved
42 rejected reason=user
43 dispatched
43 succeeded
Un rechazo es el resultado de una acción, no una ausencia de acción. Si el diario lo omite, quien revise el registro después verá que B se ejecutó sin explicación para la solicitud anterior que falta. Eso invita a una conclusión equivocada: quizá un atacante evitó la aprobación, quizá la aplicación perdió datos o quizá un revisor aprobó algo que no vio.
Después prueba una expiración. Deja que expire la aprobación de A mientras B ya está aprobada. La aplicación debe registrar una sola vez la expiración de A, hacer que B sea elegible y enviar B. No debe producir tanto expired como rejected porque un temporizador en segundo plano y un clic tardío compitieron. Elige una única decisión terminal mediante una operación atómica de comparación e intercambio sobre el estado del ticket. El evento perdedor debe ver que el ticket ya se resolvió y no hacer nada.
Por último, cancela B después de aprobarla, pero antes de que A se resuelva. B no debe enviarse nunca. Su estado terminal debe indicar cancelación, y resolver A más tarde no debe reactivarla. Esto detecta un fallo común de las colas: el despachador guarda una lista antigua de tickets aprobados y luego envía una entrada después de que un controlador de cancelación la eliminó de la cola visible.
Una buena prueba comprueba cada resultado por separado. No te conformes con una sola afirmación de que el destino recibió el estado final esperado. El estado final puede parecer correcto después de una secuencia incorrecta si B sobrescribe A, los reintentos ocultan un duplicado o el destino aplica su propia regla de conflicto.
Conserva el ticket durante los reintentos y ante destinos lentos
Una solicitud conserva su lugar en la fila cuando el canal la reintenta. Crear un ticket nuevo después de un fallo de conexión cambia el significado de la aprobación y puede permitir que una solicitud posterior se adelante.
Supón que el ticket 42 se envía, la conexión TCP se cierra antes de que la puerta de enlace reciba una respuesta y el destino quizá haya aplicado la escritura o quizá no. El ticket 43 espera. La puerta de enlace tiene varias políticas posibles: informar de un resultado desconocido y detenerse, reintentar con un token de idempotencia si el destino admite uno o exigir una nueva decisión humana. No tiene permiso para descartar silenciosamente el 42 y enviar el 43 como si el 42 nunca hubiera existido.
La política correcta depende de la operación remota. Un endpoint que acepta un identificador de idempotencia puede hacer que un reintento sea lo bastante seguro como para reutilizar el ticket original. Un comando ciego por SSH normalmente no puede hacerlo. En ese caso, informa de un resultado desconocido para el 42, mantén el carril bloqueado o abandónalo explícitamente después de una intervención humana y registra el motivo. Liberar automáticamente el 43 puede agravar el daño si el 43 supone que el 42 falló.
Prueba esto con un destino que reciba A, escriba su registro de llegada y cierre la respuesta antes de completar el intercambio HTTP. La puerta de enlace debe conservar un rastro como este:
42 accepted
42 approved
42 dispatched attempt=1
42 outcome=unknown
43 accepted
43 approved waiting-for=42
Que el ticket 43 termine ejecutándose es una decisión operativa que debe estar documentada. El comportamiento inaceptable es un diario que afirma que el 42 falló sin que la puerta de enlace tenga pruebas, seguido de un B correcto que dependía de esa afirmación.
Las solicitudes lentas exponen otro error de implementación. Un despachador que mantiene un mutex mientras espera la respuesta remota puede serializar todo por accidente, incluidos los carriles no relacionados y el trabajo de la interfaz. Un despachador que libera demasiado pronto el estado de orden permite que el siguiente ticket se adelante. Mantén reducido el estado de admisión y envío del carril, persiste el evento de envío, entrega la solicitud al canal y espera el resultado sin permitir que otro ticket del mismo carril pase cuando se aplica una serialización estricta.
El diario de auditoría debe conservar la causalidad
Un registro de auditoría encadenado mediante hashes demuestra que los registros conservados no se modificaron en silencio; no hace comprensible por sí solo una secuencia de eventos confusa. El modelo de eventos todavía necesita suficiente información para explicar una carrera de aprobaciones concurrentes.
Registra un evento de aceptación antes de que aparezca cualquier tarjeta. Registra la decisión de aprobación con el ticket y el actor o método de interacción. Registra el envío antes de que la solicitud HTTP o el comando SSH entre en su canal. Registra el resultado terminal del canal sin reemplazar ningún evento anterior. Cada evento necesita su propia posición de adición, además de la hora de reloj.
Los relojes son útiles para depurar, pero no pueden definir el orden por sí solos. Dos eventos pueden compartir la misma resolución temporal, los relojes pueden retroceder y una aplicación puede poner en cola escrituras procedentes de distintos hilos. Una posición de adición o un número de secuencia establece el orden en que el diario aceptó cada evento. El ticket de cada solicitud establece el orden previsto en el carril. Conserva ambos.
Para el ejercicio de las dos escrituras, compara estos datos:
- Las posiciones de aceptación muestran 42 antes que 43.
- Los eventos de aprobación pueden mostrar 43 antes que 42.
- Las posiciones de envío muestran 42 antes que 43 cuando ambas llamadas tienen éxito.
- El archivo de llegada del destino muestra 42 antes que 43.
- Los resultados terminales se vinculan a los mismos tickets en lugar de reemplazarlos.
Sallyport proyecta las vistas Sessions y Activity desde un único registro de auditoría cifrado y encadenado mediante hashes, y sp audit verify puede comprobar esa cadena sin conexión sobre el texto cifrado. Por eso es especialmente importante elegir bien los eventos: la verificación puede mostrar que los registros no se modificaron, mientras que los tickets y los tipos de evento indican a una persona qué ocurrió realmente.
No ordenes el diario visible por hora de finalización y lo llames historial. Eso presenta una carrera de red como si fuera una secuencia de decisiones. Ordena la línea temporal principal por la posición de adición duradera y muestra al lado el número de ticket y las marcas de tiempo. Una vista filtrada para un proceso de agente debe conservar las posiciones originales para que el revisor pueda ver que otra solicitud esperó entre dos eventos sin tener que adivinar.
FIFO se aplica a efectos en conflicto, no a cada byte
Una cola FIFO es una promesa sobre efectos que entran en conflicto. No es una razón para forzar todas las operaciones de red a pasar por un solo hilo ni para retrasar una lectura inofensiva detrás de una escritura sin resolver.
Empieza por clasificar cada acción. Un comando de despliegue que modifica un entorno compartido necesita un carril. Un HTTP POST que crea una acción de facturación necesita un carril, quizá uno por cuenta. Una consulta que lee un artefacto estático normalmente puede ejecutarse de forma independiente. Un comando SSH que solo informa del espacio en disco puede ser de observación, pero ten cuidado: los comandos suelen tener efectos ocultos a través de archivos de inicio del shell, archivos temporales o envoltorios de comandos remotos. Trata los comandos ambiguos como escrituras hasta que puedas describir sus efectos.
La alternativa popular es decir que todas las aprobaciones son independientes porque el revisor puede inspeccionar la solicitud. Resulta atractiva porque elimina el diseño de la cola. Falla cuando el revisor entiende cada solicitud por separado, pero no sabe que B supone que A ya ocurrió. La revisión humana no corrige la falta de contexto de serialización.
El error opuesto es una única cola global. Simplifica el orden y hace que la aplicación parezca bloqueada cuando un host remoto se detiene. Los carriles separados necesitan nombres claros, una selección estable de recursos y campos del diario que los identifiquen. Si no puedes explicar por qué dos acciones comparten un carril, tampoco puedes probar la promesa de orden.
No uses la aprobación por llamada como sustituto de los carriles. Exigir un clic para cada uso ayuda a una persona a evaluar cada acción. No define si dos acciones aprobadas pueden adelantarse. Son controles distintos, con modos de fallo distintos.
Convierte la carrera en un criterio para publicar
Un fallo de cola rara vez aparece como un bloqueo evidente. Suele aparecer después como un estado remoto inexplicable, una aprobación que parecía aplicarse a la acción equivocada o un diario incapaz de aclarar una revisión de incidente. Trata el ejercicio de las dos escrituras como una prueba de publicación para cada tipo de acción que pueda cambiar un estado compartido.
El criterio de publicación debe comprobar algo más que las respuestas correctas. Genera llamadas superpuestas, invierte la secuencia de aprobación, rechaza la primera solicitud, deja que expire, cancela la segunda solicitud y fuerza un resultado desconocido después del envío. En cada caso, guarda la secuencia de tickets, los estados de las tarjetas, los eventos de envío, el registro de llegada del destino y las entradas del diario.
Cuando uno de esos registros no coincida, resiste la tentación de llamarlo un problema de visualización. Un problema de visualización puede ser la primera señal visible de que distintas partes de la aplicación eligieron definiciones distintas de orden. Corrige el contrato en la admisión y haz que la tarjeta, el despachador, la prueba del destino y el diario informen del mismo contrato.
La primera acción práctica es pequeña: añade un ticket inmutable a un carril de escritura en conflicto y haz que la prueba apruebe primero el ticket posterior. Si esa solicitud llega al destino antes que su vecina anterior, la cola de aprobaciones todavía no controla el trabajo que afirma aprobar.
FAQ
¿Cómo determino el orden de dos llamadas de agentes concurrentes?
Usa un ticket monotónico asignado cuando la puerta de enlace acepta la solicitud, antes de que aparezca la tarjeta de aprobación. Una marca de tiempo por sí sola ofrece pruebas débiles porque los valores iguales, los cambios del reloj y los procesos separados pueden volverla ambigua.
¿Debería ejecutarse primero la solicitud aprobada primero?
El orden de aprobación no debería cambiar el orden de admisión. Si la solicitud B recibe la aprobación antes que la solicitud A, B debe permanecer retenida hasta que A alcance un resultado terminal o hasta que el sistema documente explícitamente otro contrato de serialización.
¿Qué ocurre cuando se rechaza la primera escritura de la cola?
Una solicitud anterior rechazada sigue ocupando su posición en la cola. Registra el rechazo y permite que continúe la siguiente solicitud aprobada. No borres silenciosamente la solicitud rechazada del historial.
¿Una aprobación que agota el tiempo puede bloquear todas las solicitudes posteriores?
Una expiración necesita el mismo tratamiento que un rechazo: una decisión terminal vinculada a un único ticket. La siguiente solicitud solo puede continuar después de que el registro de expiración sea duradero y visible en el diario.
¿Todas las acciones de los agentes necesitan una única cola FIFO global?
Separa las colas según el recurso cuyo estado puede entrar en conflicto. Dos escrituras en el mismo documento remoto necesitan serialización, mientras que las llamadas independientes a servicios no relacionados pueden usar carriles separados si el diario conserva el orden de admisión de cada carril.
¿Por qué pueden no coincidir los registros del servidor con la cola de aprobaciones?
Un proxy o un servidor HTTP puede recibir las solicitudes en un orden distinto al de la puerta de enlace cuando las conexiones compiten. La prueba debe comparar los tickets de la puerta de enlace con los registros de llegada posteriores, en lugar de tratar la llegada por red como prueba de una planificación correcta.
¿Cuál es una forma segura de probar escrituras concurrentes?
Un endpoint de escritura debe crear un efecto observable y ordenado, como añadir un ticket y una carga útil a un archivo local. Probar con solicitudes GET oculta el fallo porque las lecturas suelen tolerar el reordenamiento.
¿El orden del diario debería seguir la ejecución o la finalización?
El diario de actividad debe conservar la secuencia causal de cada solicitud: aceptada, decisión de aprobación, enviada y resultado terminal. El tiempo de finalización puede variar, pero no debe reescribir cuándo la puerta de enlace admitió o envió una solicitud.
¿La identidad del proceso basta para auditar una aprobación?
Una identidad de proceso firmada indica quién hizo la solicitud, mientras que un ticket de cola indica qué lugar ocupa esa solicitud entre las llamadas en conflicto. Ambos datos son necesarios para auditar una decisión más adelante.
¿Qué casos debe cubrir una prueba de regresión de la cola de aprobaciones?
Ejecuta repetidamente la carrera de dos escrituras con tiempos de aprobación invertidos, rechazo, expiración, cancelación y un primer destino deliberadamente lento. Considera cualquier discrepancia entre el orden de los tickets, las tarjetas, los envíos y la causalidad del diario como un bloqueo para publicar ese carril.