Rust in Everything
基于 sqlx 0.9.0

消息与读写缓冲

主线位置:查询主线的最底层。PgConnection::run 里每一个 write_msg(Parse { .. })、每一次 recv(),都落在本页的 PgStream 和 BufferedSocket 上。一次查询的全过程已经用抓包看过一次查询发了哪些消息;这一页讲消息怎么编码、怎么攒在缓冲里、怎么从字节流里切出来,以及 recv 在把消息交给调用方之前先截走了哪些。

flowchart TB
  wm["write_msg(消息)"] --> enc["EncodeMessage 编码"]
  enc --> wbuf["写缓冲 Vec u8"]
  wbuf -->|flush| sock["socket(TCP 或 TLS)"]
  sock --> rbuf["读缓冲 BytesMut"]
  rbuf --> ru["recv_unchecked 切一条"]
  ru --> rv{recv 看类型}
  rv -->|ErrorResponse| err["Err(Error::Database)"]
  rv -->|"Notice / ParameterStatus"| swallow["处理掉,继续读"]
  rv -->|其他| caller[交给调用方]

一条消息:类型字节加长度

除了连接开头的两条,Postgres 协议的每条消息都是同一个格式:1 个字节的类型,4 个字节的大端长度(包括这 4 个字节本身,不包括类型字节),然后是消息体。两个例外是 StartupMessage 和 SSLRequest:它们没有类型字节,直接从长度开始,长度后面的 4 个字节(协议版本号或请求码)告诉服务端这是哪一种(见连接选项与建立连接)。

类型字节在两个方向上是重复使用的,message/mod.rs 因此用了两个枚举:

// Note: we can't use the same enum for both frontend and backend message formats
// because there are duplicated format codes between them.
//
// For example, `Close` (frontend) and `CommandComplete` (backend) both use format code `C`.
// <https://www.postgresql.org/docs/current/protocol-message-formats.html>
#[derive(Debug, PartialOrd, PartialEq)]
#[repr(u8)]
pub enum FrontendMessageFormat {
    Bind = b'B',
    Close = b'C',
    // ...
    Describe = b'D',
    Execute = b'E',
    #[expect(dead_code)]
    Flush = b'H',
    Parse = b'P',
    // ...
    PasswordPolymorphic = b'p',
    Query = b'Q',
    Sync = b'S',
    Terminate = b'X',
}

D 在前端是 Describe,在后端是 DataRow;S 在前端是 Sync,在后端是 ParameterStatus。Flush 标着 #[expect(dead_code)]:sqlx 想让服务端回话时一律发 Sync,从不发 Flush。后端那个枚举 BackendMessageFormat 没有写判别值,由 try_from_u8 从字节翻译过来,不认识的字节报 unknown message type。

每种消息是一个类型,前端消息实现 FrontendMessage,后端消息实现 BackendMessage:

pub(crate) trait FrontendMessage: Sized {
    /// The format prefix of this message.
    const FORMAT: FrontendMessageFormat;

    /// Return the amount of space, in bytes, to reserve in the buffer passed to [`Self::encode_body()`].
    fn body_size_hint(&self) -> Saturating<usize>;

    /// Encode this type as a Frontend message in the Postgres protocol.
    ///
    /// The implementation should *not* include `Self::FORMAT` or the length prefix.
    fn encode_body(&self, buf: &mut Vec<u8>) -> Result<(), Error>;
    // ...
}

pub(crate) trait BackendMessage: Sized {
    /// The expected message format.
    ///
    /// <https://www.postgresql.org/docs/current/protocol-message-formats.html>
    const FORMAT: BackendMessageFormat;

    /// Decode this type from a Backend message in the Postgres protocol.
    ///
    /// The format code and length prefix have already been read and are not at the start of `bytes`.
    fn decode_body(buf: Bytes) -> Result<Self, Error>;
}

