diff --git a/src/backend/types.ts b/src/backend/types.ts index 10f5eb3a7..6d9a1230a 100644 --- a/src/backend/types.ts +++ b/src/backend/types.ts @@ -642,6 +642,21 @@ export interface IDevWatcherConfig { }>; } +/** + * System jobs config (`jobs`). An extension that runs jobs types its own keys + * by augmenting this interface. + * + * - `namespace` — keys this deployment's jobs and schedules, so deployments + * sharing one database or Redis never run each other's. Defaults to `domain`; + * one that can't be held falls back to `default` (`resolveJobsNamespace`). + * - `enabled` — `false` keeps this node from polling for jobs (logged at start); + * the rest of the fleet still runs them. Test servers turn it off. + */ +export interface IJobsConfig { + namespace?: string; + enabled?: boolean; +} + /** * Root config. Only `port` and `extensions` are mandatory; they are added in * `IConfig` below. Every config field is declared here with a doc comment so @@ -1291,6 +1306,9 @@ interface IConfigOptional { internalSecret?: string; }; + /** System jobs (`services.jobs`); see {@link IJobsConfig}. */ + jobs?: IJobsConfig; + /** * Key-value sharing beyond watching. * diff --git a/src/backend/util/jobsNamespace.test.ts b/src/backend/util/jobsNamespace.test.ts new file mode 100644 index 000000000..f66905d2d --- /dev/null +++ b/src/backend/util/jobsNamespace.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { IConfig } from '../types.js'; +import { resolveJobsNamespace } from './jobsNamespace.js'; + +const config = (partial: Partial): IConfig => partial as IConfig; + +describe('resolveJobsNamespace', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('takes jobs.namespace first, then the domain, then "default"', () => { + expect( + resolveJobsNamespace( + config({ jobs: { namespace: 'ns-1' }, domain: 'puter.test' }), + ), + ).toBe('ns-1'); + expect(resolveJobsNamespace(config({ domain: 'puter.test' }))).toBe( + 'puter.test', + ); + expect(resolveJobsNamespace(config({}))).toBe('default'); + }); + + it('falls back to "default" for a domain with a port, logging once', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + const withPort = config({ domain: 'puter.localhost:4100' }); + expect(resolveJobsNamespace(withPort)).toBe('default'); + expect(resolveJobsNamespace(withPort)).toBe('default'); + expect(error).toHaveBeenCalledTimes(1); + expect(String(error.mock.calls[0][0])).toContain( + 'puter.localhost:4100', + ); + }); + + it('falls back to "default" for an invalid jobs.namespace', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + expect( + resolveJobsNamespace( + config({ jobs: { namespace: 'a:b' }, domain: 'puter.test' }), + ), + ).toBe('default'); + expect(resolveJobsNamespace(config({ jobs: { namespace: '' } }))).toBe( + 'default', + ); + expect( + resolveJobsNamespace( + config({ jobs: { namespace: 'x'.repeat(129) } }), + ), + ).toBe('default'); + expect(error).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/backend/util/jobsNamespace.ts b/src/backend/util/jobsNamespace.ts new file mode 100644 index 000000000..78986d767 --- /dev/null +++ b/src/backend/util/jobsNamespace.ts @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import type { IConfig } from '../types.js'; + +/** A namespace must fit the jobs and schedules tables and a Redis key segment. */ +const JOBS_NAMESPACE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/; + +const warned = new Set(); + +/** + * The namespace system jobs and schedules are keyed by, so deployments sharing + * a database or Redis stay apart: `jobs.namespace`, else `domain`, else + * `default`. One that can't be held (a domain with a port, say) falls back to + * `default`, with one logged error per value. + */ +export const resolveJobsNamespace = (config: IConfig): string => { + const configured = config.jobs?.namespace ?? config.domain ?? 'default'; + if (JOBS_NAMESPACE_PATTERN.test(configured)) return configured; + if (!warned.has(configured)) { + warned.add(configured); + console.error( + `[jobs] namespace "${configured}" (jobs.namespace, else domain) must match ${JOBS_NAMESPACE_PATTERN}; using "default". Set jobs.namespace to keep deployments sharing a database apart.`, + ); + } + return 'default'; +};