- Versão Básica - Mecanismo de Retry com Atraso Exponencial
interface ParametrosRetry {
tentativasMaximas?: number;
atrasoBase?: number;
timeoutRequisicao?: number;
condicaoRetry?: (erro: any) => boolean;
}
class GerenciadorRequisicaoRetry {
private parametros: Required<ParametrosRetry>;
constructor(parametros: ParametrosRetry = {}) {
this.parametros = {
tentativasMaximas: parametros.tentativasMaximas ?? 3,
atrasoBase: parametros.atrasoBase ?? 1000,
timeoutRequisicao: parametros.timeoutRequisicao ?? 10000,
condicaoRetry: parametros.condicaoRetry ?? this.condicaoRetryPadrao
};
}
private condicaoRetryPadrao(erro: any): boolean {
return !erro.response ||
erro.code === 'ECONNABORTED' ||
erro.response.status >= 500;
}
private esperar(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private calcularAtraso(tentativaAtual: number): number {
const atrasoExponencial = this.parametros.atrasoBase * Math.pow(2, tentativaAtual);
const variacao = Math.random() * 1000;
return atrasoExponencial + variacao;
}
async executar<T>(
funcaoRequisicao: () => Promise<T>,
parametrosCustomizados?: Partial<ParametrosRetry>
): Promise<T> {
const config = { ...this.parametros, ...parametrosCustomizados };
let ultimoErro: any;
for (let tentativa = 0; tentativa <= config.tentativasMaximas; tentativa++) {
try {
const promessaTimeout = new Promise<never>((_, rejeitar) => {
setTimeout(() => rejeitar(new Error('Timeout na requisição')), config.timeoutRequisicao);
});
const resultado = await Promise.race([funcaoRequisicao(), promessaTimeout]);
return resultado;
} catch (erro: any) {
ultimoErro = erro;
const deveRetentar = tentativa < config.tentativasMaximas &&
config.condicaoRetry(erro);
if (deveRetentar) {
const atraso = this.calcularAtraso(tentativa);
console.warn(`Falha na requisição, retentando em ${atraso}ms (${tentativa + 1}/${config.tentativasMaximas}):`, erro.message);
await this.esperar(atraso);
continue;
}
break;
}
}
throw ultimoErro;
}
}
- Versão Avançada - Estratégias Variadas e Monitoramento de Eventos
enum EstrategiaRetry {
EXPONENCIAL = 'exponencial',
FIXO = 'fixo',
LINEAR = 'linear'
}
interface ParametrosRetryAvancado {
tentativasMaximas?: number;
atrasoBase?: number;
timeoutRequisicao?: number;
estrategia?: EstrategiaRetry;
condicaoRetry?: (erro: any) => boolean;
aoRetentar?: (tentativa: number, erro: any, atraso: number) => void;
aoSucesso?: (resultado: any, tentativa: number) => void;
aoFalha?: (erro: any, tentativa: number) => void;
}
class GerenciadorRequisicaoAvancado {
private parametros: Required<ParametrosRetryAvancado>;
constructor(parametros: ParametrosRetryAvancado = {}) {
this.parametros = {
tentativasMaximas: parametros.tentativasMaximas ?? 3,
atrasoBase: parametros.atrasoBase ?? 1000,
timeoutRequisicao: parametros.timeoutRequisicao ?? 10000,
estrategia: parametros.estrategia ?? EstrategiaRetry.EXPONENCIAL,
condicaoRetry: parametros.condicaoRetry ?? this.condicaoRetryPadrao,
aoRetentar: parametros.aoRetentar ?? (() => {}),
aoSucesso: parametros.aoSucesso ?? (() => {}),
aoFalha: parametros.aoFalha ?? (() => {})
};
}
private condicaoRetryPadrao(erro: any): boolean {
const errosRetentaveis = ['ECONNABORTED', 'ETIMEDOUT', 'ENOTFOUND'];
if (erro.code && errosRetentaveis.includes(erro.code)) {
return true;
}
if (erro.response) {
return erro.response.status >= 500 || erro.response.status === 429;
}
return erro.message?.includes('timeout') ||
erro.message?.includes('rede');
}
private calcularAtraso(tentativaAtual: number): number {
const base = this.parametros.atrasoBase;
switch (this.parametros.estrategia) {
case EstrategiaRetry.FIXO:
return base;
case EstrategiaRetry.LINEAR:
return base * (tentativaAtual + 1);
case EstrategiaRetry.EXPONENCIAL:
default:
const atrasoExponencial = base * Math.pow(2, tentativaAtual);
const variacao = Math.random() * base * 0.1;
return atrasoExponencial + variacao;
}
}
private esperar(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async executar<T>(
funcaoRequisicao: () => Promise<T>,
parametrosCustomizados?: Partial<ParametrosRetryAvancado>
): Promise<T> {
const config = { ...this.parametros, ...parametrosCustomizados };
let ultimoErro: any;
let contagemTentativas = 0;
for (; contagemTentativas <= config.tentativasMaximas; contagemTentativas++) {
try {
const controlador = new AbortController();
const idTimeout = setTimeout(() => controlador.abort(), config.timeoutRequisicao);
let resultado: T;
const resultadoRequisicao = funcaoRequisicao();
if (resultadoRequisicao && typeof (resultadoRequisicao as any).catch === 'function') {
resultado = await Promise.race([
resultadoRequisicao,
new Promise<never>((_, rejeitar) =>
setTimeout(() => rejeitar(new Error('Timeout na requisição')), config.timeoutRequisicao)
)
]);
} else {
resultado = await resultadoRequisicao;
}
clearTimeout(idTimeout);
if (contagemTentativas > 0) {
config.aoSucesso(resultado, contagemTentativas);
}
return resultado;
} catch (erro: any) {
clearTimeout(idTimeout);
ultimoErro = erro;
const deveRetentar = contagemTentativas < config.tentativasMaximas &&
config.condicaoRetry(erro);
if (deveRetentar) {
const atraso = this.calcularAtraso(contagemTentativas);
config.aoRetentar(contagemTentativas + 1, erro, atraso);
console.warn(`Falha na requisição, retentativa ${contagemTentativas + 1} em ${atraso}ms:`, erro.message);
await this.esperar(atraso);
continue;
}
break;
}
}
config.aoFalha(ultimoErro, contagemTentativas);
throw ultimoErro;
}
static comEstrategiaExponencial(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.EXPONENCIAL });
}
static comEstrategiaFixa(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.FIXO });
}
static comEstrategiaLinear(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.LINEAR });
}
}
- Integração com Axios
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
class AdaptadorAxiosRetry {
private instanciaAxios: AxiosInstance;
private gerenciadorRetry: GerenciadorRequisicaoAvancado;
constructor(instanciaAxios?: AxiosInstance, parametrosRetry?: ParametrosRetryAvancado) {
this.instanciaAxios = instanciaAxios || axios.create();
this.gerenciadorRetry = new GerenciadorRequisicaoAvancado(parametrosRetry);
this.configurarInterceptores();
}
private configurarInterceptores() {
this.instanciaAxios.interceptors.request.use(
(config) => {
if (config.retryConfig) {
const funcaoRequisicaoOriginal = () => this.instanciaAxios.request(config);
return this.gerenciadorRetry.executar(funcaoRequisicaoOriginal, config.retryConfig)
.then(resposta => {
throw new axios.Cancel('Requisição tratada pela lógica de retry');
})
.catch(erro => {
if (axios.isCancel(erro) && erro.message === 'Requisição tratada pela lógica de retry') {
return Promise.reject(erro);
}
throw erro;
});
}
return config;
},
(erro) => Promise.reject(erro)
);
}
requisicao<T = any>(config: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
if (config.retryConfig) {
const funcaoRequisicao = () => this.instanciaAxios.request(config);
return this.gerenciadorRetry.executar(funcaoRequisicao, config.retryConfig);
}
return this.instanciaAxios.request(config);
}
obter<T = any>(url: string, config?: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
return this.requisicao({ ...config, method: 'GET', url });
}
enviar<T = any>(url: string, dados?: any, config?: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
return this.requisicao({ ...config, method: 'POST', url, data: dados });
}
}
- Exemplos de Uso
// Instância com configuração personalizada
const gerenciadorRetry = new GerenciadorRequisicaoAvancado({
tentativasMaximas: 3,
atrasoBase: 1000,
timeoutRequisicao: 5000,
aoRetentar: (tentativa, erro, atraso) => {
console.log(`Retentativa ${tentativa}, causa: ${erro.message}`);
}
});
// Uso com Fetch API
async function buscarDadosComRetry() {
try {
const resposta = await gerenciadorRetry.executar(() =>
fetch('https://api.exemplo.com/dados').then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
);
console.log('Dados obtidos:', resposta);
} catch (erro) {
console.error('Todas as retentativas falharam:', erro);
}
}
// Integração com Axios
const adaptadorAxios = new AdaptadorAxiosRetry(axios, {
tentativasMaximas: 3,
estrategia: EstrategiaRetry.EXPONENCIAL
});
async function buscarComAxiosRetry() {
try {
const resposta = await adaptadorAxios.obter('https://api.exemplo.com/dados', {
retryConfig: {
tentativasMaximas: 2,
condicaoRetry: (erro) => {
return !erro.response || erro.response.status >= 500;
}
}
});
console.log('Resposta:', resposta.data);
} catch (erro) {
console.error('Requisição falhou:', erro);
}
}
// Instâncias com diferentes estratégias
const retryFixo = GerenciadorRequisicaoAvancado.comEstrategiaFixa({
tentativasMaximas: 5,
atrasoBase: 2000
});
const retryLinear = GerenciadorRequisicaoAvancado.comEstrategiaLinear({
tentativasMaximas: 3,
atrasoBase: 1000
});
- Versão com React Hook
import { useState, useCallback } from 'react';
export function usarRequisicaoRetry(config?: ParametrosRetryAvancado) {
const [carregando, setCarregando] = useState(false);
const [erro, setErro] = useState<any>(null);
const [dados, setDados] = useState<any>(null);
const [contagemTentativas, setContagemTentativas] = useState(0);
const gerenciadorRetry = new GerenciadorRequisicaoAvancado({
...config,
aoRetentar: (contagem, erro, atraso) => {
setContagemTentativas(contagem);
config?.aoRetentar?.(contagem, erro, atraso);
},
aoSucesso: (resultado, contagem) => {
setContagemTentativas(0);
config?.aoSucesso?.(resultado, contagem);
},
aoFalha: (erro, contagem) => {
setContagemTentativas(contagem);
config?.aoFalha?.(erro, contagem);
}
});
const executar = useCallback(async <T>(funcaoRequisicao: () => Promise<T>) => {
setCarregando(true);
setErro(null);
try {
const resultado = await gerenciadorRetry.executar(funcaoRequisicao);
setDados(resultado);
return resultado;
} catch (erroCapturado) {
setErro(erroCapturado);
throw erroCapturado;
} finally {
setCarregando(false);
}
}, [gerenciadorRetry]);
return {
executar,
carregando,
erro,
dados,
contagemTentativas,
resetar: () => {
setCarregando(false);
setErro(null);
setDados(null);
setContagemTentativas(0);
}
};
}
// Exemplo de uso em componente React
function ComponenteExemplo() {
const { executar, carregando, erro, dados, contagemTentativas } = usarRequisicaoRetry({
tentativasMaximas: 3
});
const buscarDados = async () => {
try {
await executar(() =>
fetch('/api/dados').then(res => res.json())
);
} catch (err) {
// Tratamento de erro
}
};
return (
<div>
<button onClick={buscarDados} disabled={carregando}>
{carregando ? `Carregando... (Tentativa ${contagemTentativas})` : 'Obter Dados'}
</button>
{erro && <div>Erro: {erro.message}</div>}
{dados && <div>Dados: {JSON.stringify(dados)}</div>}
</div>
);
}
Características Principais
- Múltiplas estratégias de retry: exponencial, fixo e linear
- Condições inteligentes: retentativa automática em erros de rede, timeout e status 5xx
- Controle de timeout: timeout individual por requisição
- Monitoramento de eventos: calbacks para retry, sucesso e falha
- Variação aleatória: evita efeito manada
- Suporte a TypeScript: definições completas de tipos
- Integração com frameworks: Axios, Fetch e hooks React