Skip to content

Commit 9e08ce0

Browse files
KSDaemonmarianore-muttdata
authored andcommitted
feat(schema-compiler): Use LRUCache for js data models compiled vm.Scripts (cube-js#9424)
* fix types * update lru-cache to the latest * fix lru-cache after upgrade * introduce compiledScriptCache * add proactive cache cleanup * call compilerCache.clear() on shutdown
1 parent f620d89 commit 9e08ce0

File tree

12 files changed

+80
-42
lines changed

12 files changed

+80
-42
lines changed

packages/cubejs-query-orchestrator/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@cubejs-backend/shared": "1.2.30",
3535
"csv-write-stream": "^2.0.0",
3636
"generic-pool": "^3.8.2",
37-
"lru-cache": "^5.1.1",
37+
"lru-cache": "^11.1.0",
3838
"ramda": "^0.27.2"
3939
},
4040
"devDependencies": {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getEnv, } from '@cubejs-backend/shared';
44

55
import { BaseDriver, InlineTable, } from '@cubejs-backend/base-driver';
66
import { CubeStoreDriver } from '@cubejs-backend/cubestore-driver';
7-
import LRUCache from 'lru-cache';
7+
import { LRUCache } from 'lru-cache';
88

99
import { PreAggTableToTempTable, Query, QueryBody, QueryCache, QueryWithParams } from './QueryCache';
1010
import { DriverFactory, DriverFactoryByDataSource } from './DriverFactory';
@@ -282,8 +282,8 @@ export class PreAggregations {
282282
this.getQueueEventsBus = options.getQueueEventsBus;
283283
this.touchCache = new LRUCache({
284284
max: getEnv('touchPreAggregationCacheMaxCount'),
285-
maxAge: getEnv('touchPreAggregationCacheMaxAge') * 1000,
286-
stale: false,
285+
ttl: getEnv('touchPreAggregationCacheMaxAge') * 1000,
286+
allowStale: false,
287287
updateAgeOnGet: false
288288
});
289289
}
@@ -330,7 +330,7 @@ export class PreAggregations {
330330
this.touchTablePersistTime
331331
);
332332
} catch (e: unknown) {
333-
this.touchCache.del(tableName);
333+
this.touchCache.delete(tableName);
334334

335335
throw e;
336336
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import crypto from 'crypto';
22
import csvWriter from 'csv-write-stream';
3-
import LRUCache from 'lru-cache';
3+
import { LRUCache } from 'lru-cache';
44
import { pipeline } from 'stream';
55
import { getEnv, MaybeCancelablePromise, streamToArray } from '@cubejs-backend/shared';
66
import { CubeStoreCacheDriver, CubeStoreDriver } from '@cubejs-backend/cubestore-driver';
@@ -909,7 +909,7 @@ export class QueryCache {
909909
inMemoryValue.renewalKey !== renewalKey
910910
) || renewedAgo > expiration * 1000 || renewedAgo > inMemoryCacheDisablePeriod
911911
) {
912-
this.memoryCache.del(redisKey);
912+
this.memoryCache.delete(redisKey);
913913
} else {
914914
this.logger('Found in memory cache entry', {
915915
cacheKey,

packages/cubejs-schema-compiler/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"inflection": "^1.12.0",
5050
"joi": "^17.8.3",
5151
"js-yaml": "^4.1.0",
52-
"lru-cache": "^5.1.1",
52+
"lru-cache": "^11.1.0",
5353
"moment-timezone": "^0.5.46",
5454
"node-dijkstra": "^2.5.0",
5555
"ramda": "^0.27.2",
@@ -66,7 +66,6 @@
6666
"@types/babel__traverse": "^7.20.5",
6767
"@types/inflection": "^1.5.28",
6868
"@types/jest": "^27",
69-
"@types/lru-cache": "^5.1.0",
7069
"@types/node": "^18",
7170
"@types/ramda": "^0.27.34",
7271
"@types/sqlstring": "^2.3.0",

packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import LRUCache from 'lru-cache';
1+
import { LRUCache } from 'lru-cache';
22
import { QueryCache } from '../adapter/QueryCache';
33

44
export class CompilerCache extends QueryCache {
@@ -11,13 +11,13 @@ export class CompilerCache extends QueryCache {
1111

1212
this.queryCache = new LRUCache({
1313
max: maxQueryCacheSize || 10000,
14-
maxAge: (maxQueryCacheAge * 1000) || 1000 * 60 * 10,
14+
ttl: (maxQueryCacheAge * 1000) || 1000 * 60 * 10,
1515
updateAgeOnGet: true
1616
});
1717

1818
this.rbacCache = new LRUCache({
1919
max: 10000,
20-
maxAge: 1000 * 60 * 5, // 5 minutes
20+
ttl: 1000 * 60 * 5, // 5 minutes
2121
});
2222
}
2323

packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import crypto from 'crypto';
12
import vm from 'vm';
23
import fs from 'fs';
34
import path from 'path';
@@ -47,6 +48,7 @@ export class DataSchemaCompiler {
4748
this.pythonContext = null;
4849
this.workerPool = null;
4950
this.compilerId = options.compilerId;
51+
this.compiledScriptCache = options.compiledScriptCache;
5052
}
5153

5254
compileObjects(compileServices, objects, errorsReport) {
@@ -370,6 +372,18 @@ export class DataSchemaCompiler {
370372
}
371373
}
372374

375+
getJsScript(file) {
376+
const cacheKey = crypto.createHash('md5').update(JSON.stringify(file.content)).digest('hex');
377+
378+
if (this.compiledScriptCache.has(cacheKey)) {
379+
return this.compiledScriptCache.get(cacheKey);
380+
}
381+
382+
const script = new vm.Script(file.content, { filename: file.fileName, timeout: 15000 });
383+
this.compiledScriptCache.set(cacheKey, script);
384+
return script;
385+
}
386+
373387
compileJsFile(file, errorsReport, cubes, contexts, exports, asyncModules, toCompile, compiledFiles, { doSyntaxCheck } = { doSyntaxCheck: false }) {
374388
if (doSyntaxCheck) {
375389
// There is no need to run syntax check for data model files
@@ -382,7 +396,9 @@ export class DataSchemaCompiler {
382396
}
383397

384398
try {
385-
vm.runInNewContext(file.content, {
399+
const script = this.getJsScript(file);
400+
401+
script.runInNewContext({
386402
view: (name, cube) => (
387403
!cube ?
388404
this.cubeFactory({ ...name, fileName: file.fileName, isView: true }) :

packages/cubejs-schema-compiler/src/compiler/PrepareCompiler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { SchemaFileRepository } from '@cubejs-backend/shared';
22
import { NativeInstance } from '@cubejs-backend/native';
33
import { v4 as uuidv4 } from 'uuid';
4+
import { LRUCache } from 'lru-cache';
5+
import vm from 'vm';
46

57
import { CubeValidator } from './CubeValidator';
68
import { DataSchemaCompiler } from './DataSchemaCompiler';
@@ -32,6 +34,7 @@ export type PrepareCompilerOptions = {
3234
standalone?: boolean;
3335
headCommitId?: string;
3436
adapter?: string;
37+
compiledScriptCache?: LRUCache<string, vm.Script>;
3538
};
3639

3740
export const prepareCompiler = (repo: SchemaFileRepository, options: PrepareCompilerOptions = {}) => {
@@ -49,6 +52,8 @@ export const prepareCompiler = (repo: SchemaFileRepository, options: PrepareComp
4952
const compilerCache = new CompilerCache({ maxQueryCacheSize, maxQueryCacheAge });
5053
const yamlCompiler = new YamlCompiler(cubeSymbols, cubeDictionary, nativeInstance, viewCompiler);
5154

55+
const compiledScriptCache = options.compiledScriptCache || new LRUCache<string, vm.Script>({ max: 250 });
56+
5257
const transpilers: TranspilerInterface[] = [
5358
new ValidationTranspiler(),
5459
new ImportExportTranspiler(),
@@ -66,6 +71,7 @@ export const prepareCompiler = (repo: SchemaFileRepository, options: PrepareComp
6671
preTranspileCubeCompilers: [cubeSymbols, cubeValidator],
6772
transpilers,
6873
viewCompilationGate,
74+
compiledScriptCache,
6975
viewCompilers: [viewCompiler],
7076
cubeCompilers: [cubeEvaluator, joinGraph, metaTransformer],
7177
contextCompilers: [contextEvaluator],

packages/cubejs-server-core/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"joi": "^17.8.3",
4747
"jsonwebtoken": "^9.0.2",
4848
"lodash.clonedeep": "^4.5.0",
49-
"lru-cache": "^5.1.1",
49+
"lru-cache": "^11.1.0",
5050
"moment": "^2.29.1",
5151
"node-fetch": "^2.6.0",
5252
"p-limit": "^3.1.0",
@@ -67,7 +67,6 @@
6767
"@types/fs-extra": "^9.0.8",
6868
"@types/jest": "^27",
6969
"@types/jsonwebtoken": "^9.0.2",
70-
"@types/lru-cache": "^5.1.0",
7170
"@types/node": "^18",
7271
"@types/node-fetch": "^2.5.7",
7372
"@types/ramda": "^0.27.34",

packages/cubejs-server-core/src/core/CompilerApi.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import crypto from 'crypto';
22
import R from 'ramda';
33
import { createQuery, compile, queryClass, PreAggregations, QueryFactory } from '@cubejs-backend/schema-compiler';
4-
import { v4 as uuidv4, parse as uuidParse, stringify as uuidStringify } from 'uuid';
4+
import { v4 as uuidv4, parse as uuidParse } from 'uuid';
5+
import { LRUCache } from 'lru-cache';
56
import { NativeInstance } from '@cubejs-backend/native';
67

78
export class CompilerApi {
@@ -29,6 +30,25 @@ export class CompilerApi {
2930
this.sqlCache = options.sqlCache;
3031
this.standalone = options.standalone;
3132
this.nativeInstance = this.createNativeInstance();
33+
this.compiledScriptCache = new LRUCache({
34+
max: options.compilerCacheSize || 250,
35+
ttl: options.maxCompilerCacheKeepAlive,
36+
updateAgeOnGet: options.updateCompilerCacheKeepAlive
37+
});
38+
39+
// proactively free up old cache values occasionally
40+
if (this.options.maxCompilerCacheKeepAlive) {
41+
this.compiledScriptCacheInterval = setInterval(
42+
() => this.compiledScriptCache.purgeStale(),
43+
this.options.maxCompilerCacheKeepAlive
44+
);
45+
}
46+
}
47+
48+
dispose() {
49+
if (this.compiledScriptCacheInterval) {
50+
clearInterval(this.compiledScriptCacheInterval);
51+
}
3252
}
3353

3454
setGraphQLSchema(schema) {
@@ -83,6 +103,7 @@ export class CompilerApi {
83103
allowJsDuplicatePropsInSchema: this.allowJsDuplicatePropsInSchema,
84104
standalone: this.standalone,
85105
nativeInstance: this.nativeInstance,
106+
compiledScriptCache: this.compiledScriptCache,
86107
});
87108
this.queryFactory = await this.createQueryFactory(compilers);
88109

packages/cubejs-server-core/src/core/server.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable global-require,no-return-assign */
22
import crypto from 'crypto';
33
import fs from 'fs-extra';
4-
import LRUCache from 'lru-cache';
4+
import { LRUCache } from 'lru-cache';
55
import isDocker from 'is-docker';
66
import pLimit from 'p-limit';
77

@@ -203,8 +203,10 @@ export class CubejsServerCore {
203203

204204
this.compilerCache = new LRUCache<string, CompilerApi>({
205205
max: this.options.compilerCacheSize || 250,
206-
maxAge: this.options.maxCompilerCacheKeepAlive,
207-
updateAgeOnGet: this.options.updateCompilerCacheKeepAlive
206+
ttl: this.options.maxCompilerCacheKeepAlive,
207+
updateAgeOnGet: this.options.updateCompilerCacheKeepAlive,
208+
// needed to clear the setInterval timer for proactive cache internal cleanups
209+
dispose: (v) => v.dispose(),
208210
});
209211

210212
if (this.options.contextToAppId) {
@@ -224,7 +226,7 @@ export class CubejsServerCore {
224226
// proactively free up old cache values occasionally
225227
if (this.options.maxCompilerCacheKeepAlive) {
226228
this.maxCompilerCacheKeep = setInterval(
227-
() => this.compilerCache.prune(),
229+
() => this.compilerCache.purgeStale(),
228230
this.options.maxCompilerCacheKeepAlive
229231
);
230232
}
@@ -554,7 +556,7 @@ export class CubejsServerCore {
554556
await this.orchestratorStorage.releaseConnections();
555557

556558
this.orchestratorStorage.clear();
557-
this.compilerCache.reset();
559+
this.compilerCache.clear();
558560

559561
this.reloadEnvVariables();
560562

@@ -714,6 +716,9 @@ export class CubejsServerCore {
714716
standalone: this.standalone,
715717
allowNodeRequire: options.allowNodeRequire,
716718
fastReload: options.fastReload || getEnv('fastReload'),
719+
compilerCacheSize: this.options.compilerCacheSize || 250,
720+
maxCompilerCacheKeepAlive: this.options.maxCompilerCacheKeepAlive,
721+
updateCompilerCacheKeepAlive: this.options.updateCompilerCacheKeepAlive
717722
},
718723
);
719724
}
@@ -871,6 +876,8 @@ export class CubejsServerCore {
871876
clearInterval(this.maxCompilerCacheKeep);
872877
}
873878

879+
this.compilerCache.clear();
880+
874881
if (this.scheduledRefreshTimerInterval) {
875882
await this.scheduledRefreshTimerInterval.cancel();
876883
}
@@ -914,6 +921,8 @@ export class CubejsServerCore {
914921
};
915922

916923
public async shutdown() {
924+
this.compilerCache.clear();
925+
917926
if (this.devServer) {
918927
if (!process.env.CI) {
919928
process.removeListener('uncaughtException', this.onUncaughtException);

0 commit comments

Comments
 (0)