两个 trait 都只管消息体,类型字节和长度由外面统一处理。前端一侧是 EncodeMessage:先按 body_size_hint() + 5 用 try_reserve 预留空间(大小离谱时返回错误而不是 panic),推入类型字节,再调 put_length_prefixed:

    fn put_length_prefixed<F>(&mut self, write_contents: F) -> Result<(), crate::Error>
    where
        F: FnOnce(&mut Vec<u8>) -> Result<(), crate::Error>,
    {
        // reserve space to write the prefixed length
        let offset = self.len();
        self.extend(&[0; 4]);

        // write the main body of the message
        let write_result = write_contents(self);

        let size_result = write_result.and_then(|_| {
            let size = self.len() - offset;
            i32::try_from(size)
                .map_err(|_| err_protocol!("message size out of range for protocol: {size}"))
        });

        match size_result {
            Ok(size) => {
                // now calculate the size of what we wrote and set the length value
                self[offset..(offset + 4)].copy_from_slice(&size.to_be_bytes());
                Ok(())
            }
            Err(e) => {
                // Put the buffer back to where it was.
                self.truncate(offset);
                Err(e)
            }
        }
    }

长度在写完消息体之后才知道,所以先占 4 个字节,写完再回填。两个出错分支都把缓冲截回原来的长度:一条编码失败的消息不能留半截在写缓冲里,否则下一次 flush 会把它发出去。

📝NOTE
冷知识:as i32 曾经是一个协议走私漏洞

0.8.1 之前,回填长度那一行是 let size = (self.len() - offset) as i32;。issue #3440(「Potential vulnerability: overflowing and truncating casts」)引了 DEF CON 32 的一个演讲:「encoding a value larger than 4GiB can cause the length prefix in the protocol to overflow, causing the server to interpret the rest of the string as binary protocol commands」。截断后的长度比真实的短,服务端读完这么多字节就以为消息结束了,剩下的字节由攻击者控制,会被当成新的协议消息执行。PR #3441(「fix: audit protocol handling」,首次发布于 0.8.1,对应 RUSTSEC-2024-0363)把各驱动里的截断转换逐个换成带检查的版本,这里就成了上面的 i32::try_from;PR 说明写着「only the Postgres driver actually appears to be exploitable before this patch」。FrontendMessage / BackendMessage 这两个 trait 也是这个 PR 里的一个提交(「refactor(postgres): make better use of traits to improve protocol handling」)引入的;0.8.0 里只有一个后端消息的 MessageFormat 枚举,前端消息各自实现 Encode,自己 push 类型字节、自己写长度(Sync 就是 buf.push(b'S'); buf.extend(&4_i32.to_be_bytes());)。0.8.1 的发布说明要求 Postgres 用户「upgrade ASAP as a possible exploit has been demonstrated」。同一批提交还把 pending_ready_for_query_count 的减一换成了 checked_sub(见扩展查询协议与语句缓存)。

后端一侧,ReceivedMessage::decode 先比对 T::FORMAT 和实际收到的类型,不一致就是协议错误,一致才调 decode_body。recv_expect::<ParseComplete>() 这种写法就是「我认定下一条一定是它」。

写:write_msg 只进缓冲

连接上的读写都经过 PgStream,它包着一个 BufferedSocket<Box<dyn Socket>>。Box<dyn Socket> 统一了 TCP、Unix socket 和 TLS 三种底层,注释说动态分派的开销被缓冲摊薄了:「the buffering amortizes the overhead of both the dynamic function call as well as the syscall」。写消息有两个入口:

    #[inline(always)]
    pub(crate) fn write_msg(&mut self, message: impl FrontendMessage) -> Result<(), Error> {
        self.write(EncodeMessage(message))
    }

    pub(crate) async fn send<T>(&mut self, message: T) -> Result<(), Error>
    where
        T: FrontendMessage,
    {
        self.write_msg(message)?;
        self.flush().await?;
        Ok(())
    }

write_msg 是同步函数,只往 BufferedSocket 的写缓冲里追加字节,不碰 socket。send 是写完立刻 flush,握手阶段一问一答时用它,close 发 Terminate 也用它。查询阶段几乎只用 write_msg:run 连写 Bind、Execute、Close、Sync 四条,最后一次 flush,四条消息走一次系统调用、一个 TCP 包,服务端也一次收齐。抓包里同一行出现的几条消息,就是这样一次 flush 出去的。

写缓冲是一个 Vec<u8> 加两个游标:bytes_written 是写到哪了,bytes_flushed 是发到哪了。flush 循环 socket.write 直到两者相等,然后两个游标一起归零,下一批消息从头写起,Vec 的容量留着复用。初始容量是 8192(DEFAULT_BUF_SIZE,注释说和 tokio、std 的缓冲 I/O 默认值一样)。

