mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(pace): composable DB-contention pacer primitive createDbPacer/createNoopPacer (concurrency permit + in-band EWMA + jittered cooperative sleep, abort-throws, fail-open) + named pace-mode bundles (env>config>bundle, default off) + shared embed-backfill lock key. * feat(embed): wire DB-pacing into embed paths + single-flight + bounded keyset re-entry embedStaleForSource + CLI embedAllStale/embedAll lower worker count to the resolved cap and observe()/pace() their DB ops; embed job + embed-backfill handler resolve env>config>bundle; CLI --pace flags; --background carries overrides into the job payload; single-flight via shared per-source lock; budget-timer re-arm around paced sleeps; EmbedResult.pacing telemetry. * feat(sync): shared DB-pacer permit across parallel worker engines One pacer spans the per-worker PostgresEngines (the multi-pool permit case); observe() import writes, pace() between files, dispose on all exit paths. * docs(pace): CLAUDE.md Pace Mode section + regenerated llms bundle * fix(pace): pre-landing review fixes (Codex P1/P2) - never unref() the cooperative-sleep timer (could exit mid-sleep) - pace() excludes the wall-clock budget + re-arm after each sleep - pacing only lowers concurrency, never raises above an operator cap - serialized job pace resolves at config tier so GBRAIN_PACE_* still wins - --pace-max-concurrency consumes its value token * chore: bump version and changelog (v0.42.48.0) Native DB-contention pacing for embed/sync backfills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version to v0.42.49.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
/**
|
|
* Pins the DB-pacing mode bundles + resolution chain.
|
|
* The load-bearing claim: env beats config (incident escape hatch), per-call
|
|
* beats env, and `off` resolves to a disabled bundle.
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import {
|
|
PACE_BUNDLES,
|
|
PACE_MODES,
|
|
DEFAULT_PACE_MODE,
|
|
isPaceMode,
|
|
resolvePaceMode,
|
|
loadOverridesFromConfig,
|
|
readPaceEnv,
|
|
} from '../src/core/pace-mode.ts';
|
|
|
|
describe('pace-mode bundles', () => {
|
|
test('off is the default and is disabled', () => {
|
|
expect(DEFAULT_PACE_MODE).toBe('off');
|
|
expect(PACE_BUNDLES.off.enabled).toBe(false);
|
|
});
|
|
|
|
test('enabled bundles cap concurrency below the unpaced default (20)', () => {
|
|
for (const m of ['gentle', 'balanced', 'aggressive'] as const) {
|
|
expect(PACE_BUNDLES[m].enabled).toBe(true);
|
|
expect(PACE_BUNDLES[m].maxConcurrency).toBeGreaterThanOrEqual(1);
|
|
expect(PACE_BUNDLES[m].maxConcurrency).toBeLessThan(20);
|
|
}
|
|
});
|
|
|
|
test('isPaceMode guards the union', () => {
|
|
expect(PACE_MODES.every(isPaceMode)).toBe(true);
|
|
expect(isPaceMode('turbo')).toBe(false);
|
|
expect(isPaceMode(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolvePaceMode precedence', () => {
|
|
test('unknown mode falls back to off (disabled), mode_valid=false', () => {
|
|
const r = resolvePaceMode({ mode: 'turbo' });
|
|
expect(r.resolved_mode).toBe('off');
|
|
expect(r.mode_valid).toBe(false);
|
|
expect(r.enabled).toBe(false);
|
|
});
|
|
|
|
test('config mode applies its bundle', () => {
|
|
const r = resolvePaceMode({ mode: 'balanced' });
|
|
expect(r.resolved_mode).toBe('balanced');
|
|
expect(r.maxConcurrency).toBe(PACE_BUNDLES.balanced.maxConcurrency);
|
|
});
|
|
|
|
test('env mode beats config mode (incident escape hatch)', () => {
|
|
const r = resolvePaceMode({ mode: 'gentle', envMode: 'aggressive' });
|
|
expect(r.resolved_mode).toBe('aggressive');
|
|
});
|
|
|
|
test('per-call mode beats env and config', () => {
|
|
const r = resolvePaceMode({ mode: 'gentle', envMode: 'balanced', perCallMode: 'aggressive' });
|
|
expect(r.resolved_mode).toBe('aggressive');
|
|
});
|
|
|
|
test('env knob override beats config knob override', () => {
|
|
const r = resolvePaceMode({
|
|
mode: 'balanced',
|
|
configOverrides: { maxConcurrency: 6 },
|
|
envOverrides: { maxConcurrency: 2 },
|
|
});
|
|
expect(r.maxConcurrency).toBe(2);
|
|
});
|
|
|
|
test('per-call knob beats env and config', () => {
|
|
const r = resolvePaceMode({
|
|
mode: 'balanced',
|
|
configOverrides: { maxConcurrency: 6 },
|
|
envOverrides: { maxConcurrency: 2 },
|
|
perCall: { maxConcurrency: 12 },
|
|
});
|
|
expect(r.maxConcurrency).toBe(12);
|
|
});
|
|
|
|
test('clamps an out-of-range maxConcurrency back to the bundle when enabled', () => {
|
|
const r = resolvePaceMode({ mode: 'balanced', perCall: { maxConcurrency: 0 } });
|
|
expect(r.enabled).toBe(true);
|
|
expect(r.maxConcurrency).toBe(PACE_BUNDLES.balanced.maxConcurrency);
|
|
});
|
|
|
|
test('clamps a bad ewmaAlpha back to a sane default', () => {
|
|
const r = resolvePaceMode({ mode: 'balanced', perCall: { ewmaAlpha: 5 } });
|
|
expect(r.ewmaAlpha).toBeGreaterThan(0);
|
|
expect(r.ewmaAlpha).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('config + env parsing', () => {
|
|
test('loadOverridesFromConfig parses present keys only', () => {
|
|
const ov = loadOverridesFromConfig({
|
|
'pace.max_concurrency': '5',
|
|
'pace.pace_at_ms': '400',
|
|
});
|
|
expect(ov.maxConcurrency).toBe(5);
|
|
expect(ov.paceAtMs).toBe(400);
|
|
expect(ov.maxSleepMs).toBeUndefined();
|
|
});
|
|
|
|
test('readPaceEnv reads GBRAIN_PACE_* including mode', () => {
|
|
const { envMode, envOverrides } = readPaceEnv({
|
|
GBRAIN_PACE_MODE: 'gentle',
|
|
GBRAIN_PACE_MAX_CONCURRENCY: '3',
|
|
GBRAIN_PACE_ENABLED: 'true',
|
|
});
|
|
expect(envMode).toBe('gentle');
|
|
expect(envOverrides.maxConcurrency).toBe(3);
|
|
expect(envOverrides.enabled).toBe(true);
|
|
});
|
|
|
|
test('rejects out-of-range env concurrency (falls through)', () => {
|
|
const { envOverrides } = readPaceEnv({ GBRAIN_PACE_MAX_CONCURRENCY: '99999' });
|
|
expect(envOverrides.maxConcurrency).toBeUndefined();
|
|
});
|
|
});
|