Catalog: Add refresh spread functionality

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2021-08-13 14:40:33 +02:00
parent accd1af650
commit 22f775d19f
4 changed files with 73 additions and 11 deletions
+2 -1
View File
@@ -73,7 +73,8 @@
"msw": "^0.29.0",
"sqlite3": "^5.0.1",
"supertest": "^6.1.3",
"wait-for-expect": "^3.0.2"
"wait-for-expect": "^3.0.2",
"luxon": "^2.0.2"
},
"files": [
"dist",
@@ -113,6 +113,7 @@ export class NextCatalogBuilder {
private processorsReplace: boolean;
private parser: CatalogProcessorParser | undefined;
private refreshIntervalSeconds = 100;
private refreshSpreadSeconds = { min: 10, max: 60 };
constructor(env: CatalogEnvironment) {
this.env = env;
@@ -152,6 +153,15 @@ export class NextCatalogBuilder {
return this;
}
/**
* Refresh spread configures configures the minimum and maximum number of seconds
* to wait between refreshes in addition to the configured refresh interval.
*/
setRefreshSpreadSeconds(min: number, max: number): NextCatalogBuilder {
this.refreshSpreadSeconds = { min, max };
return this;
}
/**
* Sets what policies to use for validation of entities between the pre-
* processing and post-processing stages. All such policies must pass for the
@@ -286,6 +296,7 @@ export class NextCatalogBuilder {
database: dbClient,
logger,
refreshIntervalSeconds: this.refreshIntervalSeconds,
refreshSpreadSeconds: this.refreshSpreadSeconds,
});
const integrations = ScmIntegrations.fromConfig(config);
const orchestrator = new DefaultCatalogProcessingOrchestrator({
@@ -20,7 +20,11 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { Knex } from 'knex';
import * as uuid from 'uuid';
<<<<<<< HEAD
import { Logger } from 'winston';
=======
import { DateTime } from 'luxon';
>>>>>>> d31a82869 (Catalog: Add refresh spread functionality)
import { DatabaseManager } from './DatabaseManager';
import { DefaultProcessingDatabase } from './DefaultProcessingDatabase';
import {
@@ -47,6 +51,7 @@ describe('Default Processing Database', () => {
database: knex,
logger,
refreshIntervalSeconds: 100,
refreshSpreadSeconds: { min: 10, max: 60 },
}),
};
}
@@ -960,5 +965,44 @@ describe('Default Processing Database', () => {
},
60_000,
);
it.each(databases.eachSupportedId())(
'should update the next_refresh interval with a timestamp that includes refresh spread, %p',
async databaseId => {
const { knex, db } = await createDatabase(databaseId);
const entity = JSON.stringify({
kind: 'Location',
apiVersion: '1.0.0',
metadata: {
name: 'xyz',
},
} as Entity);
await knex<DbRefreshStateRow>('refresh_state').insert({
entity_id: '2',
entity_ref: 'location:default/new-root',
unprocessed_entity: entity,
errors: '[]',
next_update_at: '2019-01-01 23:00:00',
last_discovery_at: '2021-04-01 13:37:00',
});
await db.transaction(async tx => {
// Result does not include the updated timestamp
await db.getProcessableEntities(tx, {
processBatchSize: 1,
});
});
const now = DateTime.local();
const result = await knex<DbRefreshStateRow>('refresh_state')
.where('entity_ref', 'location:default/new-root')
.select();
const nextUpdate = DateTime.fromSQL(result[0].next_update_at, {
zone: 'utc',
});
expect(nextUpdate.diff(now, 'seconds').seconds).toBeGreaterThanOrEqual(
110,
);
},
60_000,
);
});
});
@@ -49,6 +49,10 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
database: Knex;
logger: Logger;
refreshIntervalSeconds: number;
refreshSpreadSeconds: {
min: number;
max: number;
};
},
) {}
@@ -447,6 +451,17 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
}
}
// Returns the next update timestamp by combining refresh interval and refresh spread
private getNextUpdateAt(tx: Knex.Transaction): Knex.MaybeRawColumn<string> {
const { min, max } = this.options.refreshSpreadSeconds;
const refreshSpread = Math.floor(Math.random() * (max - min + 1)) + min;
const nextUpdate = this.options.refreshIntervalSeconds + refreshSpread;
return tx.client.config.client === 'sqlite3'
? tx.raw(`datetime('now', ?)`, [`${nextUpdate} seconds`])
: tx.raw(`now() + interval '${Number(nextUpdate)} seconds'`);
}
async getProcessableEntities(
txOpaque: Transaction,
request: { processBatchSize: number },
@@ -473,16 +488,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
items.map(i => i.entity_ref),
)
.update({
next_update_at:
tx.client.config.client === 'sqlite3'
? tx.raw(`datetime('now', ?)`, [
`${this.options.refreshIntervalSeconds} seconds`,
])
: tx.raw(
`now() + interval '${Number(
this.options.refreshIntervalSeconds,
)} seconds'`,
),
next_update_at: this.getNextUpdateAt(tx),
});
return {