升级到 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 mismatchpanic,见请求头与错误。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 的哈希,要么加分隔、接受一次冷缓存。