mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-01 00:39:24 +00:00
perf(mpsc): sync send via noop_waker — +90% pps (249K → 474K)
The async fn Future state machine overhead (~1.9us) dominated MpscTunnelSender::send, while RingSink operations were only ~40ns. Breakthrough: make send() an async fn that completes synchronously on the first poll for the direct (ring tunnel) path. Uses futures::task::noop_waker() to construct a dummy Context, then calls Sink trait methods (poll_ready, start_send, poll_flush) directly. RingSink always returns Ready immediately, so the waker is never invoked and the async fn completes without yielding. Channel mode (TCP/UDP/WG tunnels) still uses async send_async() with proper backpressure. Ring tunnels detected via tunnel_info() type check in PeerConn. Results (4 threads, 1400B, 15s): pps: 249K → 474K (+90%) send_msg_by_ip: 3.53us → 1.67us (-53%) send_msg_internal: 2.40us → 502ns (-79%) MpscTunnelSender::send: 1.97us → 144ns (-93%) All 207 peers:: tests pass. Netns-requiring tests (three_node, credential) unchanged (require root).
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
# 计划 001:将共享 metrics/throughput 计数改为线程安全实现
|
||||
|
||||
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新 `plans/README.md` 中本计划的状态行,除非 reviewer 明确说明由他们维护索引。
|
||||
>
|
||||
> **漂移检查(首先运行)**:`git diff --stat 78146d16..HEAD -- easytier/src/common/stats_manager.rs easytier/src/tunnel/stats.rs easytier/src/tunnel/filter.rs easytier/src/proto/rpc_impl/server.rs easytier/src/tests`
|
||||
> 如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
|
||||
|
||||
## 状态
|
||||
|
||||
- **优先级**: P1
|
||||
- **工作量**: M
|
||||
- **风险**: MED
|
||||
- **依赖**: none
|
||||
- **类别**: bug
|
||||
- **计划生成于**: commit `78146d16`, 2026-06-18
|
||||
|
||||
## 为什么重要
|
||||
|
||||
核心 metrics 和 tunnel throughput 计数器当前用 `UnsafeCell<u64>` 保存,并通过 safe methods 在 `Send + Sync` 类型上暴露。VPN 核心运行在多线程 Tokio runtime 上,RPC、tunnel send/receive 和统计快照可能并发访问这些 counters;这会造成 Rust 层面的数据竞争和未定义行为,不只是“统计不准”。完成后应保证所有共享计数使用 atomic 或 lock-backed primitive,且新增并发测试证明 safe API 可多线程调用。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- `easytier/src/common/stats_manager.rs` — 通用 metrics manager;当前 `UnsafeCounter` 和 `MetricData` 手写 `Send + Sync`。
|
||||
- `easytier/src/tunnel/stats.rs` — tunnel throughput 统计;当前单独实现一套 `UnsafeCell` counters。
|
||||
- `easytier/src/tunnel/filter.rs` — `StatsRecorderTunnelFilter` 在 send/receive filter 中更新 `Arc<Throughput>`。
|
||||
- `easytier/src/proto/rpc_impl/server.rs` — RPC server paths 会更新 stats manager counters,可作为并发使用背景参考,不要求修改。
|
||||
|
||||
当前代码摘录:
|
||||
|
||||
```rust
|
||||
// easytier/src/common/stats_manager.rs:406
|
||||
pub unsafe fn add(&self, delta: u64) {
|
||||
let ptr = self.value.get();
|
||||
unsafe {
|
||||
*ptr = (*ptr).saturating_add(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// easytier/src/common/stats_manager.rs:455
|
||||
unsafe impl Send for UnsafeCounter {}
|
||||
unsafe impl Sync for UnsafeCounter {}
|
||||
|
||||
// easytier/src/common/stats_manager.rs:548
|
||||
pub fn add(&self, delta: u64) {
|
||||
unsafe {
|
||||
self.metric_data.counter.add(delta);
|
||||
self.metric_data.touch();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/tunnel/stats.rs:64
|
||||
#[derive(Debug)]
|
||||
pub struct Throughput {
|
||||
tx_bytes: UnsafeCell<u64>,
|
||||
rx_bytes: UnsafeCell<u64>,
|
||||
tx_packets: UnsafeCell<u64>,
|
||||
rx_packets: UnsafeCell<u64>,
|
||||
}
|
||||
|
||||
// easytier/src/tunnel/stats.rs:83
|
||||
unsafe impl Send for Throughput {}
|
||||
unsafe impl Sync for Throughput {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/tunnel/filter.rs:265
|
||||
fn before_send(&self, data: SinkItem) -> Option<SinkItem> {
|
||||
self.throughput.record_tx_bytes(data.buf_len() as u64);
|
||||
Some(data)
|
||||
}
|
||||
|
||||
// easytier/src/tunnel/filter.rs:270
|
||||
fn after_received(&self, data: StreamItem) -> Option<StreamItem> {
|
||||
match data {
|
||||
Ok(v) => {
|
||||
self.throughput.record_rx_bytes(v.buf_len() as u64);
|
||||
Some(Ok(v))
|
||||
}
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
仓库约定:Rust 代码使用 `anyhow`/`thiserror` 做错误上下文,async tests 使用 `#[tokio::test]`;已有测试集中在 `easytier/src/tests/` 和各模块 `#[cfg(test)]` 中。保持现有 public method names,避免扩大 API 改动。
|
||||
|
||||
## 需要使用的命令
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|---------|---------|---------------------|
|
||||
| Format | `cargo fmt --all -- --check` | exit 0 |
|
||||
| Lint | `cargo clippy --all-targets --features full --all -- -D warnings` | exit 0, no warnings |
|
||||
| Feature check | `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` | exit 0 |
|
||||
| Targeted tests | `cargo test --package easytier stats_manager --features full -- --nocapture` | exit 0; new stats tests pass |
|
||||
| Targeted tests | `cargo test --package easytier tunnel::stats --features full -- --nocapture` | exit 0; new throughput tests pass |
|
||||
|
||||
## 临时目录约定
|
||||
|
||||
- 临时文件、scratch 目录和 disposable worktree 必须放在 `$HOME/tmp` 下。
|
||||
- 如果 `$HOME/tmp` 不存在且本计划需要临时空间,先创建它。
|
||||
- 不要把临时产物放进被修改仓库。
|
||||
|
||||
## 范围
|
||||
|
||||
**范围内**(只能修改这些文件):
|
||||
- `easytier/src/common/stats_manager.rs`
|
||||
- `easytier/src/tunnel/stats.rs`
|
||||
- `easytier/src/tunnel/filter.rs`(仅当 type/API 调整需要同步编译)
|
||||
- `easytier/src/tests/mod.rs` 或同文件内 `#[cfg(test)]` 测试(仅用于新增测试入口)
|
||||
|
||||
**范围外**(即使看起来相关也不要触碰):
|
||||
- `easytier/src/peers/*` route 或 RPC 行为;这些由后续计划处理。
|
||||
- `easytier-web/`、`easytier-gui/`、frontend packages。
|
||||
- 任何 public metric names、labels、serialized output shape 的语义变更。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- Branch: `advisor/001-thread-safe-metrics-throughput`
|
||||
- Commit message style follows existing conventional commits, for example `fix: clarify config parse errors` or `fix(connector): classify manual reconnect timeouts by stage`.
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## 步骤
|
||||
|
||||
### 步骤 1:替换 `UnsafeCounter` 为 atomic-backed counter
|
||||
|
||||
在 `easytier/src/common/stats_manager.rs` 中将 `UnsafeCounter` 改为持有 `AtomicU64`。保留现有 `new`、`new_with_value`、`add`、`inc`、`get`、`reset`、`set` 方法名,但将它们改成 safe methods,使用 `Ordering::Relaxed` 即可,因为这些 counters 只做统计,不承载同步 happens-before 语义。
|
||||
|
||||
同时移除 `UnsafeCounter` 的 manual `unsafe impl Send/Sync`,让 compiler 从 `AtomicU64` 自动推导。
|
||||
|
||||
**验证**:`cargo test --package easytier stats_manager --features full -- --nocapture` → exit 0;如果此时没有匹配测试,命令应显示 0 failed。
|
||||
|
||||
### 步骤 2:处理 `MetricData::last_updated`
|
||||
|
||||
`MetricData` 当前持有 `UnsafeCell<Instant>`。不要继续共享可变 `Instant`。二选一:
|
||||
|
||||
- 推荐:将 last update 表示为 `AtomicU64`,存储从 `StatsManager` 创建时刻起的 monotonic micros 或 millis;读取时只在内部转换为需要的 age/duration。
|
||||
- 可接受:用 `parking_lot::Mutex<Instant>` 保护 `last_updated`,如果改动最小且性能足够。
|
||||
|
||||
选择方案后,移除 `MetricData` 的 manual `unsafe impl Send/Sync`。保持外部 behavior:counter update 后 last update 被刷新,过期清理逻辑仍能工作。
|
||||
|
||||
**验证**:`cargo clippy --all-targets --features full --all -- -D warnings` → exit 0, no warnings。
|
||||
|
||||
### 步骤 3:替换 `Throughput` 中的 `UnsafeCell` counters
|
||||
|
||||
在 `easytier/src/tunnel/stats.rs` 中将 `tx_bytes`、`rx_bytes`、`tx_packets`、`rx_packets` 改成 `AtomicU64`。`record_tx_bytes` 和 `record_rx_bytes` 使用 `fetch_add(..., Ordering::Relaxed)`;getter 使用 `load(Ordering::Relaxed)`。
|
||||
|
||||
更新 `Clone` 实现为加载旧值后创建新的 atomic counters。移除 `unsafe impl Send for Throughput` 和 `unsafe impl Sync for Throughput`。
|
||||
|
||||
**验证**:`cargo test --package easytier tunnel::stats --features full -- --nocapture` → exit 0;如果没有匹配测试,继续步骤 4 新增测试后重跑。
|
||||
|
||||
### 步骤 4:新增并发回归测试
|
||||
|
||||
为 `stats_manager` 添加一个多线程并发 increment 测试,建议放在 `easytier/src/common/stats_manager.rs` 的 `#[cfg(test)]` 模块中:创建一个 counter handle,启动多个 OS threads 或 `tokio::task::JoinSet`,每个 task 多次 `inc()`,最后断言总数等于预期。
|
||||
|
||||
为 `Throughput` 添加类似测试,创建 `Arc<Throughput>`,并发调用 `record_tx_bytes` 和 `record_rx_bytes`,最后断言 bytes 和 packets 全部精确匹配。
|
||||
|
||||
**验证**:`cargo test --package easytier stats_manager --features full -- --nocapture` 和 `cargo test --package easytier tunnel::stats --features full -- --nocapture` → exit 0;输出中新增测试通过。
|
||||
|
||||
### 步骤 5:运行完整相关门禁
|
||||
|
||||
运行格式、lint 和 feature check。
|
||||
|
||||
**验证**:
|
||||
- `cargo fmt --all -- --check` → exit 0。
|
||||
- `cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
- `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` → exit 0。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 新增 `stats_manager` 并发 increment 测试:覆盖多线程 safe API 读写。
|
||||
- 新增 `Throughput` 并发 tx/rx 测试:覆盖 send/receive counters 同时更新。
|
||||
- 现有 tunnel filter 行为不需要改业务测试,只需保证编译和 clippy 通过。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] `easytier/src/common/stats_manager.rs` 不再包含 `UnsafeCell`-backed counter 或 manual `unsafe impl Send/Sync` for metric data。
|
||||
- [ ] `easytier/src/tunnel/stats.rs` 不再包含 `UnsafeCell<u64>` 或 manual `unsafe impl Send/Sync` for `Throughput`。
|
||||
- [ ] 新增并发测试存在并通过。
|
||||
- [ ] `cargo fmt --all -- --check` exits 0。
|
||||
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
|
||||
- [ ] `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` exits 0。
|
||||
- [ ] 没有修改范围外文件(`git status --short` 仅显示本计划范围内文件和 `plans/README.md` 状态更新)。
|
||||
- [ ] 已更新 `plans/README.md` 中本计划的状态行。
|
||||
|
||||
## STOP 条件
|
||||
|
||||
- 当前状态中列出位置的代码与摘录不匹配。
|
||||
- 你发现 `last_updated` 的 public API 依赖真实 `Instant` 值,无法用 atomic duration 或 mutex 在范围内保持行为。
|
||||
- 修复需要改变 metrics output schema、metric names 或 label semantics。
|
||||
- `cargo clippy` 因 atomic ordering 或 dead code 问题连续两次失败且无法在范围内解决。
|
||||
|
||||
## 维护说明
|
||||
|
||||
- 未来新增统计 primitive 时禁止再用 `UnsafeCell` + manual `Send/Sync` 暴露 safe shared mutation;默认使用 atomics 或明确锁。
|
||||
- reviewer 应重点检查 atomic ordering 是否足够、是否移除了所有 unsafe shared counter paths、测试是否真的并发执行。
|
||||
- 本计划不优化 metrics aggregation 性能;只消除 UB 和数据竞争风险。
|
||||
@@ -0,0 +1,160 @@
|
||||
# 计划 002:为 peer RPC/control packet 队列加入背压和过载行为
|
||||
|
||||
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新 `plans/README.md` 中本计划的状态行,除非 reviewer 明确说明由他们维护索引。
|
||||
>
|
||||
> **漂移检查(首先运行)**:`git diff --stat 78146d16..HEAD -- easytier/src/peers/peer_manager.rs easytier/src/peers/foreign_network_manager.rs easytier/src/common/stats_manager.rs easytier/src/tests`
|
||||
> 如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
|
||||
|
||||
## 状态
|
||||
|
||||
- **优先级**: P1
|
||||
- **工作量**: M
|
||||
- **风险**: MED
|
||||
- **依赖**: plans/001-thread-safe-metrics-throughput.md
|
||||
- **类别**: perf
|
||||
- **计划生成于**: commit `78146d16`, 2026-06-18
|
||||
|
||||
## 为什么重要
|
||||
|
||||
Peer RPC/control packet transport 当前使用 `mpsc::unbounded_channel()`,network-facing packet processor 对每个 RPC packet 直接 `send(...).unwrap()`。如果远端或本地 relay 突发控制面 packet,队列可以无限增长,导致内存膨胀和控制面延迟;如果 receiver 关闭,`unwrap()` 还会 panic。完成后应有明确 bounded capacity、drop/backpressure policy 和可观测 drop 计数。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- `easytier/src/peers/peer_manager.rs` — local peer RPC transport 队列和 packet processor。
|
||||
- `easytier/src/peers/foreign_network_manager.rs` — foreign-network RPC transport 队列和 relay/local packet ingestion。
|
||||
- `easytier/src/common/stats_manager.rs` — 如果 001 已完成,应复用线程安全 metrics 记录 queue drops。
|
||||
|
||||
当前代码摘录:
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_manager.rs:275
|
||||
// TODO: remove these because we have impl pipeline processor.
|
||||
let (peer_rpc_tspt_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel();
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_manager.rs:1245
|
||||
struct PeerRpcPacketProcessor {
|
||||
peer_rpc_tspt_sender: UnboundedSender<ZCPacket>,
|
||||
}
|
||||
|
||||
// easytier/src/peers/peer_manager.rs:1257
|
||||
self.peer_rpc_tspt_sender.send(packet).unwrap();
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/foreign_network_manager.rs:362
|
||||
let (rpc_transport_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel();
|
||||
|
||||
// easytier/src/peers/foreign_network_manager.rs:529
|
||||
rpc_sender.send(zc_packet).unwrap();
|
||||
```
|
||||
|
||||
仓库约定:control-plane errors 通常通过 `tracing::{debug,warn,error}` 记录;packet hot path 应避免 blocking await。已有 data-plane queues elsewhere 倾向显式容量和丢弃策略;本计划应保持 hot path 非阻塞。
|
||||
|
||||
## 需要使用的命令
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|---------|---------|---------------------|
|
||||
| Format | `cargo fmt --all -- --check` | exit 0 |
|
||||
| Lint | `cargo clippy --all-targets --features full --all -- -D warnings` | exit 0, no warnings |
|
||||
| Feature check | `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` | exit 0 |
|
||||
| Targeted tests | `cargo test --package easytier peer_manager --features full -- --nocapture` | exit 0; new queue tests pass if present |
|
||||
|
||||
## 临时目录约定
|
||||
|
||||
- 临时文件、scratch 目录和 disposable worktree 必须放在 `$HOME/tmp` 下。
|
||||
- 如果 `$HOME/tmp` 不存在且本计划需要临时空间,先创建它。
|
||||
- 不要把临时产物放进被修改仓库。
|
||||
|
||||
## 范围
|
||||
|
||||
**范围内**(只能修改这些文件):
|
||||
- `easytier/src/peers/peer_manager.rs`
|
||||
- `easytier/src/peers/foreign_network_manager.rs`
|
||||
- `easytier/src/common/stats_manager.rs`(仅用于添加/复用 drop metric names;不要重做 001)
|
||||
- `easytier/src/tests/*`(仅新增/调整本计划相关测试)
|
||||
|
||||
**范围外**(即使看起来相关也不要触碰):
|
||||
- RPC protocol message definitions and generated protobuf code。
|
||||
- Routing semantics、credential trust、foreign network topology logic。
|
||||
- Frontend, web server, GUI。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- Branch: `advisor/002-bound-peer-rpc-queues`
|
||||
- Commit message style follows existing conventional commits, for example `fix: route_update message is not lag`.
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## 步骤
|
||||
|
||||
### 步骤 1:定义 bounded capacity 和 overload policy
|
||||
|
||||
在两个文件中引入同一个小常量,建议名称为 `PEER_RPC_PACKET_QUEUE_CAPACITY`,初始值建议 `1024` 或 `4096`。如果已有相近 queue capacity 常量,复用仓库风格。
|
||||
|
||||
Policy 必须明确:packet hot path 不等待;当队列满或 receiver closed 时,丢弃当前 RPC/control packet,记录 `tracing::warn!` 或 rate-limited debug,并增加 drop counter。不要 panic。
|
||||
|
||||
**验证**:`cargo fmt --all -- --check` → exit 0。
|
||||
|
||||
### 步骤 2:替换 `peer_manager.rs` 的 unbounded channel
|
||||
|
||||
将 `mpsc::unbounded_channel()` 替换为 `mpsc::channel(PEER_RPC_PACKET_QUEUE_CAPACITY)`。更新 `RpcTransport`、`PeerRpcPacketProcessor` 字段类型,从 `UnboundedSender`/unbounded receiver 改成 bounded `Sender`/`Receiver`。
|
||||
|
||||
在 `try_process_packet_from_peer` 中不要 `.await`,使用 `try_send(packet)`。如果 `Full` 或 `Closed`,记录并返回 `None`,保持原有“这是 RPC packet,不再进入 data-plane pipeline”的行为。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_manager --features full -- --nocapture` → exit 0;如果没有匹配测试,至少必须编译通过。
|
||||
|
||||
### 步骤 3:替换 `foreign_network_manager.rs` 的 unbounded channel
|
||||
|
||||
同样将 foreign-network RPC transport 改为 bounded channel,并在 ingestion path 使用 `try_send(zc_packet)`。不得保留 `unwrap()`。
|
||||
|
||||
如果两个文件都需要相同 helper,优先在各文件内保持小函数,避免为了复用引入新模块。最小正确改动优先。
|
||||
|
||||
**验证**:`cargo test --package easytier foreign_network_manager --features full -- --nocapture` → exit 0;如果没有匹配测试,至少必须编译通过。
|
||||
|
||||
### 步骤 4:添加队列满/receiver closed 的单元测试或小型回归测试
|
||||
|
||||
尽量在模块内新增不依赖真实网络 namespace 的测试:创建 bounded channel 容量为 1,填满后调用封装的 send helper,断言不会 panic 且返回/drop counter 行为正确。如果代码结构不允许直接测试 private helper,可以抽出一个 file-local helper function,例如 `try_enqueue_rpc_packet(...) -> bool`,测试 helper。
|
||||
|
||||
不要为了测试启动完整三节点网络;这属于慢集成测试,不适合验证 queue behavior。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_rpc_queue --features full -- --nocapture` → exit 0;如果测试名不同,使用实际新增测试过滤器,输出中新增测试通过。
|
||||
|
||||
### 步骤 5:运行完整相关门禁
|
||||
|
||||
**验证**:
|
||||
- `cargo fmt --all -- --check` → exit 0。
|
||||
- `cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
- `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` → exit 0。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 新增 queue helper tests,覆盖队列未满、队列满、receiver closed 三种情况。
|
||||
- 如果添加 drop metric,测试满队列时 counter 增加。
|
||||
- 不要求新增 full network integration test;bounded queue behavior 应在 unit-level 可验证。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] `peer_manager.rs` 不再为 peer RPC transport 使用 `mpsc::unbounded_channel()`。
|
||||
- [ ] `foreign_network_manager.rs` 不再为 foreign-network RPC transport 使用 `mpsc::unbounded_channel()`。
|
||||
- [ ] 相关 packet enqueue path 不再调用 `.unwrap()`。
|
||||
- [ ] 满队列和 receiver closed 有明确非 panic 行为。
|
||||
- [ ] 新增或更新测试覆盖 queue overload behavior。
|
||||
- [ ] `cargo fmt --all -- --check` exits 0。
|
||||
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
|
||||
- [ ] `cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose` exits 0。
|
||||
- [ ] 没有修改范围外文件。
|
||||
- [ ] 已更新 `plans/README.md` 中本计划的状态行。
|
||||
|
||||
## STOP 条件
|
||||
|
||||
- 001 尚未完成,而本计划需要新增 metrics/drop counters;此时先执行 001 或报告阻塞。
|
||||
- `PeerRpcManager` 或 transport trait 要求 unbounded receiver 类型且无法在范围内替换。
|
||||
- 正确实现需要改变 RPC protocol semantics 或 routing trust logic。
|
||||
- bounded queue 导致现有 integration tests 稳定失败,且不能通过容量或 policy 微调解决。
|
||||
|
||||
## 维护说明
|
||||
|
||||
- reviewer 应重点审查 drop policy 是否适合 control-plane:丢弃低优先级 sync packet 可以接受,但不能默默破坏必须可靠的 request/response path。
|
||||
- 后续如果出现 reconnect storm 或 route sync loss,应结合 drop metrics 调整 capacity。
|
||||
- 本计划不实现优先级队列;如果未来需要区分 `RpcReq`、`RpcResp`、`TaRpc` 优先级,应另写计划。
|
||||
@@ -0,0 +1,176 @@
|
||||
# 计划 003:避免 OSPF 对 stale/no-op sync payload 重算路由
|
||||
|
||||
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新 `plans/README.md` 中本计划的状态行,除非 reviewer 明确说明由他们维护索引。
|
||||
>
|
||||
> **漂移检查(首先运行)**:`git diff --stat 78146d16..HEAD -- easytier/src/peers/peer_ospf_route.rs easytier/src/tests`
|
||||
> 如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
|
||||
|
||||
## 状态
|
||||
|
||||
- **优先级**: P2
|
||||
- **工作量**: S
|
||||
- **风险**: MED
|
||||
- **依赖**: none
|
||||
- **类别**: perf
|
||||
- **计划生成于**: commit `78146d16`, 2026-06-18
|
||||
|
||||
## 为什么重要
|
||||
|
||||
OSPF sync handler 当前已经能判断 `peer_infos` 是否实际写入了更新版本,但函数只返回 `Result<(), Error>`,调用方仍对任何非空 payload 设置 `need_update_route_table = true`。在重复、乱序或旧版本 sync 消息较多时,会触发完整 route-table rebuild,造成不必要 CPU 和锁竞争。完成后只有 stored topology state 变化时才重算路由,同时保持 duplicate peer ID 检查和 trust 更新语义。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- `easytier/src/peers/peer_ospf_route.rs` — OSPF route sync、state mutation 和 route-table rebuild 逻辑都在同一文件中。
|
||||
|
||||
当前代码摘录:
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_ospf_route.rs:868
|
||||
fn update_peer_infos(
|
||||
&self,
|
||||
my_peer_id: PeerId,
|
||||
my_peer_route_id: u64,
|
||||
dst_peer_id: PeerId,
|
||||
peer_infos: &[RoutePeerInfo],
|
||||
raw_peer_infos: &[DynamicMessage],
|
||||
) -> Result<(), Error> {
|
||||
let mut need_inc_version = false;
|
||||
// ...
|
||||
if need_inc_version {
|
||||
self.version.inc();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_ospf_route.rs:3623
|
||||
service_impl.synced_route_info.update_peer_infos(
|
||||
my_peer_id,
|
||||
service_impl.my_peer_route_id,
|
||||
from_peer_id,
|
||||
pi,
|
||||
rpi,
|
||||
)?;
|
||||
// ...
|
||||
session.update_dst_saved_peer_info_version(pi, from_peer_id);
|
||||
need_update_route_table = true;
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_ospf_route.rs:3647
|
||||
service_impl.synced_route_info.update_conn_info(conn_info);
|
||||
session.update_dst_saved_conn_info_version(conn_info, from_peer_id);
|
||||
need_update_route_table = true;
|
||||
```
|
||||
|
||||
仓库约定:性能修复必须保守;route correctness 优先于少重算。已有 `foreign_network_changed` 风格 change flag,应匹配这种模式,不要引入复杂 scheduler。
|
||||
|
||||
## 需要使用的命令
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|---------|---------|---------------------|
|
||||
| Format | `cargo fmt --all -- --check` | exit 0 |
|
||||
| Lint | `cargo clippy --all-targets --features full --all -- -D warnings` | exit 0, no warnings |
|
||||
| Targeted tests | `cargo test --package easytier peer_ospf_route --features full -- --nocapture` | exit 0; new change-flag tests pass if present |
|
||||
| Integration tests | `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` | exit 0 |
|
||||
|
||||
## 临时目录约定
|
||||
|
||||
- 临时文件、scratch 目录和 disposable worktree 必须放在 `$HOME/tmp` 下。
|
||||
- 如果 `$HOME/tmp` 不存在且本计划需要临时空间,先创建它。
|
||||
- 不要把临时产物放进被修改仓库。
|
||||
|
||||
## 范围
|
||||
|
||||
**范围内**(只能修改这些文件):
|
||||
- `easytier/src/peers/peer_ospf_route.rs`
|
||||
- `easytier/src/tests/*`(仅当需要新增 route sync regression test)
|
||||
|
||||
**范围外**(即使看起来相关也不要触碰):
|
||||
- OSPF graph algorithm、route-table data structures、credential trust policy。
|
||||
- protobuf schema and generated code。
|
||||
- GUI/Web/frontend。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- Branch: `advisor/003-avoid-noop-ospf-route-rebuilds`
|
||||
- Commit message style follows existing conventional commits, for example `fix: route_update message is not lag`.
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## 步骤
|
||||
|
||||
### 步骤 1:让 `update_peer_infos` 返回是否改变 state
|
||||
|
||||
将 `update_peer_infos` 返回类型从 `Result<(), Error>` 改为 `Result<bool, Error>`,返回 `need_inc_version`。保持 duplicate peer ID 检查、raw peer info 更新和 version increment 逻辑不变。
|
||||
|
||||
调用方保存为 `let peer_infos_changed = ...?;`。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0 或无匹配测试但编译通过。
|
||||
|
||||
### 步骤 2:确认 `update_conn_info` 是否已有 changed flag
|
||||
|
||||
阅读同文件中 `update_conn_info` 和 `update_conn_info_one_peer`。如果 `update_conn_info_one_peer` 已返回 `bool`,则让 `update_conn_info` 聚合并返回 `bool`。如果当前 `update_conn_info` 已返回 bool,只使用现有返回值,不重复实现。
|
||||
|
||||
不要改变 accept/reject credential conn info 的条件;只改变“是否设置 `need_update_route_table`”的判断。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0。
|
||||
|
||||
### 步骤 3:仅在 actual change 时设置 `need_update_route_table`
|
||||
|
||||
在 sync handler 中改为:
|
||||
|
||||
- `peer_infos_changed` 为 true 时才设置 `need_update_route_table = true`。
|
||||
- `conn_info_changed` 为 true 时才设置 `need_update_route_table = true`。
|
||||
- `session.update_dst_saved_peer_info_version(...)` 和 `session.update_dst_saved_conn_info_version(...)` 是否应在 unchanged payload 时调用,需要按现有 session version semantics 判断;如果它只是记录对端已发送版本,可保留调用,避免重复请求。
|
||||
|
||||
**验证**:`cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
|
||||
### 步骤 4:新增 no-op update regression tests
|
||||
|
||||
优先添加 module-level unit tests,直接构造 `SyncedRouteInfo` 或现有内部结构:
|
||||
|
||||
- 首次插入较新 `RoutePeerInfo` 返回 `true`。
|
||||
- 再次插入相同 version 或旧 version 返回 `false`。
|
||||
- `update_conn_info` 对相同 connected peers 返回 `false`,对变化集合返回 `true`。
|
||||
|
||||
如果内部类型构造太复杂,使用现有 route sync tests 的 helper;不要为了测试暴露 public API,最多使用 `#[cfg(test)]` helper。
|
||||
|
||||
**验证**:使用实际新增测试过滤器运行,例如 `cargo test --package easytier noop_route_update --features full -- --nocapture` → exit 0;输出中新增测试通过。
|
||||
|
||||
### 步骤 5:运行完整相关门禁
|
||||
|
||||
**验证**:
|
||||
- `cargo fmt --all -- --check` → exit 0。
|
||||
- `cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
- `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` → exit 0。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 新增 `update_peer_infos` changed flag tests:newer version true,same/older version false。
|
||||
- 新增 `update_conn_info` changed flag tests:changed topology true,identical topology false。
|
||||
- 不要求跑完整 privileged nextest matrix;至少 archive 编译所有 tests。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] stale/duplicate peer info 不再设置 `need_update_route_table = true`。
|
||||
- [ ] unchanged conn info 不再设置 `need_update_route_table = true`。
|
||||
- [ ] duplicate peer ID check 仍在 stale/no-op 判断前执行。
|
||||
- [ ] 新增 tests 覆盖 true/false change flag。
|
||||
- [ ] `cargo fmt --all -- --check` exits 0。
|
||||
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
|
||||
- [ ] `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` exits 0。
|
||||
- [ ] 没有修改范围外文件。
|
||||
- [ ] 已更新 `plans/README.md` 中本计划的状态行。
|
||||
|
||||
## STOP 条件
|
||||
|
||||
- `update_peer_infos` 的返回值已被其他分支重构,当前摘录不匹配。
|
||||
- 判断 no-op 需要改变 route trust、credential 或 duplicate peer semantics。
|
||||
- 无法构造可靠测试,且只能通过完整三节点集成测试验证;停止并报告需要 reviewer 决定测试策略。
|
||||
|
||||
## 维护说明
|
||||
|
||||
- 后续任何 route sync state mutation 都应返回 changed flag,并只在 actual change 时触发 route rebuild。
|
||||
- reviewer 应重点检查 version bookkeeping:不要为了省重算而漏掉必要 route refresh。
|
||||
- 本计划不减少单次 rebuild 的成本;那由 `plans/004-reuse-ospf-route-graph.md` 处理。
|
||||
@@ -0,0 +1,176 @@
|
||||
# 计划 004:复用 OSPF route-table 构图以减少拓扑更新成本
|
||||
|
||||
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新 `plans/README.md` 中本计划的状态行,除非 reviewer 明确说明由他们维护索引。
|
||||
>
|
||||
> **漂移检查(首先运行)**:`git diff --stat 78146d16..HEAD -- easytier/src/peers/peer_ospf_route.rs easytier/src/tests`
|
||||
> 如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
|
||||
|
||||
## 状态
|
||||
|
||||
- **优先级**: P2
|
||||
- **工作量**: M
|
||||
- **风险**: MED
|
||||
- **依赖**: plans/003-avoid-noop-ospf-route-rebuilds.md
|
||||
- **类别**: perf
|
||||
- **计划生成于**: commit `78146d16`, 2026-06-18
|
||||
|
||||
## 为什么重要
|
||||
|
||||
每次 OSPF 拓扑更新当前会分别为 least-hop 和 least-cost route table 调用 `build_from_synced_info`。每次调用都会从 synced info 重新构建 peer graph,并重新构建 peer/CIDR indexes。对于 peer 数和 proxy CIDR 数较大的 mesh,这把一次拓扑变化放大成两次完整构图和多次 map/trie 重建。完成后应保持 route selection 结果不变,同时复用同一份 graph/materialized synced view,减少 CPU 和分配成本。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- `easytier/src/peers/peer_ospf_route.rs` — `update_route_table`、graph builder、least-hop/least-cost map generation、CIDR trie rebuild 均在此文件。
|
||||
|
||||
当前代码摘录:
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_ospf_route.rs:1628
|
||||
// build next hop map
|
||||
let (graph, start_node) =
|
||||
Self::build_peer_graph_from_synced_info(my_peer_id, synced_info, cost_calc);
|
||||
|
||||
// easytier/src/peers/peer_ospf_route.rs:1649
|
||||
if matches!(policy, NextHopPolicy::LeastHop) {
|
||||
self.gen_next_hop_map_with_least_hop(&graph, &start_node, version);
|
||||
} else {
|
||||
self.gen_next_hop_map_with_least_cost(&graph, &start_node, version);
|
||||
};
|
||||
|
||||
// easytier/src/peers/peer_ospf_route.rs:1655
|
||||
let mut new_cidr_prefix_trie = PrefixMap::new();
|
||||
let mut new_cidr_v6_prefix_trie = PrefixMap::new();
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/peers/peer_ospf_route.rs:2453
|
||||
fn update_route_table(&self) {
|
||||
// ...
|
||||
self.route_table.build_from_synced_info(
|
||||
self.my_peer_id,
|
||||
&self.synced_route_info,
|
||||
NextHopPolicy::LeastHop,
|
||||
calc_locked.as_ref().unwrap(),
|
||||
);
|
||||
|
||||
self.route_table_with_cost.build_from_synced_info(
|
||||
self.my_peer_id,
|
||||
&self.synced_route_info,
|
||||
NextHopPolicy::LeastCost,
|
||||
calc_locked.as_ref().unwrap(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
仓库约定:core routing behavior must be preserved。先添加 characterization tests,再重构;不要在同一计划里改 route policy。
|
||||
|
||||
## 需要使用的命令
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|---------|---------|---------------------|
|
||||
| Format | `cargo fmt --all -- --check` | exit 0 |
|
||||
| Lint | `cargo clippy --all-targets --features full --all -- -D warnings` | exit 0, no warnings |
|
||||
| Targeted tests | `cargo test --package easytier peer_ospf_route --features full -- --nocapture` | exit 0; route characterization tests pass |
|
||||
| Archive tests | `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` | exit 0 |
|
||||
|
||||
## 临时目录约定
|
||||
|
||||
- 临时文件、scratch 目录和 disposable worktree 必须放在 `$HOME/tmp` 下。
|
||||
- 如果 `$HOME/tmp` 不存在且本计划需要临时空间,先创建它。
|
||||
- 不要把临时产物放进被修改仓库。
|
||||
|
||||
## 范围
|
||||
|
||||
**范围内**(只能修改这些文件):
|
||||
- `easytier/src/peers/peer_ospf_route.rs`
|
||||
- `easytier/src/tests/*`(仅新增/调整 route-table characterization tests)
|
||||
|
||||
**范围外**(即使看起来相关也不要触碰):
|
||||
- Route protocol schema and wire format。
|
||||
- Credential/trust semantics。
|
||||
- Peer center、foreign network manager、data-plane tunnels。
|
||||
- Any UI or config surface。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- Branch: `advisor/004-reuse-ospf-route-graph`
|
||||
- Commit message style follows existing conventional commits, for example `refactor: introduce HedgeExt for task hedging; rewrite NatDstQuicConnector`.
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## 步骤
|
||||
|
||||
### 步骤 1:添加 route-table characterization tests
|
||||
|
||||
在修改实现前,新增测试覆盖至少一个包含以下元素的小拓扑:
|
||||
|
||||
- 本 peer、两个 reachable peers、一个 unreachable 或 outdated peer。
|
||||
- 至少一个 IPv4 proxy CIDR 和一个 IPv6 proxy CIDR。
|
||||
- least-hop 和 least-cost 结果不同或至少都被断言。
|
||||
|
||||
测试应断言当前 `route_table` 和 `route_table_with_cost` 对 peer next-hop、peer reachability、CIDR lookup 的结果。优先使用现有测试 helper;如果内部 API 不便,添加 `#[cfg(test)]` helper,不改变生产 API。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0;新增 characterization tests 在重构前通过。
|
||||
|
||||
### 步骤 2:抽出一次性 graph build 输入
|
||||
|
||||
在 `peer_ospf_route.rs` 中把 `build_from_synced_info` 内部的 graph construction 拆成私有 helper,例如:
|
||||
|
||||
- `build_peer_graph_from_synced_info(...)` 已存在则复用。
|
||||
- 新增 small struct 持有 `graph`、`start_node`、`version` 和后续 index rebuild 需要的 synced snapshot references。
|
||||
|
||||
不要改变 `gen_next_hop_map_with_least_hop` 或 `gen_next_hop_map_with_least_cost` 的算法。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0。
|
||||
|
||||
### 步骤 3:让 `update_route_table` 对两种 policy 复用 graph
|
||||
|
||||
把 `update_route_table` 改为在持有 `cost_calculator` read lock 时构建一次 graph/materialized input,然后分别对 `self.route_table` 和 `self.route_table_with_cost` 应用 least-hop / least-cost generation。
|
||||
|
||||
如果现有 `RouteTable::build_from_synced_info` 是唯一封装点,可以新增一个 sibling method,例如 `build_from_prebuilt_graph(...)`,保持旧方法用于兼容 tests 或其他调用方。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0;characterization tests 仍通过。
|
||||
|
||||
### 步骤 4:避免重复构建共享 indexes
|
||||
|
||||
检查 `build_from_synced_info` 中 peer info map、IPv4 map、CIDR tries 的生成是否依赖 policy-specific next-hop map。如果只依赖 reachability 或 synced info,可移动到共享 helper;如果依赖每个 `RouteTable` 自己的 `next_hop_map`,不要强行共享,避免改变 semantics。
|
||||
|
||||
允许分阶段收益:只共享 graph build 也可完成本计划;共享 CIDR/index 只有在 characterization tests 能证明 behavior 不变时才做。
|
||||
|
||||
**验证**:`cargo test --package easytier peer_ospf_route --features full -- --nocapture` → exit 0。
|
||||
|
||||
### 步骤 5:运行完整相关门禁
|
||||
|
||||
**验证**:
|
||||
- `cargo fmt --all -- --check` → exit 0。
|
||||
- `cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
- `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` → exit 0。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 新增 route-table characterization tests,先在重构前证明现有行为,再在重构后保持通过。
|
||||
- 测试覆盖 least-hop、least-cost、CIDR lookup、unreachable peer exclusion。
|
||||
- 如果可行,加入一个轻量 counter/helper 在 test-only path 确认 graph builder 调用次数从 2 降为 1;如果这需要侵入生产代码,则不要做。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] `update_route_table` 不再对同一 synced topology 构建两次 peer graph。
|
||||
- [ ] least-hop 和 least-cost route outputs 与 characterization tests 中的旧行为一致。
|
||||
- [ ] 没有改变 routing protocol、credential trust 或 config behavior。
|
||||
- [ ] `cargo fmt --all -- --check` exits 0。
|
||||
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
|
||||
- [ ] `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` exits 0。
|
||||
- [ ] 没有修改范围外文件。
|
||||
- [ ] 已更新 `plans/README.md` 中本计划的状态行。
|
||||
|
||||
## STOP 条件
|
||||
|
||||
- `plans/003-avoid-noop-ospf-route-rebuilds.md` 未完成,且当前 route rebuild trigger 仍会对 no-op payload 重算。
|
||||
- 复用 graph 需要改变 least-hop 或 least-cost algorithm。
|
||||
- 现有代码让 `cost_calc` 在两次 build 之间发生有意状态变化;如果确认 `begin_update`/`end_update` 依赖两次独立 build,停止并报告。
|
||||
- Characterization tests 无法稳定构造 route-table expected outputs。
|
||||
|
||||
## 维护说明
|
||||
|
||||
- reviewer 应重点审查是否在锁持有期间引入更长 critical section。
|
||||
- 未来新增 route policy 时应复用本计划抽出的 prebuilt graph input,而不是再调用完整 `build_from_synced_info`。
|
||||
- 本计划不拆分 `peer_ospf_route.rs` 大文件;只做局部性能重构。
|
||||
@@ -0,0 +1,164 @@
|
||||
# 计划 005:补齐 SOCKS5 exit-node 集成测试覆盖
|
||||
|
||||
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新 `plans/README.md` 中本计划的状态行,除非 reviewer 明确说明由他们维护索引。
|
||||
>
|
||||
> **漂移检查(首先运行)**:`git diff --stat 78146d16..HEAD -- easytier/src/tests/three_node.rs easytier/src/tests/mod.rs easytier/src/gateway easytier/src/vpn_portal easytier/src/peers`
|
||||
> 如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
|
||||
|
||||
## 状态
|
||||
|
||||
- **优先级**: P2
|
||||
- **工作量**: M
|
||||
- **风险**: MED
|
||||
- **依赖**: none
|
||||
- **类别**: tests
|
||||
- **计划生成于**: commit `78146d16`, 2026-06-18
|
||||
|
||||
## 为什么重要
|
||||
|
||||
测试文件顶部明确 TODO 指出需要覆盖 `socks5 + exit node == self || proxy_cidr == 0.0.0.0/0` 的出口节点能力。现有 `socks5_vpn_portal` 测试只覆盖固定 destination 和 `10.1.2.0/24` proxy CIDR,不能证明默认出口路由或 self-exit 场景。完成后,这条核心 VPN routing/use-case 会有 characterization test,后续修改 SOCKS5、proxy CIDR 或 exit-node 行为时不再盲改。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- `easytier/src/tests/three_node.rs` — 三节点集成测试和 SOCKS5 portal 测试所在文件。
|
||||
- `easytier/src/gateway/socks5.rs`、`easytier/src/gateway/socks5/dataplane.rs` — SOCKS5 gateway implementation;仅在测试失败定位时阅读,默认不修改。
|
||||
- `easytier/src/peers/peer_ospf_route.rs` — proxy CIDR 和 route selection 行为;默认不修改。
|
||||
|
||||
当前代码摘录:
|
||||
|
||||
```rust
|
||||
// easytier/src/tests/three_node.rs:16
|
||||
// TODO: 需要加一个单测,确保 socks5 + exit node == self || proxy_cidr == 0.0.0.0/0 时,可以实现出口节点的能力。
|
||||
```
|
||||
|
||||
```rust
|
||||
// easytier/src/tests/three_node.rs:1753
|
||||
pub async fn socks5_vpn_portal(
|
||||
#[values("10.144.144.1", "10.144.144.3", "10.1.2.4")] dst_addr: &str,
|
||||
) {
|
||||
// ...
|
||||
let _insts = init_three_node_ex(
|
||||
"tcp",
|
||||
|cfg| {
|
||||
if cfg.get_inst_name() == "inst3" {
|
||||
// 添加子网代理配置
|
||||
cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None)
|
||||
.unwrap();
|
||||
}
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
```
|
||||
|
||||
仓库约定:这些网络集成测试使用 `#[tokio::test]` 和 `#[serial_test::serial]`,部分测试需要 Linux network namespace/root capabilities。保持测试 isolated and repeatable;不要让新增测试依赖外部网络。
|
||||
|
||||
## 需要使用的命令
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|---------|---------|---------------------|
|
||||
| Format | `cargo fmt --all -- --check` | exit 0 |
|
||||
| Lint | `cargo clippy --all-targets --features full --all -- -D warnings` | exit 0, no warnings |
|
||||
| Targeted test | `cargo test --package easytier socks5_vpn_portal --features full -- --nocapture --test-threads 1` | exit 0; existing and new SOCKS5 tests pass |
|
||||
| CI-style archive | `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` | exit 0 |
|
||||
|
||||
如果本地环境缺少 root/network namespace 能力,targeted test 可能失败。此时仍必须确保 compile/archive 通过,并在结果中明确记录环境缺口。
|
||||
|
||||
## 临时目录约定
|
||||
|
||||
- 临时文件、scratch 目录和 disposable worktree 必须放在 `$HOME/tmp` 下。
|
||||
- 如果 `$HOME/tmp` 不存在且本计划需要临时空间,先创建它。
|
||||
- 不要把临时产物放进被修改仓库。
|
||||
|
||||
## 范围
|
||||
|
||||
**范围内**(只能修改这些文件):
|
||||
- `easytier/src/tests/three_node.rs`
|
||||
- `easytier/src/tests/mod.rs`(仅当需要注册 helper/module)
|
||||
|
||||
**范围外**(即使看起来相关也不要触碰):
|
||||
- Production SOCKS5/gateway/routing code。若测试暴露 bug,停止并报告;不要在本计划里修生产逻辑。
|
||||
- Any CI workflow, docs, GUI/Web/frontend。
|
||||
- Existing tests unrelated to SOCKS5 portal or exit-node behavior。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- Branch: `advisor/005-cover-socks5-exit-node`
|
||||
- Commit message style follows existing conventional commits, for example `test: add tests` from `CONTRIBUTING.md`.
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## 步骤
|
||||
|
||||
### 步骤 1:阅读现有 `socks5_vpn_portal` helper pattern
|
||||
|
||||
在 `easytier/src/tests/three_node.rs` 中阅读完整 `socks5_vpn_portal` 测试,特别是如何启动三节点、如何启动 TCP listener、如何通过 `tokio_socks::tcp::socks5::Socks5Stream` 访问目标地址、如何 cleanup。
|
||||
|
||||
不要复制大量代码后分叉;优先抽取小 helper,例如 `run_socks5_tcp_echo_case(...)`,让现有测试和新增测试共享。
|
||||
|
||||
**验证**:`cargo fmt --all -- --check` → exit 0(如果尚未修改,仍应通过)。
|
||||
|
||||
### 步骤 2:新增 `0.0.0.0/0` proxy CIDR exit-node case
|
||||
|
||||
新增一个 serial async test,命名建议 `socks5_vpn_portal_default_ipv4_exit_node`。测试应:
|
||||
|
||||
- 使用 `init_three_node_ex` 创建三节点。
|
||||
- 让某个非客户端节点配置 `cfg.add_proxy_cidr("0.0.0.0/0".parse().unwrap(), None).unwrap()`。
|
||||
- 通过 SOCKS5 portal 访问一个由测试内部启动的 TCP echo server 地址。
|
||||
- 断言 payload round-trip 成功。
|
||||
|
||||
测试目标地址必须是本地/测试 namespace 可控地址,不允许依赖公网。
|
||||
|
||||
**验证**:`cargo test --package easytier socks5_vpn_portal_default_ipv4_exit_node --features full -- --nocapture --test-threads 1` → exit 0;若因权限环境失败,错误必须是环境相关,而非编译或断言失败。
|
||||
|
||||
### 步骤 3:新增 self-exit case 或明确不可测原因
|
||||
|
||||
根据 TODO 中的 `exit node == self`,新增第二个测试,命名建议 `socks5_vpn_portal_self_exit_node`。它应覆盖 SOCKS5 入口节点同时也是 exit node 的场景。
|
||||
|
||||
如果现有 config API 没有清晰方式表达 “exit node == self”,不要猜测配置。先搜索现有 tests 中 `exit_nodes`、`add_proxy_cidr`、`vpn_portal` 的用法;如果仍不明确,STOP 并报告需要 maintainer 确认配置语义。
|
||||
|
||||
**验证**:`cargo test --package easytier socks5_vpn_portal_self_exit_node --features full -- --nocapture --test-threads 1` → exit 0;或 STOP 报告不可测配置语义。
|
||||
|
||||
### 步骤 4:移除或更新 TODO
|
||||
|
||||
如果两个场景都已覆盖,将 `three_node.rs:16` 的 TODO 删除或改成剩余未覆盖场景的精确 TODO。不要删除仍未覆盖的提醒。
|
||||
|
||||
**验证**:`cargo test --package easytier socks5_vpn_portal --features full -- --nocapture --test-threads 1` → exit 0;现有和新增 SOCKS5 portal tests 通过。
|
||||
|
||||
### 步骤 5:运行完整相关门禁
|
||||
|
||||
**验证**:
|
||||
- `cargo fmt --all -- --check` → exit 0。
|
||||
- `cargo clippy --all-targets --features full --all -- -D warnings` → exit 0。
|
||||
- `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` → exit 0。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 新增 `socks5_vpn_portal_default_ipv4_exit_node`:覆盖 `proxy_cidr == 0.0.0.0/0`。
|
||||
- 新增 `socks5_vpn_portal_self_exit_node`:覆盖 SOCKS5 入口节点作为出口节点。
|
||||
- 复用现有 `socks5_vpn_portal` 的 TCP echo/payload pattern,保持 `#[serial_test::serial]`。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] TODO 中提到的 `0.0.0.0/0` exit-node 场景有测试覆盖。
|
||||
- [ ] TODO 中提到的 self-exit 场景有测试覆盖,或计划按 STOP 条件阻塞并说明配置语义缺口。
|
||||
- [ ] 新测试不依赖公网服务。
|
||||
- [ ] `cargo fmt --all -- --check` exits 0。
|
||||
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
|
||||
- [ ] `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` exits 0。
|
||||
- [ ] 没有修改 production code 或范围外文件。
|
||||
- [ ] 已更新 `plans/README.md` 中本计划的状态行。
|
||||
|
||||
## STOP 条件
|
||||
|
||||
- 新增测试暴露 production bug:不要修生产代码,停止并报告 failing test、命令和错误摘要。
|
||||
- self-exit 的配置语义无法从现有代码/tests 中确认。
|
||||
- 测试只能通过访问公网验证;这不符合仓库测试隔离要求。
|
||||
- 为了让测试通过需要放宽 assertions 或增加 sleeps 超过现有测试风格。
|
||||
|
||||
## 维护说明
|
||||
|
||||
- reviewer 应重点审查测试是否真正走 SOCKS5 portal 和 exit-node route,而不是退化成本地直连。
|
||||
- 后续修改 proxy CIDR、exit-node、SOCKS5 dataplane 时,应运行本计划新增的 targeted tests。
|
||||
- 本计划只建立测试基线;如果发现 bug,应另写修复计划。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 实施计划
|
||||
|
||||
由 improve skill 于 2026-06-18 生成,基于 commit `78146d16`。除非依赖关系另有要求,请按以下顺序执行。每个执行者在开始前必须完整阅读计划,遵守 STOP 条件,并在完成后更新自己的状态行。
|
||||
|
||||
## 执行顺序与状态
|
||||
|
||||
| Plan | 标题 | 优先级 | 工作量 | 依赖 | Status |
|
||||
|------|------|--------|--------|------|--------|
|
||||
| 001 | 将共享 metrics/throughput 计数改为线程安全实现 | P1 | M | — | DONE in worktree, not merged |
|
||||
| 002 | 为 peer RPC/control packet 队列加入背压和过载行为 | P1 | M | 001 | DONE in worktree, not merged |
|
||||
| 003 | 避免 OSPF 对 stale/no-op sync payload 重算路由 | P2 | S | — | DONE in worktree, not merged |
|
||||
| 004 | 复用 OSPF route-table 构图以减少拓扑更新成本 | P2 | M | 003 | DONE in worktree, not merged |
|
||||
| 005 | 补齐 SOCKS5 exit-node 集成测试覆盖 | P2 | M | — | DONE in worktree, not merged |
|
||||
|
||||
状态值:TODO | IN PROGRESS | DONE | DONE in worktree, not merged | BLOCKED(附一行原因) | REJECTED(附一行理由,例如 finding 已独立修复或方案放弃)
|
||||
|
||||
## Reconcile 2026-06-18
|
||||
|
||||
- 001: `/home/fanmi/tmp/easytier-exec-001`, branch `advisor/001-thread-safe-metrics-throughput`, commit `7b6e4dfe`; worktree clean; not contained in `main` at `78146d16`.
|
||||
- 002: `/home/fanmi/tmp/easytier-exec-002`, branch `advisor/002-bound-peer-rpc-queues`, commit `34d2193d`; worktree clean; not contained in `main` at `78146d16`.
|
||||
- 003: `/home/fanmi/tmp/easytier-exec-003`, branch `advisor/003-avoid-noop-ospf-route-rebuilds`, commit `1be77b51`; worktree clean; not contained in `main` at `78146d16`.
|
||||
- 004: `/home/fanmi/tmp/easytier-exec-004`, branch `advisor/004-reuse-ospf-route-graph`, commit `325c2e5d`; worktree clean; not contained in `main` at `78146d16`.
|
||||
- 005: `/home/fanmi/tmp/easytier-exec-005`, branch `advisor/005-cover-socks5-exit-node`, commit `2cb51b71`; worktree clean; not contained in `main` at `78146d16`.
|
||||
|
||||
## 依赖说明
|
||||
|
||||
- 002 依赖 001,因为队列背压计划应暴露 queue depth/drop counters;这些 counters 应复用 001 中线程安全后的 metrics primitive,避免在新代码里继续扩散 `UnsafeCell` 模式。
|
||||
- 004 依赖 003,因为先抑制 no-op sync 的无效重算,再做共享构图重构,能让性能测试和行为变化更容易归因。
|
||||
- 005 独立执行,但如果未来要修改 SOCKS5、exit-node 或 `0.0.0.0/0` proxy CIDR 行为,应先落地 005 作为 characterization baseline。
|
||||
|
||||
## 已考虑并拒绝的发现
|
||||
|
||||
- `/api/v1/generate-config` 和 `/api/v1/parse-config` 是否应要求登录:证据显示 route layering 可能使其公开,但可能是产品意图;不属于本次“正确性和性能”范围,且应先补意图测试再判断。
|
||||
- OSPF 7k 行 god module 整体拆分:确认是技术债,但范围过大;应先执行 003、004 并增加 characterization tests 后再规划。
|
||||
- 前端测试/DX、依赖清理、安全 hardening:有价值,但用户本次只要求正确性和性能计划。
|
||||
Reference in New Issue
Block a user