A ordem das chamadas na fila de aprovação sobrevive a uma corrida?
A ordem das chamadas na fila de aprovação determina se uma aprovação humana continua significativa quando duas alterações de agentes chegam juntas. Reproduza a corrida e examine cartões, execução e diários.

Uma aprovação humana vale pouco se uma chamada posterior do agente puder ultrapassar uma anterior porque seu cartão foi clicado primeiro. Quando duas alterações competem pelo mesmo estado remoto, o gateway precisa definir a ordem antes que alguém toque em qualquer cartão, e depois preservá-la no despacho e no registro de auditoria.
É fácil não perceber isso porque o caminho feliz tem um cartão, um clique e uma resposta bem-sucedida. O defeito aparece quando dois processos de agentes enviam solicitações no mesmo instante, um revisor aprova os cartões visíveis em uma sequência inconveniente e o destino aceita a solicitação que chegar primeiro. Já vi equipes chamarem esse comportamento de concorrência. É apenas uma decisão indefinida, tomada depois que a pessoa acreditou ter controlado a situação.
A ordem começa quando o gateway aceita a chamada
A ordem das chamadas é a sequência em que o gateway de ações aceita solicitações em uma lane para o mesmo recurso conflitante. Não é a ordem em que os cartões são exibidos, a ordem em que alguém clica neles, a ordem em que uma conexão de rede é aberta nem a ordem em que as respostas remotas retornam.
Atribua a cada chamada aceita um ticket imutável, como 42, 43 e 44. Armazene o ticket com a descrição da solicitação, a identidade do processo que fez a chamada, o destino e o identificador da lane. O ticket precisa existir antes que o gateway peça aprovação. Caso contrário, a interface só poderá informar qual solicitação o revisor viu primeiro, o que não basta para reconstruir uma corrida.
Uma lane é o escopo em que mudar a ordem pode mudar o resultado. Duas solicitações que sobrescrevem o mesmo ambiente de implantação pertencem à mesma lane. Duas chamadas que adicionam dados ao mesmo registro remoto de incidente também pertencem à mesma lane. Uma solicitação para buscar metadados de pacotes e outra para atualizar um serviço de teste separado talvez não precisem esperar uma pela outra. Ordenar todas as ações globalmente parece seguro, mas transforma uma solicitação lenta e sem relação em uma indisponibilidade para todos os agentes.
A parte difícil é escolher a lane com honestidade. Um hostname de destino costuma ser amplo demais. Um caminho de endpoint isolado costuma ser restrito demais. PATCH /documents/7 e POST /documents/7/publish afetam o mesmo documento, embora os caminhos sejam diferentes. Se o gateway não tiver informações suficientes para deduzir essa relação, coloque essas ações na mesma lane configurada. Não finja que o serviço remoto vai cumprir uma promessa de ordenação que o gateway nunca fez.
O registro de admissão deve ter um formato próximo deste:
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
Essas linhas respondem mais tarde a uma pergunta precisa: qual chamada o gateway assumiu primeiro? Elas não afirmam que o ticket 42 terminou primeiro. Um destino lento pode fazer o ticket 43 terminar depois ou antes, dependendo do modelo de execução permitido. O gateway precisa declarar esse modelo, em vez de deixar o diário inventar um depois do fato.
Uma aprovação é uma decisão, não uma permissão para ultrapassar
Um revisor decidir sobre a solicitação B antes da solicitação A não torna B anterior. Isso apenas significa que B satisfez uma condição enquanto espera sua vez.
Essa distinção fica confusa porque as interfaces de aprovação geralmente tratam cada cartão como um pedido isolado. Isso funciona para uma ação sem consequências de ordenação. Falha no caso de alterações concorrentes. Se a interface deixar os dois cartões acionáveis, um clique em B deve mover B para um estado de aprovação pendente, não enviá-lo diretamente ao despachante. O despachante verifica o ticket no início da lane. Ele despacha apenas o ticket mais antigo que recebeu uma decisão de aprovação.
Um gateway tem três resultados sensatos para o ticket no início da lane:
- A aprovação permite o despacho desse ticket.
- A rejeição registra uma recusa final e libera o próximo ticket.
- A expiração ou o cancelamento explícito registra um resultado final e libera o próximo ticket.
Há um quarto comportamento que causa problemas: uma aprovação posterior despacha imediatamente porque o ticket anterior ainda não tem decisão. Parece conveniente, porque o revisor recebe um resultado rápido. Também muda a sequência remota de acordo com o momento de uma ação na interface. Uma pessoa pode ter aberto A para verificar seus parâmetros e aprovado B como uma tarefa inofensiva de manutenção. O sistema então envia B primeiro, apesar de apresentar o par como uma fila.
A interface deve tornar o estado visível. O cartão no início da fila pode oferecer Aprovar e Rejeitar. Os cartões posteriores podem aceitar uma decisão, mas o status deve informar que estão esperando atrás do ticket 42, ou a interface pode manter seus controles desativados até que os tickets anteriores sejam resolvidos. As duas abordagens preservam a ordem. A primeira dá mais controle ao revisor; a segunda é mais difícil de interpretar mal. O que a interface nunca deve sugerir é que todo clique positivo causa execução imediata.
Os controles de autorização por sessão e de aprovação por chamada do Sallyport tornam visível o limite da solicitação, mas a ordem ainda precisa de um ticket atribuído antes da decisão de aprovação. Um revisor não consegue avaliar uma fila se o produto registra apenas uma coleção sem estrutura de cartões.
Reproduza a corrida com alterações que deixem um rastro
Um teste útil envia dois processos independentes de agentes para uma única lane de escrita e faz com que o destino registre a ordem de chegada. Não use duas leituras, duas verificações de status idempotentes ou duas solicitações que atualizem registros sem relação. Esses testes podem passar mesmo quando a fila permite ultrapassagens.
Use um endpoint HTTP descartável que aceite um corpo POST e acrescente o ticket recebido a um arquivo ou tabela de banco de dados. Ele deve retornar o ticket recebido. O destino não precisa de autenticação neste exercício se for executado apenas em um endereço de teste local. O objetivo é testar o caminho de aprovação e despacho do gateway, não a inserção de credenciais.
Este pequeno servidor Node produz um registro simples de chegadas:
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);
Configure uma ação aprovada que envie POST /write para esse endpoint e carregue o ticket da solicitação no corpo JSON. Inicie dois processos novos de agentes, em vez de pedir a um único processo que faça duas chamadas em série. Cada processo envia a mesma ação com um valor diferente, por exemplo A e B. Faça com que as duas chamadas exijam aprovação para que os cartões permaneçam pendentes ao mesmo tempo.
A evidência inicial esperada é simples e 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)
Se os cartões aparecerem na ordem inversa dos tickets, pare nesse ponto e investigue. A ordem visual inversa não prova que a execução está errada, mas convida o revisor a agir com base em uma interpretação falsa. Se apenas um cartão aparecer por vez, registre se a segunda solicitação já tem um ticket e se o diário informa que ela espera atrás da primeira. Ocultar o segundo cartão é aceitável. Ocultar sua existência, não.
Execute este exercício muitas vezes. Inicie A primeiro e depois B. Em seguida, inicie B primeiro. Inicie os dois com a maior proximidade possível, de acordo com o que o seu conjunto de testes permitir. Adicione um pequeno atraso depois que um processo entrar no gateway para tornar o agendamento menos acidental. O teste precisa de sobreposição repetida, porque uma única execução bem-sucedida muitas vezes reflete uma programação conveniente das threads, não uma regra imposta.
A ordem dos cartões, do despacho e da conclusão são diferentes
O teste deve coletar três ordens, porque elas respondem a perguntas diferentes e não devem ser reduzidas a uma única coluna.
A ordem dos cartões é a sequência em que o revisor pode ver as decisões pendentes. Ela deve seguir a ordem dos tickets dentro de uma lane, mesmo que o aplicativo desenhe os cartões em diferentes ciclos do event loop. Um cartão pode mostrar um timestamp de chegada, mas o ticket é o campo decisivo.
A ordem de execução é a sequência em que o gateway libera as ações aceitas para o canal HTTP ou SSH. Em uma lane de escrita estritamente serializada, o destino deve observar o ticket 42 antes do ticket 43. O gateway deve emitir um evento dispatched imediatamente antes de entregar a solicitação ao canal. Não deduza o despacho a partir de um evento de resposta. O destino pode receber uma alteração, modificar o estado e depois perder a conexão antes de enviar uma resposta.
A ordem de conclusão é a sequência em que respostas, timeouts ou erros de transporte retornam. Ela pode ser diferente da ordem de despacho se o sistema permitir chamadas sobrepostas em lanes separadas. Mesmo em uma única lane, uma implementação assíncrona pode registrar a conclusão tarde porque libera os logs depois da limpeza da rede. A ordem de conclusão é uma informação operacional útil, mas não deve substituir a ordem causal.
Um registro de teste compacto torna difícil ignorar a diferença:
42 accepted
43 accepted
42 card-shown
43 card-shown
43 approved
42 approved
42 dispatched
42 succeeded
43 dispatched
43 succeeded
O fato de 43 approved aparecer antes de 42 approved é esperado nesta execução. O fato de 42 dispatched aparecer antes de 43 dispatched é o contrato. Se o aplicativo registrar apenas os sucessos finais, os dois fatos desaparecerão e o investigador não poderá saber se o usuário aprovou B primeiro, se o despachante ultrapassou A ou se o destino reordenou duas solicitações já liberadas.
A RFC 9110 distingue métodos HTTP seguros de métodos que solicitam alterações de estado. Essa distinção importa aqui: um gateway pode aceitar uma ordenação mais flexível para observações, enquanto as alterações precisam de um contrato de concorrência explícito. A RFC 9110 não fornece uma ordem total útil entre duas conexões de cliente independentes. O gateway assume essa decisão quando coloca a aprovação na frente da chamada.
Aprove a segunda solicitação primeiro e mantenha-a retida
O teste básico mais forte aprova B de propósito antes de A. Ele verifica se a implementação trata a aprovação como uma transição de estado ou como um botão de envio direto.
Comece com os tickets 42 e 43 pendentes na mesma lane. Aprove o ticket 43. O cartão ou o diário deve mudar para algo equivalente a approved, waiting for 42. O arrival.log do destino deve continuar vazio. Depois, aprove o ticket 42. O destino deve receber 42 A seguido de 43 B, e o registro de atividade deve mostrar os dois despachos nessa sequência.
Repita o mesmo teste rejeitando o ticket 42. O registro esperado do destino contém apenas B. O diário ainda tem os dois tickets:
42 accepted
43 accepted
43 approved
42 rejected reason=user
43 dispatched
43 succeeded
Uma rejeição é um resultado da ação, não uma ausência de ação. Se o diário a omitir, alguém que revisar o registro mais tarde verá B sendo executado sem explicação para a solicitação anterior ausente. Isso convida a uma conclusão errada: talvez um invasor tenha contornado a aprovação, talvez o aplicativo tenha perdido dados ou talvez um revisor tenha aprovado algo que não viu.
Depois, teste um timeout. Deixe a aprovação de A expirar enquanto B já foi aprovado. O aplicativo deve registrar a expiração de A uma vez, tornar B elegível e despachar B. Não deve produzir expired e rejected ao mesmo tempo porque um timer em segundo plano e um clique tardio competiram entre si. Escolha uma única decisão final usando uma operação atômica de comparação e troca no estado do ticket. O evento que perder deve perceber que o ticket já foi resolvido e não fazer nada.
Por fim, cancele B depois de aprová-lo, mas antes que A seja resolvido. B nunca deve ser despachado. Seu estado final deve indicar cancelamento, e resolver A depois não pode reativá-lo. Isso captura um erro comum de fila: o despachante guarda uma lista antiga de tickets aprovados e envia uma entrada depois que um manipulador de cancelamento a removeu da fila visível.
Um bom teste verifica cada resultado separadamente. Não se contente com uma única verificação de que o destino recebeu o estado final esperado. O estado final pode parecer correto depois da sequência errada se B sobrescrever A, as tentativas ocultarem uma duplicação ou o destino aplicar sua própria regra de conflito.
Preserve o ticket durante tentativas e destinos lentos
Uma solicitação mantém seu lugar na fila quando o canal tenta enviá-la novamente. Criar um novo ticket depois de uma falha de conexão muda o significado da aprovação e pode permitir que uma solicitação posterior ultrapasse a anterior.
Suponha que o ticket 42 seja despachado, a conexão TCP seja fechada antes que o gateway receba uma resposta e o destino possa ou não ter aplicado a alteração. O ticket 43 espera. O gateway tem várias políticas possíveis: informar um resultado desconhecido e parar, tentar novamente com um token de idempotência se o destino oferecer suporte ou exigir uma nova decisão humana. Ele não tem permissão para descartar silenciosamente 42 e enviar 43 como se 42 nunca tivesse existido.
A política correta depende da operação remota. Um endpoint que aceita um identificador de idempotência pode tornar uma nova tentativa segura o suficiente para reutilizar o ticket original. Um comando cego por SSH muitas vezes não pode. Nesse caso, informe um resultado desconhecido para 42, mantenha a lane bloqueada ou abandone-a explicitamente após intervenção humana e registre o motivo. Liberar 43 automaticamente pode ampliar o dano quando 43 pressupõe que 42 falhou.
Teste isso com um destino que receba A, registre sua chegada e feche a resposta antes de concluir a troca HTTP. O gateway deve preservar um rastro como este:
42 accepted
42 approved
42 dispatched attempt=1
42 outcome=unknown
43 accepted
43 approved waiting-for=42
A decisão sobre se o ticket 43 será executado depois é uma escolha operacional que precisa estar documentada. O comportamento inaceitável é um diário que afirma que 42 falhou sem que o gateway tenha evidências, seguido de um B bem-sucedido que depende dessa afirmação.
Solicitações lentas expõem um erro de implementação diferente. Um despachante que mantém um mutex enquanto espera a resposta remota pode serializar tudo por acidente, inclusive lanes sem relação e o trabalho da interface. Um despachante que libera o estado de ordenação cedo demais permite que o próximo ticket avance em uma corrida. Mantenha pequenos os estados de admissão e despacho da lane, persista o evento de despacho, entregue a solicitação ao canal e depois aguarde o resultado sem permitir que outro ticket dessa lane passe quando a serialização estrita for aplicável.
O diário de auditoria precisa preservar a causalidade
Um registro de auditoria encadeado por hash prova que os registros retidos não foram alterados silenciosamente. Ele não torna compreensível uma sequência confusa de eventos. O modelo de eventos ainda precisa de informações suficientes para explicar uma corrida de aprovação concorrente.
Registre um evento de aceitação antes que qualquer cartão apareça. Registre a decisão de aprovação com o ticket e o autor ou método de interação. Registre o despacho antes que a solicitação HTTP ou o comando SSH entre no canal correspondente. Registre o resultado final do canal sem substituir nenhum evento anterior. Cada evento precisa de sua própria posição de inserção, além do horário de parede.
Relógios de parede são úteis para depuração, mas não podem definir a ordem sozinhos. Dois eventos podem ter a mesma resolução de timestamp, os relógios podem mudar e um aplicativo pode enfileirar gravações vindas de threads diferentes. Uma posição de inserção ou um número de sequência estabelece a ordem em que o diário aceitou cada evento. O ticket por solicitação estabelece a ordem pretendida na lane. Mantenha os dois.
No exercício com duas alterações, compare estes fatos:
- As posições de aceitação mostram 42 antes de 43.
- Os eventos de aprovação podem mostrar 43 antes de 42.
- As posições de despacho mostram 42 antes de 43 quando as duas chamadas são bem-sucedidas.
- O arquivo de chegada do destino mostra 42 antes de 43.
- Os resultados finais estão vinculados aos mesmos tickets, em vez de substituí-los.
O Sallyport projeta as visualizações Sessions e Activity a partir de um único registro de auditoria criptografado e encadeado por hash, e sp audit verify pode verificar essa cadeia offline sobre o texto cifrado. Isso torna especialmente importante escolher bem os eventos: a verificação pode mostrar que os registros não foram modificados, enquanto o ticket e os tipos de evento informam à pessoa o que realmente aconteceu.
Não ordene o diário visível pelo horário de conclusão e chame isso de histórico. Essa escolha apresenta uma corrida de rede como uma sequência de decisões. Ordene a linha do tempo principal pela posição de inserção persistida e mostre o número do ticket e os timestamps ao lado. Uma visualização filtrada para um processo de agente deve manter as posições originais, para que o revisor veja que outra solicitação esperou entre dois eventos sem precisar adivinhar.
FIFO se aplica a efeitos conflitantes, não a todos os bytes
Uma fila FIFO é uma promessa sobre efeitos que entram em conflito. Não é motivo para forçar toda operação de rede a passar por uma única thread ou atrasar uma leitura inofensiva atrás de uma alteração não resolvida.
Comece classificando cada ação. Um comando de implantação que altera um ambiente compartilhado precisa de uma lane. Um HTTP POST que cria uma ação de cobrança precisa de uma lane, talvez uma por conta. Uma busca que lê um artefato estático muitas vezes pode ser executada de forma independente. Um comando SSH que apenas informa o espaço em disco pode ser observacional, mas tenha cuidado: comandos frequentemente têm efeitos ocultos por meio de arquivos de inicialização do shell, arquivos temporários ou wrappers de comandos remotos. Trate comandos ambíguos como alterações até conseguir descrever seus efeitos.
A alternativa popular é dizer que toda aprovação é independente porque o revisor pode inspecionar a solicitação. Isso é atraente, pois elimina o desenho da fila. Falha quando o revisor entende cada solicitação individualmente, mas não recebe indicação de que B pressupõe que A já aconteceu. A revisão humana não corrige a ausência de contexto de serialização.
O erro oposto é uma única fila global. Ela simplifica a ordem, mas faz o aplicativo parecer travado quando um host remoto fica lento. Lanes separadas precisam de nomes claros, seleção estável de recursos e campos no diário que as identifiquem. Se você não consegue explicar por que duas ações compartilham uma lane, também não consegue testar a promessa de ordenação.
Não use a aprovação por chamada como substituta das lanes. Exigir um clique para cada uso ajuda uma pessoa a avaliar cada ação. Isso não define se duas ações aprovadas podem ultrapassar uma à outra. São controles diferentes, com modos de falha diferentes.
Transforme a corrida em critério de liberação
Um erro de fila raramente aparece como uma falha evidente. Ele surge depois como um estado remoto sem explicação, uma aprovação que parecia se aplicar à ação errada ou um diário incapaz de esclarecer uma revisão de incidente. Trate o exercício com duas alterações como um teste de liberação para todo tipo de ação que possa modificar um estado compartilhado.
O critério de liberação deve verificar mais do que respostas bem-sucedidas. Gere chamadas sobrepostas, inverta a sequência de aprovação, rejeite a primeira solicitação, deixe-a expirar, cancele a segunda e force um resultado desconhecido depois do despacho. Em cada caso, salve a sequência de tickets, os estados dos cartões, os eventos de despacho, o registro de chegada do destino e as entradas do diário.
Quando um desses registros divergir, resista à tentação de chamá-lo de problema de exibição. Um problema de exibição pode ser o primeiro sinal visível de que partes diferentes do aplicativo escolheram definições diferentes de ordem. Corrija o contrato na admissão e faça com que o cartão, o despachante, o teste do destino e o diário apresentem o mesmo contrato.
A primeira ação prática é pequena: adicione um ticket imutável a uma lane de escrita conflitante e faça o teste aprovar primeiro o ticket posterior. Se essa solicitação chegar ao destino antes de sua vizinha anterior, a fila de aprovação ainda não controla o trabalho que afirma aprovar.
FAQ
Como determinar a ordem de duas chamadas simultâneas de agentes?
Use um ticket monotônico atribuído quando o gateway aceita a solicitação, antes que o cartão de aprovação apareça. Um timestamp sozinho é uma evidência fraca, porque valores de relógio iguais, mudanças no relógio e processos separados tornam a ordem ambígua.
A primeira solicitação aprovada deve ser executada primeiro?
A ordem de aprovação não deve alterar a ordem de admissão. Se a solicitação B receber aprovação antes da solicitação A, B deve permanecer retida até que A chegue a um resultado final ou até que o sistema documente explicitamente outro contrato de serialização.
O que acontece quando a primeira alteração na fila é rejeitada?
Uma solicitação anterior rejeitada ainda ocupa sua posição na fila. Registre a rejeição e permita que a próxima solicitação aprovada prossiga. Não apague silenciosamente a solicitação rejeitada do histórico.
Uma aprovação expirada pode bloquear todas as solicitações seguintes?
Um timeout precisa receber o mesmo tratamento de uma rejeição: uma decisão final vinculada a um ticket. A solicitação seguinte só pode prosseguir depois que o registro do timeout estiver persistido e visível no diário.
Todas as ações dos agentes precisam de uma única fila FIFO global?
Separe as filas por recurso, conforme o estado que pode entrar em conflito. Duas alterações no mesmo documento remoto precisam de serialização, enquanto chamadas independentes para serviços não relacionados podem usar lanes separadas se o diário preservar a ordem de admissão de cada uma.
Por que os registros do servidor podem discordar da fila de aprovação?
Um proxy ou servidor HTTP pode receber as solicitações em uma ordem diferente da do gateway quando as conexões competem entre si. O teste deve comparar os tickets do gateway com os registros de chegada no destino, em vez de tratar a chegada pela rede como prova de agendamento correto.
Qual é uma forma segura de testar alterações simultâneas?
Um endpoint de escrita deve criar um efeito observável e ordenado, como anexar um ticket e um payload a um arquivo local. Testar com solicitações GET esconde o problema, porque as leituras geralmente toleram reordenação.
A ordem do diário deve seguir a execução ou a conclusão?
O diário de atividade deve preservar a sequência causal de cada solicitação: aceita, decisão de aprovação, despachada e resultado final. O tempo de conclusão pode variar, mas não deve reescrever quando o gateway admitiu ou despachou a solicitação.
A identidade do processo é suficiente para auditar aprovações?
Não. Uma identidade de processo assinada informa quem fez o pedido, enquanto um ticket de fila informa a posição da solicitação entre chamadas concorrentes. Os dois dados são necessários para auditar uma decisão depois.
Quais casos um teste de regressão da fila de aprovação deve cobrir?
Execute repetidamente a corrida entre duas alterações, invertendo o momento das aprovações, testando rejeição, timeout, cancelamento e um primeiro destino deliberadamente lento. Qualquer divergência entre a ordem dos tickets, dos cartões, dos despachos e a causalidade do diário deve bloquear a liberação daquela lane.