From dd45747b22feffbbf46b228188f96bdd8f7d5c22 Mon Sep 17 00:00:00 2001 From: MarMar Labs Date: Tue, 6 Oct 2026 23:40:11 -0500 Subject: [PATCH 1/2] fix: stop scheduled jobs after switching to multi-user mode Scheduled jobs created in single-user mode kept running after switching to multi-user mode, even though every /scheduled-jobs route returns 401 there, so they could no longer be viewed, paused, or deleted. Their cron timers stayed active and every restart registered them again. Boot now skips registering jobs in multi-user mode, and enqueueScheduledJob returns null and clears the job's timer instead of starting a run. --- .../utils/BackgroundWorkers/index.test.js | 38 +++++++++++++++++++ server/utils/BackgroundWorkers/index.js | 15 +++++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 server/__tests__/utils/BackgroundWorkers/index.test.js diff --git a/server/__tests__/utils/BackgroundWorkers/index.test.js b/server/__tests__/utils/BackgroundWorkers/index.test.js new file mode 100644 index 00000000000..d92735ef342 --- /dev/null +++ b/server/__tests__/utils/BackgroundWorkers/index.test.js @@ -0,0 +1,38 @@ +/* eslint-env jest */ +const mockIsMultiUserMode = jest.fn(); +const mockStart = jest.fn(); + +jest.mock("../../../models/systemSettings", () => ({ + SystemSettings: { isMultiUserMode: mockIsMultiUserMode }, +})); +jest.mock("../../../models/scheduledJobRun", () => ({ + ScheduledJobRun: { start: mockStart }, +})); + +const { BackgroundService } = require("../../../utils/BackgroundWorkers"); + +describe("BackgroundService.enqueueScheduledJob", () => { + const service = new BackgroundService(); + + beforeEach(() => { + jest.clearAllMocks(); + mockStart.mockResolvedValue(null); + jest.spyOn(service, "removeScheduledJob").mockImplementation(() => {}); + }); + + it("starts no run and drops the job's timer in multi-user mode", async () => { + mockIsMultiUserMode.mockResolvedValue(true); + + await expect(service.enqueueScheduledJob(7)).resolves.toBeNull(); + expect(mockStart).not.toHaveBeenCalled(); + expect(service.removeScheduledJob).toHaveBeenCalledWith(7); + }); + + it("still tries to start a run in single-user mode", async () => { + mockIsMultiUserMode.mockResolvedValue(false); + + await service.enqueueScheduledJob(7); + expect(mockStart).toHaveBeenCalledWith(7); + expect(service.removeScheduledJob).not.toHaveBeenCalled(); + }); +}); diff --git a/server/utils/BackgroundWorkers/index.js b/server/utils/BackgroundWorkers/index.js index cd767d896f1..46d5c49f098 100644 --- a/server/utils/BackgroundWorkers/index.js +++ b/server/utils/BackgroundWorkers/index.js @@ -272,6 +272,11 @@ class BackgroundService { */ async #bootScheduledJobs() { const { ScheduledJob } = require("../../models/scheduledJob"); + const { SystemSettings } = require("../../models/systemSettings"); + + // Scheduled jobs are only available in single-user mode. + if (await SystemSettings.isMultiUserMode()) return; + const enabledJobs = await ScheduledJob.allEnabled(); for (const job of enabledJobs) { @@ -373,10 +378,18 @@ class BackgroundService { * * @param {number} jobId - scheduled_jobs.id * @returns {Promise} the created run row, or null if skipped - * because a run is already in flight for this job. + * because a run is already in flight for this job or the instance is in + * multi-user mode. */ async enqueueScheduledJob(jobId) { const { ScheduledJobRun } = require("../../models/scheduledJobRun"); + const { SystemSettings } = require("../../models/systemSettings"); + + // Timers registered before the switch to multi-user mode stop here. + if (await SystemSettings.isMultiUserMode()) { + this.removeScheduledJob(jobId); + return null; + } const run = await ScheduledJobRun.start(jobId); // if start returns null, skip enqueuing, schueduled job already has a run in flight From d1cdc9c778cb36d09bb928fcea482639cbde13fa Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 6 Oct 2026 22:53:12 -0700 Subject: [PATCH 2/2] test: remove narrow BackgroundService enqueue test --- .../utils/BackgroundWorkers/index.test.js | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 server/__tests__/utils/BackgroundWorkers/index.test.js diff --git a/server/__tests__/utils/BackgroundWorkers/index.test.js b/server/__tests__/utils/BackgroundWorkers/index.test.js deleted file mode 100644 index d92735ef342..00000000000 --- a/server/__tests__/utils/BackgroundWorkers/index.test.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-env jest */ -const mockIsMultiUserMode = jest.fn(); -const mockStart = jest.fn(); - -jest.mock("../../../models/systemSettings", () => ({ - SystemSettings: { isMultiUserMode: mockIsMultiUserMode }, -})); -jest.mock("../../../models/scheduledJobRun", () => ({ - ScheduledJobRun: { start: mockStart }, -})); - -const { BackgroundService } = require("../../../utils/BackgroundWorkers"); - -describe("BackgroundService.enqueueScheduledJob", () => { - const service = new BackgroundService(); - - beforeEach(() => { - jest.clearAllMocks(); - mockStart.mockResolvedValue(null); - jest.spyOn(service, "removeScheduledJob").mockImplementation(() => {}); - }); - - it("starts no run and drops the job's timer in multi-user mode", async () => { - mockIsMultiUserMode.mockResolvedValue(true); - - await expect(service.enqueueScheduledJob(7)).resolves.toBeNull(); - expect(mockStart).not.toHaveBeenCalled(); - expect(service.removeScheduledJob).toHaveBeenCalledWith(7); - }); - - it("still tries to start a run in single-user mode", async () => { - mockIsMultiUserMode.mockResolvedValue(false); - - await service.enqueueScheduledJob(7); - expect(mockStart).toHaveBeenCalledWith(7); - expect(service.removeScheduledJob).not.toHaveBeenCalled(); - }); -});