diff --git a/.changeset/fresh-dodos-rush.md b/.changeset/fresh-dodos-rush.md new file mode 100644 index 0000000000..b71c2ad055 --- /dev/null +++ b/.changeset/fresh-dodos-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-node': patch +--- + +Adds an optional timeout to fact retriever registrations to stop a task if it runs too long. diff --git a/.changeset/shiny-seas-yell.md b/.changeset/shiny-seas-yell.md new file mode 100644 index 0000000000..7d5c27520c --- /dev/null +++ b/.changeset/shiny-seas-yell.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-tech-insights-backend': minor +--- + +Updates tech-insights to use backend-tasks as the Fact Retriever scheduler. + +``` + +``` diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 0093616d8f..9f02088bdf 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -15,6 +15,7 @@ export interface PluginTaskScheduler { scheduleTask( task: TaskScheduleDefinition & TaskInvocationDefinition, ): Promise; + triggerTask(id: string): Promise; } // @public diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 646a2ce737..95a9729018 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -20,6 +20,9 @@ import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { ConflictError, NotFoundError } from '@backstage/errors'; + +jest.useFakeTimers(); describe('PluginTaskManagerImpl', () => { const databases = TestDatabases.create({ @@ -80,6 +83,72 @@ describe('PluginTaskManagerImpl', () => { ); }); + describe('triggerTask', () => { + it.each(databases.eachSupportedId())( + 'can manually trigger a task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn, + }); + + await manager.triggerTask('task1'); + jest.advanceTimersByTime(5000); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'cant trigger a non-existent task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn, + }); + + await expect(() => manager.triggerTask('task2')).rejects.toThrow( + NotFoundError, + ); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'cant trigger a running task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn, + }); + + await expect(() => manager.triggerTask('task1')).rejects.toThrow( + ConflictError, + ); + }, + 60_000, + ); + }); + // This is just to test the wrapper code; most of the actual tests are in // TaskWorker.test.ts describe('createScheduledTaskRunner', () => { diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 8c3166b441..81e0f1e461 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -24,6 +24,8 @@ import { TaskScheduleDefinition, } from './types'; import { validateId } from './util'; +import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; +import { ConflictError, NotFoundError } from '@backstage/errors'; /** * Implements the actual task management. @@ -34,6 +36,37 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { private readonly logger: Logger, ) {} + async triggerTask(id: string): Promise { + const knex = await this.databaseFactory(); + + // get the task definition + const rows = await knex(DB_TASKS_TABLE) + .select({ + currentRun: 'current_run_ticket', + id: 'id', + }) + .where('id', '=', id); + + // validate the task exists + if (rows.length <= 0 || rows[0].id !== id) { + throw new NotFoundError(`Task ${id} does not exist`); + } + + if (rows[0].currentRun) { + throw new ConflictError(`task ${id} is currently running`); + } + + const updatedRows = await knex(DB_TASKS_TABLE) + .where('id', '=', id) + .whereNull('current_run_ticket') + .update({ + next_run_start_at: knex.fn.now(), + }); + if (updatedRows < 1) { + throw new ConflictError(`task ${id} is currently running`); + } + } + async scheduleTask( task: TaskScheduleDefinition & TaskInvocationDefinition, ): Promise { diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index acd651adcf..513e0dbae4 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -134,6 +134,17 @@ export interface TaskRunner { * @public */ export interface PluginTaskScheduler { + /** + * Manually triggers a task by ID. + * + * If the task doesn't exist, a NotFoundError is thrown. + * If the task is currently running, a ConflictError is thrown. + * + * @param id - The task ID + * + */ + triggerTask(id: string): Promise; + /** * Schedules a task function for coordinated exclusive invocation across * workers. This convenience method performs both the scheduling and diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index e249b1db9b..94ce27656a 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -36,6 +36,7 @@ export default async function createPlugin( logger: env.logger, config: env.config, database: env.database, + scheduler: env.scheduler, discovery: env.discovery, factRetrievers: [ createFactRetrieverRegistration({ diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 39162b54e1..1f4b7de469 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -46,8 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.16.1-next.0", - "@types/node-cron": "^3.0.1" + "@backstage/cli": "^0.16.1-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index d56e3c8082..08c6e82385 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -14,6 +14,7 @@ import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node' import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; @@ -94,6 +95,8 @@ export interface TechInsightsOptions< factRetrievers: FactRetrieverRegistration[]; // (undocumented) logger: Logger; + // (undocumented) + scheduler: PluginTaskScheduler; } // (No @packageDocumentation comment for this package) diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 65ccb137c7..475e5e98ae 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.13.2-next.0", + "@backstage/backend-tasks": "^0.2.2-next.0", "@backstage/catalog-client": "^1.0.1-next.0", "@backstage/catalog-model": "^1.0.1-next.0", "@backstage/config": "^1.0.0", @@ -47,7 +48,6 @@ "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", - "node-cron": "^3.0.0", "semver": "^7.3.5", "uuid": "^8.3.2", "winston": "^3.2.1", @@ -57,9 +57,9 @@ "@backstage/backend-test-utils": "^0.1.23-next.0", "@backstage/cli": "^0.16.1-next.0", "@types/supertest": "^2.0.8", - "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", - "supertest": "^6.1.3" + "supertest": "^6.1.3", + "wait-for-expect": "^3.0.2" }, "files": [ "dist", diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 1b5dad1f33..a019c533a1 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -22,20 +22,16 @@ import { } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { FactRetrieverEngine } from './FactRetrieverEngine'; -import { getVoidLogger } from '@backstage/backend-common'; +import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { schedule } from 'node-cron'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { TaskScheduler } from '@backstage/backend-tasks'; +import waitForExpect from 'wait-for-expect'; -jest.mock('node-cron', () => { - const original = jest.requireActual('node-cron'); - return { - ...original, - schedule: jest.fn(), - }; -}); +jest.useFakeTimers(); const testFactRetriever: FactRetriever = { - id: 'test-factretriever', + id: 'test_factretriever', version: '0.0.1', entityFilter: [{ kind: 'component' }], schema: { @@ -59,94 +55,162 @@ const testFactRetriever: FactRetriever = { ]; }), }; -const cadence = '1 * * * *'; +const defaultCadence = '1 * * * *'; describe('FactRetrieverEngine', () => { let engine: FactRetrieverEngine; - let factSchemaAssertionCallback: ( + type FactSchemaAssertionCallback = ( factSchemaDefinition: FactSchemaDefinition, ) => void; - let factInsertionAssertionCallback: (facts: TechInsightFact[]) => void; - const mockRepository: TechInsightsStore = { - insertFacts: (facts: TechInsightFact[]) => { - factInsertionAssertionCallback(facts); - return Promise.resolve(); - }, - insertFactSchema: (def: FactSchemaDefinition) => { - factSchemaAssertionCallback(def); - return Promise.resolve(); - }, - } as unknown as TechInsightsStore; + jest.setTimeout(15000); - const mockFactRetrieverRegistry: FactRetrieverRegistry = { - listRetrievers(): FactRetriever[] { - return [testFactRetriever]; - }, - listRegistrations(): FactRetrieverRegistration[] { - return [{ factRetriever: testFactRetriever, cadence }]; - }, - } as unknown as FactRetrieverRegistry; + type FactInsertionAssertionCallback = ({ + facts, + id, + }: { + id: string; + facts: TechInsightFact[]; + }) => void; - const defaultEngineConfig = { - factRetrieverContext: { - logger: getVoidLogger(), - config: ConfigReader.fromConfigs([]), - discovery: { - getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), - getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + function createMockRepository( + insertCallback: FactInsertionAssertionCallback, + assertionCallback: FactSchemaAssertionCallback, + ): TechInsightsStore { + return { + async insertFacts(f: { facts: TechInsightFact[]; id: string }) { + insertCallback(f); }, - }, - factRetrieverRegistry: mockFactRetrieverRegistry, - repository: mockRepository, - }; - - it('Should update fact retriever schemas on initialization', async () => { - factSchemaAssertionCallback = ({ id, schema, version, entityFilter }) => { - expect(id).toEqual('test-factretriever'); - expect(version).toEqual('0.0.1'); - expect(entityFilter).toEqual([{ kind: 'component' }]); - expect(schema).toEqual({ - testnumberfact: { - type: 'integer', - description: '', - }, - }); - }; - - engine = await FactRetrieverEngine.create(defaultEngineConfig); - }); - it('Should insert facts when scheduled step is run', async () => { - (schedule as jest.Mock).mockImplementation( - (cronCadence: string, retrieverAction: Function) => { - return { - cadence: cronCadence, - triggerScheduledJobNow: retrieverAction, - }; + async insertFactSchema(def: FactSchemaDefinition) { + assertionCallback(def); }, - ); + } as unknown as TechInsightsStore; + } - factSchemaAssertionCallback = () => {}; - factInsertionAssertionCallback = facts => { - expect(facts).toHaveLength(1); - expect(facts[0]).toEqual({ - ref: 'test-factretriever', - entity: { - namespace: 'a', - kind: 'a', - name: 'a', - }, - facts: { - testnumberfact: 1, - }, - }); - }; - engine = await FactRetrieverEngine.create(defaultEngineConfig); - engine.schedule(); - const job: any = engine.getJob('test-factretriever'); - job.triggerScheduledJobNow(); - expect(job.cadence!).toEqual(cadence); - expect(testFactRetriever.handler).toHaveBeenCalledWith( - expect.objectContaining({ entityFilter: testFactRetriever.entityFilter }), - ); + function createMockFactRetrieverRegistry( + cadence?: string, + factRetriever?: FactRetriever, + ): FactRetrieverRegistry { + const cron = cadence ?? defaultCadence; + const retriever: FactRetriever = factRetriever + ? factRetriever + : testFactRetriever; + return { + listRetrievers(): FactRetriever[] { + return [retriever]; + }, + listRegistrations(): FactRetrieverRegistration[] { + return [{ factRetriever: retriever, cadence: cron }]; + }, + get: (_: string): FactRetrieverRegistration => { + return { factRetriever: retriever, cadence: cron }; + }, + } as unknown as FactRetrieverRegistry; + } + + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); + + async function createEngine( + databaseId: TestDatabaseId, + insert: FactInsertionAssertionCallback, + schema: FactSchemaAssertionCallback, + cadence?: string, + factRetriever?: FactRetriever, + ): Promise { + const knex = await databases.init(databaseId); + const databaseManager: Partial = { + forPlugin: (_: string) => ({ + getClient: async () => knex, + }), + }; + const manager = databaseManager as DatabaseManager; + const scheduler = new TaskScheduler(manager, getVoidLogger()); + return await FactRetrieverEngine.create({ + factRetrieverContext: { + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), + discovery: { + getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + }, + }, + factRetrieverRegistry: createMockFactRetrieverRegistry( + cadence, + factRetriever, + ), + repository: createMockRepository(insert, schema), + scheduler: scheduler.forPlugin('tech-insights'), + }); + } + + it.each(databases.eachSupportedId())( + 'Should update fact retriever schemas on initialization', + async databaseId => { + const schemaAssertionCallback = jest.fn((def: FactSchemaDefinition) => { + expect(def.id).toEqual('test_factretriever'); + expect(def.version).toEqual('0.0.1'); + expect(def.entityFilter).toEqual([{ kind: 'component' }]); + expect(def.schema).toEqual({ + testnumberfact: { + type: 'integer', + description: '', + }, + }); + }); + + engine = await createEngine( + databaseId, + () => {}, + schemaAssertionCallback, + ); + expect(schemaAssertionCallback).toBeCalled(); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'Should insert facts when scheduled step is run', + async databaseId => { + function insertCallback({ + facts, + id, + }: { + id: string; + facts: TechInsightFact[]; + }) { + expect(facts).toHaveLength(1); + expect(id).toEqual('test_factretriever'); + expect(facts[0]).toEqual({ + entity: { + namespace: 'a', + kind: 'a', + name: 'a', + }, + facts: { + testnumberfact: 1, + }, + }); + } + + engine = await createEngine(databaseId, insertCallback, () => {}); + engine.schedule(); + const job: FactRetrieverRegistration = engine.getJobRegistration( + testFactRetriever.id, + ); + expect(job.cadence!!).toEqual(defaultCadence); + + await engine.triggerJob(job.factRetriever.id); + jest.advanceTimersByTime(5000); + + await waitForExpect(() => { + expect(testFactRetriever.handler).toHaveBeenCalledWith( + expect.objectContaining({ + entityFilter: testFactRetriever.entityFilter, + }), + ); + }); + }, + 60_000, + ); }); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts index 85e7e680c7..9f9c91b4ee 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -17,12 +17,14 @@ import { FactLifecycle, FactRetriever, FactRetrieverContext, + FactRetrieverRegistration, TechInsightFact, TechInsightsStore, } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistry } from './FactRetrieverRegistry'; -import { schedule, validate, ScheduledTask } from 'node-cron'; import { Logger } from 'winston'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Duration } from 'luxon'; function randomDailyCron() { const rand = (min: number, max: number) => @@ -37,26 +39,30 @@ function duration(startTimestamp: [number, number]): string { } export class FactRetrieverEngine { - private scheduledJobs = new Map(); - constructor( private readonly repository: TechInsightsStore, private readonly factRetrieverRegistry: FactRetrieverRegistry, private readonly factRetrieverContext: FactRetrieverContext, private readonly logger: Logger, + private readonly scheduler: PluginTaskScheduler, private readonly defaultCadence?: string, + private readonly defaultTimeout?: Duration, ) {} static async create({ repository, factRetrieverRegistry, factRetrieverContext, + scheduler, defaultCadence, + defaultTimeout, }: { repository: TechInsightsStore; factRetrieverRegistry: FactRetrieverRegistry; factRetrieverContext: FactRetrieverContext; + scheduler: PluginTaskScheduler; defaultCadence?: string; + defaultTimeout?: Duration; }) { await Promise.all( factRetrieverRegistry @@ -69,39 +75,46 @@ export class FactRetrieverEngine { factRetrieverRegistry, factRetrieverContext, factRetrieverContext.logger, + scheduler, defaultCadence, + defaultTimeout, ); } schedule() { const registrations = this.factRetrieverRegistry.listRegistrations(); const newRegs: string[] = []; - registrations.forEach(registration => { - const { factRetriever, cadence, lifecycle } = registration; - if (!this.scheduledJobs.has(factRetriever.id)) { - const cronExpression = - cadence || this.defaultCadence || randomDailyCron(); - if (!validate(cronExpression)) { - this.logger.warn( - `Validation failed for cron expression ${cronExpression} when trying to schedule fact retriever ${factRetriever.id}`, - ); - return; - } - const job = schedule( - cronExpression, - this.createFactRetrieverHandler(factRetriever, lifecycle), + registrations.forEach(async registration => { + const { factRetriever, cadence, lifecycle, timeout } = registration; + const cronExpression = + cadence || this.defaultCadence || randomDailyCron(); + const timeLimit = + timeout || this.defaultTimeout || Duration.fromObject({ minutes: 5 }); + try { + await this.scheduler.scheduleTask({ + id: factRetriever.id, + frequency: { cron: cronExpression }, + fn: this.createFactRetrieverHandler(factRetriever, lifecycle), + timeout: timeLimit, + }); + } catch (e) { + throw new Error( + `Failed to schedule fact retriever ${factRetriever.id}, ${e}`, ); - this.scheduledJobs.set(factRetriever.id, job); - newRegs.push(factRetriever.id); } + newRegs.push(factRetriever.id); }); this.logger.info( - `Scheduled ${newRegs.length} fact retrievers to Fact Retriever Engine.`, + `Scheduled ${newRegs.length} fact retrievers to Fact Retriever Engine through Backend Tasks`, ); } - getJob(ref: string) { - return this.scheduledJobs.get(ref); + getJobRegistration(ref: string): FactRetrieverRegistration { + return this.factRetrieverRegistry.get(ref); + } + + async triggerJob(ref: string): Promise { + return this.scheduler.triggerTask(ref); } private createFactRetrieverHandler( diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts index 719f8bf504..b122f52f0a 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -39,14 +39,14 @@ export class FactRetrieverRegistry { this.retrievers.set(registration.factRetriever.id, registration); } - get(retrieverReference: string): FactRetriever { + get(retrieverReference: string): FactRetrieverRegistration { const registration = this.retrievers.get(retrieverReference); if (!registration) { throw new NotFoundError( `Tech insight fact retriever with identifier '${retrieverReference}' is not registered.`, ); } - return registration.factRetriever; + return registration; } listRetrievers(): FactRetriever[] { diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 0b7d3b7c45..2ddc5aeebb 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -15,7 +15,7 @@ */ import { buildTechInsightsContext } from './techInsightsContextBuilder'; import { createRouter } from './router'; -import { getVoidLogger } from '@backstage/backend-common'; +import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import request from 'supertest'; import express from 'express'; @@ -23,6 +23,7 @@ import { PersistenceContext } from './persistence/persistenceContext'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { DateTime } from 'luxon'; import { Knex } from 'knex'; +import { TaskScheduler } from '@backstage/backend-tasks'; describe('Tech Insights router tests', () => { let app: express.Express; @@ -44,6 +45,18 @@ describe('Tech Insights router tests', () => { }); beforeAll(async () => { + const databaseManager: Partial = { + forPlugin: () => ({ + getClient: () => { + return Promise.resolve({ + migrate: { + latest: () => {}, + }, + }) as unknown as Promise; + }, + }), + }; + const manager = databaseManager as DatabaseManager; const techInsightsContext = await buildTechInsightsContext({ database: { getClient: () => { @@ -56,6 +69,9 @@ describe('Tech Insights router tests', () => { }, logger: getVoidLogger(), factRetrievers: [], + scheduler: new TaskScheduler(manager, getVoidLogger()).forPlugin( + 'tech-insights', + ), config: ConfigReader.fromConfigs([]), discovery: { getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 75e24b7c06..5e599ab664 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -33,6 +33,7 @@ import { PersistenceContext, } from './persistence/persistenceContext'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; /** * @public @@ -61,6 +62,7 @@ export interface TechInsightsOptions< config: Config; discovery: PluginEndpointDiscovery; database: PluginDatabaseManager; + scheduler: PluginTaskScheduler; } /** @@ -101,6 +103,7 @@ export const buildTechInsightsContext = async < discovery, database, logger, + scheduler, } = options; const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); @@ -111,6 +114,7 @@ export const buildTechInsightsContext = async < ); const factRetrieverEngine = await FactRetrieverEngine.create({ + scheduler, repository: persistenceContext.techInsightsStore, factRetrieverRegistry, factRetrieverContext: { diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 45426587e3..7d9f2e00b7 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -6,6 +6,7 @@ import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; +import { Duration } from 'luxon'; import { DurationLike } from 'luxon'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -66,6 +67,7 @@ export type FactRetrieverContext = { export type FactRetrieverRegistration = { factRetriever: FactRetriever; cadence?: string; + timeout?: Duration; lifecycle?: FactLifecycle; }; diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index 24b6764ba4..a5df4bae85 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { DateTime, DurationLike } from 'luxon'; +import { DateTime, Duration, DurationLike } from 'luxon'; import { Config } from '@backstage/config'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Logger } from 'winston'; @@ -243,6 +242,13 @@ export type FactRetrieverRegistration = { */ cadence?: string; + /** + * A duration to determine how long the fact retriever should be allowed to run, + * defaults to 5 minutes. + * + */ + timeout?: Duration; + /** * Fact lifecycle definition * diff --git a/yarn.lock b/yarn.lock index 09d924c5bb..ea2b0fcf94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6259,11 +6259,6 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node-cron@^3.0.0", "@types/node-cron@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.1.tgz#e01a874d4c2aa1a02ebc64cfd1cd8ebdbad7a996" - integrity sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg== - "@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7": version "2.5.12" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" @@ -17997,7 +17992,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment-timezone@^0.5.31, moment-timezone@^0.5.x: +moment-timezone@^0.5.x: version "0.5.34" resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== @@ -18272,13 +18267,6 @@ node-cache@^5.1.2: dependencies: clone "2.x" -node-cron@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" - integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== - dependencies: - moment-timezone "^0.5.31" - node-dir@^0.1.17: version "0.1.17" resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"