Rust in Everything
基于 pingora-http 0.9.0

升级到 0.9.0:E0596 / E0594 与 CacheKey 的 E0061

主线位置:请求主线里改请求头、响应头的那几个钩子(upstream_request_filter、response_filter),和缓存主线的 cache_key_callback。RequestHeader / ResponseHeader 为什么去掉 DerefMut、0.8.0 上绕过方法改头会怎样,见请求头与错误;CacheKey 为什么去掉 namespace,见键、元数据与可缓存性。本页按升级时实际看到的顺序走一遍。

flowchart TB
  old["0.8.0 的代码"] --> b1["第一次编译"]
  b1 --> e61["E0061 CacheKey"]
  e61 --> help{"照 rustc 提示"}
  help -->|删第三个参数| b2["第二次编译"]
  b2 --> e596["E0596 E0594"]
  e596 --> fix["改用方法"]
  fix --> runok["编译通过"]
  runok --> same["所有键相同"]
  help -->|自己拼 primary| right["键和 0.8.0 相同"]

实验 crate exp-pingora/pita-up08(pingora-http 和 pingora-cache 0.8.0)和 exp-pingora/pita-up09(同一份 src/main.rs,依赖换成 =0.9.0;bin step2、rustc_help、fixed);M2 Max、macOS 27、rustc 1.99.0,debug。

现象

一段在 0.8.0 上正常的代码:通过 DerefMut 直接改请求头、URI 和响应头,再用带命名空间的三参数 CacheKey::new 建两个键:

  let mut req = RequestHeader::build("GET", b"/a", None).unwrap();
  req.headers.insert("x-debug", "1".parse().unwrap());
  req.uri = "/b".parse().unwrap();
  let mut resp = ResponseHeader::build(200, None).unwrap();
  resp.insert_header("server", "app").unwrap();
  resp.headers.remove("server");

  let a = CacheKey::new("site-a", "/a.html", "");
  let b = CacheKey::new("site-a", "/b.html", "");
  println!("uri={} headers={} resp headers={}", req.uri, req.headers.len(), resp.headers.len());
  println!("a={} b={}", a.combined(), b.combined());

0.8.0 上的输出:

uri=/b headers=1 resp headers=0
a=865cc51fbe7cc35df4c6139c64a8118e b=cbc148ad3199aa13674f7d28c477f50e

依赖换成 0.9.0,第一次 cargo build 只有两个错误:

error[E0061]: this function takes 2 arguments but 3 arguments were supplied
   --> src/main.rs:14:11
    |
 14 |   let a = CacheKey::new("site-a", "/a.html", "");
    |           ^^^^^^^^^^^^^                      -- unexpected argument #3 of type `&'static str`
    |
note: associated function defined here
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pingora-cache-0.9.0/src/key.rs:223:12
    |
223 |     pub fn new<B, S>(primary: B, user_tag: S) -> Self
    |            ^^^
help: remove the extra argument
    |
 14 -   let a = CacheKey::new("site-a", "/a.html", "");
 14 +   let a = CacheKey::new("site-a", "/a.html");
    |
...
error: could not compile `pita-up09` (bin "pita-up09") due to 2 previous errors

改头的那三行一个错误都没报。照 help 删掉第三个参数(bin step2),第二次编译才轮到它们:

warning: variable does not need to be mutable
 --> src/bin/step2.rs:7:7
  |
7 |   let mut req = RequestHeader::build("GET", b"/a", None).unwrap();
  |       ----^^^
...
error[E0596]: cannot borrow data in dereference of `RequestHeader` as mutable
 --> src/bin/step2.rs:8:3
  |
8 |   req.headers.insert("x-debug", "1".parse().unwrap());
  |   ^^^^^^^^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `RequestHeader`

error[E0594]: cannot assign to data in dereference of `RequestHeader`
 --> src/bin/step2.rs:9:3
  |
9 |   req.uri = "/b".parse().unwrap();
  |   ^^^^^^^ cannot assign
...
error[E0596]: cannot borrow data in dereference of `ResponseHeader` as mutable
  --> src/bin/step2.rs:12:3
   |
12 |   resp.headers.remove("server");
   |   ^^^^^^^^^^^^ cannot borrow as mutable
...
error: could not compile `pita-up09` (bin "step2") due to 3 previous errors; 1 warning emitted
  • 错误分两轮出现:E0061 是类型检查阶段的错误,rustc 在类型检查失败时不进入借用检查,E0596 / E0594 是借用检查报的,所以第一轮看不到。一个大项目第一次编译看到的错误数,不是升级的全部工作量。
  • 第一条是 warning「variable does not need to be mutable」,建议去掉 req 的 mut。这是个误导:mut 本来就是给那两行改头用的,它们现在是错误,rustc 才认为 mut 没被用到。照做的话,换成 insert_header 时还得加回来。
  • 方法调用(insert、remove)报 E0596,字段赋值(req.uri = ..)报 E0594。

更要紧的是 E0061 那条 help。只把 CacheKey 两行按它改(bin rustc_help):

  let a = CacheKey::new("site-a", "/a.html");
  let b = CacheKey::new("site-a", "/b.html");
