fix(update-checker): cached through updates (#4046)

This commit is contained in:
Meier Lukas
2025-09-12 18:50:41 +02:00
committed by GitHub
parent f7471b6c63
commit 26eb2c08c9
13 changed files with 76 additions and 53 deletions

View File

@@ -21,6 +21,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
@@ -32,6 +33,7 @@
"@homarr/log": "workspace:^",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",

View File

@@ -26,7 +26,6 @@ export class JobManager implements IJobManager {
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be updated as it is set to "never"`);
if (!validateCron(cron)) {
throw new Error(`Invalid cron expression: ${cron}`);
}
@@ -45,7 +44,6 @@ export class JobManager implements IJobManager {
logger.info(`Disabling cron job name="${name}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be disabled as it is set to "never"`);
await this.updateConfigurationAsync(name, { isEnabled: false });
await this.jobGroup.stopAsync(name);

View File

@@ -13,6 +13,7 @@ import { db } from "@homarr/db";
import { logger } from "@homarr/log";
import { JobManager } from "./job-manager";
import { onStartAsync } from "./on-start";
const server = fastify({
maxParamLength: 5000,
@@ -32,6 +33,7 @@ server.register(fastifyTRPCPlugin, {
});
void (async () => {
await onStartAsync();
await jobGroup.initializeAsync();
await jobGroup.startAllAsync();

View File

@@ -0,0 +1,7 @@
import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache";
import { cleanupSessionsAsync } from "./session-cleanup";
export async function onStartAsync() {
await cleanupSessionsAsync();
await invalidateUpdateCheckerCacheAsync();
}

View File

@@ -0,0 +1,18 @@
import { logger } from "@homarr/log";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" });
/**
* Invalidates the update checker cache on startup to ensure fresh data.
* It is important as we want to avoid showing pending updates after the update to latest version.
*/
export async function invalidateUpdateCheckerCacheAsync() {
try {
const handler = updateCheckerRequestHandler.handler({});
await handler.invalidateAsync();
localLogger.debug("Update checker cache invalidated");
} catch (error) {
localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
}
}

View File

@@ -0,0 +1,39 @@
import { env } from "@homarr/auth/env";
import { db, eq, inArray } from "@homarr/db";
import { sessions, users } from "@homarr/db/schema";
import { supportedAuthProviders } from "@homarr/definitions";
import { logger } from "@homarr/log";
const localLogger = logger.child({ module: "sessionCleanup" });
/**
* Deletes sessions for users that have inactive auth providers.
* Sessions from other providers are deleted so they can no longer be used.
*/
export async function cleanupSessionsAsync() {
try {
const currentAuthProviders = env.AUTH_PROVIDERS;
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
const subQuery = db
.select({ id: users.id })
.from(users)
.where(inArray(users.provider, inactiveAuthProviders))
.as("sq");
const sessionsWithInactiveProviders = await db
.select({ userId: sessions.userId })
.from(sessions)
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
await db.delete(sessions).where(inArray(sessions.userId, userIds));
if (sessionsWithInactiveProviders.length > 0) {
localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
} else {
localLogger.debug("No sessions to delete");
}
} catch (error) {
localLogger.error(new Error("Failed to clean up sessions", { cause: error }));
}
}

View File

@@ -72,8 +72,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
});
if (defaultCronExpression === "never") return null;
const scheduledTask = createTask(
configuration?.cronExpression ?? defaultCronExpression,
() => void catchingCallbackAsync(),
@@ -120,7 +118,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
options: CreateCronJobOptions = { runOnStart: false },
) => {
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) {
if (!validate(defaultCronExpression)) {
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
}
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
@@ -132,8 +130,6 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
// This is a type guard to check if the cron expression is valid and give the user a type hint
return returnValue as unknown as ValidateCron<TExpression> extends true
? typeof returnValue
: TExpression extends "never"
? typeof returnValue
: "Invalid cron expression";
: "Invalid cron expression";
};
};

View File

@@ -8,4 +8,3 @@ export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
export const EVERY_HOUR = checkCron("0 * * * *") satisfies string;
export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string;
export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string;
export const NEVER = "never";

View File

@@ -45,7 +45,6 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
}
const scheduledTask = await job.createTaskAsync();
if (!scheduledTask) continue;
tasks.set(job.name, scheduledTask);
}

View File

@@ -21,7 +21,6 @@ import { refreshNotificationsJob } from "./jobs/integrations/notifications";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { sessionCleanupJob } from "./jobs/session-cleanup";
import { updateCheckerJob } from "./jobs/update-checker";
import { createCronJobGroup } from "./lib";
@@ -39,7 +38,6 @@ export const jobGroup = createCronJobGroup({
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
sessionCleanup: sessionCleanupJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,

View File

@@ -1,38 +0,0 @@
import { env } from "@homarr/auth/env";
import { NEVER } from "@homarr/cron-jobs-core/expressions";
import { db, eq, inArray } from "@homarr/db";
import { sessions, users } from "@homarr/db/schema";
import { supportedAuthProviders } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { createCronJob } from "../lib";
/**
* Deletes sessions for users that have inactive auth providers.
* Sessions from other providers are deleted so they can no longer be used.
*/
export const sessionCleanupJob = createCronJob("sessionCleanup", NEVER, {
runOnStart: true,
}).withCallback(async () => {
const currentAuthProviders = env.AUTH_PROVIDERS;
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
const subQuery = db
.select({ id: users.id })
.from(users)
.where(inArray(users.provider, inactiveAuthProviders))
.as("sq");
const sessionsWithInactiveProviders = await db
.select({ userId: sessions.userId })
.from(sessions)
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
await db.delete(sessions).where(inArray(sessions.userId, userIds));
if (sessionsWithInactiveProviders.length > 0) {
logger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
} else {
logger.debug("No sessions to delete");
}
});

View File

@@ -3255,9 +3255,6 @@
"dnsHole": {
"label": "DNS Hole Data"
},
"sessionCleanup": {
"label": "Session Cleanup"
},
"updateChecker": {
"label": "Update checker"
},

6
pnpm-lock.yaml generated
View File

@@ -373,6 +373,9 @@ importers:
'@homarr/analytics':
specifier: workspace:^0.1.0
version: link:../../packages/analytics
'@homarr/auth':
specifier: workspace:^0.1.0
version: link:../../packages/auth
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../../packages/common
@@ -406,6 +409,9 @@ importers:
'@homarr/redis':
specifier: workspace:^0.1.0
version: link:../../packages/redis
'@homarr/request-handler':
specifier: workspace:^0.1.0
version: link:../../packages/request-handler
'@homarr/server-settings':
specifier: workspace:^0.1.0
version: link:../../packages/server-settings