mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
fix(relay): give the catalog request the warm-up budget and report reachability before catalog shape
- the catalog fetch is the coldest request of the sequence (DNS, TCP, TLS to the director) and ran on the 1.5 s sample clock; a timeout withheld the hint for an hour - a lone region that is unreachable or flapping now logs all-unreachable / all-rejected, so a census of client-side network breakage is not hidden by a roll wave - tests pin the budget sequence, the requestTimeoutMs override, and the aggregated warning text
This commit is contained in:
@@ -203,7 +203,13 @@ describe('RelayControlOrigin pending-connection replay', () => {
|
||||
// both the pairing authority and the E2EE device binding.
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
fakes.controlConnect.mockResolvedValue(
|
||||
ack({ pendingConns: [{ connId: 'conn-unknown', connTicket: TICKET }] })
|
||||
ack({
|
||||
pendingConns: [
|
||||
{ connId: 'conn-unknown', connTicket: TICKET },
|
||||
{ connId: 'conn-unknown-2', connTicket: TICKET },
|
||||
{ connId: 'conn-unknown-3', connTicket: TICKET }
|
||||
]
|
||||
})
|
||||
)
|
||||
const { origin, owned } = createOrigin()
|
||||
|
||||
@@ -211,7 +217,11 @@ describe('RelayControlOrigin pending-connection replay', () => {
|
||||
|
||||
expect(fakes.transports[0]!.openConnection).not.toHaveBeenCalled()
|
||||
expect(owned).toEqual([])
|
||||
// One aggregated line per ack, not one per entry.
|
||||
expect(warn).toHaveBeenCalledOnce()
|
||||
expect(warn.mock.calls[0]![0]).toBe(
|
||||
'[relay] 3 pending connection(s) not replayable: relay stated no kind/device'
|
||||
)
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
|
||||
@@ -459,9 +459,34 @@ describe('Relay region preference', () => {
|
||||
} finally {
|
||||
timeout.mockRestore()
|
||||
}
|
||||
// One catalog request, then per origin one warm-up and three samples.
|
||||
expect(budgets.filter((ms) => ms === 4_500)).toHaveLength(2)
|
||||
expect(budgets.filter((ms) => ms === 1_500)).toHaveLength(7)
|
||||
// The catalog request, then per origin one warm-up and three samples. The catalog
|
||||
// and the warm-ups are the cold requests; only the samples run on the short clock.
|
||||
// Regions measure in parallel, so the two warm-ups are issued before any sample.
|
||||
expect(budgets).toEqual([4_500, 4_500, 4_500, 1_500, 1_500, 1_500, 1_500, 1_500, 1_500])
|
||||
})
|
||||
|
||||
it('lets requestTimeoutMs override every budget, including the warm-up', async () => {
|
||||
const budgets: number[] = []
|
||||
const timeout = vi.spyOn(AbortSignal, 'timeout').mockImplementation((ms) => {
|
||||
budgets.push(ms)
|
||||
return new AbortController().signal
|
||||
})
|
||||
try {
|
||||
await new RelayRegionPreferenceResolver({
|
||||
directorUrl: DIRECTOR,
|
||||
userDataPath: userDataPath(),
|
||||
fetch: async (url) =>
|
||||
String(url).endsWith('/v1/regions')
|
||||
? Response.json({ v: 1, regions: BOTH_REGIONS })
|
||||
: cancelTrackingResponse(200, () => {}),
|
||||
requestTimeoutMs: 250,
|
||||
now: () => 1_000
|
||||
}).resolve()
|
||||
} finally {
|
||||
timeout.mockRestore()
|
||||
}
|
||||
expect(budgets).toHaveLength(9)
|
||||
expect(new Set(budgets)).toEqual(new Set([250]))
|
||||
})
|
||||
|
||||
it('probes only the canonical health path and cancels its body', async () => {
|
||||
|
||||
@@ -215,10 +215,12 @@ export class RelayRegionPreferenceResolver {
|
||||
): Promise<RelayRegionProbeReport[]> {
|
||||
let catalog: RelayRegionCatalog
|
||||
try {
|
||||
// The catalog request is the coldest of the sequence: it pays DNS, TCP,
|
||||
// and TLS to the director, so it gets the warm-up budget, not the sample one.
|
||||
catalog = await fetchRelayRegionCatalog(
|
||||
this.options.directorUrl,
|
||||
fetch,
|
||||
this.options.requestTimeoutMs ?? PROBE_TIMEOUT_MS
|
||||
this.options.requestTimeoutMs ?? WARMUP_TIMEOUT_MS
|
||||
)
|
||||
} catch (error) {
|
||||
onCatalogFailure?.()
|
||||
|
||||
@@ -172,6 +172,38 @@ describe('Relay region probe log', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('reports a lone unreachable region as all-unreachable, not catalog-incomplete', async () => {
|
||||
// Why: a support census counts all-unreachable to spot client-side network breakage;
|
||||
// a roll wave that shortens the catalog must not hide those runs.
|
||||
const path = userDataPath()
|
||||
const { resolver, events } = resolverWithLog({
|
||||
path,
|
||||
fetch: catalogFetch([{ region: 'asia-east2', probeOrigins: [ASIA] }]),
|
||||
probe: sampledProbe({})
|
||||
})
|
||||
await expect(resolver.resolve()).resolves.toBeUndefined()
|
||||
expect(probeEvents(events)[0]!.reason).toBe('all-unreachable')
|
||||
})
|
||||
|
||||
it('reports a lone flapping region as all-rejected, and a lone healthy one as catalog-incomplete', async () => {
|
||||
const path = userDataPath()
|
||||
const flapping = resolverWithLog({
|
||||
path,
|
||||
fetch: catalogFetch([{ region: 'asia-east2', probeOrigins: [ASIA] }]),
|
||||
probe: sampledProbe({ [ASIA]: [90, 30, 40, 900] })
|
||||
})
|
||||
await expect(flapping.resolver.resolve()).resolves.toBeUndefined()
|
||||
expect(probeEvents(flapping.events)[0]!.reason).toBe('all-rejected')
|
||||
|
||||
const healthy = resolverWithLog({
|
||||
path: userDataPath(),
|
||||
fetch: catalogFetch([{ region: 'asia-east2', probeOrigins: [ASIA] }]),
|
||||
probe: sampledProbe({ [ASIA]: [90, 30, 32, 34] })
|
||||
})
|
||||
await expect(healthy.resolver.resolve()).resolves.toBeUndefined()
|
||||
expect(probeEvents(healthy.events)[0]!.reason).toBe('catalog-incomplete')
|
||||
})
|
||||
|
||||
it('names a held incumbent apart from a fresh measurement', async () => {
|
||||
const path = userDataPath()
|
||||
writeCache(path, 'us-central1', 500)
|
||||
|
||||
@@ -130,13 +130,13 @@ function refreshReason(
|
||||
// a selection that is not the fastest reading is a deliberate hold.
|
||||
return best && selected.region !== best.region ? 'held-previous' : 'measured'
|
||||
}
|
||||
if (reports.length < RELAY_REGIONS.length) {
|
||||
return 'catalog-incomplete'
|
||||
// Reachability first: a support census counting all-unreachable to spot
|
||||
// client-side network breakage must not lose those runs to a roll wave.
|
||||
if (reports.every((report) => report.verdict === 'unreachable')) {
|
||||
return 'all-unreachable'
|
||||
}
|
||||
if (reports.some((report) => report.verdict === 'measured')) {
|
||||
return 'sole-survivor-forbidden'
|
||||
if (!reports.some((report) => report.verdict === 'measured')) {
|
||||
return 'all-rejected'
|
||||
}
|
||||
return reports.every((report) => report.verdict === 'unreachable')
|
||||
? 'all-unreachable'
|
||||
: 'all-rejected'
|
||||
return reports.length < RELAY_REGIONS.length ? 'catalog-incomplete' : 'sole-survivor-forbidden'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user