Rust in Everything
基于 tower 0.5.3

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 部分。

结构

Reconnect:

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:状态机

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 一次就返回。

📝NOTE
没有蠢问题

问: 还没连上,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 被丢掉了。

📝NOTE
冷知识:连接失败原来是 poll_ready 的错误

#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 返回的是旧的连接错误,即使已经重新连上。

💬 关联讨论

发起讨论
加载中...