Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f9700d5
Fix idle range completion scan
marcus-pousette-hp Jun 22, 2026
1414e16
Fix stuck range completion drain
marcus-pousette-hp Jun 23, 2026
76376ac
Remove unnecessary range update boundary state
marcus-pousette-hp Jun 23, 2026
60823b1
Fix queued idle range drain race
marcus-pousette-hp Jun 23, 2026
5403eb5
Restore peer updates at max range boundary
marcus-pousette-hp Jul 13, 2026
37224a9
Yield range drains to pending seeks
marcus-pousette-hp Jul 14, 2026
ffd7033
Assert seeks precede range scan completion
marcus-pousette-hp Jul 14, 2026
fe789a1
Restart range drain after cancellation
marcus-pousette-hp Jul 14, 2026
62096ed
Simplify range drain state and tests
marcus-pousette-hp Jul 15, 2026
aded7ec
Remove redundant range empty checks
marcus-pousette-hp Jul 15, 2026
11e0082
Merge main into fix/idle-range-completion
marcus-pousette-hp Jul 16, 2026
0e24e5a
Fix seek processing during range drains
marcus-pousette-hp Jul 16, 2026
51738b8
Clear range test timeout
marcus-pousette-hp Jul 16, 2026
176da45
Simplify range and seek processing
marcus-pousette-hp Jul 22, 2026
8d77204
Clarify range and seek scheduling tests
marcus-pousette-hp Jul 22, 2026
64bc2a5
Simplify range drain seek test
marcus-pousette-hp Jul 22, 2026
2407590
Clarify range drain state handling
marcus-pousette-hp Jul 23, 2026
f62c8b9
Remove unnecessary range test catch
marcus-pousette-hp Jul 24, 2026
7e81e3a
Assert re-queuing after drain
lejeunerenard Jul 28, 2026
59b21e1
Lint `test/replicate.js`
lejeunerenard Jul 28, 2026
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
144 changes: 108 additions & 36 deletions lib/replicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ class RangeRequest extends Attachable {
this.ranges[rangeIndex] = h
}

if (!this.resolved && this.replicator._updatesRunning) {
this.replicator._updatesQueued = true
}

if (this.end === -1) {
this.replicator._alwaysLatestBlock--
}
Expand Down Expand Up @@ -1541,8 +1545,8 @@ class Peer {
}

_requestSeek(s) {
// if replicator is updating the seeks etc, bail and wait for it to drain
if (this.replicator._updatesPending > 0) return false
// if replicator is updating the seeks, bail and wait for it to drain
if (this.replicator._updatingSeeks) return false
if (this.replicator.pushOnly) return false

const { length, fork } = this.core.state
Expand Down Expand Up @@ -2028,7 +2032,9 @@ module.exports = class Replicator {
this._hadPeers = false
this._active = 0
this._ifAvailable = 0
this._updatesPending = 0
this._updatingSeeks = false
this._updatesRunning = false
this._updatesQueued = false
this._applyingReorg = null
this._manifestPeer = null
this._notDownloadingLinger = notDownloadingLinger
Expand Down Expand Up @@ -2614,55 +2620,117 @@ module.exports = class Replicator {
}
}

_updateRanges(index, limit) {
index = Math.min(index, this._ranges.length - 1)

let checked = 0
let resolved = 0
let updateAll = false
let remaining = Math.min(limit, this._ranges.length)

while (remaining-- > 0 && this._ranges.length > 0) {
if (index >= this._ranges.length) index = 0

const r = this._ranges[index]

clampRange(this.core, r)
checked++

if (r.end !== -1 && r.start >= r.end) {
this._resolveRangeRequest(r)
resolved++
// Crossing into the capped window exposes another range to peer scheduling.
if (this._ranges.length === MAX_RANGES) updateAll = true
} else {
index++
}
}

index = this._ranges.length === 0 ? 0 : index % this._ranges.length

return { checked, resolved, index, updateAll }
}

// "slow" updates here - async but not allowed to ever throw
async _updateNonPrimary(updateAll) {
// Check if running, if so skip it and the running one will issue another update for us (debounce)
while (++this._updatesPending === 1) {
let len = Math.min(MAX_RANGES, this._ranges.length)
this._updatesQueued = true
if (this._updatesRunning) {
if (this._inflight.idle || updateAll) this.queueUpdateAll()
return
}

for (let i = 0; i < len; i++) {
const r = this._ranges[i]
this._updatesRunning = true

clampRange(this.core, r)
while (this._updatesQueued) {
this._updatesQueued = false

if (r.end !== -1 && r.start >= r.end) {
this._resolveRangeRequest(r)
i--
if (len > this._ranges.length) len--
if (this._ranges.length === MAX_RANGES) updateAll = true
Comment thread
lejeunerenard marked this conversation as resolved.
}
}
let checkedSinceResolve = 0
let rangeIndex = 0
const drain = this._inflight.idle || updateAll
const checkedSeeks = new Set()

for (let i = 0; i < this._seeks.length; i++) {
const s = this._seeks[i]
while (true) {
const limit = Math.min(MAX_RANGES, this._ranges.length - checkedSinceResolve)
const {
checked,
resolved,
index,
updateAll: resultUpdateAll
} = this._updateRanges(rangeIndex, limit)
rangeIndex = index

let err = null
let res = null
if (resultUpdateAll) updateAll = true

try {
res = await s.seeker.update()
} catch (error) {
err = error
}
if (resolved > 0) checkedSinceResolve = 0
else checkedSinceResolve += checked

const continueRanges = checked > 0 && checkedSinceResolve < this._ranges.length && drain

this._updatingSeeks = true

let checkedSeek = false
for (const s of this._seeks.slice()) {
Comment thread
lejeunerenard marked this conversation as resolved.
if (checkedSeeks.has(s) || !this._seeks.includes(s)) continue
checkedSeeks.add(s)
checkedSeek = true

let err = null
let res = null

try {
res = await s.seeker.update()
} catch (error) {
err = error
}

if (!res && !err) continue
const seekIndex = this._seeks.indexOf(s)
if (seekIndex === -1 || (!res && !err)) continue

if (i < this._seeks.length - 1) this._seeks[i] = this._seeks.pop()
else this._seeks.pop()
const h = this._seeks.pop()
if (h !== s) this._seeks[seekIndex] = h

i--
if (err) s.reject(err)
else s.resolve(res)
}

this._updatingSeeks = false

if (
(!continueRanges || (checkedSeek && this._seeks.length > 0)) &&
(this._inflight.idle || updateAll)
) {
this.queueUpdateAll()
}
if (!continueRanges) break
Comment thread
lejeunerenard marked this conversation as resolved.

if (err) s.reject(err)
else s.resolve(res)
await yieldToLoop()
if (this.destroyed) break
}

// No additional updates scheduled - break
if (--this._updatesPending === 0) break
// Debounce the additional updates - continue
this._updatesPending = 0
if (this.destroyed) break
}

if (this._inflight.idle || updateAll) this.queueUpdateAll()
this._updatesRunning = false
}

_clearRequest(peer, req) {
Expand Down Expand Up @@ -3398,6 +3466,10 @@ function incrementRx(stats1, stats2) {

function noop() {}

function yieldToLoop() {
return new Promise((resolve) => setImmediate(resolve))
}

function backoff(times) {
const sleep = times < 2 ? 200 : times < 5 ? 500 : times < 40 ? 1000 : 5000
return new Promise((resolve) => setTimeout(resolve, sleep))
Expand Down
Loading