fix(server): hata yönetimini iyileştir
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.
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user