mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
* 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.
122 lines
4.5 KiB
TypeScript
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'
|
|
}
|
|
}
|