Files
orca/cloud/dev/scripts/relay-same-cap-job-mode-conditions.test.mjs
T
Jinwoo Hong 07dad6739a refactor(relay): sample fleet health inside the same-cap roll instead of a separate monitor run (#24443)
* refactor(relay): sample fleet health inside the same-cap roll instead of a separate monitor run

A same-cap wave no longer consumes a 15-minute monitor dry-run and its sealed,
single-use, five-minute-fresh evidence. Each apply wave now samples fleet health
itself right before isolation, with the monitor's evaluator, thresholds, and
tolerances, for a window sized to the cell's host count (3/5/8 min), plus three
lookback rules: no cell container exit in 10 min, no minute over 500 director
503s in 10 min, and director concurrency p99 within the monitor bar over 4 min.

Removes the monitor-run inputs, the gate's consume/authorize steps, the
break-glass override, and the same-cap-only authorization shapes in
relay-monitor-evidence.mjs. The monitor workflow and the rehome enable path are
unchanged.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010

* fix(relay): bound the pre-drain sample overrun and keep the drain token fresh

Review follow-ups: alternating tolerated readings could hold the sample open
until its step timeout, so cap the overrun at three samples past the window;
record why a read failed; mint a fresh admin ID token for the drain after the
sample; raise the job timeout to 90 min so a long sample cannot cancel the
job past the failsafe.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010

* feat(relay): exempt the rolled cell and existing-only cells from the pre-drain crash rule

The exit rule counted every relay container exit fleet-wide, so a cell that
crashes every few hours (c25, 12 a week) blocked the very roll that fixes it,
and existing-only legacy cells (c5, 15 a week) blocked rolls they take no part
in. Exits are now grouped by instance, each instance is named by its own newest
runtime-metrics log line, and only exits on general or migration-only cells
other than the target count. An exit no configured cell can be named for trips
the rule; a failed lookup is a failed read. relay-observability.tf joins the
evidence-code set because the rule depends on its filter.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010

* test(relay): cover re-asking for an unnamed exiting instance; note the boot-exit risk

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
2026-10-01 15:36:17 -04:00

164 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { readRelayWorkflow } from './relay-repository.mjs'
const parent = readRelayWorkflow('deploy-relay-production-same-cap.yml')
const job = readRelayWorkflow('deploy-relay-production-same-cap-job.yml')
function jobAcceptedModes() {
const match = /\[\[ "\$\{DEPLOY_MODE\}" =~ \^\(([a-z|-]+)\)\$ \]\]/.exec(job)
assert.ok(match, 'the job no longer validates DEPLOY_MODE')
return new Set(match[1].split('|'))
}
// Mirrors the gate step's dispatch-mode to job-mode mapping.
function parentJobModes() {
const options = /mode:\n\s+description:[^\n]*\n(?:\s+[a-z]+:[^\n]*\n)*?\s+options: \[([^\]]+)\]/
.exec(parent)
assert.ok(options, 'the parent has no dispatch mode options')
const collapsed = /if \[\[ "\$\{MODE\}" =~ \^\(([a-z|-]+)\)\$ \]\]; then\n\s+echo 'job-mode=([a-z]+)'/
.exec(parent)
assert.ok(collapsed, 'the parent no longer collapses apply modes into one job mode')
const collapsedModes = new Set(collapsed[1].split('|'))
return new Set(
options[1].split(',').map((mode) => mode.trim()).map((mode) =>
collapsedModes.has(mode) ? collapsed[2] : mode
)
)
}
function stepCondition(name) {
const start = job.indexOf(`- name: ${name}\n`)
assert.notEqual(start, -1, `job has no step named ${name}`)
const step = job.slice(start, job.indexOf('\n - ', start + 1))
const condition = /^ {8}if: \$\{\{ (.+) \}\}$/m.exec(step)
assert.ok(condition, `${name} has no if condition`)
return condition[1]
}
// Just enough of the expression grammar for the step conditions this file compares.
function evaluate(expression, context) {
const tokens = expression.match(/'[^']*'|[A-Za-z_.]+|==|!=|&&|\|\||[()]/g)
assert.equal(tokens.join(''), expression.replaceAll(' ', ''), `unparsed: ${expression}`)
let index = 0
const value = () => {
const token = tokens[index++]
if (token === '(') {
const inner = or()
assert.equal(tokens[index++], ')')
return inner
}
if (token.startsWith("'")) return token.slice(1, -1)
assert.ok(Object.hasOwn(context, token), `unknown operand ${token}`)
return context[token]
}
const comparison = () => {
const left = value()
if (tokens[index] !== '==' && tokens[index] !== '!=') return left
const operator = tokens[index++]
const right = value()
return operator === '==' ? left === right : left !== right
}
const and = () => {
let result = comparison()
while (tokens[index] === '&&') {
index++
result = comparison() && result
}
return result
}
const or = () => {
let result = and()
while (tokens[index] === '||') {
index++
result = or() || result
}
return result
}
const result = or()
assert.equal(index, tokens.length, `unparsed tail: ${expression}`)
return result
}
test('the parent passes only modes the job accepts', () => {
assert.deepEqual(parentJobModes(), jobAcceptedModes())
})
// A comparison against a mode the job never receives is constant, so its step is dead (#24259).
test('every job mode comparison names a mode the job can receive', () => {
const accepted = jobAcceptedModes()
const compared = [...job.matchAll(/inputs\.mode\s*[!=]=\s*'([^']*)'/g)].map(([, mode]) => mode)
assert.ok(compared.length > 0)
for (const mode of compared) assert.ok(accepted.has(mode), `job compares against ${mode}`)
})
test('the headroom gate runs whenever the drain runs, and in verify', () => {
const headroom = stepCondition("Require free general-cell slots for the selected cell's hosts")
const drain = stepCondition('Reversibly isolate and drain only the selected cell')
for (const mode of parentJobModes()) {
for (const resume of ['true', 'false']) {
const context = { 'inputs.mode': mode, 'env.ROLLBACK_RESUME': resume }
if (evaluate(drain, context)) {
assert.ok(evaluate(headroom, context), `drain without headroom check: ${mode}/${resume}`)
}
}
}
assert.ok(evaluate(headroom, { 'inputs.mode': 'verify', 'env.ROLLBACK_RESUME': 'false' }))
})
// The roll's fleet-health gate lives inside the job now; a separate monitor run must not creep back.
test('neither same-cap workflow depends on monitor evidence', () => {
for (const [name, text] of [['parent', parent], ['job', job]]) {
for (const pattern of [
/monitor-run-(id|attempt)/,
/MONITOR_RUN_/,
/relay-monitor-evidence/,
/relay-monitor-dry-run-/,
/monitor-consumed/,
/gate-override/,
/GATE_OVERRIDE/,
/SKIP_RELAY_MONITOR_GATE/,
/--state-file/
]) {
assert.doesNotMatch(text, pattern, `${name} still references ${pattern}`)
}
}
})
test('every apply wave samples fleet health right before it drains', () => {
const sampleName = 'Sample fleet health for a window sized to this drain'
const sample = stepCondition(sampleName)
const drain = stepCondition('Reversibly isolate and drain only the selected cell')
for (const mode of parentJobModes()) {
for (const resume of ['true', 'false']) {
const context = { 'inputs.mode': mode, 'env.ROLLBACK_RESUME': resume }
if (mode === 'apply' && evaluate(drain, context)) {
assert.ok(evaluate(sample, context), `apply drain without a sample: ${resume}`)
}
}
}
assert.equal(evaluate(sample, { 'inputs.mode': 'verify' }), false)
assert.equal(evaluate(sample, { 'inputs.mode': 'rollback' }), false)
const at = (name) => {
const index = job.indexOf(`- name: ${name}\n`)
assert.notEqual(index, -1, name)
return index
}
const order = [
'Recheck aggregate SQL, pool, reconnect, migration, and selector safety',
"Require free general-cell slots for the selected cell's hosts",
sampleName,
'Reversibly isolate and drain only the selected cell'
].map(at)
assert.deepEqual(order, [...order].sort((left, right) => left - right))
const step = job.slice(at(sampleName), job.indexOf('\n - ', at(sampleName) + 1))
// The window is sized from the headroom step's own count, and each wave offsets its selector.
assert.match(step, /pnpm incident:relay-pre-drain-sample/)
assert.match(step, /--target-hosts "\$\{TARGET_HOSTS\}"/)
// The crash rule exempts the cell being rolled, so it must be told which one that is.
assert.match(step, /--target-cell-id "\$\{TARGET_CELL_ID\}"/)
assert.match(step, /--wave-index "\$\{WAVE_INDEX\}"/)
assert.match(step, /--selector-wave-delta "\$\{SELECTOR_WAVE_DELTA\}"/)
assert.match(job, /echo "TARGET_HOSTS=\$\{TARGET_HOSTS\}" >> "\$\{GITHUB_ENV\}"/)
})