From 9883af5d02024e73c33fad86f88d5449dc4905e2 Mon Sep 17 00:00:00 2001 From: borja Date: Wed, 19 Nov 2025 15:37:04 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1ade=20auto-ensure=20con=20reinte?= =?UTF-8?q?ntos=20y=20backoff=20en=20webhook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- src/services/webhook-manager.ts | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/services/webhook-manager.ts b/src/services/webhook-manager.ts index 73f3086..994bd55 100644 --- a/src/services/webhook-manager.ts +++ b/src/services/webhook-manager.ts @@ -244,4 +244,96 @@ export class WebhookManager { return false; } } + + // ===== Auto-ensure loop con reintentos y backoff ===== + + private static autoEnsureTimer: any = null; + private static autoEnsureActive: boolean = false; + private static autoEnsureAttempts: number = 0; + + private static getRetryConfig() { + const n = (v: any, d: number) => { + const x = Number(v); + return Number.isFinite(x) && x >= 0 ? x : d; + }; + const initial = n(process.env.WEBHOOK_RETRY_INITIAL_DELAY_MS, 1000); + const max = Math.max(initial, n(process.env.WEBHOOK_RETRY_MAX_DELAY_MS, 60000)); + const degraded = Math.max(max, n(process.env.WEBHOOK_RETRY_DEGRADED_INTERVAL_MS, 300000)); + const jitter = String(process.env.WEBHOOK_RETRY_JITTER || 'true').toLowerCase(); + return { + initialDelayMs: initial, + maxDelayMs: max, + degradedIntervalMs: degraded, + jitter: ['true', '1', 'yes', 'on'].includes(jitter), + maxAttempts: Number.isFinite(Number(process.env.WEBHOOK_RETRY_MAX_ATTEMPTS)) + ? Math.max(0, Number(process.env.WEBHOOK_RETRY_MAX_ATTEMPTS)) + : Infinity + }; + } + + private static jitter(val: number, enabled: boolean): number { + if (!enabled) return val; + const factor = 0.5 + Math.random(); // 0.5x - 1.5x + return Math.max(1, Math.floor(val * factor)); + } + + private static nextDelayMs(attempt: number, cfg: ReturnType): number { + // attempt 0 -> initial, 1 -> 2x, etc. capped at max, luego degradado cada degradedIntervalMs + const pow = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(2, attempt)); + return this.jitter(pow, cfg.jitter); + } + + static startAutoEnsure(): void { + if (this.autoEnsureActive) { + return; + } + this.autoEnsureActive = true; + this.autoEnsureAttempts = 0; + + const cfg = this.getRetryConfig(); + + const attemptOnce = async () => { + // Validación de config: si falta algo, abortar sin reintentos + try { + this.validateConfig(); + } catch (e) { + console.error('❌ Webhook auto-ensure aborted due to invalid configuration:', e instanceof Error ? e.message : e); + this.stopAutoEnsure(); + return; + } + + try { + await this.registerWebhook(); + const ok = await this.verifyWebhook(); + if (ok) { + console.log('✅ Webhook is active'); + this.stopAutoEnsure(); + return; + } + throw new Error('verify returned false'); + } catch (err) { + if (!this.autoEnsureActive) return; // pudo ser parado durante espera + const a = this.autoEnsureAttempts++; + const delay = a < 10 ? this.nextDelayMs(a, cfg) : cfg.degradedIntervalMs; + const msg = err instanceof Error ? `${err.message}` : String(err); + console.warn(`⚠️ Webhook ensure attempt #${a + 1} failed: ${msg}. Retrying in ${delay}ms`); + this.autoEnsureTimer = setTimeout(() => { + // Evitar múltiples overlapped + if (!this.autoEnsureActive) return; + attemptOnce(); + }, delay); + } + }; + + // Disparar el primer intento sin esperar + attemptOnce(); + } + + static stopAutoEnsure(): void { + if (this.autoEnsureTimer) { + try { clearTimeout(this.autoEnsureTimer); } catch {} + this.autoEnsureTimer = null; + } + this.autoEnsureActive = false; + } }