Files
orca/cloud/apps/relay/src/cell-boot-without-database-postgres.test.ts
T
Jinwoo Hong 54ded3bc18 fix(relay): commit the cell counter in one round trip; cells boot without the database (#25765)
* fix(relay): commit the cell counter in one round trip; cells boot without the DB

Step 2 (option B) cell image:
- One-round-trip counter commit at acquireActivity, releaseActivity and
  activateControl: the final counter UPDATE and COMMIT go as one simple-query
  message. Server errors mean COMMIT never ran (retry as today; 22012 = no row,
  rolled back and disambiguated outside the transaction); a lost connection is
  never retried.
- Cells skip the schema apply and region backfill, so they listen while the
  database is down and turn ready on their first successful query.
- G13: rehome target connection headroom folded into the existing NOWAIT
  UPDATE, excluding the host's own reservation by key.
- fixLevel on every runtime metrics line, plus declared (not applied) cell
  fix-level metrics and alert.
- Per-desktop drain disconnect-gap measurement from existing log lines.
- Census test that fails on floating database promises; fixes two shutdown
  sites. Lock-wait sample keeps the combined role.

* fix(relay): make the outdated-image alert creatable: one PromQL condition, 1 h lookback, fixed floor

A PromQL condition must be the only condition in its policy, and alerts on
log-based metrics may look back at most 25 h. Replace the 6-day/7-day design
with relay_cell_min_fix_level (tfvars, raised by a targeted apply after each
wave) and one query: a serving cell below the floor or reporting no level,
sustained 6 h. Drops the separate without-level metric.

* fix(relay): review fixes: gap-script ordering, wider promise census, fused-path guard, row-busy as scheduled

- Drain gap script: sort closes by time (gcloud exports newest first) and
  refuse an invalid drain start.
- Census: any floating promise in relay src, including callback-discarded
  and never-read ones, with a reviewed never-rejects list.
- Test the fused counter commit through the store the server builds, so a
  wrapper that stops forwarding commitWithFinal fails CI.
- Same-cap shadow gate: a row-busy refusal (the host's own release still
  holds its row) is a scheduled 503, like an own early retry. No client
  change.

* test(relay): judge drain redials by no host refused twice, not a refusal count

The row-busy count tracks how many releases are still in flight at the
dial (80 of 180 every run at 1 s, against a bar of 90). What matters is
that the release has finished by the next dial: assert no host is refused
twice, keep the time-to-placed p95 bound.

* fix(relay): cap row-busy as scheduled at the drain-return admissions; bound the gap script's window

Shadow gate: a row-busy refusal of a drained host follows its drain-return
lane admission, so per minute only that many (plus a rounding margin of 2)
are scheduled; the rest stay non-drain, so row contention the drain does
not explain still fails the budget. Gap script: --drain-ended-at excludes
the new container's closes after the roll; later grants still close a gap.
2026-10-06 00:53:51 -04:00

122 lines
4.5 KiB
TypeScript

import { createServer as createHttpServer, type Server } from 'node:http'
import { createServer as createTcpServer, connect, type AddressInfo } from 'node:net'
import { afterEach, describe, expect, it } from 'vitest'
import type { RelayConfig } from './config.js'
import { openRelayDatabase, type RelayDatabase } from './database.js'
import { createRelayReadiness } from './relay-readiness.js'
import { createRelayServer } from './relay-server.js'
const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL
const describePostgres = databaseUrl ? describe : describe.skip
// c25 exited after 45 s of boot retries when its database was unreachable, and the MIG
// recreated it into the same loop. A cell now opens a lazy pool and listens at once.
describePostgres('cell boot while PostgreSQL is unreachable', () => {
const cleanups: Array<() => Promise<void>> = []
afterEach(async () => {
for (const cleanup of cleanups.splice(0).reverse()) await cleanup()
})
it('listens with /health 200 and /ready 503, then turns ready once the database answers', async () => {
const proxyPort = await unusedPort()
const target = new URL(databaseUrl!)
const viaProxy = new URL(databaseUrl!)
viaProxy.hostname = '127.0.0.1'
viaProxy.port = String(proxyPort)
const startedAt = performance.now()
const database = await openRelayDatabase({
databaseUrl: viaProxy.toString(),
dataDir: '',
appliesPostgresSchema: false
})
cleanups.push(async () => await database.close())
// Nothing dialled: the 2 s connect timeout would show here otherwise.
expect(performance.now() - startedAt).toBeLessThan(500)
await expect(database.query('SELECT 1')).rejects.toThrow()
const jwks = await listen(
createHttpServer((_request, response) => {
response.setHeader('content-type', 'application/json')
response.end('{"keys":[]}')
})
)
cleanups.push(async () => await close(jwks))
const jwksUrl = `http://127.0.0.1:${(jwks.address() as AddressInfo).port}/jwks`
const relay = createRelayServer(cellConfig(jwksUrl), database)
const server = await listen(relay.server)
cleanups.push(async () => await close(server))
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
expect((await fetch(`${base}/health`)).status).toBe(200)
expect((await fetch(`${base}/ready`)).status).toBe(503)
const proxy = await listenOn(
createTcpServer((socket) => {
const upstream = connect(Number(target.port), target.hostname)
socket.pipe(upstream).pipe(socket)
socket.on('error', () => upstream.destroy())
upstream.on('error', () => socket.destroy())
}),
proxyPort
)
cleanups.push(async () => await close(proxy))
const readiness = createRelayReadiness(database, jwksUrl, { cacheMs: 0 })
await expect(readiness.check()).resolves.toBe(true)
}, 20_000)
})
async function unusedPort(): Promise<number> {
const probe = await listen(createTcpServer())
const { port } = probe.address() as AddressInfo
await close(probe)
return port
}
async function listen<T extends Server | ReturnType<typeof createTcpServer>>(server: T): Promise<T> {
return await listenOn(server, 0)
}
async function listenOn<T extends Server | ReturnType<typeof createTcpServer>>(
server: T,
port: number
): Promise<T> {
await new Promise<void>((resolve) => server.listen(port, '127.0.0.1', resolve))
return server
}
async function close(server: Server | ReturnType<typeof createTcpServer>): Promise<void> {
if ('closeAllConnections' in server) server.closeAllConnections()
await new Promise<void>((resolve) => server.close(() => resolve()))
}
function cellConfig(jwksUrl: string): RelayConfig {
return {
port: 0,
publicUrl: 'https://c25.relay.example.test',
cellUrl: 'https://c25.relay.example.test',
region: 'asia-east2',
authIssuer: 'https://auth.example.test',
authAudience: 'orca-relay',
jwksUrl,
assignmentSigningKey: new Uint8Array(32),
role: 'cell',
cellId: 'production-gce-c25',
cells: [],
adminAudience: 'https://relay.example.test/v1/admin/drain',
deployServiceAccount: 'deploy@example.test',
runtimeServiceAccount: 'relay-cell@example.test',
adminJwksUrl: jwksUrl,
databasePoolMax: 10,
publicAssignmentsEnabled: true,
publicAssignmentConcurrency: 2,
publicAssignmentQueueMax: 128,
publicAssignmentWaitMs: 4_000,
publicResolveConcurrency: 1,
publicResolveWaitMs: 5_000,
publicAssignmentRetryAfterSeconds: 5,
dataDir: './data'
}
}