Rebuild: edge:node --max-mbit caps a node's download
Test and deploy / test (push) Successful in 38s
Test and deploy / deploy (push) Skipped

For workers on a shared line (poweredge-kantoor: 160 Mbit/s): the range
stream is read at most at the cap, TCP holds the coordinator back. The
cap is the node's, split over its processes (also EDGE_MAX_MBIT).
REBUILD.md: where workers run (connections set the speed; remote workers
take CPU off homelab).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-09 09:01:57 +02:00
1 parent 104248d8a3
commit 42b6cac3ee
5 files changed
+67 -4

No files matched your search

+23 -4
View File
@@ -5,7 +5,7 @@ import { spawn, type ChildProcess } from 'node:child_process'
import { availableParallelism } from 'node:os'
import { promisify } from 'node:util'
import { gzip, unzip } from 'node:zlib'
import { RangeIndexer, decodeFrames, isKeepalive, toWire, type EdgeBatch, type Lease } from '#lib/edge/index'
import { RangeIndexer, decodeFrames, isKeepalive, throttle, toWire, type EdgeBatch, type Lease } from '#lib/edge/index'
const IDLE_MS = 30_000
const unzipAsync = promisify(unzip)
@@ -37,6 +37,9 @@ export default class EdgeNode extends BaseCommand {
@flags.number({ description: 'Worker processes (one core each; default: EDGE_PROCESSES or 1, 0 = one per core)' })
declare processes?: number
@flags.number({ description: 'Download cap for the whole node in Mbit/s, shared by its processes (default: EDGE_MAX_MBIT or none)' })
declare maxMbit?: number
private stopping = false
async run() {
@@ -82,13 +85,21 @@ export default class EdgeNode extends BaseCommand {
* (each gives its range back).
*/
private async supervise(count: number) {
this.logger.info(`Starting ${count} worker processes.`)
const cap = this.cap()
this.logger.info(`Starting ${count} worker processes${cap ? ` (download cap ${cap} Mbit/s, ${(cap / count).toFixed(1)} each)` : ''}.`)
const children = new Set<ChildProcess>()
let stopping = false
let stopped: () => void
const done = new Promise<void>((resolve) => (stopped = resolve))
const start = (slot: number) => {
const args = [process.argv[1], 'edge:node', `--batch-articles=${this.batchArticles}`, ...(this.ranges ? [`--ranges=${this.ranges}`] : [])]
const args = [
process.argv[1],
'edge:node',
`--batch-articles=${this.batchArticles}`,
...(this.ranges ? [`--ranges=${this.ranges}`] : []),
// Each child gets its share of the node's cap.
...(cap ? [`--max-mbit=${cap / count}`] : []),
]
const child = spawn(process.execPath, [...process.execArgv, ...args], { env: { ...process.env, EDGE_CHILD: String(slot) }, stdio: 'inherit' })
children.add(child)
child.on('exit', (code) => {
@@ -111,6 +122,12 @@ export default class EdgeNode extends BaseCommand {
await this.terminate()
}
/** The node's download cap in Mbit/s, or null. */
private cap(): number | null {
const mbit = this.maxMbit ?? (process.env.EDGE_MAX_MBIT ? Number(process.env.EDGE_MAX_MBIT) : null)
return mbit && mbit > 0 ? mbit : null
}
private async runRange(api: Api, lease: Lease) {
const { range, rules } = lease
const started = Date.now()
@@ -157,7 +174,9 @@ export default class EdgeNode extends BaseCommand {
try {
// Inflating runs on a thread-pool thread: the next chunk inflates while this one is indexed.
let pending: { from: number; to: number; text: Promise<Buffer> } | null = null
for await (const frame of decodeFrames(await api.stream(range.id))) {
const cap = this.cap()
const stream = await api.stream(range.id)
for await (const frame of decodeFrames(cap ? throttle(stream, (cap * 1e6) / 8) : stream)) {
if (this.stopping) break
if (isKeepalive(frame)) continue
const next = { from: frame.from, to: frame.to, text: unzipAsync(frame.data) }