Skip to content

Commit b9ff371

Browse files
authored
feat(query-ochestrator): Reduce number of cache set for used flag (#9828)
Right now, we use both flags: used and touch. We already had LRU cache for a long time with the touch flag to reduce the number of updates in the Cache Store, but we didn't do it for the used. This leads to a situation where, for a schema with 100 pre-aggs, we will do 200 cache sets per 1 minute because the default refresh interval is set to 30 seconds.
1 parent 394eb84 commit b9ff371

File tree

3 files changed

+52
-16
lines changed

3 files changed

+52
-16
lines changed

packages/cubejs-backend-shared/src/env.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,13 @@ const variables: Record<string, (...args: any) => any> = {
649649
.default(8192)
650650
.asInt(),
651651

652+
/**
653+
* Max number of elements
654+
*/
655+
usedPreAggregationCacheMaxCount: (): number => get('CUBEJS_USED_PRE_AGG_CACHE_MAX_COUNT')
656+
.default(8192)
657+
.asInt(),
658+
652659
/**
653660
* Max number of elements
654661
*/

packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,11 @@ type PreAggregationQueryBody = QueryBody & {
242242
};
243243

244244
export class PreAggregations {
245-
public options: PreAggregationsOptions;
245+
public readonly options: PreAggregationsOptions;
246246

247-
public externalDriverFactory: DriverFactory;
247+
public readonly externalDriverFactory: DriverFactory;
248248

249-
public structureVersionPersistTime: any;
249+
public readonly structureVersionPersistTime: any;
250250

251251
private readonly touchTablePersistTime: number;
252252

@@ -260,6 +260,8 @@ export class PreAggregations {
260260

261261
private readonly queue: Record<string, QueryQueue> = {};
262262

263+
private readonly usedCache: LRUCache<string, true>;
264+
263265
private readonly touchCache: LRUCache<string, true>;
264266

265267
public constructor(
@@ -277,6 +279,25 @@ export class PreAggregations {
277279
this.dropPreAggregationsWithoutTouch = options.dropPreAggregationsWithoutTouch || getEnv('dropPreAggregationsWithoutTouch');
278280
this.usedTablePersistTime = options.usedTablePersistTime || getEnv('dbQueryTimeout');
279281
this.externalRefresh = options.externalRefresh;
282+
283+
/**
284+
* Comment for both caches: used and touch:
285+
*
286+
* Memory usage: By default, it defines max as 8096 keys,
287+
* let's assume that avg key size is 64 symbols, it's around 100 bytes
288+
* with V8's internal stuff:
289+
*
290+
* 100 x 8096 = 809 bytes, max 1Mb in memory.
291+
*
292+
* However, this is a theoretical limit. In practice, even a couple of thousand
293+
* is a very large number
294+
*/
295+
this.usedCache = new LRUCache({
296+
max: getEnv('usedPreAggregationCacheMaxCount'),
297+
ttl: Math.round(this.usedTablePersistTime / 2) * 1000,
298+
allowStale: false,
299+
updateAgeOnGet: false
300+
});
280301
this.touchCache = new LRUCache({
281302
max: getEnv('touchPreAggregationCacheMaxCount'),
282303
ttl: getEnv('touchPreAggregationCacheMaxAge') * 1000,
@@ -301,11 +322,23 @@ export class PreAggregations {
301322
}
302323

303324
public async addTableUsed(tableName: string): Promise<void> {
304-
await this.queryCache.getCacheDriver().set(
305-
this.tablesUsedRedisKey(tableName),
306-
true,
307-
this.usedTablePersistTime
308-
);
325+
if (this.usedCache.has(tableName)) {
326+
return;
327+
}
328+
329+
try {
330+
this.usedCache.set(tableName, true);
331+
332+
await this.queryCache.getCacheDriver().set(
333+
this.tablesUsedRedisKey(tableName),
334+
true,
335+
this.usedTablePersistTime
336+
);
337+
} catch (e: unknown) {
338+
this.usedCache.delete(tableName);
339+
340+
throw e;
341+
}
309342
}
310343

311344
public async tablesUsed() {

packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export class QueryCache {
130130
protected memoryCache: LRUCache<string, CacheEntry>;
131131

132132
public constructor(
133-
protected readonly redisPrefix: string,
133+
protected readonly cachePrefix: string,
134134
protected readonly driverFactory: DriverFactoryByDataSource,
135135
protected readonly logger: any,
136136
public readonly options: QueryCacheOptions
@@ -165,11 +165,7 @@ export class QueryCache {
165165
}
166166

167167
public getKey(catalog: string, key: string): string {
168-
if (this.cacheDriver instanceof CubeStoreCacheDriver) {
169-
return `${this.redisPrefix}#${catalog}:${key}`;
170-
} else {
171-
return `${catalog}_${this.redisPrefix}_${key}`;
172-
}
168+
return `${this.cachePrefix}#${catalog}:${key}`;
173169
}
174170

175171
/**
@@ -469,7 +465,7 @@ export class QueryCache {
469465
const queueOptions = await this.options.queueOptions(dataSource);
470466
if (!this.queue[dataSource]) {
471467
this.queue[dataSource] = QueryCache.createQueue(
472-
`SQL_QUERY_${this.redisPrefix}_${dataSource}`,
468+
`SQL_QUERY_${this.cachePrefix}_${dataSource}`,
473469
() => this.driverFactory(dataSource),
474470
(client, req) => {
475471
this.logger('Executing SQL', { ...req });
@@ -537,7 +533,7 @@ export class QueryCache {
537533
public getExternalQueue() {
538534
if (!this.externalQueue) {
539535
this.externalQueue = QueryCache.createQueue(
540-
`SQL_QUERY_EXT_${this.redisPrefix}`,
536+
`SQL_QUERY_EXT_${this.cachePrefix}`,
541537
this.options.externalDriverFactory,
542538
(client, q) => {
543539
this.logger('Executing SQL', {

0 commit comments

Comments
 (0)