mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
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.
275 lines
7.5 KiB
TypeScript
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 {}
|
|
}
|