Rust in Everything
基于 tokio-metrics 0.5.2

no field mean_poll_duration on type RuntimeMetrics

主线位置:运行时主线的读取端。RuntimeIntervals::next() 返回的 RuntimeMetrics 有几个字段,取决于编译时有没有 --cfg tokio_unstable。

flowchart TB
  code["读 m.mean_poll_duration"] --> cfg{"编译时带 --cfg tokio_unstable?"}
  cfg -->|带| ok["41 个字段,编译通过"]
  cfg -->|不带| gone["只有 10 个稳定字段"]
  gone --> err["E0609:no field"]
  docs["docs.rs:带 cfg 构建"] --> see["文档里看得到这个字段"]
  see --> code
  env["RUSTFLAGS 环境变量"] -->|设了就覆盖| conf[".cargo/config.toml 的 rustflags"]
  conf --> cfg

复现

use tokio_metrics::RuntimeMonitor;

#[tokio::main]
async fn main() {
  let monitor = RuntimeMonitor::new(&tokio::runtime::Handle::current());
  let m = monitor.intervals().next().unwrap();
  println!("{:?}", m.mean_poll_duration);
}

不带任何 rustflags 编译:

error[E0609]: no field `mean_poll_duration` on type `tokio_metrics::RuntimeMetrics`
 --> src/bin/e0609.rs:7:22
  |
7 |   println!("{:?}", m.mean_poll_duration);
  |                      ^^^^^^^^^^^^^^^^^^ unknown field
  |
help: a field with a similar name exists
  |
7 -   println!("{:?}", m.mean_poll_duration);
7 +   println!("{:?}", m.max_busy_duration);
  |

同一份代码加上 RUSTFLAGS="--cfg tokio_unstable" 就能编译,打印 0ns(刚开始采样,区间里没有 poll)。

编译器的建议要当心:max_busy_duration 只是名字像,量的是「区间里最忙的那个 worker 忙了多久」,和「每次 poll 平均多久」毫无关系。照着建议改,代码能编译,看板上的数字是错的。

为什么:字段在宏里被 cfg 删掉

RuntimeMetrics 不是手写的结构体,是 define_runtime_metrics! 宏生成的。宏把字段分成 stable 和 unstable 两组,后一组的每个字段都加上 #[cfg(tokio_unstable)]:

        pub struct RuntimeMetrics {
            $(
                $(#[$($attributes)*])*
                #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
                $vis $name: $ty,
            )*
            $(
                $(#[$($unstable_attributes)*])*
                #[cfg(tokio_unstable)]
                #[cfg_attr(docsrs, doc(cfg(all(feature = "rt", tokio_unstable))))]
                $unstable_vis $unstable_name: $unstable_ty,
            )*
        }

mean_poll_duration 在 unstable 那一组。不带 cfg 时它不是「值为 0」,而是不存在。稳定的 10 个、不稳定的 31 个分别是哪些,见稳定与不稳定两层字段。

文档里为什么看得到?docs.rs 构建时传了 --cfg tokio_unstable(Cargo.toml 第 60 行,怎么读这一卷里贴过原文)。上面宏里的 doc(cfg(all(feature = "rt", tokio_unstable))) 会在文档上给这些字段打一个「需要 rt 和 tokio_unstable」的标注,但它很小,从示例代码复制过来时容易忽略。字段自己的文档示例也用 # #[cfg(tokio_unstable)] 包着,隐藏行在页面上看不见。

修法一:不需要它,就用 cfg 包住读它的代码

大多数程序只想要稳定字段。读不稳定字段的代码跟着同一个 cfg 走,两种构建都能编译:

use tokio_metrics::RuntimeMonitor;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
  let monitor = RuntimeMonitor::new(&tokio::runtime::Handle::current());
  let mut iv = monitor.intervals();
  tokio::spawn(async {}).await.unwrap();
  let m = iv.next().unwrap();
  println!("cfg!(tokio_unstable) = {}", cfg!(tokio_unstable));
  println!("stable: workers={} live_tasks={}", m.workers_count, m.live_tasks_count);
  // 不稳定字段只在带 --cfg tokio_unstable 编译时存在,读它的代码也得跟着 cfg
  #[cfg(tokio_unstable)]
  println!("unstable: polls={} mean_poll={:?}", m.total_polls_count, m.mean_poll_duration);
}
# 不带 cfg
cfg!(tokio_unstable) = false
stable: workers=2 live_tasks=0

# RUSTFLAGS="--cfg tokio_unstable"
cfg!(tokio_unstable) = true
stable: workers=2 live_tasks=0
unstable: polls=1 mean_poll=3.617µs

自己的 crate 里写 cfg(tokio_unstable) 时,rustc 会对未登记的 cfg 名报 unexpected_cfgs 警告。在 Cargo.toml 里登记一下,和 tokio-metrics 自己的做法一样:

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }

修法二:需要它,就打开 cfg

tokio_unstable 是传给 rustc 的 --cfg,不是 feature,只能由构建最终程序的人设:写在项目的 .cargo/config.toml 里,或者用 RUSTFLAGS 环境变量。两者不是相加,而是环境变量一旦设了就整个替换掉配置文件。一个只打印 cfg!(tokio_unstable) 的程序,.cargo/config.toml 里写着:

[build]
rustflags = ["--cfg", "tokio_unstable"]

cargo 1.99.0 下各种组合:

配置文件环境变量cfg!(tokio_unstable)
[build] rustflags不设true
[build] rustflagsRUSTFLAGS="-C debuginfo=0"false
[build] rustflagsRUSTFLAGS=""false
[build] rustflagsCARGO_ENCODED_RUSTFLAGS=""false
[target.aarch64-apple-darwin] rustflags不设true
[target.aarch64-apple-darwin] rustflagsRUSTFLAGS="-C debuginfo=0"false

空串也算「设了」。CI 或 Dockerfile 里为了别的目的加一句 RUSTFLAGS=...,配置文件里的 cfg 就悄悄没了,接着是本页的 E0609;如果读字段的代码像修法一那样用 cfg 包着,则连错都不报,只是那几行日志不见了。所以要打开 cfg 时,在程序启动时把 cfg!(tokio_unstable) 打一行日志,比相信配置文件可靠。

打开之后还有三件事:rustflags 一变,整个依赖树都要重新编译;tokio 的 unstable API 不在 semver 承诺之内;用 dx 构建全栈应用时,flags 会不会传进 wasm 客户端取决于写在哪一节。这几件事放在在本站中实测的「打开 tokio_unstable 要付什么」一节,用本站的构建方式逐个核实。

小结

  • RuntimeMetrics 的 31 个不稳定字段由宏加上 #[cfg(tokio_unstable)],不带 cfg 时不存在,读它就是 E0609。
  • 编译器会建议一个名字相近的稳定字段(max_busy_duration),意思完全不同,别照改。
  • docs.rs 带 cfg 构建,文档上看得到这些字段,标注在字段旁边。
  • 不需要不稳定字段:读它的代码用 #[cfg(tokio_unstable)] 包住,并在 [lints.rust] 里登记这个 cfg。
  • 需要它:.cargo/config.toml 或 RUSTFLAGS 二选一;RUSTFLAGS 哪怕是空串也会覆盖配置文件,启动时打印 cfg!(tokio_unstable) 确认。

💬 关联讨论

发起讨论
加载中...