mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(relay): make the control lease jitter symmetric
Pullfrog: shortening-only jitter raised the mean rebind rate ~15%. Grant 55 min +/- 5 min instead; same cohort spread, unchanged steady-state load. Auth expiry (5 min token) and the 75 s silence watchdog are enforced separately, so a grant up to 60 min risks nothing.
This commit is contained in:
@@ -296,24 +296,28 @@ describe('control lease jitter', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('grants a lease inside [55min - jitter, 55min] so cohorts drift apart', async () => {
|
||||
it('grants a lease uniformly around 55min so cohorts drift apart at the same mean rate', async () => {
|
||||
const now = 1_700_000_000_000
|
||||
const helloAck = (socket: FakeSocket) =>
|
||||
JSON.parse(
|
||||
String(socket.send.mock.calls.find((call) => String(call[0]).includes('host-hello-ack'))![0])
|
||||
) as { leaseExpiresAt: number }
|
||||
|
||||
const shortest = harness({ now: () => now, random: () => 0.999999 })
|
||||
const shortest = harness({ now: () => now, random: () => 0 })
|
||||
const shortestAck = helloAck(await activeHost(shortest))
|
||||
const longest = harness({ now: () => now, random: () => 0 })
|
||||
const centered = harness({ now: () => now, random: () => 0.5 })
|
||||
const centeredAck = helloAck(await activeHost(centered))
|
||||
const longest = harness({ now: () => now, random: () => 0.999999 })
|
||||
const longestAck = helloAck(await activeHost(longest))
|
||||
|
||||
expect(longestAck.leaseExpiresAt).toBe(now + CONTROL_LEASE_MS)
|
||||
expect(shortestAck.leaseExpiresAt).toBeGreaterThan(now + CONTROL_LEASE_MS - CONTROL_LEASE_JITTER_MS)
|
||||
expect(shortestAck.leaseExpiresAt).toBeLessThan(longestAck.leaseExpiresAt)
|
||||
// Nine of ten hosts that connected together now differ by minutes, not zero.
|
||||
expect(shortestAck.leaseExpiresAt).toBe(now + CONTROL_LEASE_MS - CONTROL_LEASE_JITTER_MS)
|
||||
expect(centeredAck.leaseExpiresAt).toBe(now + CONTROL_LEASE_MS)
|
||||
expect(longestAck.leaseExpiresAt).toBeLessThan(now + CONTROL_LEASE_MS + CONTROL_LEASE_JITTER_MS)
|
||||
// The mean grant stays at 55 min, so steady-state rebind load is unchanged;
|
||||
// hosts that connected together now differ by minutes, not zero.
|
||||
expect(longestAck.leaseExpiresAt - shortestAck.leaseExpiresAt).toBeGreaterThan(9 * 60 * 1000)
|
||||
shortest.registry.drain(0)
|
||||
centered.registry.drain(0)
|
||||
longest.registry.drain(0)
|
||||
vi.advanceTimersByTime(0)
|
||||
})
|
||||
@@ -325,14 +329,14 @@ describe('control lease jitter', () => {
|
||||
const first = await activeHost(h)
|
||||
const session = h.registry.get({ userId: identity.sub, relayHostId: identity.relayHostId })!
|
||||
const firstLease = session.leaseExpiresAt
|
||||
roll = 0.5
|
||||
roll = 0.75
|
||||
const rebind = new FakeSocket()
|
||||
await (
|
||||
h.registry as unknown as {
|
||||
activate: (...args: unknown[]) => Promise<void>
|
||||
}
|
||||
).activate(rebind as unknown as WebSocket, identity, session, 1, true, 1, '1.4.197')
|
||||
expect(session.leaseExpiresAt).toBe(now + CONTROL_LEASE_MS - CONTROL_LEASE_JITTER_MS / 2)
|
||||
expect(session.leaseExpiresAt).toBe(now + CONTROL_LEASE_MS + CONTROL_LEASE_JITTER_MS / 2)
|
||||
expect(session.leaseExpiresAt).not.toBe(firstLease)
|
||||
expect(first.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.PEER_DROPPED, 'control rebound')
|
||||
h.registry.drain(0)
|
||||
|
||||
@@ -127,12 +127,15 @@ function send(socket: WebSocket, type: string, message: object): void {
|
||||
// stalled predecessor only accumulates doomed sockets.
|
||||
const ACTIVATION_QUEUE_WAIT_MS = 30_000
|
||||
|
||||
// Why: hosts rebind 1-2 min before this expires, so every host that (re)connected
|
||||
// in the same minute (a cell recreate dumps hundreds at once) rebinds as one
|
||||
// cohort every cycle, forever, and each cohort lands on the cell-inventory lock
|
||||
// as a single wave. Jittering the grant walks the cohort apart across cycles.
|
||||
// Why: hosts rebind a few minutes before this expires, so every host that
|
||||
// (re)connected in the same minute (a cell recreate dumps hundreds at once)
|
||||
// rebinds as one cohort every cycle, forever, and each cohort lands on the
|
||||
// cell-inventory lock as a single wave. A symmetric jitter walks the cohort
|
||||
// apart across cycles without raising the mean rebind rate. The lease only
|
||||
// drives the desktop's rotation timer; auth expiry (5 min token) and the 75 s
|
||||
// silence watchdog are enforced separately, so a longer grant risks nothing.
|
||||
export const CONTROL_LEASE_MS = 55 * 60 * 1000
|
||||
export const CONTROL_LEASE_JITTER_MS = 10 * 60 * 1000
|
||||
export const CONTROL_LEASE_JITTER_MS = 5 * 60 * 1000
|
||||
|
||||
export class HostSessionRegistry {
|
||||
private readonly sessions = new Map<string, HostSession>()
|
||||
@@ -150,8 +153,10 @@ export class HostSessionRegistry {
|
||||
private readonly random: () => number = Math.random
|
||||
) {}
|
||||
|
||||
// Uniform over [CONTROL_LEASE_MS - jitter, CONTROL_LEASE_MS + jitter).
|
||||
private controlLeaseExpiresAt(): number {
|
||||
return this.now() + CONTROL_LEASE_MS - Math.floor(this.random() * CONTROL_LEASE_JITTER_MS)
|
||||
const offset = Math.floor((this.random() * 2 - 1) * CONTROL_LEASE_JITTER_MS)
|
||||
return this.now() + CONTROL_LEASE_MS + offset
|
||||
}
|
||||
|
||||
async acceptClient(
|
||||
|
||||
Reference in New Issue
Block a user