The range stream polls the corpus as well as the spool while it waits
Test and deploy / test (push) Successful in 56s
Test and deploy / edge (push) Successful in 2m13s
Test and deploy / deploy (push) Successful in 4m11s

The corpus fill claims chunks of a leased range ahead of the leased run (the
prefetcher's claim keys are per range and chunk), stores them in the corpus
only, and the run leaves them there. The stream looked at the corpus once per
chunk and then polled the spool only: when the store came seconds after that
look, the stream waited its whole hour and the worker lost the group and its
open batch (three groups on 2026-10-27, LOG.md (118)). Now the wait loop checks
both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Fable 5.1 committed 2026-10-27 09:19:16 +01:00
1 parent 4af5e06400
commit 31fc204c76
3 files changed
+54 -11

No files matched your search

+15 -11
View File
@@ -5,7 +5,7 @@ import db from '@adonisjs/lucid/services/db'
import Leases, { LeaseLost } from '#services/leases'
import GroupLeases from '#services/group_leases'
import BatchStore, { StaleBatch } from '#services/batch_store'
import Spool from '#services/spool'
import Spool, { type ChunkSpan } from '#services/spool'
import Corpus from '#services/corpus'
import { KEEPALIVE, encodeFrame, fromWire, type WireBatch } from '#lib/edge/index'
@@ -99,19 +99,22 @@ export default class NodeController {
const spans = Spool.chunksOf({ start: Number(range.start_id), end: Number(range.end_id) }, Number(range.next_id))
const group = range.capture ? await db.from('groups').where('id', range.group_id).select('name').first() : null
const corpusRange = group ? { group: group.name as string, start: Number(range.start_id), end: Number(range.end_id) } : null
// The chunk from the corpus or the spool, whichever has it. Both are polled while waiting: the corpus fill
// claims a chunk ahead of the leased run (the prefetcher's claims are per range and chunk), stores it in the
// corpus only, and the run then leaves it there — a stream that looked at the corpus once, before that
// store, waited the whole hour on the spool (three groups on 2026-10-27, LOG.md (118)).
const stored = async (span: ChunkSpan) =>
corpusRange && (await Corpus.has(corpusRange, span)) ? Corpus.read(corpusRange, span) : null
async function* frames() {
for (const span of spans) {
if (corpusRange && (await Corpus.has(corpusRange, span))) {
const data = await Corpus.read(corpusRange, span)
if (data) {
yield encodeFrame({ ...span, data })
await Leases.heartbeat(rangeId, nodeId)
continue
}
}
const waitUntil = Date.now() + CHUNK_WAIT_MS
let quietSince = Date.now()
while (!(await Spool.has(rangeId, span))) {
let data = await stored(span)
while (!data) {
if (await Spool.has(rangeId, span)) {
data = await Spool.read(rangeId, span)
break
}
if (Date.now() > waitUntil) throw new Error(`Chunk ${span.from}–${span.to} of range ${rangeId} wasn't spooled in time`)
if (Date.now() - quietSince >= KEEPALIVE_MS) {
quietSince = Date.now()
@@ -119,8 +122,9 @@ export default class NodeController {
await Leases.heartbeat(rangeId, nodeId)
}
await sleep(500)
data = await stored(span)
}
yield encodeFrame({ ...span, data: await Spool.read(rangeId, span) })
yield encodeFrame({ ...span, data })
// Streaming keeps the lease alive.
await Leases.heartbeat(rangeId, nodeId)
}