Skip to content

调度 ​

调度回答三个问题:当前需要哪些块、哪个来源可以领取、什么时候才算完成。整体结构见 传输层,Peer 消息见 消息编码。

策略、队列和执行 ​

flowchart TB
    D["download.rs<br/>整文件或按需模式"] --> P["discovery.rs / scheduler.rs<br/>有并发上限的 Peer 连接池"]
    D --> C["CDN workers"]
    subgraph Q["block_queue.rs:同一个锁下原子更新"]
        Policy["download_policy.rs<br/>窗口、来源资格与期限"]
        Pending["待取块 pending"]
        Leased["独占租约 leased"]
        Done["已完成 verified"]
        Policy -->|"决定领取资格"| Pending
        Pending -->|"领取 BlockLease"| Leased
        Leased -->|"提交成功"| Done
        Leased -->|"失败或取消,仍有需求"| Pending
    end
    P -->|"Peer 领取"| Pending
    C -->|"CDN 领取"| Pending
    P --> FetchP["PeerDownload.fetch"]
    C --> FetchC["CDN Source.fetch"]
    FetchP --> Commit["Destination.commit"]
    FetchC --> Commit
    Commit --> Disk["验证 → 写入 → 磁盘复核<br/>→ 数据同步 → 检查点同步"]
    Disk -->|"成功后完成租约"| Done
    Done --> N["更新进度、唤醒读取"]
  • 策略不执行网络 I/O,负责窗口、来源可用性、紧急期限和来源错误。
  • 队列保存待取、在途、已完成状态,并发领取时独占一个块。策略与块状态在同一把锁下更新,网络和磁盘 I/O 不持有这把队列锁。
  • 执行层创建、取消和等待 worker;discovery.rs 管理发现刷新和连接上限,scheduler.rs 执行单 Peer 任务,CDN worker 由 download.rs 组织。
  • Destination持有块租约直到校验和持久化完成,防止来源切换时两个任务同时写同一块。

从取块到完成 ​

Peer worker 持续执行以下循环:

text
领取块租约
  → Request / 接收 pieces 和 proof
  → 验证长度与 Merkle 证明
  → 写入目标并从磁盘复核
  → 同步目标数据
  → 追加证明检查点并同步
  → 完成租约、更新进度、通知等待者
  → Have / BlockDone
  → 领取下一块

CDN 使用同一个提交入口,提交后不发送 Peer 确认。来源错误会让未完成租约归还;队列暂时为空而仍有在途块时,worker 等待通知,不把“没有可领任务”误判成完成。

磁盘写入或检查点写入失败会停止传输,并阻止后续排队提交。网络任务取消时,已经开始的阻塞提交仍拥有租约、目标锁和任务计数;整文件下载进入 CDN 阶段前会等待这些操作结束。

整文件与 VOD ​

行为download / resume_downloadopen 按需读取
初始需求所有未验证块首次 read/seek 建立当前窗口
排序未完成块顺序取,失败块归还到队首当前块,然后前向预取块
Peer 重试刷新候选,失败后退避重试刷新连接池;窗口变化后可重连已有候选
CDN 启动Peer 候选耗尽或控制面失败无 Peer 或当前读取块超过等待预算
队列衔接等待 Peer/磁盘任务退出,再按已验证位图建 CDN 队列Peer 与 CDN 共用同一个队列
镜像失败当前镜像 worker 全部退出后换镜像当前块依次尝试镜像
完成全部块持久化并最终同步窗口读完仍可继续;全文件同步后才 Completed

max_download_peers 默认 4,范围 1–16,分别限制 Peer 并发和 CDN worker 数。VOD 两类来源可以同时运行,这个参数不是二者相加后的总连接上限。每个 worker 最多持有一个块,不是按字节自适应调整的预算。

Peer 建连上限 45 秒,发现阶段上限 15 秒。来源可以只持有部分文件:每个连接仅领取初始 Bitfield 或后续 Have 标记的块。没有可用任务的连接最多等待三秒后让出名额。候选每五秒刷新,已有连接工作时仍可补充发现;失败或空闲连接退避五秒后重试,SDK 请求并保留 peer_query_limit 个候选(默认下载并发的两倍,可配置 1–50),Track 可能返回更少;刷新发起新的轮换查询。过滤和分页语义见 Track。整文件下载在没有连接且十秒无进展后结束 P2P 阶段;VOD 持续发现直到完成或取消,应用读取仍受 read_timeout 限制。当前不实现 rarest-first、endgame 或自适应吞吐调度。

VOD 的读取窗口 ​

rust
use std::{io::SeekFrom, time::Duration};
use tokio::io::{AsyncReadExt, AsyncSeekExt};

let mut file = client.open(&seed, "video.mp4")
    .prefetch_blocks(8)
    .read_timeout(Duration::from_secs(30))
    .await?;
file.seek(SeekFrom::Start(offset)).await?;
let n = file.read(&mut buffer).await?;

预取数量默认 8,范围 0–64;0 表示只取当前块。例如当前位置在 block 20,预取设为 2,则需求为 20 → 21 → 22。seek 到 block 100 后,新需求为 100 → 101 → 102:旧窗口里尚未开始的任务被撤下,在途块仍可完成并缓存;失败的旧窗口租约不会重新入队。

seek 到 EOF 或其后不建立预取任务;EOF 返回 0,负位置或 u64 溢出返回 InvalidInput。单次 read 等待缺块时,取消不会额外推进游标;read_exact/read_to_end 这样的组合操作可能已消费前面的块。seek 会丢弃旧读取等待。每次缺块等待受 read_timeout 限制,超时返回 TimedOut,后台已开始的取块仍可完成。

当前读取块优先给 P2P min(2 秒, read_timeout / 2)。预算耗尽后,只有 CDN 证明表通过验证才允许中断持有该块的慢连接,归还租约并由 CDN 领取;其他 Peer 继续工作。时序见 CDN 回退。

进度和取消 ​

progress() 和 subscribe_progress() 只统计持久化成功的块,包含 verified bytes/blocks、active peers、active CDN requests 和状态。watch 通知会合并,慢消费者不会阻塞取块。

取消终止网络任务,阻塞磁盘操作清理期间计数仍可能更新。VOD 来源错误可以只影响某次读取;磁盘写入错误发布 Failed;读取复核发现外部篡改时返回 InvalidData,可重新 open 来重验并修复。部分文件读完不等于全文件完成,Drop 仍未完成的读取句柄会发布 Cancelled。

设计稿,随时会改