flush 可以被取消:一次 socket.write 写了一半,future 被 drop,bytes_flushed 停在中间,剩下的字节还在缓冲里,下次 flush 接着发。这也是为什么 wait_until_ready 第一件事是检查写缓冲是否为空、不空就先 flush:上一次留下的半截消息必须先发完。

读:try_read 和取消安全

读的一侧是 recv_unchecked:

    pub(crate) async fn recv_unchecked(&mut self) -> Result<ReceivedMessage, Error> {
        // NOTE: to not break everything, this should be cancel-safe;
        // DO NOT modify `buf` unless a full message has been read
        self.inner
            .try_read(|buf| {
                // all packets in postgres start with a 5-byte header
                // this header contains the message type and the total length of the message
                let Some(mut header) = buf.get(..5) else {
                    return Ok(ControlFlow::Continue(5));
                };

                let format = BackendMessageFormat::try_from_u8(header.get_u8())?;

                let message_len = header.get_u32() as usize;

                let expected_len = message_len
                    .checked_add(1)
                    // this shouldn't really happen but is mostly a sanity check
                    .ok_or_else(|| {
                        err_protocol!("message_len + 1 overflows usize: {message_len}")
                    })?;

                if buf.len() < expected_len {
                    return Ok(ControlFlow::Continue(expected_len));
                }

                // `buf` SHOULD NOT be modified ABOVE this line

                // pop off the format code since it's not counted in `message_len`
                buf.advance(1);

                // consume the message, including the length prefix
                let mut contents = buf.split_to(message_len).freeze();

                // cut off the length prefix
                contents.advance(4);

                Ok(ControlFlow::Break(ReceivedMessage { format, contents }))
            })
            .await
    }

闭包看着读缓冲回答一个问题:「够一条完整消息了吗?」不够就返回 Continue(还需要的总长度),并且不动缓冲;够了才把这条消息切下来,返回 Break。try_read 负责循环:

    pub async fn try_read<F, R>(&mut self, mut try_read: F) -> Result<R, Error>
    where
        F: FnMut(&mut BytesMut) -> Result<ControlFlow<R, usize>, Error>,
    {
        loop {
            let read_len = match try_read(&mut self.read_buf.read)? {
                ControlFlow::Continue(read_len) => read_len,
                ControlFlow::Break(ret) => return Ok(ret),
            };

            self.read_buf.read(read_len, &mut self.socket).await?;
        }
    }

唯一的 .await 在 read_buf.read 上:从 socket 读,把新数据追加到读缓冲末尾。future 在这里被 drop,最多是读缓冲里多了几个字节,已有的内容一个都没少,下次 recv 从同一个位置重新判断。这就是注释里说的「cancel-safe」。

读缓冲是两块 BytesMut:read 是已读到但还没被消费的字节,available 是空闲容量。从 socket 读进 available,再 unsplit 接到 read 后面;切消息用 split_to(..).freeze(),消息体变成 Bytes,和读缓冲共用同一块分配,不拷贝。DataRow 解码后的 storage 就是这块 Bytes,PgRow 拿着它(见 Row 与 FromRow)。split_to、unsplit 什么时候能零拷贝、什么时候不能,见 bytes 卷的 split 与 freeze。

遇到一条很大的消息(比如一个几 MB 的 bytea),读缓冲会一直长到装得下它,之后也不缩回去。Connection::shrink_buffers() 把两边缓冲缩回 8192;它的文档说这种保留「can give the outward appearance of a memory leak, but is in fact the intended behavior」,同时提醒调得太频繁会让全局分配器来回折腾。

📝NOTE
冷知识:分两次 .await 读的头和体

0.8.2 之前,recv_unchecked 是先 self.inner.read(5).await 读头,再 self.inner.read(size).await 读体。issue #3419(「bug: memory leak in PgStream」)里,一个用 PgListener 的服务偶尔报 unknown message type: '\0',每次内存涨 1.2 GB。维护者的分析是:「A cancellation between these two .awaits in PgStream::recv_unchecked() can leave the stream pointing at non-header data」,下一次 recv 把消息体中间的字节当成头,读出一个随意的长度,「attempts to allocate and read a message of that size」。PgListener::recv 常被放进 select!,取消正好落在两次 .await 之间。PR #3467(「fix cancellation issues with PgListener, PgStream::recv()」,首次发布于 0.8.2)把它改成了现在的 try_read 加闭包,同时修复了 #2956 和 #886。select! 的取消安全问题见 tokio 卷的select! 的取消安全。

