Reconnect
主线位置:就绪主线。连接、重连都发生在
poll_ready里;请求主线上,响应 future 要么是内层的 future,要么带着上一次连接失败的错误。
stateDiagram-v2 [*] --> Idle: new [*] --> Connected: with_connection Idle --> Connecting: make_service Connecting --> Connected: 连接成功 Connecting --> Idle: 连接失败,错误存起来 Connected --> Idle: 内层 poll_ready 报错 Connected --> Connected: 内层就绪,可以 call
Reconnect 包的不是一个服务,而是一个能造服务的服务:MakeService,即 Service<Target, Response = S>,S 本身又是一个服务。典型的是「给一个地址,返回一条连接」。MakeService 本身见第 06 部分。
结构
pub struct Reconnect<M, Target>
where
M: Service<Target>,
{
mk_service: M,
state: State<M::Future, M::Response>,
target: Target,
error: Option<M::Error>,
}
#[derive(Debug)]
enum State<F, S> {
Idle,
Connecting(F),
Connected(S),
}
target是连接目标,每次连接都clone一份交给make_service,所以要求Target: Clone。state的三个状态:Idle没有连接,Connecting拿着连接 future,Connected拿着造出来的服务。error存最近一次连接失败的错误,留给下一次call。
new 从 Idle 开始,第一次 poll_ready 才连接(模块文档说的 lazily connect);with_connection 接收一条已经建好的连接,从 Connected 开始。
Service 实现的约束里有一条 M::Future: Unpin:连接 future 存在 State::Connecting 里,poll_ready 用 Pin::new(f) 来 poll 它,不做 pin 投影。用 async 块写的连接 future 通常不是 Unpin,要先 Box::pin。Reconnect 没有实现 Clone。
poll_ready:状态机
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
loop {
match &mut self.state {
State::Idle => {
trace!("poll_ready; idle");
match self.mk_service.poll_ready(cx) {
Poll::Ready(r) => r?,
Poll::Pending => {
trace!("poll_ready; MakeService not ready");
return Poll::Pending;
}
}
let fut = self.mk_service.make_service(self.target.clone());
self.state = State::Connecting(fut);
continue;
}
State::Connecting(ref mut f) => {
trace!("poll_ready; connecting");
match Pin::new(f).poll(cx) {
Poll::Ready(Ok(service)) => {
self.state = State::Connected(service);
}
Poll::Pending => {
trace!("poll_ready; not ready");
return Poll::Pending;
}
Poll::Ready(Err(e)) => {
trace!("poll_ready; error");
self.state = State::Idle;
self.error = Some(e);
break;
}
}
}
State::Connected(ref mut inner) => {
trace!("poll_ready; connected");
match inner.poll_ready(cx) {
Poll::Ready(Ok(())) => {
trace!("poll_ready; ready");
return Poll::Ready(Ok(()));
}
Poll::Pending => {
trace!("poll_ready; not ready");
return Poll::Pending;
}
Poll::Ready(Err(_)) => {
trace!("poll_ready; error");
self.state = State::Idle;
}
}
}
}
}
Poll::Ready(Ok(()))
}
一次 poll_ready 可以连着走好几个状态:Idle 里 make_service 之后 continue,马上 poll 连接 future;连上了就进 Connected,马上问新服务是否就绪。只有遇到 Pending、就绪、或者连接失败才返回。
四种错误,四种结局:
| 错误从哪来 | Reconnect 怎么做 | 调用方看到什么 |
|---|---|---|
MakeService::poll_ready 报错 | r? 直接返回 | poll_ready 返回 Err,按 Service 规则这个 Reconnect 该丢弃了 |
| 连接 future 报错 | 回到 Idle,错误存进 self.error,break 出循环 | poll_ready 返回 Ready(Ok),下一次 call 返回这个错误 |
已连接的服务 poll_ready 报错 | 丢掉这条连接,回到 Idle,错误被丢弃,循环继续,立即重连 | 什么也看不到,最多是这次 poll_ready 变慢或 Pending |
| 已连接的服务的响应 future 报错 | 不经过 Reconnect | 这次 call 的结果是错误;连接留着,不重连 |
所以触发重连的只有内层 poll_ready 的错误,也就是内层说「这个服务坏了」的那种错误(规则见 Service trait)。一次请求失败不算。重连没有退避,也没有次数上限:每次 poll_ready 回到 Idle 都会立即再连一次。要退避就在外面套重试,见第 05 部分。
call:先交错误,再交请求
call:
fn call(&mut self, request: Request) -> Self::Future {
if let Some(error) = self.error.take() {
return ResponseFuture::error(error);
}
let service = match self.state {
State::Connected(ref mut service) => service,
_ => panic!("service not ready; poll_ready must be called first"),
};
let fut = service.call(request);
ResponseFuture::new(fut)
}
先查 self.error:上一次连接失败了,这次 call 就返回那个错误,请求不发出去,被丢掉。这也是 poll_ready 在连接失败时敢返回 Ready(Ok) 的原因:状态是 Idle,正常情况下 call 会 panic,但 error 里有东西,call 走不到 panic 那一行。
ResponseFuture 有两个变体:Future 包着内层的 future,Error 包着存下来的连接错误,poll 一次就返回。
问: 还没连上,poll_ready 就说「就绪」,这不是违反契约吗?
答: 按字面看是在绕契约,原因写在引入这个行为的 #386 里:如果连接失败让 poll_ready 返回错误,按规则这个服务就得被丢掉,套在外面的中间件也会这么做。PR 举的例子是 Buffer:「if poll_ready returns the error then we must throw away the service, and buffer will just keep returning the failed connection error, even if we can successfully reestablish.」于是连接失败被降级成「这一个请求失败」,服务本身还能用,下一次 poll_ready 重新连接。PR 也承认 Reconnect 是个特例:它是从 MakeService 到 Service 的适配器,「the poll_ready contract here doesn't mean the same as a regular Service because it has to bundle connecting」。
实验:连接失败、请求失败、连接断开
连接器第 1 次连接会被拒绝;连接上的服务收到 "boom" 时这一个请求失败,收到 "kill" 后它的 poll_ready 开始报错:
tower = { version = "=0.5.3", features = ["reconnect", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::{ready, Ready};
use std::task::{Context, Poll};
use tower::reconnect::Reconnect;
use tower::{BoxError, Service, ServiceExt};
// A connection. Its poll_ready fails once it has been "killed".
struct Conn {
id: u32,
broken: bool,
}
impl Service<&'static str> for Conn {
type Response = String;
type Error = BoxError;
type Future = Ready<Result<String, BoxError>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
if self.broken {
return Poll::Ready(Err("connection reset".into()));
}
Poll::Ready(Ok(()))
}
fn call(&mut self, req: &'static str) -> Self::Future {
match req {
"boom" => ready(Err("bad request".into())),
"kill" => {
self.broken = true;
ready(Ok(format!("conn {} handled {req}", self.id)))
}
_ => ready(Ok(format!("conn {} handled {req}", self.id))),
}
}
}
// Makes connections; attempts listed in `fail` are refused.
struct Connector {
attempts: u32,
fail: Vec<u32>,
}
impl Service<&'static str> for Connector {
type Response = Conn;
type Error = BoxError;
type Future = Ready<Result<Conn, BoxError>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, target: &'static str) -> Self::Future {
self.attempts += 1;
let n = self.attempts;
if self.fail.contains(&n) {
println!(" connect #{n} to {target}: refused");
return ready(Err("connection refused".into()));
}
println!(" connect #{n} to {target}: ok");
ready(Ok(Conn { id: n, broken: false }))
}
}
async fn send(svc: &mut Reconnect<Connector, &'static str>, req: &'static str) {
let r = svc.ready().await.unwrap().call(req).await;
println!("{req:>5} -> {:?}", r.map_err(|e| e.to_string()));
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("scenario 1");
let mut svc = Reconnect::new(Connector { attempts: 0, fail: vec![1] }, "db:5432");
send(&mut svc, "a").await;
send(&mut svc, "b").await;
send(&mut svc, "boom").await;
send(&mut svc, "c").await;
send(&mut svc, "kill").await;
send(&mut svc, "d").await;
println!("scenario 2: ready() twice after a failed connect");
let mut svc = Reconnect::new(Connector { attempts: 0, fail: vec![1] }, "db:5432");
svc.ready().await.unwrap();
svc.ready().await.unwrap();
send(&mut svc, "e").await;
send(&mut svc, "f").await;
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
scenario 1
connect #1 to db:5432: refused
a -> Err("connection refused")
connect #2 to db:5432: ok
b -> Ok("conn 2 handled b")
boom -> Err("bad request")
c -> Ok("conn 2 handled c")
kill -> Ok("conn 2 handled kill")
connect #3 to db:5432: ok
d -> Ok("conn 3 handled d")
scenario 2: ready() twice after a failed connect
connect #1 to db:5432: refused
connect #2 to db:5432: ok
e -> Err("connection refused")
f -> Ok("conn 2 handled f")
场景 1 逐行对应上面的表:
a:连接 1 被拒,ready()照样成功,错误由call交出来,请求a没有发出去。b:下一次ready()从Idle重新连接,连接 2 成功。boom:响应出错,但c仍然走连接 2,没有重连。kill之后连接 2 的poll_ready报connection reset。d的ready()里,Reconnect丢掉连接 2、丢掉这个错误,当场连上连接 3,d由它处理。调用方从头到尾没见过connection reset。
场景 2 是这个设计的一个副作用:连接失败后,调用方没有 call,而是又 ready() 了一次。第二次 ready() 重连成功,但 self.error 还留着。随后的 call("e") 先查 error,返回的是上一次连接失败的错误,而此时连接 2 是好的,请求 e 被丢掉了。
#437(0.4.0,2020 年 4 月)之前,Connecting 状态的连接 future 失败时,poll_ready 直接返回这个错误。issue #383 报告的现象是:用 tonic 的客户端循环发请求,中途把服务端停掉再启动,「even if the server is brought back up and is healthy that the client is never able to reconnect」。
#437 的说明只有一句「Reopened from #386」,改法写在 #386 里:「These changes reconnect to return the make service failure in the future instead of in poll_ready. This change allows us to reuse the reconnect service on failed reconnects.」提交里多了 error: Option<M::Error> 字段,Connecting 分支从 ret = Err(e.into()) 改成了现在的 self.error = Some(e),ResponseFuture 也多了 Error 这个变体。
小结
Reconnect把MakeService变成服务,状态是Idle、Connecting、Connected;new懒连接,with_connection从已有连接开始。连接 future 要Unpin,Target要Clone。- 一次
poll_ready可以连走多个状态,直到Pending、就绪或连接失败。 - 连接失败:回到
Idle,错误存起来,poll_ready返回Ready(Ok),下一次call交出错误、丢掉请求。只有MakeService自己的poll_ready报错才让Reconnect报废。 - 已连接的服务
poll_ready报错时,连接和错误一起丢掉,当场重连;响应 future 的错误不触发重连。重连没有退避和上限。 - 连接失败后如果先
ready()再call,call返回的是旧的连接错误,即使已经重新连上。