feat: Go SDK(并顺带修掉两处由其测试暴露的数据竞争) - #257
Merged
Merged
Conversation
此前 GET 对「key 不存在」和「读取失败」一律回 StatusError,客户端无从区分——于是 「查不到」这一最常规的语义无法被正确表达:客户端要么把它当故障重试,要么把真实故障 当成查不到。这也是任何客户端 SDK 无法提供 ErrKeyNotFound 的根因。 新增 proto.StatusNotFound,handleGet 在 errors.Is(err, storage.ErrKeyNotFound) 时回该 状态;分片转发路径的 !found 同样回该状态,而转发本身失败仍回 StatusError。 向后兼容:旧客户端只判 status == StatusOK,未知状态一律视为失败,行为与此前收到 StatusError 时一致,故该取值可安全新增。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Get 此前只在锁内拷贝 active/dirty 的表指针便释放锁,随后无锁遍历跳表;而 Put/Delete 正持写锁改写同一张 active 的节点指针。这是对链式结构的无同步并发访问:读者可能跟到 只连了一半的新节点,读出错值,甚至解引用空指针导致进程崩溃。且它位于最热路径上—— 每一次 GET 与 PUT 都在其中。 之所以长期未被发现:既有测试没有对同一 MemTable 并发做 Get+Put,压测走的是编译好的 二进制、未开 race 检测。本次由新增的 SDK 并发用例(client 包,-race)首次暴露。 改为在锁内完成 active 的查找。只圈住内存查找而不圈住 SSTable 查找:后者要读磁盘, 若一并持锁会让写入停等 I/O。dirty 一经交换即不再被写入(Put 只改 active),故其查找 无需持锁。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add/Remove 在 cm.mu 内写这张 map,而 Len 由 acceptLoop 在每次接受连接时调用(MaxConn 准入判断),二者天然并发。无锁读 map 撞上并发写 map 在 Go 中不只是竞态告警:运行时可能 直接抛出 "concurrent map read and map write" 使进程崩溃。改为持读锁。 同由新增的 SDK 并发用例暴露——此前无测试并发压 accept 路径。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WriteSSTable 全无调用方,且与刚修复的 Get 犯同一个错误:锁内拷贝 active 指针、锁外 collectAllEntry 全表遍历,与并发的 Put 构成数据竞争。既然不可达,直接删除而非修复。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
此前仓库没有 SDK:cmd/ban-cli 是 package main 的交互式演示,无法被引用,且单连接、 无超时、无重试。新增 client 包补齐生产使用所必需的能力: - 连接池:BanNet 是严格请求-响应协议(一条连接收到响应才能发下一帧),故并发只能由多条 连接提供。池以带缓冲 channel 实现,容量即 PoolSize,天然限制客户端侧并发请求数;连接 惰性建立,损坏的连接不放回池中以免残留响应与后续请求串话。 - context:超时/取消经 context 传入并映射为连接 deadline。请求中途的取消同样生效——阻塞 的 read/write 无法被 context 直接打断,故以看门狗把连接 deadline 置为「立即」将其打断。 - 有界重试:服务端过载时专门返回 overloaded 以示可重试,此前无任何客户端利用该设计。SDK 对其指数退避重试,次数受 MaxRetries 与 context 截止时间双重约束;确定性拒绝(键不存在、 被策略丢弃)不重试。 - 哨兵错误:ErrKeyNotFound / ErrOverloaded / ErrDropped / ErrServer / ErrClosed / ErrProtocol,以 errors.Is 判别。 线格式在 SDK 内自行实现而不导入 bannet:后者是服务端实现包(含监听、连接管理、worker 池),不应进入客户端依赖图。代价是两份实现可能漂移,故 wire_compat_test.go 逐字节交叉 校验 SDK 与 bannet.DataPack 的编码结果(含空负载、二进制负载、超长 msgID),并固定帧头 长度与状态码到哨兵错误的映射。 测试对真实服务端而非桩:在进程内起 BanNet + KVServer(standalone,数据落临时目录), 覆盖读写往返、缺失键、删除后读取、空 value 不等于墓碑、连接池并发一致性(-race,串话会 表现为读到别的 key 的值)、池上限、context 取消、不可达地址快速失败、Close 幂等。 另以真实 ban-server 二进制手工验证过一轮(含 16×50 并发)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🐯 BanGD 数据库内核评审模型原始输出片段(调试用) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
新增可引用的 Go SDK。本 PR 最重要的产出不是 SDK 本身,而是它的并发测试暴露出的两处数据竞争——其中一处位于每次 GET/PUT 的最热路径上。
一、两处数据竞争(均为既有缺陷,非本次引入)
1.
MemTable.Get无锁遍历跳表(最热路径)这是对链式结构的无同步并发访问。后果不止是竞态告警:读者可能跟到只连了一半的新节点,读出错值,甚至解引用空指针使进程崩溃。而它位于每一次 GET 与 PUT 的路径上。
为何长期未被发现:既有测试没有对同一
MemTable并发做 Get+Put;压测走的是编译好的二进制、未开 race 检测。本 PR 的 SDK 并发用例是仓库里第一个在-race下并发压同一实例的测试。修复:把 active 的查找收进锁内。只圈内存查找、不圈 SSTable 查找——后者要读磁盘,一并持锁会让写入停等 I/O。
dirty一经交换即不再被写入,故无需持锁。2.
ConnManager.Len无锁读 mapAdd/Remove在锁内写这张 map,而Len由acceptLoop每次接受连接时调用(MaxConn准入判断)。无锁读 map 撞上并发写 map 在 Go 中可能直接抛出concurrent map read and map write使进程崩溃。这正是我在上一轮报告过、建议优先修的那处。3. 顺带删除
WriteSSTable与
Get犯同一个错误(锁外全表遍历),且零调用方。不可达即删除,不修。二、协议:GET 新增
notfound状态此前 GET 对「key 不存在」和「读取失败」一律回
StatusError,客户端无从区分——这是任何 SDK 无法提供ErrKeyNotFound的根因:要么把「查不到」当故障重试,要么把真实故障当成查不到。新增
proto.StatusNotFound。向后兼容:旧客户端只判== StatusOK,未知状态一律视为失败,行为与此前收到StatusError一致。分片转发路径的!found也回该状态,而转发本身失败仍回StatusError。三、SDK
cmd/ban-cli是package main的交互式演示,无法被引用,且单连接、无超时、无重试。新增client包:PoolSize;连接惰性建立;损坏的连接不放回池中,否则残留的半个响应会与后续请求串话。overloaded以示可重试,此前无任何客户端利用该设计。SDK 据此指数退避,受MaxRetries与 context 截止时间双重约束;确定性拒绝(键不存在、被策略丢弃)不重试。ErrKeyNotFound/ErrOverloaded/ErrDropped/ErrServer/ErrClosed/ErrProtocol。线格式自行实现而不导入
bannet:后者是服务端实现包(含监听、连接管理、worker 池),不应进入客户端依赖图。代价是可能漂移,故wire_compat_test.go逐字节交叉校验 SDK 与bannet.DataPack的编码(含空负载、二进制负载、超长 msgID),并固定帧头长度与状态码映射。验证
测试对真实服务端而非桩:进程内起 BanNet + KVServer(standalone,数据落临时目录),覆盖读写往返、缺失键、删除后读取、空 value 不等于墓碑、连接池并发一致性(
-race,串话会表现为读到别的 key 的值)、池上限、context 取消、不可达地址快速失败、Close幂等。另以真实
ban-server二进制手工跑过一轮:Put/Get→ 缺失键 → 删除后读取 → 16×50 并发(池 4 连接)全部通过。go build ./...(含-tags pprof)、go vet ./...、go test -race ./...全绿,race 用例连跑 3 次稳定。🤖 Generated with Claude Code