146 lines
4.5 KiB
JavaScript
146 lines
4.5 KiB
JavaScript
const Database = require('better-sqlite3');
|
|
const path = require('path');
|
|
const log = require('./logger');
|
|
|
|
const DB_PATH = path.join('/mnt/webdata/storage', 'jobs.db');
|
|
const db = new Database(DB_PATH);
|
|
|
|
// WAL mode for better concurrency
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('busy_timeout = 5000');
|
|
|
|
// Create jobs table
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id TEXT PRIMARY KEY,
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
result TEXT,
|
|
progress INTEGER DEFAULT 0,
|
|
error TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
started_at INTEGER,
|
|
finished_at INTEGER,
|
|
user_id TEXT
|
|
)
|
|
`);
|
|
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)`);
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(type)`);
|
|
|
|
// Migrate pre-existing DBs: add user_id column if missing.
|
|
try { db.exec(`ALTER TABLE jobs ADD COLUMN user_id TEXT`); } catch {}
|
|
// Reap jobs stuck in 'processing' after a crash/restart (never reaped by cleanup otherwise).
|
|
try {
|
|
const t = Date.now();
|
|
db.prepare(`UPDATE jobs SET status = 'error', error = 'Прервано перезапуском сервиса', finished_at = ?, updated_at = ? WHERE status = 'processing'`).run(t, t);
|
|
} catch {}
|
|
|
|
// Prepared statements
|
|
const stmts = {
|
|
insert: db.prepare(`INSERT INTO jobs (id, type, status, payload, progress, created_at, updated_at, user_id) VALUES (?, ?, 'pending', ?, 0, ?, ?, ?)`),
|
|
get: db.prepare(`SELECT * FROM jobs WHERE id = ?`),
|
|
updateStatus: db.prepare(`UPDATE jobs SET status = ?, updated_at = ? WHERE id = ?`),
|
|
updateProgress: db.prepare(`UPDATE jobs SET progress = ?, updated_at = ? WHERE id = ?`),
|
|
finish: db.prepare(`UPDATE jobs SET status = 'done', progress = 100, result = ?, finished_at = ?, updated_at = ? WHERE id = ?`),
|
|
fail: db.prepare(`UPDATE jobs SET status = 'error', error = ?, finished_at = ?, updated_at = ? WHERE id = ?`),
|
|
pending: db.prepare(`SELECT * FROM jobs WHERE type = ? AND status = 'pending' ORDER BY created_at LIMIT ?`),
|
|
cleanup: db.prepare(`DELETE FROM jobs WHERE finished_at < ? AND status IN ('done', 'error')`),
|
|
stats: db.prepare(`SELECT status, COUNT(*) as count FROM jobs GROUP BY status`),
|
|
activeCount: db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'processing'`),
|
|
queueDepth: db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'pending'`),
|
|
};
|
|
|
|
function generateId(prefix) {
|
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function addJob(type, payload, userId) {
|
|
const id = generateId(type);
|
|
const now = Date.now();
|
|
stmts.insert.run(id, type, JSON.stringify(payload), now, now, userId != null ? String(userId) : null);
|
|
log.info(`Job added: ${id}`, { type });
|
|
return id;
|
|
}
|
|
|
|
function getJob(id) {
|
|
const row = stmts.get.get(id);
|
|
if (!row) return null;
|
|
row.payload = JSON.parse(row.payload);
|
|
if (row.result) row.result = JSON.parse(row.result);
|
|
return row;
|
|
}
|
|
|
|
function startJob(id) {
|
|
const now = Date.now();
|
|
db.prepare(`UPDATE jobs SET status = 'processing', started_at = ?, updated_at = ? WHERE id = ?`).run(now, now, id);
|
|
}
|
|
|
|
function updateProgress(id, progress) {
|
|
stmts.updateProgress.run(progress, Date.now(), id);
|
|
}
|
|
|
|
function finishJob(id, result) {
|
|
const now = Date.now();
|
|
stmts.finish.run(JSON.stringify(result), now, now, id);
|
|
log.info(`Job done: ${id}`);
|
|
}
|
|
|
|
function failJob(id, error) {
|
|
const now = Date.now();
|
|
stmts.fail.run(String(error).slice(0, 500), now, now, id);
|
|
log.error(`Job failed: ${id}`, { error: String(error).slice(0, 200) });
|
|
}
|
|
|
|
function getPending(type, limit = 5) {
|
|
return stmts.pending.all(type, limit).map(row => {
|
|
row.payload = JSON.parse(row.payload);
|
|
return row;
|
|
});
|
|
}
|
|
|
|
function getStats() {
|
|
const rows = stmts.stats.all();
|
|
const stats = {};
|
|
for (const r of rows) stats[r.status] = r.count;
|
|
return stats;
|
|
}
|
|
|
|
function getActiveCount() {
|
|
return stmts.activeCount.get().count;
|
|
}
|
|
|
|
function getQueueDepth() {
|
|
return stmts.queueDepth.get().count;
|
|
}
|
|
|
|
// Cleanup old finished jobs (older than 1 hour)
|
|
function cleanup() {
|
|
const cutoff = Date.now() - 3600000;
|
|
const result = stmts.cleanup.run(cutoff);
|
|
if (result.changes > 0) log.info(`Cleaned up ${result.changes} old jobs`);
|
|
}
|
|
|
|
// Run cleanup every 10 min
|
|
setInterval(cleanup, 600000);
|
|
|
|
// Graceful close
|
|
process.on('exit', () => { try { db.close(); } catch {} });
|
|
|
|
module.exports = {
|
|
addJob,
|
|
getJob,
|
|
startJob,
|
|
updateProgress,
|
|
finishJob,
|
|
failJob,
|
|
getPending,
|
|
getStats,
|
|
getActiveCount,
|
|
getQueueDepth,
|
|
cleanup,
|
|
db,
|
|
};
|