Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 44 additions & 20 deletions apps/realtime/src/handlers/file-doc-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ interface Backing {
readerClosed: boolean
/** Failed reads served, so a test can prove the loop is not spinning at the read cadence. */
reads: number
/** When each read was attempted, so a test can assert the BACKOFF rather than a count in a window. */
readTimes: number[]
/** When each FAILED read was attempted, so a test can measure one backoff interval exactly. */
failedReadTimes: number[]
/** Reads that returned (the idle steady state) — the event that ends a failure streak. */
idleReads: number
/** `connect()` calls, so a test can prove a closed reader is re-opened rather than abandoned. */
connects: number
}
Expand Down Expand Up @@ -66,8 +68,8 @@ function makeClient(): any {
},
xRead: async (streams: { key: string; id: string }[]) => {
b().reads++
b().readTimes.push(Date.now())
if (b().readerClosed) {
b().failedReadTimes.push(Date.now())
client.isOpen = false
throw new Error('The client is closed')
}
Expand All @@ -77,8 +79,12 @@ function makeClient(): any {
const after = (b().streams.get(key) ?? []).filter((e) => seqOf(e.id) > seqOf(id))
if (after.length) res.push({ name: key, messages: after.map((e) => ({ ...e })) })
}
if (res.length) return res
if (res.length) {
b().idleReads++
return res
}
await sleep(5)
b().idleReads++
return null
},
set: async (key: string, val: string, opts?: { NX?: boolean }) => {
Expand Down Expand Up @@ -155,7 +161,8 @@ describe('FileDocStore', () => {
failXAdd: 0,
readerClosed: false,
reads: 0,
readTimes: [],
failedReadTimes: [],
idleReads: 0,
connects: 0,
}
stores = []
Expand Down Expand Up @@ -201,29 +208,46 @@ describe('FileDocStore', () => {
const doc = new Y.Doc()
await store.attachRoom(NAME, doc)

// Build a streak of two failures (retries back off ~0.5s, then ~1s).
// Build a streak of two failures (the retries back off ~0.5s, then ~1s).
state.backing!.readerClosed = true
await sleep(800)
// Redis comes back. Wait past the pending backoff so a read actually lands — and it returns
// nothing new, which is the idle case this test is about.
await vi.waitFor(
() => expect(state.backing!.failedReadTimes.length).toBeGreaterThanOrEqual(2),
{
timeout: 5000,
interval: 25,
}
)

// Redis comes back. Wait for a read to actually RETURN — waiting a fixed span instead is a race:
// the pending backoff can outlast it, no idle read lands, and the streak survives into the phase
// below, which then measures the wrong backoff and fails. That is an event, so wait on the event.
state.backing!.readerClosed = false
await sleep(1000)
const idleBefore = state.backing!.idleReads
await vi.waitFor(() => expect(state.backing!.idleReads).toBeGreaterThan(idleBefore), {
timeout: 5000,
interval: 25,
})

// A fresh blip must retry at the START of the backoff curve, not partway up it. Assert the DELAY
// itself: counting attempts inside a fixed window cannot tell the two apart, because the jittered
// delay for a carried streak (1.6–2.4s) overlaps any window wide enough to catch a reset one.
// Measure FAILURE to FAILURE so the sample is exactly one backoff — a straggler successful read
// landing just after the flag flips would otherwise become the first sample and pass trivially.
state.backing!.readerClosed = true
state.backing!.readTimes.length = 0
await vi.waitFor(() => expect(state.backing!.readTimes.length).toBeGreaterThanOrEqual(2), {
timeout: 5000,
interval: 50,
})
const [first, second] = state.backing!.readTimes
state.backing!.failedReadTimes.length = 0
await vi.waitFor(
() => expect(state.backing!.failedReadTimes.length).toBeGreaterThanOrEqual(2),
{
timeout: 6000,
interval: 25,
}
)
const [first, second] = state.backing!.failedReadTimes

// Streak reset ⇒ the first delay is 500ms ±20% ⇒ 400–600ms. Streak carried over ⇒ it is the third
// delay, 2000ms ±20% ⇒ 1600–2400ms. Disjoint ranges, so this cannot pass on the wrong one without
// the machine stalling the shorter sleep by 65%.
expect(second - first).toBeLessThan(1000)
// Streak reset ⇒ the first delay is 500ms ±20% ⇒ at most 600ms. Streak carried over ⇒ it is the
// third delay, 2000ms ±20% ⇒ at least 1600ms. The bound sits between them with room on both
// sides, so a loaded machine stretching the short sleep does not flip the verdict.
expect(second - first).toBeLessThan(1200)
doc.destroy()
})

Expand Down
Loading