A fonte pode ser o Parameter Store, o Consul, um objeto no S3 ou um arquivo montado por ConfigMap. Para o recarregador, tudo isso é uma função que devolve um corpo e um identificador de versão, como ETag ou número de revisão, para que o ciclo não reaplique a mesma configuração a cada intervalo. O ponto que não pode faltar é a validação completa antes de aplicar: tipo, faixa, valores permitidos e regras entre campos. Uma versão que passa por campo mas é incoerente como conjunto, como um timeout de 200 ms para um provedor que nunca responde abaixo de 500 ms, precisa ser rejeitada inteira.
// recarregador.js
const esquema = {
limiteSemRevisao: { tipo: 'number', min: 0, max: 1_000_000 },
timeoutGatewayMs: { tipo: 'number', min: 100, max: 30_000 },
poolMaxConexoes: { tipo: 'number', min: 1, max: 200 },
provedorPagamento: { tipo: 'string', valores: ['stripe', 'adyen', 'pagarme'] },
};
export function validarConfig(bruto) {
const saida = {};
for (const [chave, regra] of Object.entries(esquema)) {
const valor = bruto[chave];
if (typeof valor !== regra.tipo) throw new Error(`${chave}: esperado ${regra.tipo}`);
if (regra.min !== undefined && valor < regra.min) throw new Error(`${chave}: abaixo de ${regra.min}`);
if (regra.max !== undefined && valor > regra.max) throw new Error(`${chave}: acima de ${regra.max}`);
if (regra.valores && !regra.valores.includes(valor)) throw new Error(`${chave}: valor invalido`);
saida[chave] = valor;
}
// regras entre campos: uma config valida campo a campo ainda pode ser incoerente
if (saida.timeoutGatewayMs < 500 && saida.provedorPagamento === 'adyen') {
throw new Error('timeoutGatewayMs abaixo de 500 ms nao e compativel com adyen');
}
return saida;
}
// Fonte generica: qualquer coisa que devolva { etag, corpo } (SSM, Consul, S3, arquivo)
export function iniciarRecarregador({ fonte, config, intervaloMs = 15_000, metricas }) {
let etagAtual = null;
let parado = false;
const ciclo = async () => {
if (parado) return;
try {
const { etag, corpo } = await fonte.ler({ seDiferenteDe: etagAtual });
if (etag !== etagAtual) {
const aplicada = config.aplicar(corpo); // valida e so entao troca
etagAtual = etag;
metricas.gauge('config_version', aplicada.versao);
metricas.gauge('config_last_success_timestamp', Date.now() / 1000);
console.info('configuracao aplicada', { versao: aplicada.versao, etag });
}
} catch (erro) {
// versao invalida ou fonte fora do ar: a ultima boa conhecida continua valendo
metricas.counter('config_reload_failures_total').inc();
console.error('configuracao rejeitada, mantendo a anterior', { erro: erro.message });
} finally {
if (!parado) setTimeout(ciclo, intervaloMs).unref();
}
};
ciclo();
return () => {
parado = true;
};
}
Quando a validação falha, ou a fonte está fora do ar, o processo continua com a última versão boa conhecida e sobe um contador. É a diferença entre uma mudança errada que afeta ninguém e uma que derruba a frota. Em uma instância recém-iniciada o mesmo princípio vale com um complemento: o processo deve subir com um snapshot válido, vindo da fonte ou de uma cópia local gravada na última aplicação bem-sucedida, e só então começar a aceitar tráfego. Subir com valores padrão silenciosos porque a fonte demorou é a forma mais comum de ter uma instância diferente das outras sem ninguém perceber.
| Mecanismo de detecção | Latência até aplicar | Carga na fonte | Quando usar |
|---|
| Polling com ETag ou revisão | Até um intervalo (15 a 60 s) | Uma leitura condicional por instância por intervalo | Padrão: simples, previsível, funciona com qualquer fonte |
| Watch ou stream (etcd, Consul, SSE) | Segundos | Uma conexão aberta por instância | Quando a latência de minutos não serve, como kill switch |
| Sinal SIGHUP com arquivo local | Imediata, manual | Nenhuma | Processo único ou poucos hosts com orquestração própria |
| Push por fila ou webhook | Segundos | Mensagem por mudança | Quando já existe fila; exige polling de segurança para instâncias que perderam a mensagem |