From 821d9ac66cd754a0f5a53c26162d255871dd1420 Mon Sep 17 00:00:00 2001 From: ZeroDay Date: Tue, 7 Jul 2026 06:50:46 +0000 Subject: [PATCH] =?UTF-8?q?fix(scheduled-runner):=20=D1=80=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=B9=20=D1=82=D1=80=D0=B0=D0=BD=D0=B7=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BD=D1=8B=D1=85=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20(52?= =?UTF-8?q?2/5xx/429/=D1=81=D0=B5=D1=82=D1=8C)=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20=D0=BC=D0=B3=D0=BD=D0=BE=D0=B2=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20failed=20+=20send=5Fattempts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/db.js | 1 + src/services/scheduledPostsRunner.js | 39 +++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/config/db.js b/src/config/db.js index 36bc2bc..27a23bb 100644 --- a/src/config/db.js +++ b/src/config/db.js @@ -224,6 +224,7 @@ const migrate = async () => { // safe column alters (existing tables on prod may lack newer columns) await query(`ALTER TABLE categories ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true`); + await query(`ALTER TABLE scheduled_posts ADD COLUMN IF NOT EXISTS send_attempts INT NOT NULL DEFAULT 0`); console.log('[DB] Migrations applied'); }; diff --git a/src/services/scheduledPostsRunner.js b/src/services/scheduledPostsRunner.js index 1d4e16e..4bd173c 100644 --- a/src/services/scheduledPostsRunner.js +++ b/src/services/scheduledPostsRunner.js @@ -401,6 +401,21 @@ async function publishOne(scheduledPost) { // массового ретрая всё накопившееся улетело бы в канал пачкой. const SKIP_OLDER_THAN_H = 3; +// Ретрай транзиентных ошибок отправки (522/5xx/429/сетевые таймауты). +// Разовый сбой Telegram-прокси (CF Worker) не должен насмерть ронять слот. +const MAX_SEND_ATTEMPTS = 5; +const RETRY_BACKOFF_MIN = [2, 5, 10, 20, 30]; // минуты между попытками +function isTransientSendError(err) { + const status = err.response?.status; + if (status && status >= 500) return true; // 500..599 включая 522 + if (status === 429) return true; // rate limit + const code = String(err.code || ''); + if (['ECONNRESET','ETIMEDOUT','ECONNREFUSED','ECONNABORTED','EAI_AGAIN','EPIPE'].includes(code)) return true; + const m = String(err.message || '').toLowerCase(); + if (m.includes('timeout') || m.includes('socket hang up') || m.includes('network')) return true; + return false; +} + async function runScheduled() { // 1) Помечаем слишком старые pending как skipped (не спамим канал задним числом) const { rows: skipped } = await query( @@ -434,12 +449,24 @@ async function runScheduled() { console.log(`[scheduled-runner] sent id=${sp.id} channel=${sp.channel_id} article=${sp.article_id}`); } catch (err) { const msg = err.response?.data?.description || err.response?.data?.error?.error_msg || err.message; - await query( - `UPDATE scheduled_posts SET status='failed', error=$1 WHERE id=$2`, - [String(msg).slice(0, 1000), sp.id] - ); - results.push({ id: sp.id, ok: false, error: msg }); - console.error(`[scheduled-runner] failed id=${sp.id}: ${msg}`); + const attempts = (sp.send_attempts || 0) + 1; + if (isTransientSendError(err) && attempts < MAX_SEND_ATTEMPTS) { + const delayMin = RETRY_BACKOFF_MIN[Math.min(attempts - 1, RETRY_BACKOFF_MIN.length - 1)]; + const retryAt = new Date(Date.now() + delayMin * 60_000); + await query( + `UPDATE scheduled_posts SET status='pending', scheduled_at=$1, send_attempts=$2, error=$3 WHERE id=$4`, + [retryAt, attempts, `retry ${attempts}/${MAX_SEND_ATTEMPTS} через ${delayMin}м: ${String(msg).slice(0, 300)}`, sp.id] + ); + results.push({ id: sp.id, ok: false, retry: attempts, error: msg }); + console.warn(`[scheduled-runner] transient id=${sp.id}, retry ${attempts}/${MAX_SEND_ATTEMPTS} in ${delayMin}m: ${msg}`); + } else { + await query( + `UPDATE scheduled_posts SET status='failed', send_attempts=$1, error=$2 WHERE id=$3`, + [attempts, String(msg).slice(0, 1000), sp.id] + ); + results.push({ id: sp.id, ok: false, error: msg }); + console.error(`[scheduled-runner] failed id=${sp.id} (attempts=${attempts}): ${msg}`); + } } } return { processed: rows.length, skipped: skipped.length, results };