diff --git a/.changeset/dirty-clocks-poke.md b/.changeset/dirty-clocks-poke.md new file mode 100644 index 0000000000..3eb63ee7a2 --- /dev/null +++ b/.changeset/dirty-clocks-poke.md @@ -0,0 +1,18 @@ +--- +'@backstage/backend-tasks': patch +--- + +`TaskScheduleDefinition` has been updated to also accept an options object containing duration information in the form of days, hours, seconds and so on. This allows for scheduling without importing `luxon`. + +```diff +-import { Duration } from 'luxon'; +// omitted other code + +const schedule = env.scheduler.createScheduledTaskRunner({ +- frequency: Duration.fromObject({ minutes: 10 }), +- timeout: Duration.fromObject({ minutes: 15 }), ++ frequency: { minutes: 10 }, ++ timeout: { minutes: 15 }, + // omitted other code +}); +``` diff --git a/.changeset/fast-stingrays-guess.md b/.changeset/fast-stingrays-guess.md new file mode 100644 index 0000000000..044dfff856 --- /dev/null +++ b/.changeset/fast-stingrays-guess.md @@ -0,0 +1,36 @@ +--- +'@backstage/create-app': patch +--- + +Simplified the search collator scheduling by removing the need for the `luxon` dependency. + +For existing installations the scheduling can be simplified by removing the `luxon` dependency and using the human friendly duration object instead. +Please note that this only applies if luxon is not used elsewhere in your installation. + +`packages/backend/package.json` + +```diff + "express": "^4.17.1", + "express-promise-router": "^4.1.0", +- "luxon": "^2.0.2", +``` + +`packages/backend/src/plugins/search.ts` + +```diff + import { Router } from 'express'; +-import { Duration } from 'luxon'; + + // omitted other code + + const schedule = env.scheduler.createScheduledTaskRunner({ +- frequency: Duration.fromObject({ minutes: 10 }), +- timeout: Duration.fromObject({ minutes: 15 }), ++ frequency: { minutes: 10 }, ++ timeout: { minutes: 15 }, + // A 3 second delay gives the backend server a chance to initialize before + // any collators are executed, which may attempt requests against the API. +- initialDelay: Duration.fromObject({ seconds: 3 }), ++ initialDelay: { seconds: 3 }, + }); +``` diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 1557a29b53..66540bd096 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -149,7 +149,6 @@ import { import { PluginEnvironment } from '../types'; import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; import { Router } from 'express'; -import { Duration } from 'luxon'; export default async function createPlugin( env: PluginEnvironment, @@ -163,9 +162,9 @@ export default async function createPlugin( }); const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 10 }), - timeout: Duration.fromObject({ minutes: 15 }), - initialDelay: Duration.fromObject({ seconds: 3 }), + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, }); indexBuilder.addCollator({ @@ -294,20 +293,18 @@ which are responsible for providing documents number of collators with the `IndexBuilder` like this: ```typescript -import { Duration } from 'luxon'; - const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine }); const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 10 }), - timeout: Duration.fromObject({ minutes: 15 }), - initialDelay: Duration.fromObject({ seconds: 3 }), + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, }); const everyHourSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ hours: 1 }), - timeout: Duration.fromObject({ minutes: 90 }), - initialDelay: Duration.fromObject({ seconds: 3 }), + frequency: { hours: 1 }, + timeout: { minutes: 90 }, + initialDelay: { seconds: 3 }, }); indexBuilder.addCollator({ @@ -332,9 +329,9 @@ a scheduled `TaskRunner` to pass into the `schedule` value, like this: ```typescript {3} const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 10 }), - timeout: Duration.fromObject({ minutes: 15 }), - initialDelay: Duration.fromObject({ seconds: 3 }), + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, }); indexBuilder.addCollator({ diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index a344f478d7..d6353d1185 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -231,7 +231,6 @@ You should now be able to add this class to your backend in `packages/backend/src/plugins/catalog.ts`: ```diff -+import { Duration } from 'luxon'; +import { FrobsProvider } from '../path/to/class'; export default async function createPlugin( @@ -248,8 +247,8 @@ You should now be able to add this class to your backend in + await env.scheduler.scheduleTask({ + id: 'run_frobs_refresh', + fn: async () => { await frobs.run(); }, -+ frequency: Duration.fromObject({ minutes: 30 }), -+ timeout: Duration.fromObject({ minutes: 10 }), ++ frequency: { minutes: 30 }, ++ timeout: { minutes: 10 }, + }); ``` diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index 316b5c5014..5091b4057b 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -70,8 +70,8 @@ builder.addEntityProvider( ...AwsS3EntityProvider.fromConfig(env.config, { logger: env.logger, schedule: env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 30 }), - timeout: Duration.fromObject({ minutes: 3 }), + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, }), }), ); diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index 5cd1f6204b..b801f7b77a 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -33,8 +33,8 @@ builder.addEntityProvider( ...GerritEntityProvider.fromConfig(env.config, { logger: env.logger, schedule: env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 30 }), - timeout: Duration.fromObject({ minutes: 3 }), + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, }), }), ); diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 355bd04227..e7028f34f1 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -49,8 +49,8 @@ schedule it: + target: 'ldaps://ds.example.net', + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ -+ frequency: Duration.fromObject({ minutes: 60 }), -+ timeout: Duration.fromObject({ minutes: 15 }), ++ frequency: { minutes: 60 }, ++ timeout: { minutes: 15 }, + }), + }), + ); diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md index d170c323c8..b3c304eb34 100644 --- a/packages/backend-tasks/README.md +++ b/packages/backend-tasks/README.md @@ -15,14 +15,13 @@ then make use of its facilities as necessary: ```typescript import { TaskScheduler } from '@backstage/backend-tasks'; -import { Duration } from 'luxon'; const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin'); await scheduler.scheduleTask({ id: 'refresh_things', frequency: { cron: '*/5 * * * *' }, // every 5 minutes, also supports Duration - timeout: Duration.fromObject({ minutes: 15 }), + timeout: { minutes: 15 }, fn: async () => { await entityProvider.run(); }, diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 0c82387cc8..c0802fa46c 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -9,6 +9,18 @@ import { DatabaseManager } from '@backstage/backend-common'; import { Duration } from 'luxon'; import { Logger } from 'winston'; +// @public +export type HumanDuration = { + years?: number; + months?: number; + weeks?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; +}; + // @public export interface PluginTaskScheduler { createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; @@ -41,10 +53,11 @@ export interface TaskScheduleDefinition { | { cron: string; } - | Duration; - initialDelay?: Duration; + | Duration + | HumanDuration; + initialDelay?: Duration | HumanDuration; scope?: 'global' | 'local'; - timeout: Duration; + timeout: Duration | HumanDuration; } // @public diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 1204431434..16550b2d7d 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -20,7 +20,10 @@ import { ConflictError, NotFoundError } from '@backstage/errors'; import { Duration } from 'luxon'; import { AbortSignal } from 'node-abort-controller'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; -import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { + parseDuration, + PluginTaskSchedulerImpl, +} from './PluginTaskSchedulerImpl'; jest.useFakeTimers(); @@ -177,8 +180,8 @@ describe('PluginTaskManagerImpl', () => { const promise = new Promise(resolve => fn.mockImplementation(resolve)); await manager.scheduleTask({ id: 'task1', - timeout: Duration.fromMillis(5000), - frequency: Duration.fromMillis(5000), + timeout: { milliseconds: 5000 }, + frequency: { milliseconds: 5000 }, fn, scope: 'local', }); @@ -294,4 +297,12 @@ describe('PluginTaskManagerImpl', () => { 60_000, ); }); + + describe('parseDuration', () => { + it('should parse durations', () => { + expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); + expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); + expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + }); + }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 6db8b9f4f5..e43d0c1be6 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -15,6 +15,7 @@ */ import { Knex } from 'knex'; +import { Duration } from 'luxon'; import { Logger } from 'winston'; import { LocalTaskWorker } from './LocalTaskWorker'; import { TaskWorker } from './TaskWorker'; @@ -61,12 +62,10 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { await worker.start( { version: 2, - cadence: - 'cron' in task.frequency - ? task.frequency.cron - : task.frequency.toISO(), - initialDelayDuration: task.initialDelay?.toISO(), - timeoutAfterDuration: task.timeout.toISO(), + cadence: parseDuration(task.frequency), + initialDelayDuration: + task.initialDelay && parseDuration(task.initialDelay), + timeoutAfterDuration: parseDuration(task.timeout), }, { signal: task.signal, @@ -78,12 +77,10 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { worker.start( { version: 2, - cadence: - 'cron' in task.frequency - ? task.frequency.cron - : task.frequency.toISO(), - initialDelayDuration: task.initialDelay?.toISO(), - timeoutAfterDuration: task.timeout.toISO(), + cadence: parseDuration(task.frequency), + initialDelayDuration: + task.initialDelay && parseDuration(task.initialDelay), + timeoutAfterDuration: parseDuration(task.timeout), }, { signal: task.signal, @@ -102,3 +99,17 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { }; } } + +export function parseDuration( + frequency: TaskScheduleDefinition['frequency'], +): string { + if ('cron' in frequency) { + return frequency.cron; + } + + if (Duration.isDuration(frequency)) { + return frequency.toISO(); + } + + return Duration.fromObject(frequency).toISO(); +} diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index f6695a8d73..990d6332fc 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -21,4 +21,5 @@ export type { TaskInvocationDefinition, TaskRunner, TaskScheduleDefinition, + HumanDuration, } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index f9ada1dd17..86c0c5c462 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -19,6 +19,21 @@ import { Duration } from 'luxon'; import { AbortSignal } from 'node-abort-controller'; import { z } from 'zod'; +/** + * Human friendly durations object + * @public + */ +export type HumanDuration = { + years?: number; + months?: number; + weeks?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; +}; + /** * A function that can be called as a scheduled task. * @@ -74,14 +89,15 @@ export interface TaskScheduleDefinition { */ cron: string; } - | Duration; + | Duration + | HumanDuration; /** * The maximum amount of time that a single task invocation can take, before * it's considered timed out and gets "released" such that a new invocation * is permitted to take place (possibly, then, on a different worker). */ - timeout: Duration; + timeout: Duration | HumanDuration; /** * The amount of time that should pass before the first invocation happens. @@ -100,7 +116,7 @@ export interface TaskScheduleDefinition { * work; its main intended use is for individual machines to get a chance to * reach some equilibrium at startup before triggering heavy batch workloads. */ - initialDelay?: Duration; + initialDelay?: Duration | HumanDuration; /** * Sets the scope of concurrency control / locking to apply for invocations of diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index fc91d98cfe..a0ea83ab61 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -26,7 +26,6 @@ import { } from '@backstage/plugin-search-backend-node'; import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; import { Router } from 'express'; -import { Duration } from 'luxon'; import { PluginEnvironment } from '../types'; async function createSearchEngine( @@ -57,11 +56,11 @@ export default async function createPlugin( }); const schedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 10 }), - timeout: Duration.fromObject({ minutes: 15 }), + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, // A 3 second delay gives the backend server a chance to initialize before // any collators are executed, which may attempt requests against the API. - initialDelay: Duration.fromObject({ seconds: 3 }), + initialDelay: { seconds: 3 }, }); // Collators are responsible for gathering documents known to plugins. This diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index c162dd880e..3bd070ee9b 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -36,7 +36,6 @@ "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "luxon": "^2.0.2", "pg": "^8.3.0", "winston": "^3.2.1" }, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs index 27c42fca31..e9469dcc1f 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs @@ -8,7 +8,6 @@ import { PluginEnvironment } from '../types'; import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; import { Router } from 'express'; -import { Duration } from 'luxon'; export default async function createPlugin( env: PluginEnvironment, @@ -23,11 +22,11 @@ export default async function createPlugin( }); const schedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 10 }), - timeout: Duration.fromObject({ minutes: 15 }), + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, // A 3 second delay gives the backend server a chance to initialize before // any collators are executed, which may attempt requests against the API. - initialDelay: Duration.fromObject({ seconds: 3 }), + initialDelay: { seconds: 3 }, }); // Collators are responsible for gathering documents known to plugins. This