updating tasks and tech-insights to work together
Signed-off-by: Phil Gore <pgore@ea.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
6bc4253672
commit
231fee736b
@@ -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.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-backend': minor
|
||||
---
|
||||
|
||||
Updates tech-insights to use backend-tasks as the Fact Retriever scheduler.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -15,6 +15,7 @@ export interface PluginTaskScheduler {
|
||||
scheduleTask(
|
||||
task: TaskScheduleDefinition & TaskInvocationDefinition,
|
||||
): Promise<void>;
|
||||
triggerTask(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<void> {
|
||||
const knex = await this.databaseFactory();
|
||||
|
||||
// get the task definition
|
||||
const rows = await knex<DbTasksRow>(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<DbTasksRow>(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<void> {
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
/**
|
||||
* Schedules a task function for coordinated exclusive invocation across
|
||||
* workers. This convenience method performs both the scheduling and
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<FactRetrieverEngine> {
|
||||
const knex = await databases.init(databaseId);
|
||||
const databaseManager: Partial<DatabaseManager> = {
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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<string, ScheduledTask>();
|
||||
|
||||
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<void> {
|
||||
return this.scheduler.triggerTask(ref);
|
||||
}
|
||||
|
||||
private createFactRetrieverHandler(
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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<DatabaseManager> = {
|
||||
forPlugin: () => ({
|
||||
getClient: () => {
|
||||
return Promise.resolve({
|
||||
migrate: {
|
||||
latest: () => {},
|
||||
},
|
||||
}) as unknown as Promise<Knex>;
|
||||
},
|
||||
}),
|
||||
};
|
||||
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'),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user