1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

chore: improve the performance of our instance stats (#8766)

## About the changes
Our stats are used for many places and many times to publish prometheus
metrics and some other things.

Some of these queries are heavy, traversing all tables to calculate
aggregates.

This adds a feature flag to be able to memoize 1 minute (by default) how
long to keep the calculated values in memory.

We can use the key of the function to individually control which ones
are memoized or not and for how long using a numeric variant.

Initially, this will be disabled and we'll test in our instances first
This commit is contained in:
Gastón Fournier 2024-11-18 09:45:34 +01:00 committed by GitHub
parent 0ce976a0d5
commit 39d227c33b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 265 additions and 73 deletions

View File

@ -6,11 +6,18 @@ import { createFakeGetActiveUsers } from './getActiveUsers';
import { createFakeGetProductionChanges } from './getProductionChanges';
import { registerPrometheusMetrics } from '../../metrics';
import { register } from 'prom-client';
import type { IClientInstanceStore } from '../../types';
import type {
IClientInstanceStore,
IFlagResolver,
IUnleashStores,
} from '../../types';
import { createFakeGetLicensedUsers } from './getLicensedUsers';
let instanceStatsService: InstanceStatsService;
let versionService: VersionService;
let clientInstanceStore: IClientInstanceStore;
let stores: IUnleashStores;
let flagResolver: IFlagResolver;
let updateMetrics: () => Promise<void>;
beforeEach(() => {
jest.clearAllMocks();
@ -18,7 +25,8 @@ beforeEach(() => {
register.clear();
const config = createTestConfig();
const stores = createStores();
flagResolver = config.flagResolver;
stores = createStores();
versionService = new VersionService(
stores,
config,
@ -74,3 +82,96 @@ test('get snapshot should not call getStats', async () => {
test('before the snapshot is refreshed we can still get the appCount', async () => {
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
});
describe.each([true, false])(
'When feature enabled is %s',
(featureEnabled: boolean) => {
beforeEach(() => {
jest.spyOn(flagResolver, 'getVariant').mockReturnValue({
name: 'memorizeStats',
enabled: featureEnabled,
feature_enabled: featureEnabled,
});
});
test(`should${featureEnabled ? ' ' : ' not '}memoize query results`, async () => {
const segmentStore = stores.segmentStore;
jest.spyOn(segmentStore, 'count').mockReturnValue(
Promise.resolve(5),
);
expect(segmentStore.count).toHaveBeenCalledTimes(0);
expect(await instanceStatsService.segmentCount()).toBe(5);
expect(segmentStore.count).toHaveBeenCalledTimes(1);
expect(await instanceStatsService.segmentCount()).toBe(5);
expect(segmentStore.count).toHaveBeenCalledTimes(
featureEnabled ? 1 : 2,
);
});
test(`should${featureEnabled ? ' ' : ' not '}memoize async query results`, async () => {
const trafficDataUsageStore = stores.trafficDataUsageStore;
jest.spyOn(
trafficDataUsageStore,
'getTrafficDataUsageForPeriod',
).mockReturnValue(
Promise.resolve([
{
day: new Date(),
trafficGroup: 'default',
statusCodeSeries: 200,
count: 5,
},
{
day: new Date(),
trafficGroup: 'default',
statusCodeSeries: 400,
count: 2,
},
]),
);
expect(
trafficDataUsageStore.getTrafficDataUsageForPeriod,
).toHaveBeenCalledTimes(0);
expect(await instanceStatsService.getCurrentTrafficData()).toBe(7);
expect(
trafficDataUsageStore.getTrafficDataUsageForPeriod,
).toHaveBeenCalledTimes(1);
expect(await instanceStatsService.getCurrentTrafficData()).toBe(7);
expect(
trafficDataUsageStore.getTrafficDataUsageForPeriod,
).toHaveBeenCalledTimes(featureEnabled ? 1 : 2);
});
test(`getStats should${featureEnabled ? ' ' : ' not '}be memorized`, async () => {
const featureStrategiesReadModel =
stores.featureStrategiesReadModel;
jest.spyOn(
featureStrategiesReadModel,
'getMaxFeatureEnvironmentStrategies',
).mockReturnValue(
Promise.resolve({
feature: 'x',
environment: 'default',
count: 3,
}),
);
expect(
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
).toHaveBeenCalledTimes(0);
expect(
(await instanceStatsService.getStats())
.maxEnvironmentStrategies,
).toBe(3);
expect(
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
).toHaveBeenCalledTimes(1);
expect(
(await instanceStatsService.getStats())
.maxEnvironmentStrategies,
).toBe(3);
expect(
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
).toHaveBeenCalledTimes(featureEnabled ? 1 : 2);
});
},
);

View File

@ -30,7 +30,8 @@ import { CUSTOM_ROOT_ROLE_TYPE } from '../../util';
import type { GetActiveUsers } from './getActiveUsers';
import type { ProjectModeCount } from '../project/project-store';
import type { GetProductionChanges } from './getProductionChanges';
import { format } from 'date-fns';
import { format, minutesToMilliseconds } from 'date-fns';
import memoizee from 'memoizee';
import type { GetLicensedUsers } from './getLicensedUsers';
export type TimeRange = 'allTime' | '30d' | '7d';
@ -58,6 +59,8 @@ export interface InstanceStats {
strategies: number;
SAMLenabled: boolean;
OIDCenabled: boolean;
passwordAuthEnabled: boolean;
SCIMenabled: boolean;
clientApps: { range: TimeRange; count: number }[];
activeUsers: Awaited<ReturnType<GetActiveUsers>>;
licensedUsers: Awaited<ReturnType<GetLicensedUsers>>;
@ -193,55 +196,94 @@ export class InstanceStatsService {
this.trafficDataUsageStore = trafficDataUsageStore;
}
memory = new Map<string, () => Promise<any>>();
memorize<T>(key: string, fn: () => Promise<T>): Promise<T> {
const variant = this.flagResolver.getVariant('memorizeStats', {
memoryKey: key,
});
if (variant.feature_enabled) {
const minutes =
variant.payload?.type === 'number'
? Number(variant.payload.value)
: 1;
let memoizedFunction = this.memory.get(key);
if (!memoizedFunction) {
memoizedFunction = memoizee(() => fn(), {
promise: true,
maxAge: minutesToMilliseconds(minutes),
});
this.memory.set(key, memoizedFunction);
}
return memoizedFunction();
} else {
return fn();
}
}
getProjectModeCount(): Promise<ProjectModeCount[]> {
return this.projectStore.getProjectModeCounts();
return this.memorize('getProjectModeCount', () =>
this.projectStore.getProjectModeCounts(),
);
}
getToggleCount(): Promise<number> {
return this.featureToggleStore.count({
archived: false,
});
return this.memorize('getToggleCount', () =>
this.featureToggleStore.count({
archived: false,
}),
);
}
getArchivedToggleCount(): Promise<number> {
return this.featureToggleStore.count({
archived: true,
});
return this.memorize('hasOIDC', () =>
this.featureToggleStore.count({
archived: true,
}),
);
}
async hasOIDC(): Promise<boolean> {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.oidc',
);
return this.memorize('hasOIDC', async () => {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.oidc',
);
return settings?.enabled || false;
return settings?.enabled || false;
});
}
async hasSAML(): Promise<boolean> {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.saml',
);
return this.memorize('hasSAML', async () => {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'unleash.enterprise.auth.saml',
);
return settings?.enabled || false;
return settings?.enabled || false;
});
}
async hasPasswordAuth(): Promise<boolean> {
const settings = await this.settingStore.get<{ disabled: boolean }>(
'unleash.auth.simple',
);
return this.memorize('hasPasswordAuth', async () => {
const settings = await this.settingStore.get<{ disabled: boolean }>(
'unleash.auth.simple',
);
return (
typeof settings?.disabled === 'undefined' ||
settings.disabled === false
);
return (
typeof settings?.disabled === 'undefined' ||
settings.disabled === false
);
});
}
async hasSCIM(): Promise<boolean> {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'scim',
);
return this.memorize('hasSCIM', async () => {
const settings = await this.settingStore.get<{ enabled: boolean }>(
'scim',
);
return settings?.enabled || false;
return settings?.enabled || false;
});
}
async getStats(): Promise<InstanceStats> {
@ -281,8 +323,8 @@ export class InstanceStatsService {
this.getRegisteredUsers(),
this.countServiceAccounts(),
this.countApiTokensByType(),
this.getActiveUsers(),
this.getLicencedUsers(),
this.memorize('getActiveUsers', this.getActiveUsers.bind(this)),
this.memorize('getLicencedUsers', this.getLicencedUsers.bind(this)),
this.getProjectModeCount(),
this.contextFieldCount(),
this.groupCount(),
@ -297,17 +339,39 @@ export class InstanceStatsService {
this.hasPasswordAuth(),
this.hasSCIM(),
this.appCount ? this.appCount : this.getLabeledAppCounts(),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_IMPORTED,
}),
this.getProductionChanges(),
this.memorize('deprecatedFilteredCountFeaturesExported', () =>
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
),
this.memorize('deprecatedFilteredCountFeaturesImported', () =>
this.eventStore.deprecatedFilteredCount({
type: FEATURES_IMPORTED,
}),
),
this.memorize(
'getProductionChanges',
this.getProductionChanges.bind(this),
),
this.countPreviousDayHourlyMetricsBuckets(),
this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
this.featureStrategiesReadModel.getMaxConstraintValues(),
this.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
this.memorize(
'maxFeatureEnvironmentStrategies',
this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies.bind(
this.featureStrategiesReadModel,
),
),
this.memorize(
'maxConstraintValues',
this.featureStrategiesReadModel.getMaxConstraintValues.bind(
this.featureStrategiesReadModel,
),
),
this.memorize(
'maxConstraintsPerStrategy',
this.featureStrategiesReadModel.getMaxConstraintsPerStrategy.bind(
this.featureStrategiesReadModel,
),
),
]);
return {
@ -333,6 +397,8 @@ export class InstanceStatsService {
strategies,
SAMLenabled,
OIDCenabled,
passwordAuthEnabled,
SCIMenabled,
clientApps: Object.entries(clientApps).map(([range, count]) => ({
range: range as TimeRange,
count,
@ -348,82 +414,104 @@ export class InstanceStatsService {
}
groupCount(): Promise<number> {
return this.groupStore.count();
return this.memorize('groupCount', () => this.groupStore.count());
}
roleCount(): Promise<number> {
return this.roleStore.count();
return this.memorize('roleCount', () => this.roleStore.count());
}
customRolesCount(): Promise<number> {
return this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE });
return this.memorize('customRolesCount', () =>
this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE }),
);
}
customRolesCountInUse(): Promise<number> {
return this.roleStore.filteredCountInUse({
type: CUSTOM_ROOT_ROLE_TYPE,
});
return this.memorize('customRolesCountInUse', () =>
this.roleStore.filteredCountInUse({
type: CUSTOM_ROOT_ROLE_TYPE,
}),
);
}
segmentCount(): Promise<number> {
return this.segmentStore.count();
return this.memorize('segmentCount', () => this.segmentStore.count());
}
contextFieldCount(): Promise<number> {
return this.contextFieldStore.count();
return this.memorize('contextFieldCount', () =>
this.contextFieldStore.count(),
);
}
strategiesCount(): Promise<number> {
return this.strategyStore.count();
return this.memorize('strategiesCount', () =>
this.strategyStore.count(),
);
}
environmentCount(): Promise<number> {
return this.environmentStore.count();
return this.memorize('environmentCount', () =>
this.environmentStore.count(),
);
}
countPreviousDayHourlyMetricsBuckets(): Promise<{
enabledCount: number;
variantCount: number;
}> {
return this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets();
return this.memorize('countPreviousDayHourlyMetricsBuckets', () =>
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
);
}
countApiTokensByType(): Promise<Map<string, number>> {
return this.apiTokenStore.countByType();
return this.memorize('countApiTokensByType', () =>
this.apiTokenStore.countByType(),
);
}
getRegisteredUsers(): Promise<number> {
return this.userStore.count();
return this.memorize('getRegisteredUsers', () =>
this.userStore.count(),
);
}
countServiceAccounts(): Promise<number> {
return this.userStore.countServiceAccounts();
return this.memorize('countServiceAccounts', () =>
this.userStore.countServiceAccounts(),
);
}
async getCurrentTrafficData(): Promise<number> {
const traffic =
await this.trafficDataUsageStore.getTrafficDataUsageForPeriod(
format(new Date(), 'yyyy-MM'),
);
return this.memorize('getCurrentTrafficData', async () => {
const traffic =
await this.trafficDataUsageStore.getTrafficDataUsageForPeriod(
format(new Date(), 'yyyy-MM'),
);
const counts = traffic.map((item) => item.count);
return counts.reduce((total, current) => total + current, 0);
const counts = traffic.map((item) => item.count);
return counts.reduce((total, current) => total + current, 0);
});
}
async getLabeledAppCounts(): Promise<
Partial<{ [key in TimeRange]: number }>
> {
const [t7d, t30d, allTime] = await Promise.all([
this.clientInstanceStore.getDistinctApplicationsCount(7),
this.clientInstanceStore.getDistinctApplicationsCount(30),
this.clientInstanceStore.getDistinctApplicationsCount(),
]);
this.appCount = {
'7d': t7d,
'30d': t30d,
allTime,
};
return this.appCount;
return this.memorize('getLabeledAppCounts', async () => {
const [t7d, t30d, allTime] = await Promise.all([
this.clientInstanceStore.getDistinctApplicationsCount(7),
this.clientInstanceStore.getDistinctApplicationsCount(30),
this.clientInstanceStore.getDistinctApplicationsCount(),
]);
this.appCount = {
'7d': t7d,
'30d': t30d,
allTime,
};
return this.appCount;
});
}
getAppCountSnapshot(range: TimeRange): number | undefined {

View File

@ -85,6 +85,8 @@ class InstanceAdminController extends Controller {
return {
OIDCenabled: true,
SAMLenabled: false,
passwordAuthEnabled: true,
SCIMenabled: false,
clientApps: [
{ range: 'allTime', count: 15 },
{ range: '30d', count: 9 },

View File

@ -61,7 +61,8 @@ export type IFlagKey =
| 'simplifyProjectOverview'
| 'flagOverviewRedesign'
| 'showUserDeviceCount'
| 'deleteStaleUserSessions';
| 'deleteStaleUserSessions'
| 'memorizeStats';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;