recv:哪些消息到不了调用方

recv 是 recv_unchecked 外面的一层循环。服务端有几种消息随时可能到,跟当前在跑的查询无关,recv 在这里统一处理,调用方(run、establish、prepare)只看到和自己有关的那些:

    pub(crate) async fn recv(&mut self) -> Result<ReceivedMessage, Error> {
        loop {
            let message = self.recv_unchecked().await?;

            match message.format {
                BackendMessageFormat::ErrorResponse => {
                    // An error returned from the database server.
                    return Err(message.decode::<PgDatabaseError>()?.into());
                }

                BackendMessageFormat::NotificationResponse => {
                    if let Some(buffer) = &mut self.notifications {
                        let notification: Notification = message.decode()?;
                        let _ = buffer.send(notification).await;

                        continue;
                    }
                }

                BackendMessageFormat::ParameterStatus => {
                    // informs the frontend about the current (initial)
                    // setting of backend parameters

                    let ParameterStatus { name, value } = message.decode()?;
                    // TODO: handle `client_encoding`, `DateStyle` change

                    match name.as_str() {
                        "server_version" => {
                            self.server_version_num = parse_server_version(&value);
                        }
                        _ => {
                            self.parameter_statuses.insert(name, value);
                        }
                    }

                    continue;
                }

                BackendMessageFormat::NoticeResponse => {
                    // do we need this to be more configurable?
                    // if you are reading this comment and think so, open an issue

                    let notice: Notice = message.decode()?;

                    let (log_level, tracing_level) = match notice.severity() {
                        PgSeverity::Fatal | PgSeverity::Panic | PgSeverity::Error => {
                            (Level::Error, tracing::Level::ERROR)
                        }
                        PgSeverity::Warning => (Level::Warn, tracing::Level::WARN),
                        PgSeverity::Notice => (Level::Info, tracing::Level::INFO),
                        PgSeverity::Debug => (Level::Debug, tracing::Level::DEBUG),
                        PgSeverity::Info | PgSeverity::Log => (Level::Trace, tracing::Level::TRACE),
                    };
                    // ...
                    if log_is_enabled {
                        sqlx_core::private_tracing_dynamic_event!(
                            target: "sqlx::postgres::notice",
                            tracing_level,
                            message = notice.message()
                        );
                    }

                    continue;
                }

                _ => {}
            }

            return Ok(message);
        }
    }
消息recv 怎么处理调用方看到什么
ErrorResponse解码成 PgDatabaseError,return ErrErr(Error::Database(..))
NoticeResponse按严重级别打一个 sqlx::postgres::notice 事件什么也看不到
ParameterStatusserver_version 解析成数字,其他存进 parameter_statuses什么也看不到
NotificationResponse有 notifications 通道(PgListener)就送进去有通道时看不到;没有时原样返回
其他不处理原样返回

PgDatabaseError 有哪些字段、怎么判断错误码,见错误。下面逐个看另外三种。

NoticeResponse:只是一条日志

raise notice / raise warning 产生 NoticeResponse。sqlx 不提供任何接口把它交给应用,只打一条 tracing 事件,target 固定是 sqlx::postgres::notice。src/bin/notice_error.rs 的第一段(订阅者用 EnvFilter::from_default_env()):

  println!("--- notice ---");
  let r = sqlx::raw_sql("do $$ begin raise notice 'hello from pg1'; raise warning 'careful'; end $$")
    .execute(&mut conn)
    .await?;
  settle().await;
  println!("rows_affected = {}", r.rows_affected());

