Skip to content

Commit 35712c2

Browse files
committed
refactor PreAggregations: move every class to a separate file
1 parent 7b60c03 commit 35712c2

File tree

5 files changed

+1831
-1779
lines changed

5 files changed

+1831
-1779
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { TableStructure } from '@cubejs-backend/base-driver';
2+
import { DriverFactory } from './DriverFactory';
3+
import { QueryCache, QueryTuple, QueryWithParams } from './QueryCache';
4+
import {
5+
PreAggregationDescription,
6+
PreAggregations,
7+
TableCacheEntry,
8+
tablesToVersionEntries,
9+
VersionEntriesObj,
10+
VersionEntry
11+
} from './PreAggregations';
12+
13+
type PreAggregationLoadCacheOptions = {
14+
requestId?: string,
15+
dataSource: string,
16+
tablePrefixes?: string[],
17+
};
18+
19+
export class PreAggregationLoadCache {
20+
private driverFactory: DriverFactory;
21+
22+
private queryCache: QueryCache;
23+
24+
// eslint-disable-next-line no-use-before-define
25+
private preAggregations: PreAggregations;
26+
27+
private queryResults: any;
28+
29+
private externalDriverFactory: any;
30+
31+
private requestId: any;
32+
33+
private versionEntries: { [redisKey: string]: Promise<VersionEntriesObj> };
34+
35+
private tables: { [redisKey: string]: TableCacheEntry[] };
36+
37+
private tableColumnTypes: { [cacheKey: string]: { [tableName: string]: TableStructure } };
38+
39+
// TODO this is in memory cache structure as well however it depends on
40+
// data source only and load cache is per data source for now.
41+
// Make it per data source key in case load cache scope is broaden.
42+
private queryStageState: any;
43+
44+
private dataSource: string;
45+
46+
private tablePrefixes: string[] | null;
47+
48+
public constructor(
49+
clientFactory: DriverFactory,
50+
queryCache,
51+
preAggregations,
52+
options: PreAggregationLoadCacheOptions = { dataSource: 'default' }
53+
) {
54+
this.dataSource = options.dataSource;
55+
this.driverFactory = clientFactory;
56+
this.queryCache = queryCache;
57+
this.preAggregations = preAggregations;
58+
this.queryResults = {};
59+
this.externalDriverFactory = preAggregations.externalDriverFactory;
60+
this.requestId = options.requestId;
61+
this.tablePrefixes = options.tablePrefixes;
62+
this.versionEntries = {};
63+
this.tables = {};
64+
this.tableColumnTypes = {};
65+
}
66+
67+
protected async tablesFromCache(preAggregation, forceRenew?) {
68+
let tables = forceRenew ? null : await this.queryCache.getCacheDriver().get(this.tablesCachePrefixKey(preAggregation));
69+
if (!tables) {
70+
tables = await this.preAggregations.getLoadCacheQueue(this.dataSource).executeInQueue(
71+
'query',
72+
`Fetch tables for ${preAggregation.preAggregationsSchema}`,
73+
{
74+
preAggregation, requestId: this.requestId
75+
},
76+
0,
77+
{ requestId: this.requestId }
78+
);
79+
}
80+
return tables;
81+
}
82+
83+
public async fetchTables(preAggregation: PreAggregationDescription) {
84+
if (preAggregation.external && !this.externalDriverFactory) {
85+
throw new Error('externalDriverFactory is not provided. Please use CUBEJS_DEV_MODE=true or provide Cube Store connection env variables for production usage.');
86+
}
87+
88+
const newTables = await this.fetchTablesNoCache(preAggregation);
89+
await this.queryCache.getCacheDriver().set(
90+
this.tablesCachePrefixKey(preAggregation),
91+
newTables,
92+
this.preAggregations.options.preAggregationsSchemaCacheExpire || 60 * 60
93+
);
94+
return newTables;
95+
}
96+
97+
private async fetchTablesNoCache(preAggregation: PreAggregationDescription) {
98+
const client = preAggregation.external ?
99+
await this.externalDriverFactory() :
100+
await this.driverFactory();
101+
if (this.tablePrefixes && client.getPrefixTablesQuery && this.preAggregations.options.skipExternalCacheAndQueue) {
102+
return client.getPrefixTablesQuery(preAggregation.preAggregationsSchema, this.tablePrefixes);
103+
}
104+
return client.getTablesQuery(preAggregation.preAggregationsSchema);
105+
}
106+
107+
public tablesCachePrefixKey(preAggregation: PreAggregationDescription) {
108+
return this.queryCache.getKey('SQL_PRE_AGGREGATIONS_TABLES', `${preAggregation.dataSource}${preAggregation.preAggregationsSchema}${preAggregation.external ? '_EXT' : ''}`);
109+
}
110+
111+
protected async getTablesQuery(preAggregation) {
112+
const redisKey = this.tablesCachePrefixKey(preAggregation);
113+
if (!this.tables[redisKey]) {
114+
const tables = this.preAggregations.options.skipExternalCacheAndQueue && preAggregation.external ?
115+
await this.fetchTablesNoCache(preAggregation) :
116+
await this.tablesFromCache(preAggregation);
117+
if (tables === undefined) {
118+
throw new Error('Pre-aggregation tables are undefined.');
119+
}
120+
this.tables[redisKey] = tables;
121+
}
122+
return this.tables[redisKey];
123+
}
124+
125+
public async getTableColumnTypes(preAggregation: PreAggregationDescription, tableName: string): Promise<TableStructure> {
126+
const prefixKey = this.tablesCachePrefixKey(preAggregation);
127+
if (!this.tableColumnTypes[prefixKey]?.[tableName]) {
128+
if (!this.preAggregations.options.skipExternalCacheAndQueue && preAggregation.external) {
129+
throw new Error(`Lambda union with source data feature is supported only by external rollups stored in Cube Store but was invoked for '${preAggregation.preAggregationId}'`);
130+
}
131+
const client = await this.externalDriverFactory();
132+
const columnTypes = await client.tableColumnTypes(tableName);
133+
if (!this.tableColumnTypes[prefixKey]) {
134+
this.tableColumnTypes[prefixKey] = {};
135+
}
136+
this.tableColumnTypes[prefixKey][tableName] = columnTypes;
137+
}
138+
return this.tableColumnTypes[prefixKey][tableName];
139+
}
140+
141+
private async calculateVersionEntries(preAggregation): Promise<VersionEntriesObj> {
142+
let versionEntries = tablesToVersionEntries(
143+
preAggregation.preAggregationsSchema,
144+
await this.getTablesQuery(preAggregation)
145+
);
146+
// It presumes strong consistency guarantees for external pre-aggregation tables ingestion
147+
if (!preAggregation.external) {
148+
// eslint-disable-next-line
149+
const [active, toProcess, queries] = await this.fetchQueryStageState();
150+
const targetTableNamesInQueue = (Object.keys(queries))
151+
// eslint-disable-next-line no-use-before-define
152+
.map(q => PreAggregations.targetTableName(queries[q].query.newVersionEntry));
153+
154+
versionEntries = versionEntries.filter(
155+
// eslint-disable-next-line no-use-before-define
156+
e => targetTableNamesInQueue.indexOf(PreAggregations.targetTableName(e)) === -1
157+
);
158+
}
159+
160+
const byContent: { [key: string]: VersionEntry } = {};
161+
const byStructure: { [key: string]: VersionEntry } = {};
162+
const byTableName: { [key: string]: VersionEntry } = {};
163+
164+
versionEntries.forEach(e => {
165+
const contentKey = `${e.table_name}_${e.content_version}`;
166+
if (!byContent[contentKey]) {
167+
byContent[contentKey] = e;
168+
}
169+
const structureKey = `${e.table_name}_${e.structure_version}`;
170+
if (!byStructure[structureKey]) {
171+
byStructure[structureKey] = e;
172+
}
173+
if (!byTableName[e.table_name]) {
174+
byTableName[e.table_name] = e;
175+
}
176+
});
177+
178+
return { versionEntries, byContent, byStructure, byTableName };
179+
}
180+
181+
public async getVersionEntries(preAggregation): Promise<VersionEntriesObj> {
182+
if (this.tablePrefixes && !this.tablePrefixes.find(p => preAggregation.tableName.split('.')[1].startsWith(p))) {
183+
throw new Error(`Load cache tries to load table ${preAggregation.tableName} outside of tablePrefixes filter: ${this.tablePrefixes.join(', ')}`);
184+
}
185+
const redisKey = this.tablesCachePrefixKey(preAggregation);
186+
if (!this.versionEntries[redisKey]) {
187+
this.versionEntries[redisKey] = this.calculateVersionEntries(preAggregation).catch(e => {
188+
delete this.versionEntries[redisKey];
189+
throw e;
190+
});
191+
}
192+
return this.versionEntries[redisKey];
193+
}
194+
195+
public async keyQueryResult(sqlQuery: QueryWithParams, waitForRenew: boolean, priority: number) {
196+
const [query, values, queryOptions]: QueryTuple = Array.isArray(sqlQuery) ? sqlQuery : [sqlQuery, [], {}];
197+
198+
if (!this.queryResults[this.queryCache.queryRedisKey([query, values])]) {
199+
this.queryResults[this.queryCache.queryRedisKey([query, values])] = await this.queryCache.cacheQueryResult(
200+
query,
201+
<string[]>values,
202+
[query, <string[]>values],
203+
60 * 60,
204+
{
205+
renewalThreshold: this.queryCache.options.refreshKeyRenewalThreshold
206+
|| queryOptions?.renewalThreshold || 2 * 60,
207+
renewalKey: [query, values],
208+
waitForRenew,
209+
priority,
210+
requestId: this.requestId,
211+
dataSource: this.dataSource,
212+
useInMemory: true,
213+
external: queryOptions?.external
214+
}
215+
);
216+
}
217+
return this.queryResults[this.queryCache.queryRedisKey([query, values])];
218+
}
219+
220+
public hasKeyQueryResult(keyQuery) {
221+
return !!this.queryResults[this.queryCache.queryRedisKey(keyQuery)];
222+
}
223+
224+
public async getQueryStage(stageQueryKey) {
225+
const queue = await this.preAggregations.getQueue(this.dataSource);
226+
await this.fetchQueryStageState(queue);
227+
return queue.getQueryStage(stageQueryKey, undefined, this.queryStageState);
228+
}
229+
230+
protected async fetchQueryStageState(queue?) {
231+
queue = queue || await this.preAggregations.getQueue(this.dataSource);
232+
if (!this.queryStageState) {
233+
this.queryStageState = await queue.fetchQueryStageState();
234+
}
235+
return this.queryStageState;
236+
}
237+
238+
public async reset(preAggregation) {
239+
await this.tablesFromCache(preAggregation, true);
240+
this.tables = {};
241+
this.tableColumnTypes = {};
242+
this.queryStageState = undefined;
243+
this.versionEntries = {};
244+
}
245+
}

0 commit comments

Comments
 (0)