Axum 中 Generator 的作用域为何会导致 Handler 约束错误?
结论
问题在于 Generator 是否跨越了 .await。names::Generator 内部持有 ThreadRng,没有实现 Send。第一种写法会使异步函数返回的 Future 成为 !Send,进而使调用它的 Axum handler 无法满足 Handler 的要求。第二种写法在 .await 前销毁了临时的 Generator,因此不会再因它产生这个问题。
原问题没有提供 names 的具体版本。在可核对的 names 0.14.0 源码中,Generator 包含 rng: ThreadRng 字段,其类型文档列出了 !Send 和 !Sync。项目中的具体情况应以锁定版本和编译诊断为准。Generator 类型文档、源码
为什么 service 层的变量会影响 handler?
async fn 返回一个 Future。执行到 .await 时,如果等待的操作尚未完成,Future 会保存恢复执行所需的状态。
第一种写法在循环外创建 generator:
let mut generator = Generator::default();
for _ in 0..10 {
let slug = generator
.next()
.unwrap_or_else(|| format!("branch-{}", Uuid::new_v4()));
if self.branch_repo.find_by_slug(&slug).await?.is_none() {
return Ok(slug);
}
}
如果查询发现 slug 已存在,下一轮循环还要调用 generator.next(),所以等待查询期间必须保留 generator。它是 !Send,持有它的 Future 也无法满足 Send。
当 handler 直接或间接 .await 这个 service 方法时,handler 的 Future 需要保存尚未完成的内部 Future,因此也会受到影响。Axum 要求 handler 返回的 Future 实现 Send;不满足这一条件时,路由注册处会报告 Handler<_, _> 不满足。Axum Handler 文档
因此,即使 handler 的参数和返回值签名没有变化,修改 service 层代码仍可能改变其 Future 的 trait 实现。通过 Arc 调用 service,也不会让异步函数中的局部变量自动实现 Send。
为什么临时 Generator 可以解决问题?
第二种写法在生成 slug 的语句中创建 Generator:
let slug = Generator::default()
.next()
.unwrap_or_else(|| format!("branch-{}", Uuid::new_v4()));
Generator 是临时值,语句结束时就会被销毁。next() 返回的 String 拥有自身数据,后续查询使用这个字符串,无需继续持有 Generator。
执行到下一条语句中的 .await 时,临时的 Generator 已经不存在,也就无需保存在挂起状态中。只要其他被保留的值和被等待的 Future 也满足相应要求,就不会再因为这个 Generator 导致 Future 成为 !Send。
按给出的两段代码,写法从循环外复用同一个 Generator,改成了每次循环使用一个临时 Generator。这里需要关注的是它能否在 .await 前结束生命周期。
如何明确控制作用域?
可以使用一个没有 .await 的内层代码块,让生成器的生命周期在查询之前结束:
async fn generate_unique_slug(&self) -> Result<String> {
for _ in 0..10 {
let slug = {
let mut generator = Generator::default();
generator
.next()
.unwrap_or_else(|| format!("branch-{}", Uuid::new_v4()))
};
if self.branch_repo.find_by_slug(&slug).await?.is_none() {
return Ok(slug);
}
}
Ok(format!("branch-{}", Uuid::new_v4()))
}
这段代码沿用问题中的 Result、Generator 和 Uuid 导入以及 repository 接口。代码块结束时,只有生成的 String 被传到外层,generator 已被销毁。
如果需要复用同一个生成器,可以先在同步作用域内生成一组候选字符串,等生成器的作用域结束后再逐个执行异步查询。这会提前生成候选值,需要根据实际需求选择。
如何定位类似错误?
在报错的 branch::create handler 定义上添加 #[axum::debug_handler],并在现有 Axum 依赖配置中启用 macros feature。这个宏能提供比路由注册处的 trait 错误更具体的诊断。debug_handler 文档
检查诊断是否指出 Future 不是 Send,以及哪个值被保留到了 .await 之后。E0277 本身不能证明原因一定是 Send;handler 参数的 extractor 约束、响应类型等不满足要求时,也可能出现类似错误。
修复时应优先检查跨越 .await 的值及其作用域。给已有的 !Send 类型外包一层 Arc,不能消除这里的约束问题。
备注:内容仅供参考。