RUST_LOG=sqlx::postgres::notice=info 时(M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

--- notice ---
  [conn 1] C->S Query("do $$ begin raise notice 'hello from pg1…")
  [conn 1] S->C NoticeResponse(S=NOTICE C=00000 M=hello from pg1)
  [conn 1] S->C NoticeResponse(S=WARNING C=01000 M=careful)
  [conn 1] S->C CommandComplete("DO"), ReadyForQuery(I)
 INFO sqlx::postgres::notice: hello from pg1
 WARN sqlx::postgres::notice: careful
rows_affected = 0

NOTICE 映射成 INFO,WARNING 映射成 WARN。不设 RUST_LOG 时 EnvFilter 默认只放 ERROR,这两行都不会出现。打日志之前先用 log_enabled! 和 private_tracing_dynamic_enabled! 问一遍这个级别开没开,没开就连消息文本都不取。

ParameterStatus:存下来,几乎不用

握手时服务端发来十几条 ParameterStatus,会话中途 SET 了某些参数(DateStyle、TimeZone、application_name 等)也会再发一条。parameter_statuses 只有一个读者:describe 用它判断对面是不是 CockroachDB、Materialize 或 QuestDB(看有没有 crdb_version、mz_version、questdb_version 这几个键)。TODO: handle client_encoding, DateStyle change 说的是:参数变了,sqlx 不跟着变。

握手时 sqlx 要求 DateStyle=ISO, MDY,因为它要自己解析文本格式的日期。会话里改掉它会怎样(src/bin/datestyle.rs):

  println!("--- set DateStyle ---");
  conn.execute("set DateStyle = 'German'").await?;
  settle().await;
  quiet(&p, true);

  // simple query: text format, parsed by sqlx
  let row = conn.fetch_one("select date '2026-10-24' as d").await?;
  let text = row.try_get::<chrono::NaiveDate, _>("d");
  println!("simple query (text):    {text:?}");

  // prepared statement: binary format, DateStyle does not matter
  let row = sqlx::query("select date '2026-10-24' as d").fetch_one(&mut conn).await?;
  let bin = row.try_get::<chrono::NaiveDate, _>("d");
  println!("prepared query (binary): {bin:?}");
--- set DateStyle ---
  [conn 1] C->S Query("set DateStyle = 'German'")
  [conn 1] S->C CommandComplete("SET"), ParameterStatus(DateStyle=German, DMY), ReadyForQuery(I)
simple query (text):    Err(ColumnDecode { index: "\"d\"", source: ParseError(Invalid) })
prepared query (binary): Ok(2026-10-24)

服务端用一条 ParameterStatus(DateStyle=German, DMY) 通知了变化,sqlx 收下存好,然后照旧按 ISO 格式解析,简单查询返回的文本日期 24.10.2026 解析失败。扩展查询的结果是二进制格式,日期就是一个天数,和 DateStyle 无关。

NotificationResponse:没有监听器时

NotificationResponse 是 LISTEN 之后别的会话(或自己)NOTIFY 产生的,服务端在事务边界之间随时可能发来。notifications 通道只有 PgListener 会设置(见 LISTEN/NOTIFY 与 COPY)。在普通的 PgConnection 上直接 LISTEN(src/bin/stray_notify.rs):

  // LISTEN on a plain PgConnection (not a PgListener), then NOTIFY ourselves
  conn.execute("listen pg1_chan").await?;
  settle().await;
  let r = conn.execute("notify pg1_chan, 'hi'").await;
  settle().await;
  println!("notify: {r:?}");
  let r = sqlx::query_scalar::<_, i32>("select 1").fetch_one(&mut conn).await;
  settle().await;
  println!("next query: {r:?}");
  [conn 1] C->S Query("listen pg1_chan")
  [conn 1] S->C CommandComplete("LISTEN"), ReadyForQuery(I)
  [conn 1] C->S Query("notify pg1_chan, 'hi'")
  [conn 1] S->C CommandComplete("NOTIFY"), NotificationResponse, ReadyForQuery(I)
notify: Err(Protocol("execute: unexpected message: NotificationResponse (sqlx_postgres::connection::executor:366)"))
  [conn 1] C->S Parse(sqlx_s_1, "select 1"), Describe(S, sqlx_s_1), Sync
  ...
next query: Ok(1)

recv 把通知原样交给 run,run 的 match 里没有这一支,落进兜底分支报协议错误。NOTIFY 本身已经在服务端成功提交,客户端却拿到一个 Err。这里是自己通知自己,所以错误落在 NOTIFY 上;如果是另一个会话发的通知,它会落在这条连接上碰巧正在跑的任意一条查询上。要收通知,就用 PgListener。

ErrorResponse 之后,连接还能用

协议规定:扩展查询里一条消息出错,服务端发一个 ErrorResponse,然后丢弃后面收到的所有消息,直到 Sync,再回一个 ReadyForQuery。sqlx 每组消息都以 Sync 结尾,所以一次出错的影响不会越过这一组。客户端这边,recv 在 ErrorResponse 上直接返回 Err,后面那个 ReadyForQuery 还没读;但每次 write_sync 都给 pending_ready_for_query_count 记了一笔,下一次查询开头的 wait_until_ready 会把它读掉(机制见扩展查询协议与语句缓存)。

src/bin/notice_error.rs 后面几段在同一条连接上依次制造几种错误:

  println!("--- error during Execute, after two rows ---");
  let r = sqlx::query_scalar::<_, i32>("select 10 / (3 - g) from generate_series(1, $1) g")
    .bind(3_i32)
    .fetch_all(&mut conn)
    .await;
  settle().await;
  println!("result: {}", r.unwrap_err());

  println!("--- next query on the same connection ---");
  let v: Vec<i32> = sqlx::query_scalar("select 10 / (3 - g) from generate_series(1, $1) g")
    .bind(2_i32)
    .fetch_all(&mut conn)
    .await?;
  settle().await;
  println!("result: {v:?}");

  println!("--- error during Parse ---");
  let r = sqlx::query("selec 1").execute(&mut conn).await;
  settle().await;
  println!("result: {}", r.unwrap_err());
  let v: i32 = sqlx::query_scalar("select 2").fetch_one(&mut conn).await?;
  settle().await;
  println!("result: {v}");

  // ... 一段简单查询,见动脑筋

  println!("--- error inside a transaction ---");
  let mut tx = conn.begin().await?;
  let _ = sqlx::query("select 1 / $1").bind(0_i32).execute(&mut *tx).await;
  let r = sqlx::query("select 1").execute(&mut *tx).await;
  settle().await;
  println!("result: {}", r.unwrap_err());
  tx.rollback().await?;
  settle().await;
--- error during Execute, after two rows ---
  [conn 1] C->S Parse(sqlx_s_1, "select 10 / (3 - g) from generate_series…"), Describe(S, sqlx_s_1), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute(limit 0), Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 4), DataRow(列长 4), ErrorResponse(S=ERROR C=22012 M=division by zero)
  [conn 1] S->C ReadyForQuery(I)
