Skip to content

Commit 7d4ecdc

Browse files
feat(api-gateway): Introduce cache mode option for /cubesql API (#9972)
* add cache option to the query * Pass CacheMode from /cubesql to backend * imject CacheMode into more places * update normalizeQuery with cache mode * pass cache mode within graphql * pass new cache mode in sqlApiLoad in API GW * fix types imports * update preAggs to use cache option instead of renewQuery * code polish * comments with types * fix query type * set default cacheMode = 'stale-if-slow' in normalize() * more types and polish * backbone code for 'stale-if-slow' & 'stale-while-revalidate' * make query cache aware of queryBody.cache === 'must-revalidate' * First attempt to implement 'no-cache' scenario * add cache to open api spec and regenerate rust client * pass cache mode to cubeScan * cargo clippy/fmt * Implement background refresh * add cache mode descriptions * remove query cache mode from normalize query * pass cacheMode to getSqlResponseInternal * remove obsolete * add cacheMode as input param in orchestratorApi * open api spec fix * fix cubesql after introducing cacheMode * rename cache → cacheMode * clean up obsolete * pass cache_mode from SqlApiLoadPayload * fix important * move 'no-cache' variant into queryCache.cachedQueryResult() * remove cacheMode from client query body types (it's incorrect) * switch RefreshScheduler to use cacheMode instead of renewQuery * remove obsolete continueWait flag * fix refresh scheduler * add fallback to renewQuery in api gw * fix tests * Docs * Deprecation * refactor api gw: move copy/paste into this.normalizeCacheMode() * fix tests snapshots * return cacheMode into query object * fix tests * some cleanup (removed renewQuery) * update cacheMod in graphql * return back cache as public prop in query * fix * lint fix # Conflicts: # packages/cubejs-api-gateway/package.json * fix subscribe() * remove cache from subscribe * fix CacheMode serialization * refactor: move normalizeQueryCacheMode to normalize --------- Co-authored-by: Igor Lukanin <[email protected]>
1 parent 765cd77 commit 7d4ecdc

File tree

35 files changed

+364
-134
lines changed

35 files changed

+364
-134
lines changed

DEPRECATION.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ features:
6464
| Removed | [`initApp` hook](#initapp-hook) | v0.35.0 | v0.35.0 |
6565
| Removed | [`/v1/run-scheduled-refresh` REST API endpoint](#v1run-scheduled-refresh-rest-api-endpoint) | v0.35.0 | v0.36.0 |
6666
| Removed | [Node.js 18](#nodejs-18) | v0.36.0 | v1.3.0 |
67-
| Deprecated | [`CUBEJS_SCHEDULED_REFRESH_CONCURRENCY`](#cubejs_scheduled_refresh_concurrency) | v1.2.7 | |
67+
| Deprecated | [`CUBEJS_SCHEDULED_REFRESH_CONCURRENCY`](#cubejs_scheduled_refresh_concurrency) | v1.2.7 | |
6868
| Deprecated | [Node.js 20](#nodejs-20) | v1.3.0 | |
69+
| Deprecated | [`renewQuery` parameter of the `/v1/load` endpoint](#renewquery-parameter-of-the-v1load-endpoint) | v1.3.73 | |
6970

7071
### Node.js 8
7172

@@ -412,3 +413,8 @@ This environment variable was renamed to [`CUBEJS_SCHEDULED_REFRESH_QUERIES_PER_
412413
413414
Node.js 20 is in maintenance mode from [November 22, 2024][link-nodejs-eol]. This means
414415
no more new features, only security updates. Please upgrade to Node.js 22 or higher.
416+
417+
### `renewQuery` parameter of the `/v1/load` endpoint
418+
419+
This parameter is deprecated and will be removed in future releases. See [cache control](https://cube.dev/docs/product/apis-integrations/rest-api#cache-control)
420+
options and use the new `cache` parameter of the `/v1/load` endpoint instead.

docs/pages/product/apis-integrations/rest-api.mdx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ accessible for everyone.
161161
| API scope | REST API endpoints | Accessible by default? |
162162
| --- | --- | --- |
163163
| `meta` | [`/v1/meta`][ref-ref-meta] | ✅ Yes |
164-
| `data` | [`/v1/load`][ref-ref-load] | ✅ Yes |
164+
| `data` | [`/v1/load`][ref-ref-load], [`/v1/cubesql`][ref-ref-cubesql] | ✅ Yes |
165165
| `graphql` | `/graphql` | ✅ Yes |
166166
| `sql` | [`/v1/sql`][ref-ref-sql] | ✅ Yes |
167167
| `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No |
@@ -248,9 +248,20 @@ should be unique for each separate request. `spanId` should define user
248248
interaction span such us `Continue wait` retry cycle and it's value shouldn't
249249
change during one single interaction.
250250

251-
## Troubleshooting
251+
## Cache control
252252

253-
### `Continue wait`
253+
[`/v1/load`][ref-ref-load] and [`/v1/cubesql`][ref-ref-cubesql] endpoints of the REST API
254+
allow to control the cache behavior. The following querying strategies with regards to
255+
the cache are supported:
256+
257+
| Strategy | Description |
258+
| --- | --- |
259+
| `stale-if-slow` | If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, tries to return fresh value from the data source. If the data source query is slow (hits [`Continue wait`](#continue-wait)), returns stale value from cache. |
260+
| `stale-while-revalidate`| If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, returns stale data from cache and updates cache in background. |
261+
| `must-revalidate` | If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, always waits for fresh value from the data source, even if slow (hits one or more [`Continue wait`](#continue-wait) intervals). |
262+
| `no-cache` | Skips [refresh key][ref-refresh-keys] checks. Always returns fresh data from the data source, regardless of cache or query performance. |
263+
264+
## `Continue wait`
254265

255266
If the request takes too long to be processed, the REST API responds with
256267
`{ "error": "Continue wait" }` and the status code 200.
@@ -295,6 +306,7 @@ warehouse][ref-data-warehouses].
295306
[ref-ref-load]: /product/apis-integrations/rest-api/reference#base_pathv1load
296307
[ref-ref-meta]: /product/apis-integrations/rest-api/reference#base_pathv1meta
297308
[ref-ref-sql]: /product/apis-integrations/rest-api/reference#base_pathv1sql
309+
[ref-ref-cubesql]: /product/apis-integrations/rest-api/reference#base_pathv1cubesql
298310
[ref-ref-paj]: /product/apis-integrations/rest-api/reference#base_pathv1pre-aggregationsjobs
299311
[ref-security-context]: /product/auth/context
300312
[ref-graphql-api]: /product/apis-integrations/graphql-api
@@ -313,4 +325,5 @@ warehouse][ref-data-warehouses].
313325
[ref-traditional-databases]: /product/configuration/data-sources#transactional-databases
314326
[ref-pre-aggregations]: /product/caching/using-pre-aggregations
315327
[ref-javascript-sdk]: /product/apis-integrations/javascript-sdk
316-
[ref-recipe-real-time-data-fetch]: /product/apis-integrations/recipes/real-time-data-fetch
328+
[ref-recipe-real-time-data-fetch]: /product/apis-integrations/recipes/real-time-data-fetch
329+
[ref-refresh-keys]: /product/data-modeling/reference/cube#refresh_key

docs/pages/product/apis-integrations/rest-api/query-format.mdx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ The default value is `false`.
4141
- `timezone`: A [time zone][ref-time-zone] for your query. You can set the
4242
desired time zone in the [TZ Database Name](https://en.wikipedia.org/wiki/Tz_database)
4343
format, e.g., `America/Los_Angeles`.
44-
- `renewQuery`: If `renewQuery` is set to `true`, Cube will renew all
45-
[`refreshKey`][ref-schema-ref-preaggs-refreshkey] for queries and query
46-
results in the foreground. However, if the
47-
[`refreshKey`][ref-schema-ref-preaggs-refreshkey] (or
48-
[`refreshKey.every`][ref-schema-ref-preaggs-refreshkey-every]) doesn't
49-
indicate that there's a need for an update this setting has no effect. The
50-
default value is `false`.
51-
> **NOTE**: Cube provides only eventual consistency guarantee. Using a small
52-
> [`refreshKey.every`][ref-schema-ref-preaggs-refreshkey-every] value together
53-
> with `renewQuery` to achieve immediate consistency can lead to endless
54-
> refresh loops and overall system instability.
5544
- `ungrouped`: If set to `true`, Cube will run an [ungrouped
5645
query][ref-ungrouped-query].
5746
- `joinHints`: Query-time [join hints][ref-join-hints], provided as an array of

docs/pages/product/apis-integrations/rest-api/reference.mdx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ By default, it's `/cubejs-api`.
1313

1414
Run the query to the REST API and get the results.
1515

16-
| Parameter | Description |
17-
| ----------- | --------------------------------------------------------------------------------------------------------------------- |
18-
| `query` | Either a single URL encoded Cube [Query](/product/apis-integrations/rest-api/query-format), or an array of queries |
19-
| `queryType` | If multiple queries are passed in `query` for [data blending][ref-recipes-data-blending], this must be set to `multi` |
16+
| Parameter | Description | Required |
17+
| ----------- | --------------------------------------------------------------------------------------------------------------------- | --- |
18+
| `query` | Either a single URL encoded Cube [Query](/product/apis-integrations/rest-api/query-format), or an array of queries | ✅ Yes |
19+
| `queryType` | If multiple queries are passed in `query` for [data blending][ref-recipes-data-blending], this must be set to `multi` | ❌ No |
20+
| `cache` | See [cache control][ref-cache-control]. `stale-if-slow` by default | ❌ No |
2021

2122
Response
2223

@@ -319,9 +320,10 @@ This endpoint is part of the [SQL API][ref-sql-api].
319320

320321
</InfoBox>
321322

322-
| Parameter | Description |
323-
| --- | --- |
324-
| `query` | The SQL query to run. |
323+
| Parameter | Description | Required |
324+
| --- | --- | --- |
325+
| `query` | The SQL query to run. | ✅ Yes |
326+
| `cache` | See [cache control][ref-cache-control]. `stale-if-slow` by default | ❌ No |
325327

326328
Response: a stream of newline-delimited JSON objects. The first object contains
327329
the `schema` property with column names and types. The following objects contain
@@ -639,4 +641,5 @@ Keep-Alive: timeout=5
639641
[ref-query-wpd]: /product/apis-integrations/queries#query-with-pushdown
640642
[ref-sql-api]: /product/apis-integrations/sql-api
641643
[ref-orchestration-api]: /product/apis-integrations/orchestration-api
642-
[ref-folders]: /product/data-modeling/reference/view#folders
644+
[ref-folders]: /product/data-modeling/reference/view#folders
645+
[ref-cache-control]: /product/apis-integrations/rest-api#cache-control

packages/cubejs-api-gateway/openspec.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,13 @@ components:
484484
properties:
485485
queryType:
486486
type: "string"
487+
cache:
488+
type: "string"
489+
enum:
490+
- stale-if-slow
491+
- stale-while-revalidate
492+
- must-revalidate
493+
- no-cache
487494
query:
488495
type: "object"
489496
$ref: "#/components/schemas/V1LoadRequestQuery"

packages/cubejs-api-gateway/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dependencies": {
3030
"@cubejs-backend/native": "1.3.82",
3131
"@cubejs-backend/shared": "1.3.82",
32+
"@cubejs-backend/query-orchestrator": "1.3.82",
3233
"@ungap/structured-clone": "^0.3.4",
3334
"assert-never": "^1.4.0",
3435
"body-parser": "^1.19.0",

packages/cubejs-api-gateway/src/gateway.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getRealType,
1313
parseUtcIntoLocalDate,
1414
QueryAlias,
15+
CacheMode,
1516
} from '@cubejs-backend/shared';
1617
import {
1718
ResultArrayWrapper,
@@ -28,6 +29,7 @@ import type {
2829
} from 'express';
2930
import { createProxyMiddleware } from 'http-proxy-middleware';
3031

32+
import { QueryBody } from '@cubejs-backend/query-orchestrator';
3133
import {
3234
QueryType,
3335
ApiScopes,
@@ -50,7 +52,9 @@ import {
5052
PreAggJob,
5153
PreAggJobStatusItem,
5254
PreAggJobStatusResponse,
53-
SqlApiRequest, MetaResponseResultFn,
55+
SqlApiRequest,
56+
MetaResponseResultFn,
57+
RequestQuery,
5458
} from './types/request';
5559
import {
5660
CheckAuthInternalOptions,
@@ -177,7 +181,13 @@ class ApiGateway {
177181

178182
public constructor(
179183
protected readonly apiSecret: string,
184+
/**
185+
* It actually returns a Promise<CompilerApi>
186+
*/
180187
protected readonly compilerApi: (ctx: RequestContext) => Promise<any>,
188+
/**
189+
* It actually returns a Promise<OrchestratorApi>
190+
*/
181191
protected readonly adapterApi: (ctx: RequestContext) => Promise<any>,
182192
protected readonly logger: any,
183193
protected readonly options: ApiGatewayOptions,
@@ -311,6 +321,7 @@ class ApiGateway {
311321
context: req.context,
312322
res: this.resToResultFn(res),
313323
queryType: req.query.queryType,
324+
cacheMode: req.query.cache,
314325
});
315326
}));
316327

@@ -320,7 +331,8 @@ class ApiGateway {
320331
query: req.body.query,
321332
context: req.context,
322333
res: this.resToResultFn(res),
323-
queryType: req.body.queryType
334+
queryType: req.body.queryType,
335+
cacheMode: req.body.cache,
324336
});
325337
}));
326338

@@ -329,7 +341,8 @@ class ApiGateway {
329341
query: req.query.query,
330342
context: req.context,
331343
res: this.resToResultFn(res),
332-
queryType: req.query.queryType
344+
queryType: req.query.queryType,
345+
cacheMode: req.query.cache,
333346
});
334347
}));
335348

@@ -425,7 +438,7 @@ class ApiGateway {
425438
try {
426439
await this.assertApiScope('data', req.context?.securityContext);
427440

428-
await this.sqlServer.execSql(req.body.query, res, req.context?.securityContext);
441+
await this.sqlServer.execSql(req.body.query, res, req.context?.securityContext, req.body.cache);
429442
} catch (e: any) {
430443
this.handleError({
431444
e,
@@ -1200,6 +1213,7 @@ class ApiGateway {
12001213
context: RequestContext,
12011214
persistent = false,
12021215
memberExpressions: boolean = false,
1216+
cacheMode?: CacheMode,
12031217
): Promise<[QueryType, NormalizedQuery[], NormalizedQuery[]]> {
12041218
let query = this.parseQueryParam(inputQuery);
12051219

@@ -1240,7 +1254,7 @@ class ApiGateway {
12401254
}
12411255

12421256
return {
1243-
normalizedQuery: (normalizeQuery(currentQuery, persistent)),
1257+
normalizedQuery: (normalizeQuery(currentQuery, persistent, cacheMode)),
12441258
hasExpressionsInQuery
12451259
};
12461260
});
@@ -1285,13 +1299,13 @@ class ApiGateway {
12851299
type: 'Query Rewrite completed',
12861300
queryRewriteId,
12871301
normalizedQueries,
1288-
duration: new Date().getTime() - startTime,
1302+
duration: Date.now() - startTime,
12891303
query
12901304
}, context);
12911305

12921306
normalizedQueries = normalizedQueries.map(q => remapToQueryAdapterFormat(q));
12931307

1294-
if (normalizedQueries.find((currentQuery) => !currentQuery)) {
1308+
if (normalizedQueries.some((currentQuery) => !currentQuery)) {
12951309
throw new Error('queryTransformer returned null query. Please check your queryTransformer implementation');
12961310
}
12971311

@@ -1637,12 +1651,11 @@ class ApiGateway {
16371651
normalizedQuery: NormalizedQuery,
16381652
sqlQuery: any,
16391653
): Promise<ResultWrapper> {
1640-
const queries = [{
1654+
const queries: QueryBody[] = [{
16411655
...sqlQuery,
16421656
query: sqlQuery.sql[0],
16431657
values: sqlQuery.sql[1],
1644-
continueWait: true,
1645-
renewQuery: normalizedQuery.renewQuery,
1658+
cacheMode: normalizedQuery.cacheMode,
16461659
requestId: context.requestId,
16471660
context,
16481661
persistent: false,
@@ -1665,8 +1678,7 @@ class ApiGateway {
16651678
...totalQuery,
16661679
query: totalQuery.sql[0],
16671680
values: totalQuery.sql[1],
1668-
continueWait: true,
1669-
renewQuery: normalizedTotal.renewQuery,
1681+
cacheMode: normalizedTotal.cacheMode,
16701682
requestId: context.requestId,
16711683
context
16721684
});
@@ -1782,12 +1794,11 @@ class ApiGateway {
17821794
this.log({ type: 'Load Request', query, streaming: true }, context);
17831795
const [, normalizedQueries] = await this.getNormalizedQueries(query, context, true);
17841796
const sqlQuery = (await this.getSqlQueriesInternal(context, normalizedQueries))[0];
1785-
const q = {
1797+
const q: QueryBody = {
17861798
...sqlQuery,
17871799
query: sqlQuery.sql[0],
17881800
values: sqlQuery.sql[1],
1789-
continueWait: true,
1790-
renewQuery: false,
1801+
cacheMode: 'stale-if-slow',
17911802
requestId: context.requestId,
17921803
context,
17931804
persistent: true,
@@ -1826,6 +1837,7 @@ class ApiGateway {
18261837
context,
18271838
res,
18281839
apiType = 'rest',
1840+
cacheMode,
18291841
...props
18301842
} = request;
18311843
const requestStarted = new Date();
@@ -1847,7 +1859,7 @@ class ApiGateway {
18471859
}, context);
18481860

18491861
const [queryType, normalizedQueries] =
1850-
await this.getNormalizedQueries(query, context);
1862+
await this.getNormalizedQueries(query, context, false, false, cacheMode);
18511863

18521864
if (
18531865
queryType !== QueryTypeEnum.REGULAR_QUERY &&
@@ -1941,6 +1953,7 @@ class ApiGateway {
19411953
const {
19421954
context,
19431955
res,
1956+
cacheMode,
19441957
} = request;
19451958
const requestStarted = new Date();
19461959

@@ -1955,7 +1968,7 @@ class ApiGateway {
19551968
}
19561969

19571970
const [queryType, normalizedQueries] =
1958-
await this.getNormalizedQueries(query, context, request.streaming, request.memberExpressions);
1971+
await this.getNormalizedQueries(query, context, request.streaming, request.memberExpressions, cacheMode);
19591972

19601973
const compilerApi = await this.getCompilerApi(context);
19611974
let metaConfigResult = await compilerApi.metaConfig(request.context, {
@@ -1970,17 +1983,16 @@ class ApiGateway {
19701983
normalizedQueries.map(q => ({ ...q, disableExternalPreAggregations: request.sqlQuery }))
19711984
);
19721985

1973-
let results;
1986+
let results: any[];
19741987

19751988
let slowQuery = false;
19761989

19771990
const streamResponse = async (sqlQuery) => {
1978-
const q = {
1991+
const q: QueryBody = {
19791992
...sqlQuery,
19801993
query: sqlQuery.query || sqlQuery.sql[0],
19811994
values: sqlQuery.values || sqlQuery.sql[1],
1982-
continueWait: true,
1983-
renewQuery: false,
1995+
cacheMode: 'stale-if-slow',
19841996
requestId: context.requestId,
19851997
context,
19861998
persistent: true,
@@ -1995,11 +2007,10 @@ class ApiGateway {
19952007
};
19962008

19972009
if (request.sqlQuery) {
1998-
const finalQuery = {
2010+
const finalQuery: QueryBody = {
19992011
query: request.sqlQuery[0],
20002012
values: request.sqlQuery[1],
2001-
continueWait: true,
2002-
renewQuery: normalizedQueries[0].renewQuery,
2013+
cacheMode: request.cacheMode,
20032014
requestId: context.requestId,
20042015
context,
20052016
...sqlQueries[0],
@@ -2133,7 +2144,7 @@ class ApiGateway {
21332144
};
21342145
}
21352146

2136-
protected parseQueryParam(query): Query | Query[] {
2147+
protected parseQueryParam(query: RequestQuery | 'undefined'): Query | Query[] {
21372148
if (!query || query === 'undefined') {
21382149
throw new UserError('Query param is required');
21392150
}

0 commit comments

Comments
 (0)