1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

Client instance service (#5126)

This commit is contained in:
Mateusz Kwasniewski 2023-10-23 15:22:30 +02:00 committed by GitHub
parent 1d1aa27ca3
commit 8d8a975c6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 32 additions and 180 deletions

View File

@ -24,9 +24,6 @@ async function getSetup() {
base, base,
clientFeatureToggleStore: stores.clientFeatureToggleStore, clientFeatureToggleStore: stores.clientFeatureToggleStore,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
@ -43,7 +40,6 @@ const callGetAll = async (controller: FeatureController) => {
let base; let base;
let request; let request;
let destroy;
let flagResolver; let flagResolver;
@ -51,7 +47,6 @@ beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
request = setup.request; request = setup.request;
destroy = setup.destroy;
flagResolver = { flagResolver = {
isEnabled: () => { isEnabled: () => {
return false; return false;
@ -59,10 +54,6 @@ beforeEach(async () => {
}; };
}); });
afterEach(() => {
destroy();
});
test('should get empty getFeatures via client', () => { test('should get empty getFeatures via client', () => {
expect.assertions(1); expect.assertions(1);
return request return request

View File

@ -28,25 +28,16 @@ async function getSetup() {
return { return {
base, base,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let base; let base;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
base = setup.base; base = setup.base;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('should get ui config', async () => { test('should get ui config', async () => {

View File

@ -20,25 +20,16 @@ async function getSetup() {
return { return {
base, base,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let base; let base;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(async () => {
await destroy();
}); });
test('should get all context definitions', () => { test('should get all context definitions', () => {

View File

@ -19,25 +19,16 @@ async function getSetup() {
stores, stores,
perms, perms,
config, config,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('/api/admin/metrics/seen-toggles is deprecated', () => { test('/api/admin/metrics/seen-toggles is deprecated', () => {

View File

@ -33,16 +33,11 @@ describe('Public Signup API', () => {
request: supertest(app), request: supertest(app),
stores, stores,
perms, perms,
destroy: () => {
services.clientInstanceService.destroy();
services.publicSignupTokenService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
const user = { const user = {
username: 'some-username', username: 'some-username',
@ -55,12 +50,8 @@ describe('Public Signup API', () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => {
destroy();
});
const expireAt = (addDays: number = 7): Date => { const expireAt = (addDays: number = 7): Date => {
const now = new Date(); const now = new Date();
now.setDate(now.getDate() + addDays); now.setDate(now.getDate() + addDays);

View File

@ -5,8 +5,6 @@ import permissions from '../../../test/fixtures/permissions';
import getApp from '../../app'; import getApp from '../../app';
import { createServices } from '../../services'; import { createServices } from '../../services';
let destroy;
async function getSetup() { async function getSetup() {
const randomBase = `/random${Math.round(Math.random() * 1000)}`; const randomBase = `/random${Math.round(Math.random() * 1000)}`;
const perms = permissions(); const perms = permissions();
@ -18,10 +16,6 @@ async function getSetup() {
const services = createServices(stores, config); const services = createServices(stores, config);
const app = await getApp(config, stores, services); const app = await getApp(config, stores, services);
destroy = () => {
services.clientInstanceService.destroy();
};
return { return {
base: randomBase, base: randomBase,
strategyStore: stores.strategyStore, strategyStore: stores.strategyStore,
@ -30,10 +24,6 @@ async function getSetup() {
}; };
} }
afterEach(() => {
destroy();
});
test('add version numbers for /strategies', async () => { test('add version numbers for /strategies', async () => {
const { request, base } = await getSetup(); const { request, base } = await getSetup();
return request return request

View File

@ -21,26 +21,18 @@ async function getSetup() {
perms, perms,
tagStore: stores.tagStore, tagStore: stores.tagStore,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let base; let base;
let tagStore; let tagStore;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
tagStore = setup.tagStore; tagStore = setup.tagStore;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('should get empty getTags via admin', () => { test('should get empty getTags via admin', () => {

View File

@ -19,5 +19,4 @@ test('should enable prometheus', async () => {
.get('/internal-backstage/prometheus') .get('/internal-backstage/prometheus')
.expect('Content-Type', /text/) .expect('Content-Type', /text/)
.expect(200); .expect(200);
services.clientInstanceService.destroy();
}); });

View File

@ -19,10 +19,7 @@ async function getSetup(opts?: IUnleashOptions) {
request: supertest(app), request: supertest(app),
stores: db.stores, stores: db.stores,
services, services,
destroy: async () => { destroy: db.destroy,
services.clientInstanceService.destroy();
await db.destroy();
},
}; };
} }
@ -31,7 +28,7 @@ let stores: IUnleashStores;
let services: IUnleashServices; let services: IUnleashServices;
let destroy; let destroy;
beforeEach(async () => { beforeAll(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
stores = setup.stores; stores = setup.stores;
@ -39,10 +36,14 @@ beforeEach(async () => {
services = setup.services; services = setup.services;
}); });
afterEach(() => { afterAll(() => {
destroy(); destroy();
}); });
afterEach(async () => {
await stores.featureToggleStore.deleteAll();
});
test('should validate client metrics', () => { test('should validate client metrics', () => {
return request return request
.post('/api/client/metrics') .post('/api/client/metrics')

View File

@ -14,20 +14,14 @@ async function getSetup() {
return { return {
request: supertest(app), request: supertest(app),
stores, stores,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => { afterEach(() => {
destroy();
getLogger.setMuteError(false); getLogger.setMuteError(false);
}); });

View File

@ -15,21 +15,15 @@ async function getSetup() {
return { return {
request: supertest(app), request: supertest(app),
stores, stores,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => { afterEach(() => {
destroy();
getLogger.setMuteError(false); getLogger.setMuteError(false);
}); });

View File

@ -39,16 +39,11 @@ describe('Public Signup API', () => {
request: supertest(app), request: supertest(app),
stores, stores,
perms, perms,
destroy: () => {
services.clientInstanceService.destroy();
services.publicSignupTokenService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
const user = { const user = {
username: 'some-username', username: 'some-username',
@ -61,12 +56,8 @@ describe('Public Signup API', () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => {
destroy();
});
const expireAt = (addDays: number = 7): Date => { const expireAt = (addDays: number = 7): Date => {
const now = new Date(); const now = new Date();
now.setDate(now.getDate() + addDays); now.setDate(now.getDate() + addDays);

View File

@ -5,45 +5,11 @@ import FakeEventStore from '../../../test/fixtures/fake-event-store';
import { createTestConfig } from '../../../test/config/test-config'; import { createTestConfig } from '../../../test/config/test-config';
import { FakePrivateProjectChecker } from '../../features/private-project/fakePrivateProjectChecker'; import { FakePrivateProjectChecker } from '../../features/private-project/fakePrivateProjectChecker';
/**
* A utility to wait for any pending promises in the test subject code.
* For instance, if the test needs to wait for a timeout/interval handler,
* and that handler does something async, advancing the timers is not enough:
* We have to explicitly wait for the second promise.
* For more info, see https://stackoverflow.com/a/51045733/2868829
*
* Usage in test code after advancing timers, but before making assertions:
*
* test('hello', async () => {
* jest.useFakeTimers('modern');
*
* // Schedule a timeout with a callback that does something async
* // before calling our spy
* const spy = jest.fn();
* setTimeout(async () => {
* await Promise.resolve();
* spy();
* }, 1000);
*
* expect(spy).not.toHaveBeenCalled();
*
* jest.advanceTimersByTime(1500);
* await flushPromises(); // this is required to make it work!
*
* expect(spy).toHaveBeenCalledTimes(1);
*
* jest.useRealTimers();
* });
*/
function flushPromises() {
return Promise.resolve(setImmediate);
}
let config; let config;
beforeAll(() => { beforeAll(() => {
config = createTestConfig({}); config = createTestConfig({});
}); });
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => { test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -75,8 +41,8 @@ test('Multiple registrations of same appname and instanceid within same time per
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(7000);
await flushPromises(); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
expect(appStoreSpy).toHaveBeenCalledTimes(1); expect(appStoreSpy).toHaveBeenCalledTimes(1);
expect(bulkSpy).toHaveBeenCalledTimes(1); expect(bulkSpy).toHaveBeenCalledTimes(1);
@ -93,7 +59,6 @@ test('Multiple registrations of same appname and instanceid within same time per
}); });
test('Multiple unique clients causes multiple registrations', async () => { test('Multiple unique clients causes multiple registrations', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -136,16 +101,14 @@ test('Multiple unique clients causes multiple registrations', async () => {
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
jest.advanceTimersByTime(7000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await flushPromises();
const registrations = appStoreSpy.mock.calls[0][0]; const registrations = appStoreSpy.mock.calls[0][0];
expect(registrations.length).toBe(2); expect(registrations.length).toBe(2);
jest.useRealTimers();
}); });
test('Same client registered outside of dedup interval will be registered twice', async () => { test('Same client registered outside of dedup interval will be registered twice', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -155,8 +118,6 @@ test('Same client registered outside of dedup interval will be registered twice'
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
const bulkInterval = secondsToMilliseconds(2);
const clientMetrics = new ClientInstanceService( const clientMetrics = new ClientInstanceService(
{ {
clientMetricsStoreV2: null, clientMetricsStoreV2: null,
@ -168,7 +129,6 @@ test('Same client registered outside of dedup interval will be registered twice'
}, },
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
bulkInterval,
); );
const client1 = { const client1 = {
appName: 'test_app', appName: 'test_app',
@ -181,14 +141,13 @@ test('Same client registered outside of dedup interval will be registered twice'
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await flushPromises();
expect(appStoreSpy).toHaveBeenCalledTimes(2); expect(appStoreSpy).toHaveBeenCalledTimes(2);
expect(bulkSpy).toHaveBeenCalledTimes(2); expect(bulkSpy).toHaveBeenCalledTimes(2);
@ -198,11 +157,9 @@ test('Same client registered outside of dedup interval will be registered twice'
expect(firstRegistrations.appName).toBe(secondRegistrations.appName); expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId); expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
jest.useRealTimers();
}); });
test('No registrations during a time period will not call stores', async () => { test('No registrations during a time period will not call stores', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -211,7 +168,7 @@ test('No registrations during a time period will not call stores', async () => {
const clientInstanceStore: any = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
new ClientInstanceService( const clientMetrics = new ClientInstanceService(
{ {
clientMetricsStoreV2: null, clientMetricsStoreV2: null,
strategyStore: null, strategyStore: null,
@ -223,8 +180,9 @@ test('No registrations during a time period will not call stores', async () => {
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
); );
jest.advanceTimersByTime(6000);
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
expect(appStoreSpy).toHaveBeenCalledTimes(0); expect(appStoreSpy).toHaveBeenCalledTimes(0);
expect(bulkSpy).toHaveBeenCalledTimes(0); expect(bulkSpy).toHaveBeenCalledTimes(0);
jest.useRealTimers();
}); });

View File

@ -29,8 +29,6 @@ export default class ClientInstanceService {
seenClients: Record<string, IClientApp> = {}; seenClients: Record<string, IClientApp> = {};
private timers: NodeJS.Timeout[] = [];
private clientMetricsStoreV2: IClientMetricsStoreV2; private clientMetricsStoreV2: IClientMetricsStoreV2;
private strategyStore: IStrategyStore; private strategyStore: IStrategyStore;
@ -47,10 +45,6 @@ export default class ClientInstanceService {
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
private bulkInterval: number;
private announcementInterval: number;
constructor( constructor(
{ {
clientMetricsStoreV2, clientMetricsStoreV2,
@ -73,8 +67,6 @@ export default class ClientInstanceService {
flagResolver, flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>, }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
privateProjectChecker: IPrivateProjectChecker, privateProjectChecker: IPrivateProjectChecker,
bulkInterval = secondsToMilliseconds(5),
announcementInterval = minutesToMilliseconds(5),
) { ) {
this.clientMetricsStoreV2 = clientMetricsStoreV2; this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.strategyStore = strategyStore; this.strategyStore = strategyStore;
@ -87,18 +79,6 @@ export default class ClientInstanceService {
this.logger = getLogger( this.logger = getLogger(
'/services/client-metrics/client-instance-service.ts', '/services/client-metrics/client-instance-service.ts',
); );
this.bulkInterval = bulkInterval;
this.announcementInterval = announcementInterval;
this.timers.push(
setInterval(() => this.bulkAdd(), this.bulkInterval).unref(),
);
this.timers.push(
setInterval(
() => this.announceUnannounced(),
this.announcementInterval,
).unref(),
);
} }
public async registerInstance( public async registerInstance(
@ -248,8 +228,4 @@ export default class ClientInstanceService {
async removeInstancesOlderThanTwoDays(): Promise<void> { async removeInstancesOlderThanTwoDays(): Promise<void> {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays(); return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
} }
destroy(): void {
this.timers.forEach(clearInterval);
}
} }

View File

@ -165,6 +165,18 @@ export const scheduleServices = async (
'removeInstancesOlderThanTwoDays', 'removeInstancesOlderThanTwoDays',
); );
schedulerService.schedule(
clientInstanceService.bulkAdd.bind(clientInstanceService),
secondsToMilliseconds(5),
'bulkAddInstances',
);
schedulerService.schedule(
clientInstanceService.announceUnannounced.bind(clientInstanceService),
minutesToMilliseconds(5),
'announceUnannounced',
);
schedulerService.schedule( schedulerService.schedule(
projectService.statusJob.bind(projectService), projectService.statusJob.bind(projectService),
hoursToMilliseconds(24), hoursToMilliseconds(24),

View File

@ -30,8 +30,6 @@ export class PublicSignupTokenService {
private logger: Logger; private logger: Logger;
private timer: NodeJS.Timeout;
private readonly unleashBase: string; private readonly unleashBase: string;
constructor( constructor(
@ -146,9 +144,4 @@ export class PublicSignupTokenService {
private getMinimumDate(date1: Date, date2: Date): Date { private getMinimumDate(date1: Date, date2: Date): Date {
return date1 < date2 ? date1 : date2; return date1 < date2 ? date1 : date2;
} }
destroy(): void {
clearInterval(this.timer);
this.timer = null;
}
} }

View File

@ -12,7 +12,7 @@ const { APPLICATION_CREATED } = require('../../../lib/types/events');
let stores; let stores;
let db; let db;
let clientInstanceService; let clientInstanceService: ClientInstanceService;
let config: IUnleashConfig; let config: IUnleashConfig;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('client_metrics_service_serial', getLogger); db = await dbInit('client_metrics_service_serial', getLogger);
@ -25,13 +25,10 @@ beforeAll(async () => {
stores, stores,
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
bulkInterval,
announcementInterval,
); );
}); });
afterAll(async () => { afterAll(async () => {
await clientInstanceService.destroy();
await db.destroy(); await db.destroy();
}); });
test('Apps registered should be announced', async () => { test('Apps registered should be announced', async () => {
@ -58,11 +55,11 @@ test('Apps registered should be announced', async () => {
}; };
await clientInstanceService.registerClient(clientRegistration, '127.0.0.1'); await clientInstanceService.registerClient(clientRegistration, '127.0.0.1');
await clientInstanceService.registerClient(differentClient, '127.0.0.1'); await clientInstanceService.registerClient(differentClient, '127.0.0.1');
await new Promise((res) => setTimeout(res, 1200)); await clientInstanceService.bulkAdd(); // in prod called by a SchedulerService
const first = await stores.clientApplicationsStore.getUnannounced(); const first = await stores.clientApplicationsStore.getUnannounced();
expect(first.length).toBe(2); expect(first.length).toBe(2);
await clientInstanceService.registerClient(clientRegistration, '127.0.0.1'); await clientInstanceService.registerClient(clientRegistration, '127.0.0.1');
await new Promise((res) => setTimeout(res, secondsToMilliseconds(2))); await clientInstanceService.announceUnannounced(); // in prod called by a SchedulerService
const second = await stores.clientApplicationsStore.getUnannounced(); const second = await stores.clientApplicationsStore.getUnannounced();
expect(second.length).toBe(0); expect(second.length).toBe(0);
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();