1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00
unleash.unleash/src/lib/db/client-instance-store.ts
Jaanus Sellin 3c73ce9dd9
fix: increase performance of outdated SDK query (#7226)
Joining might not always be the best solution. If a table contains too
much data, and you later run sorting on top of it, it will be slow.

In this case, we will first reduce the instances table to a minimal
version because instances usually share the same SDK versions. Only
after that, we join.

Based on some customer data, we reduced query time from 3000ms to 60ms.
However, this will vary based on the number of instances the customer
has.
2024-05-31 12:39:52 +03:00

275 lines
7.5 KiB
TypeScript

import type EventEmitter from 'events';
import type { Logger, LogProvider } from '../logger';
import type {
IClientInstance,
IClientInstanceStore,
INewClientInstance,
} from '../types/stores/client-instance-store';
import { subDays } from 'date-fns';
import type { Db } from './db';
const metricsHelper = require('../util/metrics-helper');
const { DB_TIME } = require('../metric-events');
const COLUMNS = [
'app_name',
'instance_id',
'sdk_version',
'client_ip',
'last_seen',
'created_at',
'environment',
];
const TABLE = 'client_instances';
const mapRow = (row) => ({
appName: row.app_name,
instanceId: row.instance_id,
sdkVersion: row.sdk_version,
clientIp: row.client_ip,
lastSeen: row.last_seen,
createdAt: row.created_at,
environment: row.environment,
});
const mapToDb = (client) => ({
app_name: client.appName,
instance_id: client.instanceId,
sdk_version: client.sdkVersion || '',
client_ip: client.clientIp,
last_seen: client.lastSeen || 'now()',
environment: client.environment || 'default',
});
export default class ClientInstanceStore implements IClientInstanceStore {
private db: Db;
private logger: Logger;
private eventBus: EventEmitter;
private metricTimer: Function;
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
this.db = db;
this.eventBus = eventBus;
this.logger = getLogger('client-instance-store.ts');
this.metricTimer = (action) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'instance',
action,
});
}
async removeInstancesOlderThanTwoDays(): Promise<void> {
const rows = await this.db(TABLE)
.whereRaw("created_at < now() - interval '2 days'")
.del();
if (rows > 0) {
this.logger.debug(`Deleted ${rows} instances`);
}
}
async setLastSeen({
appName,
instanceId,
environment,
clientIp,
}: INewClientInstance): Promise<void> {
await this.db(TABLE)
.insert({
app_name: appName,
instance_id: instanceId,
environment,
last_seen: new Date(),
client_ip: clientIp,
})
.onConflict(['app_name', 'instance_id', 'environment'])
.merge({
last_seen: new Date(),
client_ip: clientIp,
});
}
async bulkUpsert(instances: INewClientInstance[]): Promise<void> {
const rows = instances.map(mapToDb);
await this.db(TABLE)
.insert(rows)
.onConflict(['app_name', 'instance_id', 'environment'])
.merge();
}
async delete({
appName,
instanceId,
}: Pick<INewClientInstance, 'appName' | 'instanceId'>): Promise<void> {
await this.db(TABLE)
.where({
app_name: appName,
instance_id: instanceId,
})
.del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
async get({
appName,
instanceId,
}: Pick<
INewClientInstance,
'appName' | 'instanceId'
>): Promise<IClientInstance> {
const row = await this.db(TABLE)
.where({
app_name: appName,
instance_id: instanceId,
})
.first();
return mapRow(row);
}
async exists({
appName,
instanceId,
}: Pick<INewClientInstance, 'appName' | 'instanceId'>): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE app_name = ? AND instance_id = ?) AS present`,
[appName, instanceId],
);
const { present } = result.rows[0];
return present;
}
async insert(details: INewClientInstance): Promise<void> {
const stopTimer = this.metricTimer('insert');
await this.db(TABLE)
.insert(mapToDb(details))
.onConflict(['app_name', 'instance_id', 'environment'])
.merge();
stopTimer();
}
async getAll(): Promise<IClientInstance[]> {
const stopTimer = this.metricTimer('getAll');
const rows = await this.db
.select(COLUMNS)
.from(TABLE)
.orderBy('last_seen', 'desc');
const toggles = rows.map(mapRow);
stopTimer();
return toggles;
}
async getByAppName(appName: string): Promise<IClientInstance[]> {
const rows = await this.db
.select()
.from(TABLE)
.where('app_name', appName)
.orderBy('last_seen', 'desc');
return rows.map(mapRow);
}
async getByAppNameAndEnvironment(
appName: string,
environment: string,
): Promise<IClientInstance[]> {
const rows = await this.db
.select()
.from(TABLE)
.where('app_name', appName)
.where('environment', environment)
.orderBy('last_seen', 'desc')
.limit(1000);
return rows.map(mapRow);
}
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
const sdkPrefix = `${sdkName}%`;
const rows = await this.db
.select()
.from(TABLE)
.whereLike('sdk_version', sdkPrefix)
.orderBy('last_seen', 'desc');
return rows.map(mapRow);
}
async groupApplicationsBySdk(): Promise<
{ sdkVersion: string; applications: string[] }[]
> {
const rows = await this.db
.select([
'sdk_version as sdkVersion',
this.db.raw('ARRAY_AGG(DISTINCT app_name) as applications'),
])
.from(TABLE)
.groupBy('sdk_version');
return rows;
}
async groupApplicationsBySdkAndProject(
projectId: string,
): Promise<{ sdkVersion: string; applications: string[] }[]> {
const rows = await this.db
.with(
'instances',
this.db
.select('app_name', 'sdk_version')
.distinct()
.from('client_instances'),
)
.select([
'i.sdk_version as sdkVersion',
this.db.raw('ARRAY_AGG(DISTINCT cme.app_name) as applications'),
])
.from('client_metrics_env as cme')
.leftJoin('features as f', 'f.name', 'cme.feature_name')
.leftJoin('instances as i', 'i.app_name', 'cme.app_name')
.where('f.project', projectId)
.groupBy('i.sdk_version');
return rows;
}
async getDistinctApplications(): Promise<string[]> {
const rows = await this.db
.distinct('app_name')
.select(['app_name'])
.from(TABLE)
.orderBy('app_name', 'desc');
return rows.map((r) => r.app_name);
}
async getDistinctApplicationsCount(daysBefore?: number): Promise<number> {
let query = this.db.from(TABLE);
if (daysBefore) {
query = query.where(
'last_seen',
'>',
subDays(new Date(), daysBefore),
);
}
return query
.countDistinct('app_name')
.then((res) => Number(res[0].count));
}
async deleteForApplication(appName: string): Promise<void> {
return this.db(TABLE).where('app_name', appName).del();
}
destroy(): void {}
}