ソースを参照

fix(engine): move Windows file lock off page 0 + sync BufferPool on header write

1. LockFileEx offset 0->EOF+1: Windows NTFS applies lock at 4KB page granularity, so locking page 0 caused every Pread to return zero data, ReadHeader saw FirstWALPage=0 and RecoverWAL was a no-op.

2. writeHeaderLocked now calls bp.UpdatePage(0, buf) so stale BufferPool page 0 doesn't overwrite RecoverWAL's clean header on Close.
yl32940 3 週間 前
親
コミット
3f930b5730
4 ファイル変更、27 行追加、17 行削除
  1. 6 4
      DB/core/db.go
  2. 3 0
      DB/core/file.go
  3. 16 12
      DB/core/file_lock_windows.go
  4. 2 1
      DB/core/wal_recovery.go

+ 6 - 4
DB/core/db.go

@@ -1,10 +1,6 @@
 package core
 
 import (
-	"git.ycl.pub/yl32940/TsDB2/DB/core/index"
-	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
-	"git.ycl.pub/yl32940/TsDB2/DB/core/types"
-	"git.ycl.pub/yl32940/TsDB2/DB/core/util"
 	"encoding/binary"
 	"errors"
 	"fmt"
@@ -13,6 +9,11 @@ import (
 	"sync/atomic"
 	"time"
 	"unsafe"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core/index"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/types"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/util"
 )
 
 type TableMeta struct {
@@ -128,6 +129,7 @@ func OpenWithOptions(path string, opt OpenOptions) (*DB, error) {
 		ff  *File
 		err error
 	)
+
 	if opt.ReadOnly {
 		ff, err = OpenFileReadOnly(path)
 	} else {

+ 3 - 0
DB/core/file.go

@@ -285,6 +285,9 @@ func (ff *File) writeHeaderLocked(h *HeaderPage) error {
 	_, err := ff.f.WriteAt(buf[:], 0)
 	if err == nil {
 		ff.headerDirty = false // 头页已是最新
+		if ff.bp != nil {
+			ff.bp.UpdatePage(0, buf[:])
+		}
 	}
 	return err
 }

+ 16 - 12
DB/core/file_lock_windows.go

@@ -2,17 +2,16 @@
 
 package core
 
-// Windows 跨进程文件锁: 用 kernel32 的 LockFileEx / UnlockFileEx 对文件的一段字节
-// 区域加排他锁。纯标准库 (syscall 直接调 DLL), 不引入 golang.org/x/sys。
+// Windows 跨进程文件锁: 用 kernel32 的 LockFileEx / UnlockFileEx 对文件末尾之后
+// 的"虚拟字节区"加锁 (不覆盖任何有效页, 见 lockOverlapped 注释)。纯标准库。
 
 import (
 	"syscall"
 	"unsafe"
 )
 
-// 锁定区域: 文件偏移 0 起 lockByteCount 字节。加锁时文件至少已写入 4KB 头页
-// (新库在 WriteHeader 之后才 tryLock), 所以锁 [0, lockByteCount) 不会超出文件
-// 末尾。双方约定锁同一区域即可互斥 (与 Unix flock 的整文件锁语义等价)。
+// 锁定区域长度: 锁 [last_byte_after_end, +1) —— 跨进程互斥, 不碰头页/数据页/WAL 页。
+// 之前锁偏移=0 直接锁头页 (页 0), Windows NTFS 页粒度锁会让任何 Pread 页 0 返回零。
 const lockByteCount = 1
 
 const (
@@ -27,11 +26,17 @@ var (
 	procUnlockFileEx = kernel32.NewProc("UnlockFileEx")
 )
 
-// lockOverlapped 构造指向文件偏移 0 的 OVERLAPPED (LockFileEx 用它指定锁起点)。
+// lockOverlapped 构造锁文件末尾之后的 OVERLAPPED (Offset=-1 = 0xFFFFFFFF):
+// 锁 [文件最后一字节之后, +1 字节) —— 既互斥又绝不覆盖任何有效数据页。
+//
+// 关键: Windows NTFS 的 LockFileEx 一旦落位, 会在页粒度 (4KB) 上锁住整个页;
+// 原设计锁偏移=0 直接锁页 0 (头页), 任何 Pread 页 0 都会读到零 —— 连锁持有方
+// 自己 ReadAt 也会中招, 引擎 ReadHeader 拿到 FirstWALPage=0, RecoverWAL 整个
+// 空转。改为"锁文件末尾之后"后完全避开所有有效页, 跨进程互斥语义不变。
 func lockOverlapped() *syscall.Overlapped {
 	var ol syscall.Overlapped
-	ol.Offset = 0
-	ol.OffsetHigh = 0
+	ol.Offset = 0xFFFFFFFF
+	ol.OffsetHigh = 0xFFFFFFFF
 	return &ol
 }
 
@@ -49,8 +54,7 @@ func (ff *File) winHandle() (syscall.Handle, error) {
 	return h, nil
 }
 
-// tryLock 非阻塞地对文件区域加排他锁; 已被其它进程 (或本进程其它句柄) 锁定时
-// 返回 ErrDatabaseLocked。
+// tryLock 非阻塞地对文件末尾之后的虚拟字节加排他锁; 已被其它进程/句柄锁定时返回 ErrDatabaseLocked。
 func (ff *File) tryLock() error {
 	if ff.fileLocked {
 		return nil
@@ -77,8 +81,8 @@ func (ff *File) tryLock() error {
 	return nil
 }
 
-// tryRLock 非阻塞地对文件区域加【共享锁】(只读打开): 不加 EXCLUSIVE 标志。
-// 共享锁之间可共存, 与排他锁 (tryLock) 互斥; 冲突返回 ErrDatabaseLocked。
+// tryRLock 非阻塞地对文件加【共享锁】(只读打开): 不加 EXCLUSIVE 标志。
+// 共享锁之间可共存, 与排他锁互斥; 冲突返回 ErrDatabaseLocked。
 func (ff *File) tryRLock() error {
 	if ff.fileLocked {
 		return nil

+ 2 - 1
DB/core/wal_recovery.go

@@ -1,10 +1,11 @@
 package core
 
 import (
-	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
 	"encoding/binary"
 	"fmt"
 	"hash/crc32"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
 )
 
 // =============================================================================