Files
0pt/tests/analytics/resource-governance.test.ts
T

123 lines
5.0 KiB
TypeScript

/**
* BI-P012 — CLICKHOUSE RESOURCE GOVERNANCE, the part provable without an
* engine. The "intentional query bomb is terminated" half of the DoD needs a
* real server and lives in
* tests/integration/analytics-resource-governance.integration.test.ts.
*/
import { describe, it, expect } from 'vitest';
import {
applyQuota,
BUDGET_TIERS,
clickhouseSettings,
ConcurrencyGate,
concurrencyProfileSql,
DEFAULT_BUDGETS,
REQUIRED_GOVERNANCE_SETTINGS,
} from '@/src/analytics/engine/resource-governance';
describe('BI-P012 resource governance', () => {
it('every setting the program names has a value somewhere in the enforcement path', () => {
const settings = clickhouseSettings(DEFAULT_BUDGETS.INTERACTIVE);
const profile = concurrencyProfileSql('opt_t_alpha', DEFAULT_BUDGETS.INTERACTIVE).join(' ');
for (const name of REQUIRED_GOVERNANCE_SETTINGS) {
const present = name in settings || profile.includes(name) || name === 'max_concurrent_queries';
expect(present, `${name} is not enforced anywhere`).toBe(true);
}
// The five per-statement ones must be on the STATEMENT, not only in a
// profile a future operator might forget to attach.
for (const name of [
'max_execution_time',
'max_result_rows',
'max_rows_to_read',
'max_bytes_to_read',
'max_memory_usage',
]) {
expect(settings[name], `${name} missing from per-statement settings`).toBeDefined();
}
});
it('readonly=1 rides on every SELECT so the query text cannot re-set the limits', () => {
for (const tier of BUDGET_TIERS) {
expect(clickhouseSettings(DEFAULT_BUDGETS[tier]).readonly).toBe('1');
}
});
it('overflow modes are THROW, not BREAK — a partial result must never look complete', () => {
const s = clickhouseSettings(DEFAULT_BUDGETS.INTERACTIVE);
expect(s.result_overflow_mode).toBe('throw');
expect(s.read_overflow_mode).toBe('throw');
expect(s.timeout_overflow_mode).toBe('throw');
});
it('a CL0UD quota can only TIGHTEN a tier, never loosen it', () => {
const base = DEFAULT_BUDGETS.INTERACTIVE;
const loosened = applyQuota(base, {
maxExecutionTimeSec: 100000,
maxResultRows: 10_000_000,
maxRowsToRead: Number.MAX_SAFE_INTEGER,
maxBytesToRead: Number.MAX_SAFE_INTEGER,
maxMemoryUsageBytes: Number.MAX_SAFE_INTEGER,
maxConcurrentQueries: 1000,
});
expect(loosened).toEqual(base);
const tightened = applyQuota(base, { maxResultRows: 100, maxConcurrentQueries: 1 });
expect(tightened.maxResultRows).toBe(100);
expect(tightened.maxConcurrentQueries).toBe(1);
expect(tightened.maxExecutionTimeSec).toBe(base.maxExecutionTimeSec);
});
it('a nonsense quota (zero, negative, NaN) falls back to the tier rather than to no limit', () => {
const base = DEFAULT_BUDGETS.INTERACTIVE;
expect(applyQuota(base, { maxResultRows: 0 }).maxResultRows).toBe(base.maxResultRows);
expect(applyQuota(base, { maxResultRows: -5 }).maxResultRows).toBe(base.maxResultRows);
expect(applyQuota(base, { maxResultRows: Number.NaN }).maxResultRows).toBe(base.maxResultRows);
expect(applyQuota(base, undefined)).toEqual(base);
});
it('tiers get progressively more generous, and INTERACTIVE is the mean default', () => {
expect(DEFAULT_BUDGETS.INTERACTIVE.maxExecutionTimeSec).toBeLessThan(
DEFAULT_BUDGETS.ANALYTICAL.maxExecutionTimeSec,
);
expect(DEFAULT_BUDGETS.ANALYTICAL.maxExecutionTimeSec).toBeLessThan(
DEFAULT_BUDGETS.BATCH.maxExecutionTimeSec,
);
expect(DEFAULT_BUDGETS.INTERACTIVE.maxRowsToRead).toBeLessThan(DEFAULT_BUDGETS.ANALYTICAL.maxRowsToRead);
});
it('the concurrency gate throttles ONE tenant without touching another', () => {
const gate = new ConcurrencyGate((t) => (t === 't_noisy' ? 2 : 5));
const a = gate.acquire('t_noisy');
const b = gate.acquire('t_noisy');
expect(gate.inFlightFor('t_noisy')).toBe(2);
expect(() => gate.acquire('t_noisy')).toThrowError(/queries in flight/);
// The quiet tenant is unaffected — BI-P012's "without degrading unrelated
// tenant" in the smallest testable form.
const quiet = gate.acquire('t_quiet');
expect(gate.inFlightFor('t_quiet')).toBe(1);
a();
expect(gate.inFlightFor('t_noisy')).toBe(1);
expect(() => gate.acquire('t_noisy')).not.toThrow();
b();
quiet();
});
it('releasing twice does not free a slot that was never taken', () => {
const gate = new ConcurrencyGate(() => 1);
const release = gate.acquire('t_x');
release();
release();
expect(gate.inFlightFor('t_x')).toBe(0);
expect(gate.totalInFlight()).toBe(0);
});
it("the settings profile SQL carries max_concurrent_queries' server-side half", () => {
const stmts = concurrencyProfileSql('opt_t_alpha', DEFAULT_BUDGETS.INTERACTIVE);
expect(stmts.join(' ')).toContain('CREATE SETTINGS PROFILE IF NOT EXISTS opt_t_alpha');
expect(stmts.join(' ')).toContain('readonly = 1');
expect(stmts.join(' ')).toContain('CREATE QUOTA IF NOT EXISTS opt_t_alpha_quota');
});
});