# AGENTS.md
Rust 库 crate:实现 PostgreSQL wire protocol v3,支持 SCRAM-SHA-256/MD5/明文认证。同步 API,通过全局静态 `DB_POOL` 实现连接池。错误类型:`PgsqlError` 枚举。
## 命令
```bash
# 构建
cargo build
# 格式化(检查 / 修复)
cargo fmt --check
cargo fmt
# Lint(完成前必须通过)
cargo clippy --all-targets --all-features -- -D warnings
# 更严格的 lint(可选)
cargo clippy --all-targets --all-features -- -D warnings -W clippy::pedantic
# 测试 — 仅单元测试(无需 PostgreSQL,约 1.4 秒)
cargo test --lib
# 测试 — 按名称子串运行单个测试
cargo test --lib test_name_substring
# 示例: cargo test --lib hostport_out_of_range
# 测试 — 运行单个模块的测试
cargo test --lib config::tests
cargo test --lib pools::tests
# 测试 — 完整测试套件(需要 PostgreSQL 运行在 localhost:5432,用户名=postgres,密码=111111)
cargo test
# 覆盖率
cargo llvm-cov --lib
cargo llvm-cov --lib --show-missing-lines
# 发布
cargo publish --dry-run
```
## 项目结构
```
src/
├── packet.rs # 2818 行 — 协议报文打包/解包(最大文件,含 unsafe)
├── connect.rs # 1237 — TCP 生命周期、启动/认证、查询/执行、参数化查询、带重试的 read()、TCP Keepalive/Nodelay、连接年龄追踪
├── pools.rs # 542 — 连接池(事务隔离+生命周期管理)、ConnectionGuard、SlotGuard、重试逻辑、空闲连接清理
├── format.rs # 583 — OID→JsonValue 类型转换、PG 数组解析
├── lib.rs # 267 — 公开 API:Pgsql 结构体、connect()、pools()
├── error.rs # 199 — PgsqlError 枚举,实现 Display、Error、From
└── config.rs # 79 — Config 结构体、默认值、URL 拼接、端口校验
tests/index.rs — 集成测试(需要运行中的 PostgreSQL)
examples/index.rs — 使用 tokio-postgres,不是本 crate(容易误导)
```
## 改哪里找哪里
| 添加 PG 消息类型 | `packet.rs` | `MessageType::form()` — 按字节标签匹配 |
| 添加字段类型转换 | `format.rs` | `FieldFormat::from_u16()` — 按 (code, type_oid) 匹配 |
| 修改认证流程 | `connect.rs` | `authenticate()`、`scram_auth()`、`md5_auth()` |
| 修改连接池行为 | `pools.rs` | `get_connect()`、`release_conn()` |
| 修改默认值 | `config.rs` | `Config::new()` — localhost:5432, postgres/111111 |
| 添加错误变体 | `error.rs` | `PgsqlError` 枚举 — 需实现 Display + From |
| 修改读取重试 | `connect.rs` | `read()` — MAX_RETRIES、deadline、MAX_MESSAGE_SIZE |
| 参数化查询 | `connect.rs` + `packet.rs` | `query_params()`、`execute_params()`、`pack_query_params()`、`Query::Bind` |
| 参数化查询(便捷) | `connect.rs` | `query_str()`、`execute_str()` — 所有参数非 NULL 的简化版 |
## 模块边界(禁止越界)
| `packet.rs` | 协议编解码 | 网络 I/O、连接池逻辑 |
| `connect.rs` | Socket 生命周期、读写 | 连接池策略 |
| `pools.rs` | 连接池管理、重试 | 协议细节 |
| `format.rs` | OID→JsonValue 转换 | 报文解析 |
| `error.rs` | 仅错误类型 | 业务逻辑 |
| `config.rs` | 配置解析 | 连接逻辑 |
## 代码风格
### 导入顺序:crate → 外部依赖 → std
```rust
use crate::config::Config;
use crate::error::PgsqlError;
use log::{error, warn};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
```
### 错误处理
- 返回 `Result<T, PgsqlError>` — 禁止 `Result<T, String>`
- 映射 I/O 错误:`.map_err(|e| PgsqlError::Connection(e.to_string()))?`
- 新增的解析/协议路径禁止 `.unwrap()` — 使用可失败转换
- Mutex 加锁:`unwrap_or_else(PoisonError::into_inner)`(现有模式)
### 类型
- 协议字段使用显式数值宽度:`u16`、`u32`、`i32` — 禁止用 `usize` 表示线上数据
- `json::JsonValue` 用于配置输入和查询结果行
- `BTreeMap<String, String>` 用于有序键值映射(不用 HashMap)
- `Vec<u8>` 用于原始协议缓冲区
### 命名
- 结构体:`PascalCase` — `Connect`、`Packet`、`ConnectionGuard`、`FieldFormat`
- 枚举变体:`PascalCase` — `PgsqlError::Protocol(String)`、`FieldFormat::BigInt`
- 函数:`snake_case` — `pack_first()`、`get_connect()`、`from_u16()`
- 常量:`SCREAMING_SNAKE` — `MAX_RETRIES`、`MAX_MESSAGE_SIZE`、`DB_POOL`
### 日志
- 仅使用 `log` crate 宏:`debug!`、`warn!`、`error!`
- 库代码中禁止 `println!`
### 注释
- 代码中已有大量中文注释 — 编辑附近代码时保留原有注释
- 文档注释使用中文:`/// 第一包`、`/// 调试`、`/// 账号`
- 如需翻译,整个代码块统一翻译
### 测试
- 每个源文件底部使用 `#[cfg(test)] mod tests` 内联测试
- Mock TCP 服务器:`TcpListener::bind("127.0.0.1:0")` 获取随机端口
- 辅助函数:`pg_msg(tag: u8, payload: &[u8]) -> Vec<u8>` 构建 PG 协议消息
- `#[cfg(test)]` 常量覆盖:MAX_RETRIES=3、MAX_MESSAGE_SIZE=128、deadline=200ms
- `pools.rs` 使用单个 `pools_all_paths` 测试以避免全局 `DB_POOL` 竞态
- 连接池测试中清理 `DB_POOL`:`DB_POOL.lock().unwrap_or_else(|e| e.into_inner()).clear()`
## 反模式(硬性规则)
- **禁止** 新增 `unsafe { String::from_utf8_unchecked }` — 现有技术债,不要扩大
- **禁止** 新增解析/协议路径中使用 `.unwrap()` — 使用可失败转换
- **禁止** 在核心 crate 中引入 async — 仅同步(async 仅限 examples/)
- **禁止** 持有 mutex 期间进行 socket 操作 — 保持锁范围最小
- **禁止** 未经明确要求修改公开 API 签名
- **禁止** 类型错误抑制(`#[allow(...)]` 隐藏真实问题)
## 已知陷阱
- `packet.rs` 大量使用 `unsafe` UTF-8 转换 — 仅做小范围精确修改
- 全局 `DB_POOL` 静态变量 — 所有 `Pools` 实例共享同一个 `VecDeque<Connect>`
- `connect.rs` 的 `read()` 在 `#[cfg(test)]` 下使用缩小的常量 — 生产值不同
- `tests/index.rs` 需要运行中的 PostgreSQL 且使用默认凭据(postgres/111111)
`examples/index.rs` 演示的是 `tokio-postgres`,不是本 crate
## 连接稳定性
### TCP Keepalive + Nodelay(connect.rs)
`set_nodelay(true)` — 禁用 Nagle 算法,减少小包延迟
TCP Keepalive 通过 `socket2` crate 设置:60 秒探测间隔、15 秒重试间隔、3 次重试
防止空闲连接被防火墙/NAT/PostgreSQL 服务端静默断开
### 空闲连接清理(pools.rs)
`MAX_IDLE_SECS = 300`(5 分钟)— 超过此时间的空闲连接在归还时直接丢弃
`release_conn()` 归还连接时检查:peer_valid() → age() → idle_elapsed() → 池容量
`_cleanup_idle_connections()` 批量清理时同时检查 peer 有效性和空闲超时
### pool_max 配置
`config.rs` 默认 `pool_max = 5`,生产环境建议在配置文件中设置 `pool_max = 30`
配置路径:`[br_db.connections.pgsqlname]` 下添加 `pool_max = 30`
### 事务连接池隔离(pools.rs)
事务连接受 `txn_max = pool_max / 3`(最小 1)限制,防止长事务饿死普通查询
`PoolInner` 追踪 `txn_total`(当前事务连接数)和 `txn_max`(事务连接上限)
`acquire_connect(for_transaction=true)` 当 `txn_total >= txn_max && total >= max` 时进入 Wait
`release_transaction_conn()` 同时递减 `total` 和 `txn_total`
`SlotGuard` 感知 `for_transaction`,panic 时自动回滚 `txn_total`
### 重试策略(pools.rs acquire_connect)
最大重试 5 次(非 20 次),最坏情况 ~10s 快速失败
基础退避 200ms(非 1000ms),最大退避 2000ms(非 8000ms)
Condvar 等待超时 2s(非 3s)
测试环境下使用更小的常量(`#[cfg(test)]`)
### 查询层重试策略(br-db pgsql.rs query/execute)
`get_guard()` 快速失败(与 MySQL `try_get_conn` 行为一致)— 连接池层已内部重试,查询层不再重复重试
仅查询/执行时连接断开(Connection/Io/Timeout)才重试一次(200ms 退避)
`PgsqlError::Pool` 不在 `is_retriable_error` 中 — 避免嵌套重试放大(pool 5次 × query 3次 = 15次)
### 健康检查阈值(connect.rs is_valid)
`IDLE_THRESHOLD = 5s`(非 30s)— Cloud SQL 环境更积极检测死连接
peer_addr 检测 TCP 存活,空闲超过 5 秒才发 SELECT 1
测试环境下阈值为 0ms(始终发送 SELECT 1)
### 连接最大生命周期(pools.rs + connect.rs)
`MAX_CONN_LIFETIME_SECS = 1800`(30 分钟)— 超过则丢弃,防止长期连接累积服务端状态
`connect.rs` 新增 `created_at: Instant` 字段 + `age()` 方法
`acquire_connect()` 从池中取出连接时检查年龄,超龄直接丢弃重新循环
`release_conn()` 归还时检查年龄,超龄不入池
## 依赖
| `json` | 配置解析、结果行 |
| `hmac` + `sha2` + `base64` + `rand` | SCRAM-SHA-256 认证 |
| `md5` | MD5 认证 |
| `log` | 日志宏 |
| `socket2` | TCP Keepalive 跨平台设置 |
## 检查清单
编码前:
1. 确认改动属于哪个模块边界
2. 选择最小测试命令:`cargo test --lib`(无需数据库)
完成前:
1. `cargo fmt --check`
2. `cargo clippy --all-targets --all-features -- -D warnings`
3. `cargo test --lib`
4. 确认 diff 聚焦 — 无无关改动