消息与读写缓冲
主线位置:查询主线的最底层。
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 会把它发出去。
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」,同时提醒调得太频繁会让全局分配器来回折腾。
.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 Err | Err(Error::Database(..)) |
NoticeResponse | 按严重级别打一个 sqlx::postgres::notice 事件 | 什么也看不到 |
ParameterStatus | server_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 被取消)也是同一个机制收尾,那些情况和它们的副作用在扩展查询协议与语句缓存。
还是这条连接,用简单查询协议跑三条语句(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。