Merge pull request #11245 from backstage/jhaals/human-duration
backend-tasks: Introduce human friendly scheduling
This commit is contained in:
@@ -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
|
||||
});
|
||||
```
|
||||
@@ -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 },
|
||||
});
|
||||
```
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
+ });
|
||||
```
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 * * * *');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ export type {
|
||||
TaskInvocationDefinition,
|
||||
TaskRunner,
|
||||
TaskScheduleDefinition,
|
||||
HumanDuration,
|
||||
} from './types';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user