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:
jake committed 2026-03-23 13:59:31 -04:00
1 parent d9be566863
commit b289b7abed
17 files changed
+235 -36

No files matched your search

+2
View File
@@ -6,6 +6,7 @@ import {
getStaleNonLibraryTitles,
getTitleByIdForCron,
getTitleIdsWithStaleSeasons,
getTitlesWithFreshRecommendations,
getTitlesWithStaleOffers,
getTitlesWithStaleOffersFetchedBefore,
insertCronRunReturning,
@@ -53,4 +54,5 @@ export {
getReturningTvShows,
getTitleByIdForCron,
getTitleIdsWithStaleSeasons,
getTitlesWithFreshRecommendations,
};
+26 -3
View File
@@ -63,6 +63,27 @@ import {
const log = createLogger("metadata");
async function mapWithConcurrency<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
concurrency: number,
): Promise<PromiseSettledResult<R>[]> {
const results: PromiseSettledResult<R>[] = Array.from({ length: items.length });
let i = 0;
async function worker() {
while (i < items.length) {
const idx = i++;
try {
results[idx] = { status: "fulfilled", value: await fn(items[idx]) };
} catch (reason) {
results[idx] = { status: "rejected", reason };
}
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
return results;
}
export function updateTitleWithArtInvalidation(
title: Pick<
typeof titles.$inferSelect,
@@ -340,10 +361,12 @@ export async function refreshTitle(titleId: string) {
}
export async function refreshTvChildren(titleId: string, tmdbId: number, numberOfSeasons: number) {
// Fetch all seasons from TMDB concurrently (~40 req/s rate limit)
// Fetch seasons with limited concurrency to stay within TMDB's 40 req/s limit
const seasonNumbers = Array.from({ length: numberOfSeasons }, (_, i) => i + 1);
const fetched = await Promise.allSettled(
seasonNumbers.map((sn) => getTvSeasonDetails(tmdbId, sn)),
const fetched = await mapWithConcurrency(
seasonNumbers,
(sn) => getTvSeasonDetails(tmdbId, sn),
5,
);
const now = new Date();