1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: add job that cleans last seen every 24 hours (#5114)

This PR adds a cleanup job that removes unknown feature flags from
last_seen_at_metrics table every 24 hours since we no longer have a
foreign key on the name column in the features table.
This commit is contained in:
Fredrik Strand Oseberg 2023-10-23 11:26:48 +02:00 committed by GitHub
parent ddcd7f47d8
commit 08a1d053dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 10 deletions

View File

@ -44,7 +44,7 @@ async function createApp(
const stores = createStores(config, db); const stores = createStores(config, db);
const services = createServices(stores, config, db); const services = createServices(stores, config, db);
if (!config.disableScheduler) { if (!config.disableScheduler) {
await scheduleServices(services); await scheduleServices(services, config.flagResolver);
} }
const metricsMonitor = createMetricsMonitor(); const metricsMonitor = createMetricsMonitor();

View File

@ -6,4 +6,8 @@ export class FakeLastSeenStore implements ILastSeenStore {
data.map((lastSeen) => lastSeen); data.map((lastSeen) => lastSeen);
return Promise.resolve(); return Promise.resolve();
} }
cleanLastSeen(): Promise<void> {
return Promise.resolve();
}
} }

View File

@ -29,7 +29,6 @@ export class LastSeenService {
lastSeenStore, lastSeenStore,
}: Pick<IUnleashStores, 'featureToggleStore' | 'lastSeenStore'>, }: Pick<IUnleashStores, 'featureToggleStore' | 'lastSeenStore'>,
config: IUnleashConfig, config: IUnleashConfig,
lastSeenInterval = secondsToMilliseconds(30),
) { ) {
this.lastSeenStore = lastSeenStore; this.lastSeenStore = lastSeenStore;
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
@ -37,10 +36,6 @@ export class LastSeenService {
'/services/client-metrics/last-seen-service.ts', '/services/client-metrics/last-seen-service.ts',
); );
this.config = config; this.config = config;
this.timers.push(
setInterval(() => this.store(), lastSeenInterval).unref(),
);
} }
async store(): Promise<number> { async store(): Promise<number> {
@ -81,6 +76,10 @@ export class LastSeenService {
}); });
} }
async cleanLastSeen() {
await this.lastSeenStore.cleanLastSeen();
}
destroy(): void { destroy(): void {
this.timers.forEach(clearInterval); this.timers.forEach(clearInterval);
} }

View File

@ -58,6 +58,12 @@ export default class LastSeenStore implements ILastSeenStore {
this.logger.error('Could not update lastSeen, error: ', err); this.logger.error('Could not update lastSeen, error: ', err);
} }
} }
async cleanLastSeen() {
await this.db(TABLE)
.whereNotIn('feature_name', this.db.select('name').from('features'))
.del();
}
} }
module.exports = LastSeenStore; module.exports = LastSeenStore;

View File

@ -0,0 +1,82 @@
import dbInit, { ITestDb } from '../../../../../test/e2e/helpers/database-init';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../../../../test/e2e/helpers/test-helper';
import getLogger from '../../../../../test/fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('last_seen_at_service_e2e', getLogger);
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
disableEnvsOnRevive: true,
useLastSeenRefactor: true,
},
},
},
db.rawDatabase,
);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('should clean unknown feature toggle names from last seen store', async () => {
const { lastSeenService, featureToggleService } = app.services;
const clean = ['clean1', 'clean2', 'clean3', 'clean4'];
const dirty = ['dirty1', 'dirty2', 'dirty3', 'dirty4'];
await Promise.all(
clean.map((featureName) =>
featureToggleService.createFeatureToggle(
'default',
{ name: featureName },
'user',
),
),
);
const inserts = [...clean, ...dirty].map((feature) => {
return {
featureName: feature,
environment: 'default',
yes: 1,
no: 0,
appName: 'test',
timestamp: new Date(),
};
});
lastSeenService.updateLastSeen(inserts);
await lastSeenService.store();
// We have no method to get these from the last seen service or any other service or store
let stored = await db.rawDatabase.raw(
'SELECT * FROM last_seen_at_metrics;',
);
expect(stored.rows.length).toBe(8);
await lastSeenService.cleanLastSeen();
stored = await db.rawDatabase.raw('SELECT * FROM last_seen_at_metrics;');
expect(stored.rows.length).toBe(4);
expect(stored.rows).toMatch;
const notInDirty = stored.rows.filter(
(row) => !dirty.includes(row.feature_name),
);
expect(notInDirty.length).toBe(4);
});

View File

@ -2,4 +2,5 @@ import { LastSeenInput } from '../last-seen-service';
export interface ILastSeenStore { export interface ILastSeenStore {
setLastSeen(data: LastSeenInput[]): Promise<void>; setLastSeen(data: LastSeenInput[]): Promise<void>;
cleanLastSeen: () => Promise<void>;
} }

View File

@ -1,4 +1,9 @@
import { IUnleashConfig, IUnleashStores, IUnleashServices } from '../types'; import {
IUnleashConfig,
IUnleashStores,
IUnleashServices,
IFlagResolver,
} from '../types';
import FeatureTypeService from './feature-type-service'; import FeatureTypeService from './feature-type-service';
import EventService from './event-service'; import EventService from './event-service';
import HealthService from './health-service'; import HealthService from './health-service';
@ -98,6 +103,7 @@ import { ClientFeatureToggleService } from '../features/client-feature-toggles/c
// TODO: will be moved to scheduler feature directory // TODO: will be moved to scheduler feature directory
export const scheduleServices = async ( export const scheduleServices = async (
services: IUnleashServices, services: IUnleashServices,
flagResolver: IFlagResolver,
): Promise<void> => { ): Promise<void> => {
const { const {
schedulerService, schedulerService,
@ -110,12 +116,27 @@ export const scheduleServices = async (
maintenanceService, maintenanceService,
eventAnnouncerService, eventAnnouncerService,
featureToggleService, featureToggleService,
lastSeenService,
} = services; } = services;
if (await maintenanceService.isMaintenanceMode()) { if (await maintenanceService.isMaintenanceMode()) {
schedulerService.pause(); schedulerService.pause();
} }
if (flagResolver.isEnabled('useLastSeenRefactor')) {
schedulerService.schedule(
lastSeenService.cleanLastSeen.bind(lastSeenService),
hoursToMilliseconds(1),
'cleanLastSeen',
);
}
schedulerService.schedule(
lastSeenService.store.bind(lastSeenService),
secondsToMilliseconds(30),
'storeLastSeen',
);
schedulerService.schedule( schedulerService.schedule(
apiTokenService.fetchActiveTokens.bind(apiTokenService), apiTokenService.fetchActiveTokens.bind(apiTokenService),
minutesToMilliseconds(1), minutesToMilliseconds(1),

View File

@ -68,7 +68,6 @@ test('Should not update last seen toggles with 0 metrics', async () => {
featureToggleStore: stores.featureToggleStore, featureToggleStore: stores.featureToggleStore,
}, },
config, config,
30,
); );
const time = Date.now(); const time = Date.now();
await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb1' });
@ -115,7 +114,6 @@ test('Should not update anything for 0 toggles', async () => {
featureToggleStore: stores.featureToggleStore, featureToggleStore: stores.featureToggleStore,
}, },
config, config,
30,
); );
const time = Date.now(); const time = Date.now();
await stores.featureToggleStore.create('default', { name: 'tb1' }); await stores.featureToggleStore.create('default', { name: 'tb1' });

View File

@ -38,6 +38,7 @@ import FakeFavoriteProjectsStore from './fake-favorite-projects-store';
import { FakeAccountStore } from './fake-account-store'; import { FakeAccountStore } from './fake-account-store';
import FakeProjectStatsStore from './fake-project-stats-store'; import FakeProjectStatsStore from './fake-project-stats-store';
import { FakeDependentFeaturesStore } from '../../lib/features/dependent-features/fake-dependent-features-store'; import { FakeDependentFeaturesStore } from '../../lib/features/dependent-features/fake-dependent-features-store';
import { FakeLastSeenStore } from '../../lib/services/client-metrics/last-seen/fake-last-seen-store';
const db = { const db = {
select: () => ({ select: () => ({
@ -85,7 +86,7 @@ const createStores: () => IUnleashStores = () => {
importTogglesStore: {} as IImportTogglesStore, importTogglesStore: {} as IImportTogglesStore,
privateProjectStore: {} as IPrivateProjectStore, privateProjectStore: {} as IPrivateProjectStore,
dependentFeaturesStore: new FakeDependentFeaturesStore(), dependentFeaturesStore: new FakeDependentFeaturesStore(),
lastSeenStore: { setLastSeen: async () => {} }, lastSeenStore: new FakeLastSeenStore(),
}; };
}; };