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:
parent
ddcd7f47d8
commit
08a1d053dc
@ -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();
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
});
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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' });
|
||||||
|
3
src/test/fixtures/store.ts
vendored
3
src/test/fixtures/store.ts
vendored
@ -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(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user