Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ jobs:
- Verify README updates for new features
- Check API documentation accuracy

6. **Comments**
- An explanatory comment is **3 lines max**. Flag a longer one and ask for the
load-bearing sentence, unless the reason genuinely cannot be stated shorter
- A comment earns its place only when its absence would let a later edit
reintroduce a bug. Flag one a reader would lose nothing by deleting: a
restatement of the code under it, a banner over self-describing code,
narration of the change or of the review round that produced it, or a JSDoc
block whose tags only re-spell already-typed names
- Prefer making the code carry the meaning — a named constant, a named
intermediate value, an extracted function whose name states the intent
- Never raise this to ask for a comment to be **added**, and skip generated
files and comments another rule or tool requires

Provide detailed feedback using inline comments for specific issues.
Use top-level comments for general observations or praise.

Expand Down
6 changes: 6 additions & 0 deletions docs-mintlify/reference/configuration/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ module.exports = {
Maximum number of compiled data models to persist with in-memory cache. Defaults
to 250, but optimum value will depend on deployed environment. When the max is
reached, will start dropping the least recently used data models from the cache.
Must be a positive integer; the cache can not be disabled.

<CodeGroup>

Expand All @@ -233,6 +234,11 @@ module.exports = {

</CodeGroup>

This configuration option can also be set using the [`CUBEJS_COMPILER_CACHE_SIZE`](/reference/configuration/environment-variables#cubejs_compiler_cache_size)
environment variable, and takes precedence over it. Note that the environment
variable also sizes the SQL API's compiler cache, which this option does not
affect.

### `max_compiler_cache_keep_alive`

Maximum length of time in ms to keep compiled data models in memory. Default
Expand Down
27 changes: 27 additions & 0 deletions docs-mintlify/reference/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ The cache and queue driver to use for the Cube deployment.
It can be also set using the [`cache_and_queue_driver` configuration
option](/reference/configuration/config#cache_and_queue_driver).

## `CUBEJS_COMPILER_CACHE_SIZE`

The maximum number of compiled data models to keep in the in-memory compiler
cache. When the maximum is reached, the least recently used data models are
dropped from the cache. The optimum value depends on the deployed environment.

This variable sizes two separate caches: the data model compiler cache and the
[SQL API][ref-sql-api]'s own compiler cache. Setting it applies the same maximum
to both. When it is unset, each falls back to its own default, so the effective
default differs between them.

| Cache | Possible Values | Default in Development | Default in Production |
| ------------------------- | ----------------- | ---------------------- | --------------------- |
| Data model compiler cache | A positive number | `250` | `250` |
| SQL API compiler cache | A positive number | `100` | `100` |

<Note>
Neither cache can be disabled, so `0` is rejected rather than accepted and
ignored. The same applies to the configuration option.
</Note>

The data model compiler cache can be also sized using the [`compiler_cache_size`
configuration option](/reference/configuration/config#compiler_cache_size),
which takes precedence over this environment variable. The SQL API compiler
cache reads this environment variable only, so the configuration option does not
affect it.

## `CUBEJS_CONCURRENCY`

The number of concurrent connections each query queue has to the database.
Expand Down
8 changes: 7 additions & 1 deletion packages/cubejs-backend-native/src/node_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,13 @@ async fn handle_sql_query(
// the promise it was awaiting is abandoned rather than
// cancelled, and only reports if it later rejects - which is
// why that one does log.
if !err.message.eq_ignore_ascii_case("continue wait") {
// Matched on the error's cause, falling back to the message split
// into its `:`- and newline-delimited parts: a continue wait that
// came back through a `RepartitionExec` has been flattened to a
// string and reads `Execution error: Continue wait`, so the
// equality check this replaces let it through and reported the
// queue signal as a failed request in query history.
if !err.is_continue_wait() {
session_clone
.session_manager
.server
Expand Down
4 changes: 2 additions & 2 deletions packages/cubejs-backend-native/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ impl TransportService for NodeBridgeTransport {
.await;

if let Err(e) = &result {
if e.message.to_lowercase().contains("continue wait") {
if e.is_continue_wait() {
if throw_continue_wait {
return Err(CubeError::continue_wait());
}
Expand Down Expand Up @@ -609,7 +609,7 @@ impl TransportService for NodeBridgeTransport {
.await;

if let Err(e) = &res {
if e.message.to_lowercase().contains("continue wait") {
if e.is_continue_wait() {
if throw_continue_wait {
return Err(CubeError::continue_wait());
}
Expand Down
21 changes: 21 additions & 0 deletions packages/cubejs-backend-shared/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,27 @@ const variables: Record<string, (...args: any) => any> = {
scheduledRefreshBatchSize: () => get('CUBEJS_SCHEDULED_REFRESH_BATCH_SIZE')
.default('1')
.asInt(),
/**
* Maximum number of compiled data models to keep in the in-memory compiler cache.
*/
compilerCacheSize: () => {
const size = get('CUBEJS_COMPILER_CACHE_SIZE')
.default('250')
.asIntPositive();

// env-var's asIntPositive() lets 0 through, but every consumer of this option
// falls back to the default on a falsy value, so 0 would quietly mean 250
// instead of doing what it looks like it does.
if (size === 0) {
throw new InvalidConfiguration(
'CUBEJS_COMPILER_CACHE_SIZE',
size,
'Must be a positive integer. The compiler cache can not be disabled.',
);
}

return size;
},
nativeSqlPlanner: () => {
const explicitlySet = process.env.CUBEJS_TESSERACT_SQL_PLANNER !== undefined;
const enabled = get('CUBEJS_TESSERACT_SQL_PLANNER').default('true').asBool();
Expand Down
33 changes: 33 additions & 0 deletions packages/cubejs-backend-shared/test/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,36 @@ describe('getEnv(defaultTimezone / scheduledRefreshTimezones)', () => {
);
});
});

describe('getEnv(compilerCacheSize)', () => {
afterEach(() => {
delete process.env.CUBEJS_COMPILER_CACHE_SIZE;
});

test('defaults to 250', () => {
expect(getEnv('compilerCacheSize')).toBe(250);
});

test('reads CUBEJS_COMPILER_CACHE_SIZE', () => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = '1000';
expect(getEnv('compilerCacheSize')).toBe(1000);
});

test('throws on zero, so it is never silently coerced to the default', () => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = '0';
expect(() => getEnv('compilerCacheSize')).toThrowError(
'Value "0" is not valid for CUBEJS_COMPILER_CACHE_SIZE. Must be a positive integer. The compiler cache can not be disabled.'
);
});

test.each([
'-1',
'abc',
'1.5',
])('throws on the negative or non-integer value %j', (value) => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = value;
expect(() => getEnv('compilerCacheSize')).toThrowError(
/CUBEJS_COMPILER_CACHE_SIZE/
);
});
});
1 change: 1 addition & 0 deletions packages/cubejs-server-core/src/core/OptsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export class OptsHandler {
dashboardAppPort: 3000,
scheduledRefreshConcurrency: getEnv('scheduledRefreshQueriesPerAppId'),
scheduledRefreshBatchSize: getEnv('scheduledRefreshBatchSize'),
compilerCacheSize: getEnv('compilerCacheSize'),
preAggregationsSchema:
getEnv('preAggregationsSchema') ||
(this.isDevMode()
Expand Down
2 changes: 1 addition & 1 deletion packages/cubejs-server-core/src/core/optionsValidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const schemaOptions = Joi.object().keys({
scheduledRefreshConcurrency: Joi.number().min(1).integer(),
scheduledRefreshBatchSize: Joi.number().min(1).integer(),
// Compiler cache
compilerCacheSize: Joi.number().min(0).integer(),
compilerCacheSize: Joi.number().min(1).integer(),
updateCompilerCacheKeepAlive: Joi.boolean(),
maxCompilerCacheKeepAlive: Joi.number().min(0).integer(),
telemetry: Joi.boolean(),
Expand Down
42 changes: 42 additions & 0 deletions packages/cubejs-server-core/test/unit/OptsHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,3 +1252,45 @@ describe('OptsHandler timezone env validation', () => {
expect(core.options.scheduledRefreshTimeZones).toEqual(['UTC', 'America/New_York']);
});
});

describe('OptsHandler compilerCacheSize', () => {
beforeEach(() => {
process.env.CUBEJS_DB_TYPE = 'postgres';
});

afterEach(() => {
delete process.env.CUBEJS_COMPILER_CACHE_SIZE;
});

test('must default to 250 when neither the option nor the env variable is set', () => {
const core = new CubejsServerCoreExposed(conf);

expect(core.options.compilerCacheSize).toBe(250);
});

test('must take the value from CUBEJS_COMPILER_CACHE_SIZE', () => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = '42';

const core = new CubejsServerCoreExposed(conf);

expect(core.options.compilerCacheSize).toBe(42);
});

test('must prefer CreateOptions.compilerCacheSize over the env variable', () => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = '42';

const core = new CubejsServerCoreExposed({
...conf,
compilerCacheSize: 7,
});

expect(core.options.compilerCacheSize).toBe(7);
});

test('must throw at construction if CUBEJS_COMPILER_CACHE_SIZE is not a valid size', () => {
process.env.CUBEJS_COMPILER_CACHE_SIZE = 'abc';

expect(() => new CubejsServerCoreExposed(conf))
.toThrow(/CUBEJS_COMPILER_CACHE_SIZE/);
});
});
15 changes: 14 additions & 1 deletion packages/cubejs-server-core/test/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,20 @@ describe('index.test', () => {
};

expect(() => new CubejsServerCore(options))
.toThrowError(/"compilerCacheSize" must be greater than or equal to 0/);
.toThrowError(/"compilerCacheSize" must be greater than or equal to 1/);
});

// 0 used to validate and then get silently replaced by the default through the
// `|| 250` guards, which reads like a way to disable the compiler cache but isn't.
test('Should throw error, compilerCacheSize of 0', () => {
const options = {
externalDbType: <DatabaseType>'mysql',
devServer: true,
compilerCacheSize: 0,
};

expect(() => new CubejsServerCore(options))
.toThrowError(/"compilerCacheSize" must be greater than or equal to 1/);
});

test('Should create instance of CubejsServerCore, orchestratorOptions as func', () => {
Expand Down
12 changes: 10 additions & 2 deletions rust/cubesql/cubesql/src/compile/engine/df/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,15 @@ impl CubeScanMemoryStream {
} else {
err.message
};
if !err.message.eq_ignore_ascii_case("continue wait") {
// A continue wait also gets the `ContinueWait` cause here, the way
// `load_data` sets it below, so consumers can match on the cause
// rather than on the message. The other branch still only
// prefixes the message: unlike `load_data` this path leaves the
// incoming cause alone, and re-classifying it would change which
// Postgres error code a streaming failure reports.
if err.is_continue_wait() {
err.cause = CubeErrorCauseType::ContinueWait;
} else {
err.message = format!("Database Execution Error: {}", err.message);
}
Some(Err(ArrowError::ExternalError(Box::new(err))))
Expand Down Expand Up @@ -895,7 +903,7 @@ async fn load_data(
err.message
};

if err.message.eq_ignore_ascii_case("continue wait") {
if err.is_continue_wait() {
err.cause = CubeErrorCauseType::ContinueWait;
} else {
err.cause = CubeErrorCauseType::DatabaseExecution(err.cause.meta().cloned());
Expand Down
Loading
Loading