result: error returned from database: division by zero at line 870
--- next query on the same connection ---
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute(limit 0), Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 4), DataRow(列长 4), CommandComplete("SELECT 2"), CloseComplete, ReadyForQuery(I)
result: [5, 10]
--- error during Parse ---
  [conn 1] C->S Parse(sqlx_s_2, "selec 1"), Describe(S, sqlx_s_2), Sync
  [conn 1] S->C ErrorResponse(S=ERROR C=42601 M=syntax error at or near "selec")
  [conn 1] S->C ReadyForQuery(I)
result: error returned from database: syntax error at or near "selec" at line 1244
  [conn 1] C->S Parse(sqlx_s_3, "select 2"), Describe(S, sqlx_s_3), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_3), Execute(limit 0), Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
result: 2
...
--- error inside a transaction ---
  [conn 1] C->S Query("BEGIN")
  [conn 1] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 1] C->S Parse(sqlx_s_4, "select 1 / $1"), Describe(S, sqlx_s_4), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(T)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_4), Execute(limit 0), Close(P, unnamed), Sync
  [conn 1] S->C ErrorResponse(S=ERROR C=22012 M=division by zero)
  [conn 1] S->C ReadyForQuery(E)
  [conn 1] C->S Parse(sqlx_s_5, "select 1"), Describe(S, sqlx_s_5), Sync
  [conn 1] S->C ErrorResponse(S=ERROR C=25P02 M=current transaction is aborted, commands ignored until end of transaction block)
  [conn 1] S->C ReadyForQuery(E)
