mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
refactor: optimize cron scheduling, DB maintenance, and TMDB reliability
- Skip recommendations refresh for titles fetched within the last 7 days via `getTitlesWithFreshRecommendations`; only stale titles are re-fetched - Replace `Promise.allSettled` in `refreshTvChildren` with a `mapWithConcurrency` helper (concurrency = 5) to stay within TMDB's rate limit - Add 30s request timeout middleware to TMDB client to prevent indefinitely hung fetches - Expand `optimizeDb` weekly job to prune 30-day-old cron runs, integration events, expired sessions, and expired verifications - Add `wal_checkpoint(TRUNCATE)` to `optimizeDatabase` so the WAL is flushed before `PRAGMA optimize` - Add `recoverStaleImportJobs` called on server startup to mark any `pending`/`running` import jobs as errored after a crash/restart - Return `false` from `triggerJob` immediately if the job is already running
This commit is contained in:
@@ -86,8 +86,9 @@ export const db = new Proxy({} as ReturnType<typeof drizzle>, {
|
||||
},
|
||||
});
|
||||
|
||||
/** Run PRAGMA optimize to refresh query planner statistics. */
|
||||
/** Checkpoint the WAL and refresh query planner statistics. */
|
||||
export function optimizeDatabase() {
|
||||
getClient().run("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
getClient().run("PRAGMA optimize");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { lt } from "drizzle-orm";
|
||||
|
||||
import { db } from "../client";
|
||||
import { session, verification } from "../schema";
|
||||
|
||||
export function deleteExpiredSessions(): number {
|
||||
const now = new Date();
|
||||
const count = db
|
||||
.select({ id: session.id })
|
||||
.from(session)
|
||||
.where(lt(session.expiresAt, now))
|
||||
.all().length;
|
||||
if (count === 0) return 0;
|
||||
db.delete(session).where(lt(session.expiresAt, now)).run();
|
||||
return count;
|
||||
}
|
||||
|
||||
export function deleteExpiredVerifications(): number {
|
||||
const now = new Date();
|
||||
const count = db
|
||||
.select({ id: verification.id })
|
||||
.from(verification)
|
||||
.where(lt(verification.expiresAt, now))
|
||||
.all().length;
|
||||
if (count === 0) return 0;
|
||||
db.delete(verification).where(lt(verification.expiresAt, now)).run();
|
||||
return count;
|
||||
}
|
||||
@@ -121,7 +121,11 @@ export function purgeShellTitlesTransaction(): PurgeResult {
|
||||
|
||||
// After title cascade deletes, collect orphaned person images then delete persons
|
||||
orphanedImages.push(...collectOrphanedPersonImages());
|
||||
return { deletedTitles: toDelete.length, deletedPersons: purgeOrphanedPersons(), orphanedImages };
|
||||
return {
|
||||
deletedTitles: toDelete.length,
|
||||
deletedPersons: purgeOrphanedPersons(),
|
||||
orphanedImages,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, eq, inArray, isNotNull, lt, or } from "drizzle-orm";
|
||||
import { and, eq, gte, inArray, isNotNull, lt, or } from "drizzle-orm";
|
||||
|
||||
import { db } from "../client";
|
||||
import {
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
cronRuns,
|
||||
seasons,
|
||||
titleCast,
|
||||
titleRecommendations,
|
||||
titles,
|
||||
userTitleStatus,
|
||||
} from "../schema";
|
||||
@@ -125,3 +126,42 @@ export function getTitleByIdForCron(titleId: string) {
|
||||
export function getCastEntryForTitle(titleId: string) {
|
||||
return db.select().from(titleCast).where(eq(titleCast.titleId, titleId)).limit(1).get();
|
||||
}
|
||||
|
||||
export function deleteOldCronRuns(beforeDate: Date): number {
|
||||
const old = db
|
||||
.select({ id: cronRuns.id })
|
||||
.from(cronRuns)
|
||||
.where(lt(cronRuns.startedAt, beforeDate))
|
||||
.all();
|
||||
if (old.length === 0) return 0;
|
||||
const ids = old.map((r) => r.id);
|
||||
for (let i = 0; i < ids.length; i += 500) {
|
||||
db.delete(cronRuns)
|
||||
.where(inArray(cronRuns.id, ids.slice(i, i + 500)))
|
||||
.run();
|
||||
}
|
||||
return old.length;
|
||||
}
|
||||
|
||||
export function getTitlesWithFreshRecommendations(
|
||||
titleIds: string[],
|
||||
sinceDate: Date,
|
||||
): Set<string> {
|
||||
if (titleIds.length === 0) return new Set();
|
||||
|
||||
return new Set(
|
||||
db
|
||||
.select({ titleId: titleRecommendations.titleId })
|
||||
.from(titleRecommendations)
|
||||
.where(
|
||||
and(
|
||||
inArray(titleRecommendations.titleId, titleIds),
|
||||
// All recs for a title share the same lastFetchedAt, so any row suffices
|
||||
gte(titleRecommendations.lastFetchedAt, sinceDate),
|
||||
),
|
||||
)
|
||||
.groupBy(titleRecommendations.titleId)
|
||||
.all()
|
||||
.map((r) => r.titleId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,3 +86,25 @@ export function getActiveImportJobForUser(userId: string) {
|
||||
.where(and(eq(importJobs.userId, userId), inArray(importJobs.status, ["pending", "running"])))
|
||||
.get();
|
||||
}
|
||||
|
||||
/** Mark any running/pending import jobs as errored — called on server startup to recover from crashes. */
|
||||
export function recoverStaleImportJobs(): number {
|
||||
const stale = db
|
||||
.select({ id: importJobs.id })
|
||||
.from(importJobs)
|
||||
.where(inArray(importJobs.status, ["pending", "running"]))
|
||||
.all();
|
||||
if (stale.length === 0) return 0;
|
||||
const now = new Date();
|
||||
for (const { id } of stale) {
|
||||
db.update(importJobs)
|
||||
.set({
|
||||
status: "error",
|
||||
finishedAt: now,
|
||||
errors: JSON.stringify(["Import interrupted by server restart"]),
|
||||
})
|
||||
.where(eq(importJobs.id, id))
|
||||
.run();
|
||||
}
|
||||
return stale.length;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, eq, gte } from "drizzle-orm";
|
||||
import { and, eq, gte, inArray, lt } from "drizzle-orm";
|
||||
|
||||
import { db } from "../client";
|
||||
import { integrationEvents, integrations, userEpisodeWatches, userMovieWatches } from "../schema";
|
||||
@@ -41,3 +41,19 @@ export function updateIntegrationLastEvent(connectionId: string): void {
|
||||
.where(eq(integrations.id, connectionId))
|
||||
.run();
|
||||
}
|
||||
|
||||
export function deleteOldIntegrationEvents(beforeDate: Date): number {
|
||||
const old = db
|
||||
.select({ id: integrationEvents.id })
|
||||
.from(integrationEvents)
|
||||
.where(lt(integrationEvents.receivedAt, beforeDate))
|
||||
.all();
|
||||
if (old.length === 0) return 0;
|
||||
const ids = old.map((r) => r.id);
|
||||
for (let i = 0; i < ids.length; i += 500) {
|
||||
db.delete(integrationEvents)
|
||||
.where(inArray(integrationEvents.id, ids.slice(i, i + 500)))
|
||||
.run();
|
||||
}
|
||||
return old.length;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user