A solução é fazer o prazo atravessar a cadeia como orçamento: cada salto recebe quantos milissegundos ainda restam e desconta o que já gastou. Mandar um horário absoluto parece mais simples, mas exige relógios sincronizados entre máquinas. Se o relógio de B estiver atrasado em três segundos, ele aceita como válido um pedido que já venceu. Com o orçamento restante, cada medição é local ao processo, e o erro de latência de rede é pequeno o bastante para a margem absorver.
import http from 'node:http';
const LIMITE_CONCORRENCIA = 4; // quantas requisicoes executam ao mesmo tempo
const TAMANHO_MAXIMO_FILA = 20; // acima disso, rejeitar cedo com 503
const CABECALHO_PRAZO = 'x-prazo-restante-ms';
const fila = []; // itens aguardando execucao, cada um com prazo absoluto local
let emExecucao = 0;
function admitir(prazo, trabalho) {
return new Promise((resolver, rejeitar) => {
if (fila.length >= TAMANHO_MAXIMO_FILA) {
rejeitar(Object.assign(new Error('fila cheia'), { status: 503 }));
return;
}
fila.push({ prazo, trabalho, resolver, rejeitar });
despachar();
});
}
function despachar() {
while (emExecucao < LIMITE_CONCORRENCIA && fila.length > 0) {
const item = fila.shift();
if (Date.now() >= item.prazo) {
// o chamador ja desistiu: executar agora seria trabalho descartado
item.rejeitar(Object.assign(new Error('prazo expirado na fila'), { status: 504 }));
continue;
}
emExecucao++;
item
.trabalho()
.then(item.resolver, item.rejeitar)
.finally(() => {
emExecucao--;
despachar();
});
}
}
const processar = (pedido) =>
new Promise((resolver) => setTimeout(() => resolver({ id: pedido.id, ok: true }), 300));
http
.createServer(async (req, res) => {
const prazo = Date.now() + Number(req.headers[CABECALHO_PRAZO] ?? 1000);
let corpo = '';
for await (const trecho of req) corpo += trecho;
try {
const resultado = await admitir(prazo, () => processar(JSON.parse(corpo || '{}')));
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(resultado));
} catch (erro) {
res.writeHead(erro.status ?? 500).end(erro.message);
}
})
.listen(3001);
- Concorrência máxima por instância, dimensionada pela capacidade real das dependências, como o pool do banco
- Tamanho máximo da fila, com rejeição imediata quando ela está cheia
- Checagem do prazo na saída da fila, antes de qualquer trabalho começar
- Status 503 para fila cheia e 504 para prazo vencido, para que o chamador saiba o motivo
B aceita o pedido, mas só o põe em execução se o prazo ainda valer no momento de sair da fila. Um item que chega à frente depois de vencido recebe erro sem consumir nenhum recurso de execução.