first commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# `parallel-puller.ts`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`ParallelPuller` is the chunk-fetch scheduler that turns "I want chunk
|
||||
N of file F" into a verified `Uint8Array`. The puller asks the
|
||||
`PeerRegistry` for ranked candidates, issues a request to the best
|
||||
peer, waits `racingWindowMs`, then issues parallel requests to the
|
||||
runners-up. The first response whose server signature verifies wins;
|
||||
the other in-flight attempts are aborted via a shared
|
||||
`AbortController`.
|
||||
|
||||
The puller is the mesh's primary trust enforcement point. Every byte
|
||||
returned from a transport flows through `verifyChunkSig` before the
|
||||
puller acknowledges success. A signature failure moves the offending
|
||||
peer to the registry's denylist with `score = -Infinity` and surfaces
|
||||
`DriveMeshError(SIG_VERIFY_FAILED)` — the orchestrator can choose to
|
||||
fall back to the server rather than retry on peers.
|
||||
|
||||
## Features
|
||||
|
||||
- Top-K candidate selection (default 3).
|
||||
- Racing-window staggered issuance (default 250 ms).
|
||||
- Shared `AbortController` short-circuits losing attempts.
|
||||
- Mandatory `verifyChunkSig` gate against the configured
|
||||
`serverPubKey`.
|
||||
- Bad-sig denylisting via `registry.recordBadSig`.
|
||||
- Non-signature failures update success / failure counters so
|
||||
ranking adapts.
|
||||
- `maxParallelRequests` global cap.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
|
||||
|
||||
const puller = new ParallelPuller({
|
||||
registry,
|
||||
transports: [httpsTransport],
|
||||
serverPubKey,
|
||||
});
|
||||
const bytes = await puller.pull("file-id-hex", 0);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
|
||||
|
||||
const puller = new ParallelPuller({
|
||||
registry,
|
||||
transports: [webrtcTransport, httpsTransport],
|
||||
serverPubKey,
|
||||
racingWindowMs: 150,
|
||||
topK: 4,
|
||||
maxParallelRequests: 16,
|
||||
clock: fakeClock,
|
||||
onAttempt: (peerId, fileId, chunkId) =>
|
||||
log.debug("attempt", peerId, fileId, chunkId),
|
||||
onSuccess: (peerId, _f, _c, bytes, elapsedMs) =>
|
||||
log.info("served", peerId, bytes, elapsedMs),
|
||||
onBadSig: peerId => log.warn("denylisting", peerId),
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5_000);
|
||||
try {
|
||||
const bytes = await puller.pull(fileId, chunkId, controller.signal);
|
||||
} catch (err) {
|
||||
if (err instanceof DriveMeshError && err.code === DRIVE_MESH_CODES.ABORTED) {
|
||||
// caller-driven cancellation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ParallelPuller`:
|
||||
- `constructor(options: ParallelPullerOptions)`.
|
||||
- `pull(fileId, chunkId, signal?): Promise<Uint8Array>`.
|
||||
- `inflight: number` (read-only) — in-flight request count.
|
||||
- `ParallelPullerOptions`:
|
||||
- `registry`, `transports`, `serverPubKey` (required).
|
||||
- `maxParallelRequests?` (default `8`).
|
||||
- `racingWindowMs?` (default `250`).
|
||||
- `topK?` (default `3`).
|
||||
- `clock?`.
|
||||
- `onAttempt?(peerId, fileId, chunkId)`.
|
||||
- `onSuccess?(peerId, fileId, chunkId, bytes, elapsedMs)`.
|
||||
- `onFailure?(peerId, fileId, chunkId, reason)`.
|
||||
- `onBadSig?(peerId, fileId, chunkId)`.
|
||||
- Throws:
|
||||
- `DRIVE_MESH.CHUNK_UNAVAILABLE` — no candidate peers.
|
||||
- `DRIVE_MESH.SIG_VERIFY_FAILED` — winning peer returned bad sig.
|
||||
- `DRIVE_MESH.TRANSPORT_FAILED` — single-attempt transport failure.
|
||||
- `DRIVE_MESH.ALL_PEERS_FAILED` — every candidate failed.
|
||||
- `DRIVE_MESH.ABORTED` — caller-provided signal aborted.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`firstSettled` is hand-rolled rather than using `Promise.any`:
|
||||
`Promise.any` resolves on first success but does NOT abort losing
|
||||
attempts, leaving them in flight wastefully. The shared
|
||||
`AbortController` is the cancellation primitive — `controller.abort()`
|
||||
fires both when a winner appears and when the outer caller aborts.
|
||||
|
||||
`SIG_VERIFY_FAILED` short-circuits the "all peers failed" wrapping:
|
||||
the orchestrator distinguishes "every peer failed transport" (try
|
||||
server fallback) from "winning peer was Byzantine" (denylist + retry
|
||||
on a different peer). Re-wrapping the bad-sig error inside
|
||||
`ALL_PEERS_FAILED` would lose that distinction.
|
||||
|
||||
`maxParallelRequests` is enforced by a yielding micro-loop in
|
||||
`#awaitSlot`. The loop is bounded by the number of concurrent `pull`
|
||||
calls — drive-mesh issues `pull` from a small, controlled set of
|
||||
async contexts so the loop never spins indefinitely. Under heavier
|
||||
fan-in the orchestrator should queue requests in front of the puller
|
||||
rather than issuing every chunk concurrently.
|
||||
|
||||
Bad-sig denylist is permanent within the registry's denylist scope:
|
||||
`session` (default) clears on `mesh.stop()`, `persistent` survives.
|
||||
The puller does not concern itself with scope — the registry is the
|
||||
authority.
|
||||
Reference in New Issue
Block a user