result: error returned from database: current transaction is aborted, commands ignored until end of transaction block at line 1498
  [conn 1] C->S Query("ROLLBACK")
  [conn 1] S->C CommandComplete("ROLLBACK"), ReadyForQuery(I)
  • 第一段:两行 DataRow 已经到了,第三行除以零。ErrorResponse 之后没有 CommandComplete 和 CloseComplete:Execute 失败后,同一组里的 Close 被服务端跳过,直接回 ReadyForQuery。fetch_all 拿到 Err,前两行被丢掉。
  • 第二段:同一条连接、同一条语句 sqlx_s_1 照常执行。上一段欠下的 ReadyForQuery 在发 Bind 之前就被读掉了,线上看不到任何额外的来回:它在上一段结束时就已经到达客户端,只是还没被消费。
  • 第三段:Parse 出错,ParseComplete 等三条都没有。语句号 sqlx_s_2 在发 Parse 之前就分配掉了,下一条语句是 sqlx_s_3。服务端没有建出 sqlx_s_2,缓存里也没有它,所以跳一个号没有影响。
  • 第四段:事务里出错,ReadyForQuery 的状态字节变成 E(失败的事务块),之后任何语句都报 25P02,直到 ROLLBACK。连接是好的,事务不是。Transaction 怎么处理这种情况见事务。

所以「ErrorResponse 之后连接还能用」有一个前提:消息组以 Sync 结尾,并且客户端记得自己欠了几个 ReadyForQuery。读到一半放弃(stream 被 drop、future 被取消)也是同一个机制收尾,那些情况和它们的副作用在扩展查询协议与语句缓存。

❗IMPORTANT
动脑筋:一条 SQL 里三条语句,第二条出错

还是这条连接,用简单查询协议跑三条语句(raw_sql 的 take_arguments 是 None,一条 Query 消息里可以有多条语句):

  println!("--- simple query, error in the second statement ---");
  let r = sqlx::raw_sql("select 1; select 1 / 0; select 3").fetch_all(&mut conn).await;
  settle().await;
  println!("result: {}", r.unwrap_err());

服务端会不会执行 select 3?fetch_all 能不能拿到第一条语句的那一行?

动脑筋答案

--- simple query, error in the second statement ---
  [conn 1] C->S Query("select 1; select 1 / 0; select 3")
  [conn 1] S->C RowDescription, DataRow(列长 1), CommandComplete("SELECT 1"), ErrorResponse(S=ERROR C=22012 M=division by zero), ReadyForQuery(I)
result: error returned from database: division by zero at line 870
  • select 3 没有执行:没有第三个 RowDescription。简单查询协议里,一条 Query 消息里的多条语句在没有显式 BEGIN 时属于同一个隐式事务,一条出错,后面的全部放弃,紧接着就是 ReadyForQuery。
  • 第一条语句的结果完整到达了客户端:RowDescription、DataRow(列长 1)(文本格式的 "1",一个字节;扩展查询的 int4 是 4 个字节)、CommandComplete。
  • fetch_all 还是只拿到 Err。它是 fetch(..).try_collect(),stream 一产出 Err 就整体失败,已经收集的行被丢掉。要拿到出错之前的结果,得自己用 fetch_many 逐条处理。

小结

  • 每条消息是类型字节加 4 字节长度(不含类型字节),StartupMessage 和 SSLRequest 例外。前后端的类型字节有重复,所以是两个枚举;FrontendMessage / BackendMessage 只管消息体。
  • put_length_prefixed 先占位再回填长度,长度超出 i32 就报错并把缓冲截回去(0.8.1 修的 RUSTSEC-2024-0363)。
  • write_msg 只写缓冲,flush 一次把攒下的消息全发出去;send 是两步合一,只在握手时用。
  • recv_unchecked 通过 try_read 的闭包协议做到取消安全:不够一条完整消息就不动缓冲(0.8.2 起)。消息体是从读缓冲 split_to 出来的 Bytes,不拷贝。
  • recv 截走 NoticeResponse(变成 sqlx::postgres::notice 日志)和 ParameterStatus(存下来,DateStyle 变了也不跟),把 ErrorResponse 变成 Err;没有 PgListener 时 NotificationResponse 会让当前查询报协议错误。
  • 出错后服务端丢弃消息直到 Sync,客户端靠欠下的 ReadyForQuery 计数在下一次查询前收尾,连接照常可用;在事务里出错则要 ROLLBACK。

💬 关联讨论

发起讨论
加载中...