perf: worker N+1 batching, status runtime DB persistence, 'use strict'
This commit is contained in:
parent
81ae41325a
commit
a2266635f4
|
|
@ -1,21 +1,52 @@
|
|||
'use strict';
|
||||
|
||||
const MAX_RECENT_ERRORS = 10;
|
||||
|
||||
// Keys written to the settings table for cross-restart persistence
|
||||
const DB_KEY = {
|
||||
workerLastRun: '_worker_last_run_at',
|
||||
workerNextRun: '_worker_next_run_at',
|
||||
workerError: '_worker_last_error',
|
||||
workerStarted: '_worker_started_at',
|
||||
};
|
||||
|
||||
const state = {
|
||||
worker: {
|
||||
enabled: false,
|
||||
running: false,
|
||||
started_at: null,
|
||||
enabled: false,
|
||||
running: false,
|
||||
started_at: null,
|
||||
last_run_at: null,
|
||||
next_run_at: null,
|
||||
last_error: null,
|
||||
last_error: null,
|
||||
},
|
||||
notifications: {
|
||||
last_test_at: null,
|
||||
last_error: null,
|
||||
last_error: null,
|
||||
},
|
||||
recentErrors: [],
|
||||
};
|
||||
|
||||
// Seed from DB on first load so status survives container restarts.
|
||||
// Wrapped in try/catch — DB may not be ready at require-time in tests.
|
||||
function seedFromDb() {
|
||||
try {
|
||||
const { getSetting } = require('../db/database');
|
||||
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
|
||||
state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null;
|
||||
state.worker.last_error = getSetting(DB_KEY.workerError) || null;
|
||||
state.worker.started_at = getSetting(DB_KEY.workerStarted) || null;
|
||||
if (state.worker.last_run_at) state.worker.enabled = true;
|
||||
} catch { /* DB not ready — will be populated when workers run */ }
|
||||
}
|
||||
seedFromDb();
|
||||
|
||||
function dbSet(key, value) {
|
||||
try {
|
||||
const { setSetting } = require('../db/database');
|
||||
setSetting(key, value == null ? '' : String(value));
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
function toMessage(error) {
|
||||
if (!error) return null;
|
||||
if (typeof error === 'string') return error;
|
||||
|
|
@ -25,39 +56,46 @@ function toMessage(error) {
|
|||
function recordError(source, error) {
|
||||
const message = toMessage(error);
|
||||
if (!message) return;
|
||||
|
||||
state.recentErrors.unshift({
|
||||
timestamp: new Date().toISOString(),
|
||||
source,
|
||||
message,
|
||||
});
|
||||
|
||||
state.recentErrors.unshift({ timestamp: new Date().toISOString(), source, message });
|
||||
state.recentErrors = state.recentErrors.slice(0, MAX_RECENT_ERRORS);
|
||||
}
|
||||
|
||||
function markWorkerStarted(nextRunAt = null) {
|
||||
state.worker.enabled = true;
|
||||
state.worker.running = true;
|
||||
state.worker.started_at = state.worker.started_at || new Date().toISOString();
|
||||
const now = new Date().toISOString();
|
||||
state.worker.enabled = true;
|
||||
state.worker.running = true;
|
||||
state.worker.started_at = state.worker.started_at || now;
|
||||
state.worker.next_run_at = nextRunAt;
|
||||
dbSet(DB_KEY.workerStarted, state.worker.started_at);
|
||||
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
||||
}
|
||||
|
||||
function markWorkerSuccess(nextRunAt = null) {
|
||||
state.worker.last_run_at = new Date().toISOString();
|
||||
state.worker.last_error = null;
|
||||
const now = new Date().toISOString();
|
||||
state.worker.running = false;
|
||||
state.worker.last_run_at = now;
|
||||
state.worker.last_error = null;
|
||||
if (nextRunAt !== undefined) state.worker.next_run_at = nextRunAt;
|
||||
dbSet(DB_KEY.workerLastRun, now);
|
||||
dbSet(DB_KEY.workerError, '');
|
||||
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
||||
}
|
||||
|
||||
function markWorkerError(error, nextRunAt = null) {
|
||||
state.worker.last_run_at = new Date().toISOString();
|
||||
state.worker.last_error = toMessage(error);
|
||||
const now = new Date().toISOString();
|
||||
state.worker.running = false;
|
||||
state.worker.last_run_at = now;
|
||||
state.worker.last_error = toMessage(error);
|
||||
if (nextRunAt !== undefined) state.worker.next_run_at = nextRunAt;
|
||||
dbSet(DB_KEY.workerLastRun, now);
|
||||
dbSet(DB_KEY.workerError, state.worker.last_error || '');
|
||||
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
||||
recordError('Daily Worker', error);
|
||||
}
|
||||
|
||||
function markNotificationTestSuccess() {
|
||||
state.notifications.last_test_at = new Date().toISOString();
|
||||
state.notifications.last_error = null;
|
||||
state.notifications.last_error = null;
|
||||
}
|
||||
|
||||
function markNotificationError(error) {
|
||||
|
|
@ -71,7 +109,7 @@ function markNotificationSuccess() {
|
|||
|
||||
function getStatusRuntime() {
|
||||
return {
|
||||
worker: { ...state.worker },
|
||||
worker: { ...state.worker },
|
||||
notifications: { ...state.notifications },
|
||||
recentErrors: [...state.recentErrors],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const cron = require('node-cron');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb } = require('../db/database');
|
||||
const { buildTrackerRow, getCycleRange } = require('../services/statusService');
|
||||
const { pruneExpiredSessions } = require('../services/authService');
|
||||
const { runNotifications, runDriftNotifications } = require('../services/notificationService');
|
||||
|
|
@ -26,44 +28,77 @@ async function runDailyTasks() {
|
|||
const month = now.getMonth() + 1;
|
||||
const todayStr = now.toISOString().slice(0, 10);
|
||||
|
||||
const bills = db.prepare('SELECT * FROM bills WHERE active = 1').all();
|
||||
const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all();
|
||||
|
||||
for (const bill of bills) {
|
||||
// Use the bill's own cycle range so quarterly/annual bills look at the
|
||||
// correct payment window — not just the calendar month.
|
||||
const range = getCycleRange(year, month, bill);
|
||||
if (bills.length > 0) {
|
||||
// Batch-fetch payments for all active bills from the last 90 days to avoid
|
||||
// one query per bill. Different billing cycles (monthly/quarterly/annual) all
|
||||
// fit inside a 90-day window for the current month's due-date checks.
|
||||
const billIds = bills.map(b => b.id);
|
||||
const placeholders = billIds.map(() => '?').join(',');
|
||||
const windowStart = new Date(Date.now() - 90 * 86400000).toISOString().slice(0, 10);
|
||||
|
||||
// Bill does not apply this month (quarterly/annual in a non-due month).
|
||||
if (!range) continue;
|
||||
let allPayments = [];
|
||||
try {
|
||||
allPayments = db.prepare(`
|
||||
SELECT * FROM payments
|
||||
WHERE bill_id IN (${placeholders})
|
||||
AND paid_date >= ?
|
||||
AND deleted_at IS NULL
|
||||
`).all(...billIds, windowStart);
|
||||
} catch (err) {
|
||||
console.error('[worker] Failed to batch-fetch payments:', err.message);
|
||||
}
|
||||
|
||||
const payments = db.prepare(
|
||||
'SELECT * FROM payments WHERE bill_id = ? AND paid_date BETWEEN ? AND ? AND deleted_at IS NULL'
|
||||
).all(bill.id, range.start, range.end);
|
||||
// Group payments by bill_id in memory
|
||||
const paymentsByBill = new Map();
|
||||
for (const p of allPayments) {
|
||||
if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []);
|
||||
paymentsByBill.get(p.bill_id).push(p);
|
||||
}
|
||||
|
||||
const row = buildTrackerRow(bill, payments, year, month, todayStr);
|
||||
// Prepare autopay update statement once outside the loop
|
||||
const markAutopay = db.prepare(
|
||||
"UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?"
|
||||
);
|
||||
|
||||
// row is null when the bill's cycle produces no due date this month.
|
||||
// Guard defensively in case getCycleRange and resolveDueDate ever disagree.
|
||||
if (!row) continue;
|
||||
for (const bill of bills) {
|
||||
const range = getCycleRange(year, month, bill);
|
||||
if (!range) continue; // bill does not apply this cycle
|
||||
|
||||
// Auto-mark autopay bills as assumed_paid on due date
|
||||
if (
|
||||
bill.autopay_enabled &&
|
||||
bill.autodraft_status === 'pending' &&
|
||||
todayStr >= row.due_date
|
||||
) {
|
||||
db.prepare("UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?")
|
||||
.run(bill.id);
|
||||
// Filter pre-fetched payments to the bill's cycle range
|
||||
const payments = (paymentsByBill.get(bill.id) || []).filter(
|
||||
p => p.paid_date >= range.start && p.paid_date <= range.end
|
||||
);
|
||||
|
||||
const row = buildTrackerRow(bill, payments, year, month, todayStr);
|
||||
if (!row) continue;
|
||||
|
||||
// Auto-mark autopay bills as assumed_paid on due date
|
||||
if (
|
||||
bill.autopay_enabled &&
|
||||
bill.autodraft_status === 'pending' &&
|
||||
todayStr >= row.due_date
|
||||
) {
|
||||
try {
|
||||
markAutopay.run(bill.id);
|
||||
} catch (err) {
|
||||
console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pruneExpiredSessions();
|
||||
await runNotifications();
|
||||
|
||||
await runNotifications().catch(err => {
|
||||
console.error('[worker] Notification error (non-fatal):', err.message);
|
||||
});
|
||||
|
||||
await runDriftNotifications().catch(err => {
|
||||
console.error('[worker] Drift notification error (non-fatal):', err.message);
|
||||
});
|
||||
|
||||
// Run scheduled cleanup tasks (expired import sessions, stale temp files, etc.)
|
||||
await runAllCleanup().catch(err => {
|
||||
console.error('[worker] Cleanup error (non-fatal):', err.message);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue