mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 08:02:28 +00:00
fix(mobile): a resume during an urgent probe keeps its deadline, so a tap burst cannot starve the verdict
This commit is contained in:
@@ -12,8 +12,9 @@ const rotated = {
|
||||
relay: { v: 1, directorUrl: 'https://relay.example', cellUrl: 'https://c1.relay.example' }
|
||||
}
|
||||
const rotate = vi.hoisted(() => vi.fn())
|
||||
const needsRotation = vi.hoisted(() => vi.fn(() => true))
|
||||
vi.mock('./mobile-relay-credential-rotation', () => ({
|
||||
mobileRelayCredentialNeedsRotation: () => true,
|
||||
mobileRelayCredentialNeedsRotation: needsRotation,
|
||||
rotateMobileRelayCredential: rotate
|
||||
}))
|
||||
|
||||
@@ -40,6 +41,14 @@ function fixture(overrides: { persistResolvedRelay?: () => Promise<void> } = {})
|
||||
}
|
||||
|
||||
describe('MobileRelayCredentialRefresh', () => {
|
||||
it('does nothing unforced while the credential is still fresh', async () => {
|
||||
needsRotation.mockReturnValueOnce(false)
|
||||
const { refresh, order } = fixture()
|
||||
await refresh.run(false)
|
||||
expect(order).toEqual([])
|
||||
expect(rotate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('lifts the gate and starts the relay race only after the endpoint is durable', async () => {
|
||||
const { refresh, order } = fixture()
|
||||
await refresh.run(true)
|
||||
|
||||
@@ -276,14 +276,13 @@ describe('RpcSessionLivenessWatchdog', () => {
|
||||
})
|
||||
watchdog.start(identity)
|
||||
|
||||
// Resumes every 1.5 s on a black-holed socket: each lands inside the 2 s urgent
|
||||
// window, so none may re-arm the deadline or the verdict never comes.
|
||||
watchdog.probeNow(identity, 'resume')
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
expect(terminate).not.toHaveBeenCalled()
|
||||
// The second resume restarts the 2 s clock on the miss already booked.
|
||||
watchdog.probeNow(identity, 'resume')
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
watchdog.probeNow(identity, 'resume')
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
for (let elapsed = 0; elapsed < 6_000; elapsed += 1_500) {
|
||||
await vi.advanceTimersByTimeAsync(1_500)
|
||||
watchdog.probeNow(identity, 'resume')
|
||||
}
|
||||
expect(terminate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
|
||||
@@ -127,9 +127,12 @@ export class RpcSessionLivenessWatchdog {
|
||||
this.lastVoluntaryProbeAt = now
|
||||
// Why: a resume is a new observation on a cold radio, so it starts the urgent window
|
||||
// with a clean budget (startProbe zeroes the count on a profile switch). A resume that
|
||||
// lands while an urgent probe is already in flight restarts the clock but keeps the
|
||||
// count: otherwise repeated resumes, or a user tapping reconnect on a dead socket,
|
||||
// zero the budget on every tap and the verdict never lands.
|
||||
// lands while an urgent probe is already in flight keeps that probe's deadline and
|
||||
// count: re-arming the 2 s clock on every tap would let a user tapping reconnect, or
|
||||
// an AppState flap, hold a dead socket open for as long as they keep tapping.
|
||||
if (urgent && this.probing && this.profile === this.urgentProfile) {
|
||||
return
|
||||
}
|
||||
this.startProbe(identity, urgent ? this.urgentProfile : this.ordinaryProfile)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user