mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
* fix(relay): rehome hosts to their preferred region in either direction The regional-rehome worker only moved hosts from a us-central1 cell to an asia-east2 one, so a host whose desktop later records us-central1 stays where it was put. Rehoming now compares the fresh preference against the region of the cell the host is on and moves it to a general cell in the preferred region either way, through the same drain, migrate, safety, and rate-limit machinery. - relay_region_rehome_attempts.preferred_region accepts both regions; existing databases are upgraded in place by an idempotent named-constraint swap that is safe when several directors start at once. - A target must carry the drain protocol too: moving a host onto a cell it can never be drained off again is the trap this change exists to undo. The fleet whose health gates a rehome is now every general drainable cell, which is exactly the set of legal sources and targets. - The trust probe accepts a source cell in any region. No wire change, and no behaviour change while the durable control is off. * fix(relay): bound bidirectional rehoming with a per-host cooldown Moving hosts in both directions removed the property that made the old one-way worker self-terminating: a desktop whose region probe flips would be dragged back and forth, one full drain and migrate per flip, because the preference age never expires while the host keeps reconnecting. - relay_region_rehome_control gains host_cooldown_ms, an operator input plumbed like preference_max_age_ms (workflow, ops script, admin route, durable row) and defaulted to seven days. A host with any attempt row inside the window, whichever way that move went, is not a candidate; the claim re-reads it under lock so an attempt landing between scan and claim cannot start a second move. Skips are named host_cooldown, and the lookup rides a new index on (user_id, relay_host_id, created_at). - The candidate scan now also requires the target cell to be enabled, so it mirrors the claim-time filter exactly and stops spending batch slots on candidates that are certain to be skipped. - Region CHECK lists are rendered from the shared region list instead of being written out four times. - The operations runbook states that cells without the drain protocol are neither sources, targets, nor members of the safety gate. * fix(relay): keep rehome reads and brakes working across the cooldown rollout The ops script validated hostCooldownMs on every inspected control, so against any director image predating the field inspect, pause, disable, and failed-enable recovery all threw client-side. The workflow always runs from main while the director image is operator-supplied, so that window opened at merge and reopened on every rollback: the operator lost read-only visibility and both emergency brakes while the worker could still be enabled. The field is now validated only when the director reports it, and every apply body that echoes an inspected control omits the key when that control lacks it, so a legacy director never sees an unknown key. The write path stays fail-closed the other way: enable refuses up front, before any mutation, when the director does not report a cooldown it could honour. Also replaces two bare 'us-central1' defaults with RELAY_DEFAULT_REGION.
343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
import { pathToFileURL } from 'node:url'
|
|
import { fetchAdminOnceMore } from './relay-admin-transient-retry.mjs'
|
|
import { inspectAdmissionSelector } from './relay-admission-selector.mjs'
|
|
|
|
const DIRECTOR_ORIGIN = 'https://relay.onorca.dev'
|
|
const MODES = new Set(['inspect', 'enable', 'pause', 'disable', 'recover-enable'])
|
|
|
|
function canonicalCells(value) {
|
|
if (value === 'none') return []
|
|
const cells = value.split(',').map((cell) => cell.trim()).filter(Boolean).sort()
|
|
if (
|
|
cells.length === 0 ||
|
|
new Set(cells).size !== cells.length ||
|
|
cells.some((cell) => !/^production-gce-c(?:[1-9]|[12][0-9])$/.test(cell))
|
|
) throw new Error('selector membership is invalid')
|
|
return cells
|
|
}
|
|
|
|
function integer(value, name, { minimum = 0, maximum = Number.MAX_SAFE_INTEGER } = {}) {
|
|
const parsed = Number(value)
|
|
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
throw new Error(`${name} is invalid`)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
export function parseRegionalRehomeArguments(argv, environment = process.env) {
|
|
const values = {}
|
|
for (let index = 0; index < argv.length; index += 2) {
|
|
const key = argv[index]
|
|
const value = argv[index + 1]
|
|
if (!key?.startsWith('--') || value === undefined) throw new Error('invalid arguments')
|
|
values[key.slice(2)] = value
|
|
}
|
|
for (const key of ['mode', 'director-origin', 'expected-control-generation']) {
|
|
if (!values[key]) throw new Error(`missing --${key}`)
|
|
}
|
|
if (!MODES.has(values.mode)) throw new Error('--mode is invalid')
|
|
if (values['director-origin'] !== DIRECTOR_ORIGIN) {
|
|
throw new Error('--director-origin must be the production Relay origin')
|
|
}
|
|
const recovery = values.mode === 'recover-enable'
|
|
const mutation = values.mode !== 'inspect' && !recovery
|
|
const mutationKeys = [
|
|
'not-before',
|
|
'rate-per-minute',
|
|
'preference-max-age-ms',
|
|
'host-cooldown-ms',
|
|
'drain-grace-ms',
|
|
'confirmation'
|
|
]
|
|
if (mutation && mutationKeys.some((key) => values[key] === undefined)) {
|
|
throw new Error('mutations require the complete durable control shape')
|
|
}
|
|
if (!mutation && !recovery && mutationKeys.some((key) => values[key] !== undefined)) {
|
|
throw new Error('inspect cannot carry mutation arguments')
|
|
}
|
|
const selectorKeys = [
|
|
'expected-selector-generation',
|
|
'expected-existing-only-cells',
|
|
'expected-migration-only-cells',
|
|
'expected-general-cells'
|
|
]
|
|
if (!recovery && selectorKeys.some((key) => values[key] === undefined)) {
|
|
throw new Error('operation requires exact selector state')
|
|
}
|
|
if (recovery && selectorKeys.some((key) => values[key] !== undefined)) {
|
|
throw new Error('enable recovery cannot depend on selector diagnostics')
|
|
}
|
|
const expectedConfirmation = {
|
|
enable: 'ENABLE_REGIONAL_REHOMING',
|
|
pause: 'PAUSE_REGIONAL_REHOMING',
|
|
disable: 'DISABLE_REGIONAL_REHOMING'
|
|
}[values.mode]
|
|
if (mutation && values.confirmation !== expectedConfirmation) {
|
|
throw new Error('confirmation does not match the requested control action')
|
|
}
|
|
if (recovery && values.confirmation !== 'RECOVER_FAILED_REGIONAL_REHOME_ENABLE') {
|
|
throw new Error('confirmation does not authorize failed-enable recovery')
|
|
}
|
|
if (
|
|
recovery &&
|
|
mutationKeys
|
|
.filter((key) => key !== 'confirmation')
|
|
.some((key) => values[key] !== undefined)
|
|
) throw new Error('enable recovery cannot carry durable control shape arguments')
|
|
const token = environment.ORCA_RELAY_ADMIN_ID_TOKEN
|
|
if (!token || token.length > 8_192) throw new Error('admin identity token is unavailable')
|
|
return {
|
|
mode: values.mode,
|
|
directorOrigin: DIRECTOR_ORIGIN,
|
|
...(!recovery
|
|
? {
|
|
expectedSelectorGeneration: integer(
|
|
values['expected-selector-generation'],
|
|
'--expected-selector-generation'
|
|
),
|
|
expectedMembership: {
|
|
existingOnly: canonicalCells(values['expected-existing-only-cells']),
|
|
migrationOnly: canonicalCells(values['expected-migration-only-cells']),
|
|
general: canonicalCells(values['expected-general-cells'])
|
|
}
|
|
}
|
|
: {}),
|
|
expectedControlGeneration: integer(
|
|
values['expected-control-generation'],
|
|
'--expected-control-generation'
|
|
),
|
|
...(mutation
|
|
? {
|
|
notBefore: integer(values['not-before'], '--not-before'),
|
|
ratePerMinute: integer(values['rate-per-minute'], '--rate-per-minute', {
|
|
minimum: 1,
|
|
maximum: 120
|
|
}),
|
|
preferenceMaxAgeMs: integer(
|
|
values['preference-max-age-ms'],
|
|
'--preference-max-age-ms',
|
|
{ minimum: 60_000, maximum: 30 * 24 * 60 * 60_000 }
|
|
),
|
|
hostCooldownMs: integer(
|
|
values['host-cooldown-ms'],
|
|
'--host-cooldown-ms',
|
|
{ minimum: 60_000, maximum: 30 * 24 * 60 * 60_000 }
|
|
),
|
|
drainGraceMs: integer(values['drain-grace-ms'], '--drain-grace-ms', {
|
|
minimum: 60_000,
|
|
maximum: 60 * 60_000
|
|
})
|
|
}
|
|
: {}),
|
|
token
|
|
}
|
|
}
|
|
|
|
async function responseJson(response, label) {
|
|
const body = await response.json().catch(() => ({}))
|
|
if (!response.ok) throw new Error(`${label} returned ${response.status}: ${body.error ?? 'unknown'}`)
|
|
return body
|
|
}
|
|
|
|
function exactMembership(actual, expected) {
|
|
return ['existingOnly', 'migrationOnly', 'general'].every(
|
|
(key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key])
|
|
)
|
|
}
|
|
|
|
function assertControl(control, expected) {
|
|
if (
|
|
(expected.generation !== undefined && control?.generation !== expected.generation) ||
|
|
typeof control.enabled !== 'boolean' ||
|
|
!Number.isSafeInteger(control.observationStartedAt) ||
|
|
!Number.isSafeInteger(control.notBefore) ||
|
|
!Number.isSafeInteger(control.ratePerMinute) ||
|
|
!Number.isSafeInteger(control.preferenceMaxAgeMs) ||
|
|
// A director predating the per-host cooldown does not report it. Reading
|
|
// the control and both emergency brakes must keep working against that
|
|
// image; only enable requires the field.
|
|
(control.hostCooldownMs !== undefined &&
|
|
!Number.isSafeInteger(control.hostCooldownMs)) ||
|
|
!Number.isSafeInteger(control.drainGraceMs)
|
|
) throw new Error('director returned an invalid regional rehome control')
|
|
if (expected.enabled !== undefined && control.enabled !== expected.enabled) {
|
|
throw new Error('regional rehome enabled state does not match')
|
|
}
|
|
return control
|
|
}
|
|
|
|
// Echo the cooldown only when the director already reports it: a legacy
|
|
// director rejects the unknown key outright and would refuse every brake.
|
|
function cooldownField(before, value) {
|
|
return before.hostCooldownMs === undefined ? {} : { hostCooldownMs: value }
|
|
}
|
|
|
|
async function verifiedDisabledControl(post, generation) {
|
|
return assertControl((await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'inspect'
|
|
})).control, { generation, enabled: false })
|
|
}
|
|
|
|
async function applyDisabledControl(post, before) {
|
|
return assertControl((await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'apply',
|
|
expectedGeneration: before.generation,
|
|
enabled: false,
|
|
notBefore: before.notBefore,
|
|
ratePerMinute: before.ratePerMinute,
|
|
preferenceMaxAgeMs: before.preferenceMaxAgeMs,
|
|
...cooldownField(before, before.hostCooldownMs),
|
|
drainGraceMs: before.drainGraceMs,
|
|
confirmation: 'DISABLE_REGIONAL_REHOMING'
|
|
})).control, { generation: before.generation + 1, enabled: false })
|
|
}
|
|
|
|
async function resolveAmbiguousDisable(post, before, firstError) {
|
|
const observed = assertControl((await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'inspect'
|
|
})).control, {})
|
|
if (observed.generation === before.generation + 1 && !observed.enabled) {
|
|
return observed
|
|
}
|
|
if (observed.generation !== before.generation || !observed.enabled) {
|
|
throw new AggregateError(
|
|
[firstError],
|
|
'failed-enable recovery reached an unexpected control generation'
|
|
)
|
|
}
|
|
try {
|
|
return await applyDisabledControl(post, before)
|
|
} catch (retryError) {
|
|
try {
|
|
return await verifiedDisabledControl(post, before.generation + 1)
|
|
} catch (readbackError) {
|
|
throw new AggregateError(
|
|
[firstError, retryError, readbackError],
|
|
'failed-enable recovery exhausted two bounded CAS attempts'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function recoverRegionalRehomeEnable(config, post) {
|
|
const before = assertControl((await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'inspect'
|
|
})).control, {})
|
|
if (
|
|
before.generation < config.expectedControlGeneration ||
|
|
(before.generation === config.expectedControlGeneration && before.enabled)
|
|
) throw new Error('durable control cannot belong to the failed enable attempt')
|
|
if (!before.enabled) {
|
|
const verified = await verifiedDisabledControl(post, before.generation)
|
|
return { mode: config.mode, recovered: false, control: verified }
|
|
}
|
|
let applied
|
|
try {
|
|
applied = await applyDisabledControl(post, before)
|
|
} catch (error) {
|
|
applied = await resolveAmbiguousDisable(post, before, error)
|
|
}
|
|
const verified = await verifiedDisabledControl(post, applied.generation)
|
|
return { mode: config.mode, recovered: true, control: verified }
|
|
}
|
|
|
|
export async function operateRegionalRehome(config, dependencies = {}) {
|
|
const fetchImpl = dependencies.fetch ?? fetch
|
|
const post = dependencies.post ?? (async (path, body) => await responseJson(
|
|
// Generation-guarded writes make a retry a no-op or an explicit mismatch, never a double apply.
|
|
await fetchAdminOnceMore(
|
|
fetchImpl,
|
|
`${config.directorOrigin}${path}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
authorization: `Bearer ${config.token}`,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: JSON.stringify(body)
|
|
},
|
|
{ wait: dependencies.wait }
|
|
),
|
|
path
|
|
))
|
|
if (config.mode === 'recover-enable') {
|
|
return await recoverRegionalRehomeEnable(config, post)
|
|
}
|
|
const selector = (await inspectAdmissionSelector(post)).selector
|
|
if (
|
|
selector.generation !== config.expectedSelectorGeneration ||
|
|
!exactMembership(selector.membership, config.expectedMembership)
|
|
) throw new Error('admission selector does not match the reviewed generation and membership')
|
|
|
|
const inspected = await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'inspect'
|
|
})
|
|
const before = assertControl(inspected.control, {
|
|
generation: config.expectedControlGeneration
|
|
})
|
|
if (config.mode === 'inspect') return { mode: config.mode, selector, control: before }
|
|
if (config.mode === 'enable' && before.enabled) {
|
|
throw new Error('regional rehome is already enabled; inspect before changing its rate')
|
|
}
|
|
if (config.mode === 'pause' && !before.enabled) {
|
|
throw new Error('regional rehome is already paused')
|
|
}
|
|
const enabled = config.mode === 'enable'
|
|
if (enabled && before.hostCooldownMs === undefined) {
|
|
throw new Error(
|
|
'director does not report a per-host rehome cooldown; deploy a director that supports it before enabling'
|
|
)
|
|
}
|
|
const applied = await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'apply',
|
|
expectedGeneration: config.expectedControlGeneration,
|
|
enabled,
|
|
notBefore: config.notBefore,
|
|
ratePerMinute: config.ratePerMinute,
|
|
preferenceMaxAgeMs: config.preferenceMaxAgeMs,
|
|
...cooldownField(before, config.hostCooldownMs),
|
|
drainGraceMs: config.drainGraceMs,
|
|
confirmation: enabled
|
|
? 'ENABLE_REGIONAL_REHOMING'
|
|
: 'DISABLE_REGIONAL_REHOMING'
|
|
})
|
|
const after = assertControl(applied.control, {
|
|
generation: config.expectedControlGeneration + 1,
|
|
enabled
|
|
})
|
|
const verified = assertControl((await post('/v1/admin/regional-rehome-control', {
|
|
v: 1,
|
|
action: 'inspect'
|
|
})).control, {
|
|
generation: after.generation,
|
|
enabled
|
|
})
|
|
return { mode: config.mode, selector, control: verified }
|
|
}
|
|
|
|
export async function main(
|
|
argv = process.argv.slice(2),
|
|
environment = process.env,
|
|
dependencies = {},
|
|
write = (value) => process.stdout.write(value)
|
|
) {
|
|
const result = await operateRegionalRehome(
|
|
parseRegionalRehomeArguments(argv, environment),
|
|
dependencies
|
|
)
|
|
write(`${JSON.stringify({ event: 'relay_regional_rehome_control', ...result })}\n`)
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
process.exitCode = 1
|
|
})
|
|
}
|