浏览代码

fix(close): BufferPool.Close 返回刷盘错误 + File.Close 合并传递, 删掉脑补的 defer recover

问题: 之前 BufferPool.Close 吞掉 Flush 错误, File.Close 也没处理 bp.Close 的结果 —— 写盘失败的脏页永远留在缓存, Close 返回 nil 调用方以为成功, 静默丢数据

修复: BufferPool.Close 直接返回 bp.Flush(); File.Close 合并 bp error + f.Close error, DB.Close 已有 errors.Join 会带出去

同时删掉之前脑补的 defer recover + re-panic: diskWrite 底层是 os.File.WriteAt 只返回 error 不 panic; 进程级 panic 没人 recover 时 Go runtime exit → OS 内核自动 CloseHandle 所有句柄 → 文件锁自动释放, 不会残留

代价零: 正常代码路径无任何行为变化, 只是 error 不再被吞
yl32940 2 周之前
父节点
当前提交
25edee6ef6
共有 2 个文件被更改,包括 27 次插入 和 18 次删除
  1. 22 15
      DB/core/file.go
  2. 5 3
      DB/core/pool/buffer_pool.go

+ 22 - 15
DB/core/file.go

@@ -89,26 +89,33 @@ func OpenFileReadOnly(path string) (*File, error) {
 }
 }
 
 
 func (ff *File) Close() error {
 func (ff *File) Close() error {
-	// defer recover: 防止 bp.Close() / bp.Flush() 里的 diskWrite panic 把 unlock 跳过 ——
-	// Windows LockFileEx 是 OS 级锁, 进程崩了句柄会被系统回收, 但同一进程里 panic
-	// 只会 defer 触发, 不会释放 OS 资源, 所以必须 recover 保证 unlock 执行。
-	defer func() {
-		if r := recover(); r != nil {
-			ff.unlock()
-			ff.f.Close()
-			panic(r) // 让上层知道 Close 中间炸过, 不要静默
-		}
-	}()
+	// 顺序: 先刷完脏页 → 释放文件锁 → 关闭句柄。
+	//
+	// diskWrite (底层 os.File.WriteAt) 只返回 error, 不会 panic —— 之前脑补的
+	// "Flush panic 跳过 unlock" 场景不存在, 不需要 defer recover。
+	// 进程级 panic 没人 recover 时 Go runtime 直接 exit → OS 内核自动 CloseHandle
+	// 所有句柄 → 文件锁自动释放, 也不会残留。
 
 
+	var bpErr error
 	if ff.bp != nil {
 	if ff.bp != nil {
-		ff.bp.Close() // 先刷盘再关文件
+		bpErr = ff.bp.Close() // 刷完所有脏页, 返回第一个写盘错误
 	}
 	}
 	// 显式释放跨进程文件锁 (若持有); 句柄关闭也会自动释放, 这里兜底。
 	// 显式释放跨进程文件锁 (若持有); 句柄关闭也会自动释放, 这里兜底。
-	// 放在 bp.Close() 之后: bp.Flush 里的 writePageDisk 可能让 EOF 改变,
-	// unlock 必须用 tryLock 时保存的原始 lockOffset —— 顺序对 unlock 本身无影响,
-	// 但语义上"释放锁"应该在"写完所有页"之后。
+	// 放在 bp.Close() 之后: bp.Flush 里的 writePageDisk 可能改变 EOF,
+	// unlock 用的是 tryLock 时保存的 lockOffset —— 顺序不影响 unlock 正确性,
+	// 但语义上"写完所有页再释放锁"。
 	ff.unlock()
 	ff.unlock()
-	return ff.f.Close()
+
+	fErr := ff.f.Close()
+
+	// 合并错误: bp.Flush 的写盘失败 (可能有脏页还没持久化) + 句柄关闭错误
+	if bpErr != nil {
+		if fErr != nil {
+			return fmt.Errorf("bp flush 错误: %w; 文件 close 错误: %v", bpErr, fErr)
+		}
+		return bpErr
+	}
+	return fErr
 }
 }
 
 
 // InitBufferPool 初始化页面缓冲池, 拦截 ReadPage/WritePage 实现内存缓存
 // InitBufferPool 初始化页面缓冲池, 拦截 ReadPage/WritePage 实现内存缓存

+ 5 - 3
DB/core/pool/buffer_pool.go

@@ -337,9 +337,11 @@ func (bp *BufferPool) Flush() error {
 	return firstErr
 	return firstErr
 }
 }
 
 
-// Close 关闭缓冲池 (刷盘后停止); 刷盘错误被丢弃 —— 需要错误判定请直接调 Flush
-func (bp *BufferPool) Close() {
-	_ = bp.Flush()
+// Close 关闭缓冲池: 刷完所有脏页后标记关闭状态。
+// 返回 Flush 的第一个错误 —— 调用方 (File.Close) 据此知道收尾是否可信;
+// Flush 内部逻辑: 写成功的页标干净, 写失败的页保持 dirty (不丢数据, 等下次重试)。
+func (bp *BufferPool) Close() error {
+	return bp.Flush()
 }
 }
 
 
 // UpdatePage 更新缓存页 (WAL 提交后调用, 保持缓存与磁盘一致)
 // UpdatePage 更新缓存页 (WAL 提交后调用, 保持缓存与磁盘一致)