OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / 9fc4b91

port(0.3.0): backpressure P3 — cork-batch the serve loop
_serveJob corks peer.mux around each run of chunkData sends so protomux coalesces them into fewer stream writes (hypercore replicator corks around request/response batches, replicator.js:722/738), uncorking before it parks on drain (to flush) and in finally. A `corked` boolean keeps cork/uncork balanced across the early returns and no-ops when the mux stub lacks cork (unit tests). Real protomux exercised by the overlay-v2 transfers; chunk-backpressure adds an explicit balance+depth assert. Suite 15/15. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9fc4b91681f2b8fe9971104617aa468f6843242c
by plankatron <fleeky@gmail.com> · 2026-07-11T19:04:56.000Z

Changed files (2) +58 -14

lib/protocol.js +30 -14
……@@ -672,21 +672,37 @@ export class OverlayProtocol {
672672 // _onChunkNeed, so exactly one job streams at a time per peer.
673673 async _serveJob (peer, job) {
674674 const { path, diskPath, chunkMap, indices } = job
675 for (let i = 0; i < indices.length; i++) {
676 const index = indices[i]
677 if (index >= chunkMap.length) continue
678 const c = chunkMap[index]
679 const data = this._transferManager.readChunk(diskPath, c.offset, c.length)
680 if (!data) continue
681 const flushed = peer.msgs.chunkData.send({ path, index, data })
682 if (this._chunkServeCb) {
683 try { this._chunkServeCb({ path, index, bytes: data.length, peer, from: peer.authorizedServe.get(path) || null }) } catch {}
684 }
685 if (flushed === false && i < indices.length - 1) {
686 const alive = await this._waitForDrain(peer)
687 if (!alive) return
688 if (this._serveAuthorizer && !peer.authorizedServe.has(path)) return
675 // P3: cork the mux so a run of chunkData frames coalesces into fewer stream
676 // writes (hypercore's replicator corks around each request/response batch,
677 // replicator.js:722/738). Uncork before parking on drain (flush the batch) and
678 // in the finally. The `corked` guard keeps cork/uncork balanced across the early
679 // returns, and no-ops when the mux stub has no cork (unit tests).
680 const mux = peer.mux
681 let corked = false
682 const cork = () => { if (mux && mux.cork && !corked) { mux.cork(); corked = true } }
683 const uncork = () => { if (mux && mux.uncork && corked) { mux.uncork(); corked = false } }
684 cork()
685 try {
686 for (let i = 0; i < indices.length; i++) {
687 const index = indices[i]
688 if (index >= chunkMap.length) continue
689 const c = chunkMap[index]
690 const data = this._transferManager.readChunk(diskPath, c.offset, c.length)
691 if (!data) continue
692 const flushed = peer.msgs.chunkData.send({ path, index, data })
693 if (this._chunkServeCb) {
694 try { this._chunkServeCb({ path, index, bytes: data.length, peer, from: peer.authorizedServe.get(path) || null }) } catch {}
695 }
696 if (flushed === false && i < indices.length - 1) {
697 uncork() // flush the corked batch before we park on drain
698 const alive = await this._waitForDrain(peer)
699 if (!alive) return
700 if (this._serveAuthorizer && !peer.authorizedServe.has(path)) return
701 cork() // re-cork for the next run
702 }
689703 }
704 } finally {
705 uncork()
690706 }
691707 }
692708
test/chunk-backpressure.test.js +28 -0
……@@ -120,3 +120,31 @@ test('concurrent chunkNeed batches: one pump per peer, all delivered exactly onc
120120 t.is(sent.length, N, 'all 8 chunks delivered across the two batches')
121121 t.alike([...sent].sort((a, b) => a - b), [0, 1, 2, 3, 4, 5, 6, 7], 'each chunk delivered exactly once')
122122})
123
124test('P3 cork-batching: cork/uncork stay balanced across a paced serve', async (t) => {
125 const N = 5
126 const proto = makeProto(N)
127 const stream = makeStream()
128 const sent = []
129 let bufferFull = false
130 let corkDepth = 0
131 let maxDepth = 0
132 const peer = {
133 channel: { closed: false, get drained () { return !bufferFull } },
134 mux: {
135 stream,
136 cork () { corkDepth++; if (corkDepth > maxDepth) maxDepth = corkDepth },
137 uncork () { corkDepth-- }
138 },
139 msgs: { chunkData: { send: (m) => { sent.push(m.index); bufferFull = true; return false } } }
140 }
141
142 proto._onChunkNeed(peer, { path: 'content:x', indices: [0, 1, 2, 3, 4] })
143 await tick()
144 let guard = 0
145 while (sent.length < N && guard++ < 50) { bufferFull = false; stream.emitDrain(); await tick() }
146
147 t.is(sent.length, N, 'all chunks served')
148 t.is(corkDepth, 0, 'P3: cork/uncork balanced — no leaked cork after the serve')
149 t.ok(maxDepth === 1, 'P3: exactly one cork depth (batch corked, flushed before each drain)')
150})