a=6389c4db8eb6596b7ee63f00b6c63f15 b=6389c4db8eb6596b7ee63f00b6c63f15 same=true

能编译、能跑,两个不同路径的键却完全一样。删掉的是最后一个参数,原来的 namespace 被当成了 primary,路径成了不参与哈希的 user_tag。放进代理里,同一个命名空间下所有 URL 共用一个缓存条目,第一个被缓存的响应会发给所有路径。

出处

  • RequestHeader 在 0.9.0 只有 impl Deref,DerefMut 由 4d7061b(「Remove DerefMut from RequestHeader and ResponseHeader due to internal invariant violation which triggers panics or silent truncation」,首次进入 0.9.0)删掉。只读访问(req.headers.get(..)、req.uri.path())照样走 Deref,不受影响。删掉它的原因和 0.8.0 上的 header iter mismatch panic,见请求头与错误。
  • CacheKey::new 变成 new(primary, user_tag),由 51fb7e7(「Remove cache key namespace parameter」,首次进入 0.9.0)改的。0.8.0 的哈希是先 update(namespace) 再 update(primary),两段之间没有分隔;user_tag 从来不进哈希。
  • CHANGELOG 0.9.0 的「Potential Breaking Changes」把两条都列了:「RequestHeader and ResponseHeader no longer implement DerefMut because unrestricted mutation could violate internal invariants.」和「Removed the CacheKey namespace parameter.」。同一节里会让编译失败的还有:去掉 async_write_vec、Prometheus 移到单独的 pingora-prometheus、PeerOptions::curve 改成 Cow、boring-rs 5.x、PurgeOutcome 和 ForcedFreshness 多出新变体(对它们写了穷尽 match 的代码会报错)。还有两条不报错、只改行为:上游请求默认删逐跳头,h2 服务端默认有上限(见 Some(H2Options::new()) 丢了默认上限)。

修法

头用方法改,键自己决定怎么拼(bin fixed):

// 0.8.0 hashed namespace then primary with no separator; keep that exact hash
// (and its ambiguity) by concatenating, or frame each part to avoid collisions.
fn key_compat(ns: &str, path: &str) -> CacheKey {
  CacheKey::new(format!("{ns}{path}"), "")
}

fn key_framed(ns: &str, path: &str) -> CacheKey {
  let mut p = Vec::new();
  for part in [ns, path] {
    p.extend_from_slice(&(part.len() as u32).to_be_bytes());
    p.extend_from_slice(part.as_bytes());
  }
  CacheKey::new(p, "")
}

fn main() {
  let mut req = RequestHeader::build("GET", b"/a", None).unwrap();
  req.insert_header("x-debug", "1").unwrap();
  req.set_uri("/b".parse().unwrap());
  let mut resp = ResponseHeader::build(200, None).unwrap();
  resp.insert_header("server", "app").unwrap();
  resp.remove_header("server");
  // ...
}
uri=/b headers=1 resp headers=0
compat a=865cc51fbe7cc35df4c6139c64a8118e b=cbc148ad3199aa13674f7d28c477f50e
framed a=bef2aa7cfdffc91b1c21f4947cd6f9b6 b=4a6c9079c8f97371d7eca273f0ba3182
  • 头的部分和 0.8.0 的输出一致。insert_header、append_header、remove_header、set_uri、set_method、set_status、extensions_mut() 的完整对照表见请求头与错误。
  • key_compat 的两个哈希和 0.8.0 一字不差:直接拼接 namespace + primary 正是 0.8.0 喂给 Blake2b 的字节,升级不会让缓存变冷,也保留了 0.8.0 的歧义(("ab", "c") 和 ("a", "bc") 是同一个键)。
  • key_framed 给每段加长度前缀,没有歧义,但哈希全变了,上线时等于清空缓存。两种选哪个是取舍,51fb7e7 的说明直说「adopting framed keys creates a cold cache」。

怎么确认没中招

  • 编译错误清零之后再搜一遍旧写法,别以为第一轮的错误数就是全部:grep -rnE '\.headers\.(insert|append|remove|get_mut)\(|\.(uri|method|status|version) = ' src/ 找改头的地方;grep -rn 'CacheKey::new(' src/ 逐个看参数。
  • 给缓存键写一个测试:两个不同的路径(以及两个不同的 host)建出来的 combined() 必须不同。照 rustc 提示改的版本在这个测试上会失败(上面的 same=true)。
  • 本站网关升到 0.9.0 没有编译错误:它只用 insert_header 等方法改头,读头走 Deref,也不用缓存,见在本站中。

小结

  • 0.8.0 的代码升到 0.9.0,第一轮只报 CacheKey::new 的 E0061;DerefMut 没了引起的 E0596 / E0594 要等它改完才出现。
  • rustc 的 help「remove the extra argument」删掉的是 user_tag 的位置,结果是命名空间变成 primary、路径不进哈希,所有路径一个键。
  • 头用 insert_header、remove_header、set_uri 等方法改;键要么拼成 namespace + path 保持 0.8.0 的哈希,要么加分隔、接受一次冷缓存。

💬 关联讨论

发起讨论
加载中...