From 9b495b7bf73dba9261206b5ac78a3784e50a78a0 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 31 Jan 2026 11:04:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(server):=20hata=20y=C3=B6netimini=20iyile?= =?UTF-8?q?=C5=9Ftir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zamanlayıcı ve qbit istemcisi bileşenlerinde hata işleme yeteneklerini güçlendirir. - loop.scheduler: qbit hatalarında sistem durumunu ve sağlık bilgisini güncelleme ekler. - qbit.client: geçici ağ hatalarını (EAI_AGAIN vb.) algılayarak oturum durumunu sıfırlar. - timer.worker: global hata yakalama ekleyerek işleyicinin çökmesini engeller ve hataları günlüğe kaydeder. --- apps/server/src/loop/loop.scheduler.ts | 16 ++- apps/server/src/qbit/qbit.client.ts | 12 +++ apps/server/src/timer/timer.worker.ts | 137 +++++++++++++------------ 3 files changed, 97 insertions(+), 68 deletions(-) diff --git a/apps/server/src/loop/loop.scheduler.ts b/apps/server/src/loop/loop.scheduler.ts index 0e2a62d..3bd7b5d 100644 --- a/apps/server/src/loop/loop.scheduler.ts +++ b/apps/server/src/loop/loop.scheduler.ts @@ -1,7 +1,7 @@ import { QbitClient } from "../qbit/qbit.client" import { tickLoopJobs } from "./loop.engine" -import { getStatusSnapshot, refreshJobsStatus, setTorrentsStatus } from "../status/status.service" -import { emitStatusUpdate } from "../realtime/emitter" +import { getStatusSnapshot, refreshJobsStatus, setQbitStatus, setTorrentsStatus } from "../status/status.service" +import { emitQbitHealth, emitStatusUpdate } from "../realtime/emitter" import { logger } from "../utils/logger" export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => { @@ -20,7 +20,19 @@ export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => { jobs, }); } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; logger.error({ error }, "Loop scheduler tick failed"); + setQbitStatus({ ok: false, lastError: message }); + emitQbitHealth({ ok: false, lastError: message }); + try { + const current = await getStatusSnapshot(); + emitStatusUpdate({ + ...current, + qbit: { ...current.qbit, ok: false, lastError: message }, + }); + } catch { + // Swallow secondary status errors to keep scheduler alive. + } } }, intervalMs); }; diff --git a/apps/server/src/qbit/qbit.client.ts b/apps/server/src/qbit/qbit.client.ts index ba397b4..47cccd6 100644 --- a/apps/server/src/qbit/qbit.client.ts +++ b/apps/server/src/qbit/qbit.client.ts @@ -48,6 +48,18 @@ export class QbitClient { } return await fn(); } catch (error) { + if (axios.isAxiosError(error)) { + const code = error.code ?? ""; + const transient = + code === "EAI_AGAIN" || + code === "ENOTFOUND" || + code === "ECONNREFUSED" || + code === "ECONNRESET" || + code === "ETIMEDOUT"; + if (transient) { + this.loggedIn = false; + } + } if ( axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403) diff --git a/apps/server/src/timer/timer.worker.ts b/apps/server/src/timer/timer.worker.ts index 4349aa2..7775a5b 100644 --- a/apps/server/src/timer/timer.worker.ts +++ b/apps/server/src/timer/timer.worker.ts @@ -4,6 +4,7 @@ import { readDb, writeDb } from "../storage/jsondb"; import { TimerLog, TimerSummary } from "../types"; import { emitTimerLog, emitTimerSummary } from "../realtime/emitter"; import { nowIso } from "../utils/time"; +import { logger } from "../utils/logger"; const MAX_LOGS = 2000; @@ -17,76 +18,80 @@ const normalizeTags = (tags?: string, category?: string) => { export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => { setInterval(async () => { - const db = await readDb(); - const rules = db.timerRules ?? []; - if (rules.length === 0) { - return; - } - const torrents = await qbit.getTorrentsInfo(); - let summary: TimerSummary = - db.timerSummary ?? { - totalDeleted: 0, - totalSeededSeconds: 0, - totalUploadedBytes: 0, - updatedAt: nowIso(), - }; - - const logs: TimerLog[] = []; - - for (const torrent of torrents) { - const tags = normalizeTags(torrent.tags, torrent.category); - const matchingRules = rules.filter((rule) => { - return rule.tags.some((tag) => tags.includes(tag.toLowerCase())); - }); - if (matchingRules.length === 0) { - continue; + try { + const db = await readDb(); + const rules = db.timerRules ?? []; + if (rules.length === 0) { + return; } - const matchingRule = matchingRules.reduce((best, current) => - current.seedLimitSeconds < best.seedLimitSeconds ? current : best - ); - const addedOnMs = Number(torrent.added_on ?? 0) * 1000; - const elapsedSeconds = - addedOnMs > 0 - ? Math.max(0, (Date.now() - addedOnMs) / 1000) - : Number(torrent.seeding_time ?? 0); - const seedingSeconds = Number(torrent.seeding_time ?? 0); - if (elapsedSeconds < matchingRule.seedLimitSeconds) { - continue; + const torrents = await qbit.getTorrentsInfo(); + let summary: TimerSummary = + db.timerSummary ?? { + totalDeleted: 0, + totalSeededSeconds: 0, + totalUploadedBytes: 0, + updatedAt: nowIso(), + }; + + const logs: TimerLog[] = []; + + for (const torrent of torrents) { + const tags = normalizeTags(torrent.tags, torrent.category); + const matchingRules = rules.filter((rule) => { + return rule.tags.some((tag) => tags.includes(tag.toLowerCase())); + }); + if (matchingRules.length === 0) { + continue; + } + const matchingRule = matchingRules.reduce((best, current) => + current.seedLimitSeconds < best.seedLimitSeconds ? current : best + ); + const addedOnMs = Number(torrent.added_on ?? 0) * 1000; + const elapsedSeconds = + addedOnMs > 0 + ? Math.max(0, (Date.now() - addedOnMs) / 1000) + : Number(torrent.seeding_time ?? 0); + const seedingSeconds = Number(torrent.seeding_time ?? 0); + if (elapsedSeconds < matchingRule.seedLimitSeconds) { + continue; + } + + try { + await qbit.deleteTorrent(torrent.hash, matchingRule.deleteFiles ?? true); + } catch (error) { + continue; + } + + const logEntry: TimerLog = { + id: randomUUID(), + hash: torrent.hash, + name: torrent.name, + sizeBytes: torrent.size, + tracker: torrent.tracker, + tags, + category: torrent.category, + seedingTimeSeconds: seedingSeconds, + uploadedBytes: torrent.uploaded ?? 0, + deletedAt: nowIso(), + }; + logs.push(logEntry); + summary = { + totalDeleted: summary.totalDeleted + 1, + totalSeededSeconds: summary.totalSeededSeconds + seedingSeconds, + totalUploadedBytes: summary.totalUploadedBytes + (torrent.uploaded ?? 0), + updatedAt: nowIso(), + }; + emitTimerLog(logEntry); + emitTimerSummary(summary); } - try { - await qbit.deleteTorrent(torrent.hash, matchingRule.deleteFiles ?? true); - } catch (error) { - continue; + if (logs.length > 0) { + db.timerLogs = [...(db.timerLogs ?? []), ...logs].slice(-MAX_LOGS); + db.timerSummary = summary; + await writeDb(db); } - - const logEntry: TimerLog = { - id: randomUUID(), - hash: torrent.hash, - name: torrent.name, - sizeBytes: torrent.size, - tracker: torrent.tracker, - tags, - category: torrent.category, - seedingTimeSeconds: seedingSeconds, - uploadedBytes: torrent.uploaded ?? 0, - deletedAt: nowIso(), - }; - logs.push(logEntry); - summary = { - totalDeleted: summary.totalDeleted + 1, - totalSeededSeconds: summary.totalSeededSeconds + seedingSeconds, - totalUploadedBytes: summary.totalUploadedBytes + (torrent.uploaded ?? 0), - updatedAt: nowIso(), - }; - emitTimerLog(logEntry); - emitTimerSummary(summary); - } - - if (logs.length > 0) { - db.timerLogs = [...(db.timerLogs ?? []), ...logs].slice(-MAX_LOGS); - db.timerSummary = summary; - await writeDb(db); + } catch (error) { + logger.error({ error }, "Timer worker tick failed"); } }, intervalMs); };