A assimetria é o coração do desenho. Descer de nível deve ser imediato, porque cada segundo em um nível que não funciona é uma conversa perdida. Subir de nível deve ser lento e verificado, porque um provedor que acabou de voltar costuma voltar frágil, e mandar cem por cento do tráfego para ele no primeiro sinal de vida é a maneira mais confiável de derrubá-lo de novo.
O modo de falha específico a evitar é o pingue-pongue: o sistema sobe porque uma requisição funcionou, cai porque as dez seguintes falharam, sobe de novo, e cada oscilação custa uma leva de conversas atendidas pela metade além de encher o canal de alertas. Três mecanismos combinados resolvem, e os três são necessários. Histerese, com limiares diferentes para descer e para subir, para que o ponto de retorno não seja o mesmo ponto de saída. Tempo mínimo de permanência no nível, tipicamente de um a cinco minutos, para que nenhuma decisão seja revertida antes de ter efeito observável. E sonda de recuperação, mandando uma fração pequena do tráfego para a capacidade suspeita e só promovendo o nível quando essa fração passa por uma janela inteira.
// degraded/machine.js
// Maquina de nivel de degradacao: desce rapido, sobe devagar.
// Descer e imediato; subir exige janela minima + sonda bem sucedida.
export const LEVELS = ['full', 'reduced', 'router', 'message'];
export function createDegradationMachine({
now, // () => number, injetado para ser testavel
minDwellMs = 60_000, // tempo minimo em um nivel antes de subir
probeRatio = 0.05, // fracao do trafego que sonda o nivel acima
probeSuccessesToRecover = 20,
failuresToDegrade = 5,
} = {}) {
if (typeof now !== 'function') throw new Error('now precisa ser uma funcao');
let index = 0; // 0 = full
let enteredAt = now();
let failures = 0;
let probeSuccesses = 0;
const level = () => LEVELS[index];
return {
level,
/** Falha observada no nivel atual: desce assim que houver evidencia. */
recordFailure() {
failures += 1;
probeSuccesses = 0;
if (failures >= failuresToDegrade && index < LEVELS.length - 1) {
index += 1;
enteredAt = now();
failures = 0;
return { changed: true, level: level(), direction: 'down' };
}
return { changed: false, level: level() };
},
/**
* Sucesso observado. So conta para recuperacao se veio de uma sonda,
* senao o sucesso do proprio nivel degradado promoveria o sistema.
*/
recordSuccess({ fromProbe = false } = {}) {
failures = 0;
if (!fromProbe || index === 0) return { changed: false, level: level() };
// Tempo minimo de permanencia: evita reverter antes de haver efeito.
if (now() - enteredAt < minDwellMs) return { changed: false, level: level() };
probeSuccesses += 1;
if (probeSuccesses >= probeSuccessesToRecover) {
index -= 1; // um nivel por vez, nunca do fundo ao topo
enteredAt = now();
probeSuccesses = 0;
return { changed: true, level: level(), direction: 'up' };
}
return { changed: false, level: level() };
},
/** Decide se esta requisicao testa o nivel acima. */
shouldProbe(sample) {
if (index === 0) return false;
if (now() - enteredAt < minDwellMs) return false;
return sample < probeRatio; // sample em [0, 1), do chamador
},
snapshot() {
return { level: level(), index, enteredAt, failures, probeSuccesses };
},
};
}
Duas decisões nesse código merecem destaque porque são exatamente onde as implementações caseiras erram. O sucesso só conta para recuperação quando vem de uma sonda: sem essa distinção, o bot no nível roteador acumula sucessos das próprias respostas prontas e se promove sozinho para um nível que continua quebrado. E a subida é de um nível por vez: pular do recado direto para o pleno é o caminho mais curto para reabrir o disjuntor em segundos.