file.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. package core
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "sync"
  7. "unsafe"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/pool"
  10. )
  11. // File 只负责:打开、关闭、读、写 —— unsafe 全部收敛
  12. type File struct {
  13. f *os.File
  14. bp *pool.BufferPool // 页面缓冲池 (nil = 未启用)
  15. // headerMu 保护 HeaderPage (pageID=0) 的【内存字段】及其持久化。
  16. //
  17. // 头页是整个引擎唯一的全局可变结构: 写入路径要它分配 / 回收页号 (NextFreePage /
  18. // FreePageStart / FreePageEnd / FreePageCount), 提交路径要它发布 / 清空 WAL 链指针
  19. // (FirstWALPage / LastWALPage) 并推进 NextTxnID。两条路径并发时若不串行化:
  20. // - 两个 goroutine 同时读到同一个 NextFreePage 再各自 +1 → 同一个页号发给两方,
  21. // 后写的一方覆盖先写的一方 → 静默丢数据;
  22. // - WriteHeader 在拷贝整张头页时, 另一个 goroutine 正在改字段 → 数据竞争 (崩溃)。
  23. // 所有对 HeaderPage 字段的读写都必须落在 headerMu 内。
  24. //
  25. // 不能把 mutex 放进 HeaderPage 本身: 它按内存布局经 unsafe 直接落盘 (见 WriteHeader /
  26. // marshalHeader), 加字段会改变盘上格式。
  27. headerMu sync.Mutex
  28. // headerDirty 头页脏标记 (headerMu 保护): 自上次头页落盘以来 allocPage 是否
  29. // 又改过头页 (NextFreePage 游标 / 空闲链)。
  30. //
  31. // 旧实现在 allocPage 里每分配【一个】页就同步 pwrite 一次 4K 头页 —— 批量灌数
  32. // 期间上千张新页 (数据页 / 树节点 / 堆页) 就是上千次头页落盘, 实测占灌数 CPU
  33. // 的 ~16%。而这期间数据页本身全在缓冲池里 (未 Commit 根本不落盘), 头页急着
  34. // 持久化毫无收益 —— 崩溃后这批缓冲页本来就丢了, 头页游标回退一点反而自洽
  35. // (那些页号还没被任何盘上数据占用)。
  36. //
  37. // 落盘点收敛到三处:
  38. // - Commit: WAL 打包写头页 (wal.go 的 writeHeaderLocked 链指针收尾);
  39. // - 空闲链变更: realFreePage / recycleChain 本来就写头页;
  40. // - Close: db.Close 先 Commit (写头页) 再刷缓冲池, 兜底用 FlushHeader。
  41. // 前两处之外若有人要把数据页直接写盘, 必须先让头页落盘 (页号 > 盘上
  42. // NextFreePage 的页一旦落盘, 重开时会被二次分配覆盖)。
  43. headerDirty bool
  44. // readOnly 抢救模式 (force-inspect) 标志: 一切落盘写 (数据页 / 头页 /
  45. // 截断) 返回 ErrReadOnly, 读路径不受影响。由 OpenWithOptions 在校验
  46. // 失败但用户显式要求抢救时置位。
  47. readOnly bool
  48. // fileLocked 是否已持有跨进程文件锁 (tryLock 置位, unlock / Close 清除)。
  49. // 仅内存状态, 不落盘。锁用来阻止两个进程同时打开同一个数据库文件
  50. // (sync.Mutex 只在单进程内有效, 跨进程只能靠 OS 文件锁, 见 file_lock_*.go)。
  51. fileLocked bool
  52. // lockOffset tryLock 时锁的起始偏移 (LockFileEx 必须用完全相同的区间解锁;
  53. // 文件 size 在锁持有期间可能因 RecoverWAL / Close.Flush 而变化, 所以
  54. // 解锁时不能再 stat 一次 —— 必须保存 tryLock 时的值)。
  55. lockOffset uint64
  56. }
  57. // ErrReadOnly 抢救模式 (force-inspect) 下所有写操作的返回错误。
  58. var ErrReadOnly = errors.New("force-inspect 只读模式: 写操作被禁用")
  59. // ErrDatabaseLocked 数据库文件已被另一个进程 (持有文件锁) 占用时, Open 返回的错误。
  60. // 非阻塞: 拿不到锁立即返回本错误, 而不是无限等待 —— 调用方可稍后重试或确认是否有
  61. // 其它进程正在使用该文件。
  62. var ErrDatabaseLocked = errors.New("数据库已被另一个进程锁定(占用): 同一时刻只允许一个进程打开")
  63. func OpenFile(path string) (*File, error) {
  64. f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
  65. if err != nil {
  66. return nil, err
  67. }
  68. return &File{f: f}, nil
  69. }
  70. // OpenFileReadOnly 以只读方式打开【已存在】的数据库文件 (不创建、不可写)。
  71. func OpenFileReadOnly(path string) (*File, error) {
  72. f, err := os.Open(path) // os.O_RDONLY
  73. if err != nil {
  74. return nil, err
  75. }
  76. return &File{f: f, readOnly: true}, nil
  77. }
  78. func (ff *File) Close() error {
  79. // 顺序: 先刷完脏页 → 释放文件锁 → 关闭句柄。
  80. //
  81. // diskWrite (底层 os.File.WriteAt) 只返回 error, 不会 panic —— 之前脑补的
  82. // "Flush panic 跳过 unlock" 场景不存在, 不需要 defer recover。
  83. // 进程级 panic 没人 recover 时 Go runtime 直接 exit → OS 内核自动 CloseHandle
  84. // 所有句柄 → 文件锁自动释放, 也不会残留。
  85. var bpErr error
  86. if ff.bp != nil {
  87. bpErr = ff.bp.Close() // 刷完所有脏页, 返回第一个写盘错误
  88. }
  89. // 显式释放跨进程文件锁 (若持有); 句柄关闭也会自动释放, 这里兜底。
  90. // 放在 bp.Close() 之后: bp.Flush 里的 writePageDisk 可能改变 EOF,
  91. // unlock 用的是 tryLock 时保存的 lockOffset —— 顺序不影响 unlock 正确性,
  92. // 但语义上"写完所有页再释放锁"。
  93. ff.unlock()
  94. fErr := ff.f.Close()
  95. // 合并错误: bp.Flush 的写盘失败 (可能有脏页还没持久化) + 句柄关闭错误
  96. if bpErr != nil {
  97. if fErr != nil {
  98. return fmt.Errorf("bp flush 错误: %w; 文件 close 错误: %v", bpErr, fErr)
  99. }
  100. return bpErr
  101. }
  102. return fErr
  103. }
  104. // InitBufferPool 初始化页面缓冲池, 拦截 ReadPage/WritePage 实现内存缓存
  105. // capacity: 最大缓存页数 (建议 10000~50000)
  106. func (ff *File) InitBufferPool(capacity int) {
  107. ff.bp = pool.NewBufferPool(capacity, ff.readPageDisk, ff.writePageDisk)
  108. }
  109. // SetDirtyThreshold 设置脏页阈值, 超过时触发回调 (配合 WAL 自动 Commit)
  110. func (ff *File) SetDirtyThreshold(threshold int, onThreshold func()) {
  111. if ff.bp != nil {
  112. ff.bp.SetDirtyThreshold(threshold, onThreshold)
  113. }
  114. }
  115. // DirtyOverThreshold 脏页数是否已达到阈值 (无缓冲池时 false)。
  116. // beginOp 收尾用它判断"阈值在写操作期间被越过"并补提交。
  117. func (ff *File) DirtyOverThreshold() bool {
  118. return ff.bp != nil && ff.bp.OverThreshold()
  119. }
  120. // SetOnPageWriteCallback 设置页写入回调(用于事务登记被修改的页)
  121. func (ff *File) SetOnPageWriteCallback(callback func(pageID uint32)) {
  122. if ff.bp != nil {
  123. ff.bp.SetOnPageWriteCallback(callback)
  124. }
  125. }
  126. // LockWrites 锁定写入 —— 锁的是"页落盘"(BufferPool.WritePage), 不是写入动作本身。
  127. func (ff *File) LockWrites() {
  128. if ff.bp != nil {
  129. ff.bp.LockWrites()
  130. }
  131. }
  132. // UnlockWrites 解锁写入 (见 LockWrites 的语义说明)
  133. func (ff *File) UnlockWrites() {
  134. if ff.bp != nil {
  135. ff.bp.UnlockWrites()
  136. }
  137. }
  138. // SetBufferPoolCapacity 设置 BufferPool 容量(页数)
  139. func (ff *File) SetBufferPoolCapacity(capacity int) {
  140. if ff.bp != nil {
  141. ff.bp.SetCapacity(capacity)
  142. }
  143. }
  144. // CollectDirty 收集所有脏页快照 (配合 WAL 使用; 页不会因此离开缓存, 见 pool.CollectDirty)
  145. func (ff *File) CollectDirty() []pool.DirtyPage {
  146. if ff.bp != nil {
  147. return ff.bp.CollectDirty()
  148. }
  149. return nil
  150. }
  151. // MarkClean 把这批快照页收成干净页 (提交成功后调用, 与 CollectDirty 配对)
  152. func (ff *File) MarkClean(pages []pool.DirtyPage) {
  153. if ff.bp != nil {
  154. ff.bp.MarkClean(pages)
  155. }
  156. }
  157. // FlushBufferPool 刷盘 (Close / DropTable 等绕过 WAL 的收尾路径使用)。
  158. // 返回第一个写盘错误: 失败的页保持 dirty, 由调用方决定如何处置 (如 Close 上抛,
  159. // DropTable 清缓存前显式丢弃)。
  160. func (ff *File) FlushBufferPool() error {
  161. if ff.bp != nil {
  162. return ff.bp.Flush()
  163. }
  164. return nil
  165. }
  166. // RestoreDirty 提交失败时兜底: 保证这批页在缓存里仍然是脏页 (与 CollectDirty 配对)
  167. func (ff *File) RestoreDirty(pages []pool.DirtyPage) {
  168. if ff.bp != nil {
  169. ff.bp.RestoreDirty(pages)
  170. }
  171. }
  172. // DiscardPages 丢弃指定页的缓存 (不落盘), 供事务 Rollback 回到磁盘状态
  173. func (ff *File) DiscardPages(pageIDs map[uint32]struct{}) {
  174. if ff.bp != nil {
  175. ff.bp.DiscardPages(pageIDs)
  176. }
  177. }
  178. // -----------------------------------------------------------------------------------
  179. // ✅ 动态长度:索引页 — 缓冲池拦截层
  180. // -----------------------------------------------------------------------------------
  181. func (ff *File) WritePage(pageID uint32, buf []byte) error {
  182. if ff.bp != nil {
  183. return ff.bp.WritePage(pageID, buf) // 写缓存, 延迟落盘
  184. }
  185. return ff.writePageDisk(pageID, buf)
  186. }
  187. func (ff *File) ReadPage(pageID uint32, buf []byte) error {
  188. if ff.bp != nil {
  189. return ff.bp.ReadPage(pageID, buf) // 先查缓存
  190. }
  191. return ff.readPageDisk(pageID, buf)
  192. }
  193. // WritePageDirect 直接写盘 (绕过 BufferPool)
  194. //
  195. // 因为绕过了缓冲池, 必须让缓存中该页的旧副本失效 —— 否则后续 ReadPage 会命中
  196. // 与磁盘不一致的陈旧内容。典型场景: 页从空闲链复用时缓存里还留有旧副本, 被当作
  197. // WAL 页直写后该副本已失真; 整链回收若只同步链尾页, 就会漏掉链内这些页。
  198. //
  199. // 失效必须排在【落盘之后】。反过来的话, 从删缓存到写完盘这一小段里, 这一页同样
  200. // "两头都不在": 对一页本会话刚分配、盘上还从没写过的页, 并发读会拿到全零 (文件还
  201. // 没长到那个偏移时直接读出错), 而 btree 把全零页当成 EntryLen==0 的节点, 随后就在
  202. // btPID 里切片越界。先落盘再失效, 最坏也只是读到"马上要被覆盖的旧内容", 不会两头皆空。
  203. func (ff *File) WritePageDirect(pageID uint32, buf []byte) error {
  204. if err := ff.writePageDisk(pageID, buf); err != nil {
  205. return err
  206. }
  207. if ff.bp != nil {
  208. ff.bp.DiscardPage(pageID)
  209. }
  210. return nil
  211. }
  212. // WritePageHome 把页镜像写回它自己的位置 (WAL 提交时的"回填"步骤)。
  213. //
  214. // 与 WritePageDirect 的唯一区别是【不动缓存副本】: 这份镜像是从缓存快照里来的,
  215. // 缓存里那一份的内容只会等于它或新于它, 无差别失效反而会丢掉提交窗口里写入线程刚
  216. // 改出来的新内容 (并发写 + 提交时就是如此)。页的脏/干净状态交给提交方用
  217. // File.MarkClean 按版本号统一收尾 —— 没被改过的转干净, 改过的继续留脏。
  218. func (ff *File) WritePageHome(pageID uint32, buf []byte) error {
  219. return ff.writePageDisk(pageID, buf)
  220. }
  221. // SyncCache 同步缓存页 (WAL 提交后调用, 保持缓存与磁盘一致)
  222. func (ff *File) SyncCache(pageID uint32, data []byte) {
  223. if ff.bp != nil {
  224. ff.bp.UpdatePage(pageID, data)
  225. }
  226. }
  227. // ClearCache 清空所有缓存 (WAL 提交后调用)
  228. func (ff *File) ClearCache() {
  229. if ff.bp != nil {
  230. ff.bp.ClearAll()
  231. }
  232. }
  233. // -----------------------------------------------------------------------------------
  234. // ✅ 底层磁盘 I/O (缓冲池回源)
  235. // -----------------------------------------------------------------------------------
  236. func (ff *File) writePageDisk(pageID uint32, buf []byte) error {
  237. if ff.readOnly {
  238. return ErrReadOnly
  239. }
  240. if pageID == 0 {
  241. // 数据页 ID 从 1 起 (页 0 是头页, 走 writeHeaderLocked)。pageID-1 在
  242. // uint32 下会下溢成 0xFFFFFFFF, int64 转换后偏移 ~70TB —— 写错位置或直接报错
  243. return fmt.Errorf("writePageDisk: 非法数据页号 0 (数据页 ID 从 1 起)")
  244. }
  245. offset := int64(PageSize4K) + int64(pageID-1)*int64(len(buf))
  246. _, err := ff.f.WriteAt(buf, offset)
  247. return err
  248. }
  249. func (ff *File) readPageDisk(pageID uint32, buf []byte) error {
  250. if pageID == 0 {
  251. // 同 writePageDisk: uint32 下溢会让偏移飞到 ~70TB, 返回错误而不是读空气
  252. return fmt.Errorf("readPageDisk: 非法数据页号 0 (数据页 ID 从 1 起)")
  253. }
  254. offset := int64(PageSize4K) + int64(pageID-1)*int64(len(buf))
  255. _, err := ff.f.ReadAt(buf, offset)
  256. return err
  257. }
  258. // -----------------------------------------------------------------------------------
  259. // ✅ 头读写(固定4K)
  260. // -----------------------------------------------------------------------------------
  261. // writeHeaderLocked 把头页 (4K) 落盘。调用方必须已持有 headerMu。
  262. // 与 WriteHeader 的区别就是不重复加锁 —— 供 withHeader 内部使用, 避免自死锁。
  263. //
  264. // 这是头页出盘的【唯一汇聚点】: PrepareWrite (整页 XXH32) 在这里统一调用,
  265. // 任何路径写头页 CRC 都自动最新; 对旧 v11 库 (HeaderCrc==0), 这次写入同时
  266. // 完成"自动生成 CRC"的升级。
  267. func (ff *File) writeHeaderLocked(h *HeaderPage) error {
  268. if ff.readOnly {
  269. return ErrReadOnly
  270. }
  271. h.PrepareWrite()
  272. var buf [PageSize4K]byte
  273. copy(buf[:], (*[PageSize4K]byte)(unsafe.Pointer(h))[:])
  274. _, err := ff.f.WriteAt(buf[:], 0)
  275. if err == nil {
  276. ff.headerDirty = false // 头页已是最新
  277. if ff.bp != nil {
  278. ff.bp.UpdatePage(0, buf[:])
  279. }
  280. }
  281. return err
  282. }
  283. // withHeader 在 headerMu 保护下执行 fn。
  284. //
  285. // fn 内可以安全地读 / 改 HeaderPage 的任何字段; 若还需要把头页落盘, 必须调用
  286. // writeHeaderLocked (不能调用 WriteHeader —— 那会二次获取 headerMu, 直接死锁)。
  287. //
  288. // fn 内【不要】调用会同步触发提交的动作 (典型是经 BufferPool 缓存的 WritePage: 脏页
  289. // 超过阈值时它会在当前 goroutine 里直接调 Commit, 而 Commit 的 WAL 写入又要 headerMu):
  290. // 那会自死锁。需要这种写页请放到锁外, 见 allocPage。
  291. func (ff *File) withHeader(fn func()) {
  292. ff.headerMu.Lock()
  293. defer ff.headerMu.Unlock()
  294. fn()
  295. }
  296. // WriteHeader 线程安全地把头页 (4K) 落盘。
  297. func (ff *File) WriteHeader(h *HeaderPage) error {
  298. ff.headerMu.Lock()
  299. defer ff.headerMu.Unlock()
  300. return ff.writeHeaderLocked(h)
  301. }
  302. // FlushHeader 头页有未落盘的分配游标变更时把它写下去。
  303. //
  304. // 兜底用途: 把缓冲池里的脏页直接刷盘之前必须先调用 (db.Close / 任何绕过
  305. // Commit 的 Flush)—— 否则盘上会出现页号超过头页 NextFreePage 的页, 重开
  306. // 后被二次分配覆盖。Commit 路径自己会写头页, 通常到这里已是干净的。
  307. func (ff *File) FlushHeader(h *HeaderPage) error {
  308. ff.headerMu.Lock()
  309. defer ff.headerMu.Unlock()
  310. if !ff.headerDirty {
  311. return nil
  312. }
  313. return ff.writeHeaderLocked(h)
  314. }
  315. // IsHeaderDirty 头页自上次落盘以来是否有未持久化的真实变更 (当前即分配游标移动)。
  316. // db.Commit 据此决定要不要把头页镜像纳入本笔 WAL —— 无分配的纯改写提交不动头页。
  317. func (ff *File) IsHeaderDirty() bool {
  318. ff.headerMu.Lock()
  319. defer ff.headerMu.Unlock()
  320. return ff.headerDirty
  321. }
  322. // allocPage 线程安全地分配一个页号: 在 headerMu 内摘空闲链 / 递增游标并推进头页,
  323. // 随后把头页落盘。
  324. //
  325. // erase 决定是否把该页擦成空白页 (写缓存, 延迟落盘):
  326. // - 普通写入路径传 true: 复用的空闲页可能残留旧页类型, 必须擦干净, 否则按页号
  327. // 读回来会被当成合法但类型错误的页。
  328. // - WAL 打包路径传 false: WAL 页紧接着就在内存里被整页重造 (16KB 全写), 不需要
  329. // 擦页; 更要紧的是此时正处在 Commit 内部 (持有 commitMu), 而擦页会走
  330. // File.WritePage → BufferPool.WritePage, 脏页超阈值时同步回调 autoCommit →
  331. // 再进一次 Commit → 卡死在已持有的 commitMu 上 (fatal: all goroutines are asleep)。
  332. //
  333. // 为什么 headerMu 只罩住"选号 + 推进头页", 擦页放在锁外: 擦页可能重入提交,
  334. // 而提交的 WAL 打包又要 headerMu —— 锁内擦页同样是自死锁。
  335. //
  336. // 头页不在这里落盘 (只标脏): 分配的页本身只在缓冲池里, 未提交前盘上没有它们;
  337. // 头页游标的持久化收敛到 Commit / 空闲链变更 / Close 三处 (见 headerDirty 注释)。
  338. func (ff *File) allocPage(h *HeaderPage, erase bool) uint32 {
  339. ff.headerMu.Lock()
  340. // writePage 传 nil: 只推进头页游标, 不做任何可能重入提交的写。
  341. pid := h.newPageWithReader(ff.readPageDisk, nil)
  342. ff.headerDirty = true
  343. ff.headerMu.Unlock()
  344. if !erase {
  345. return pid
  346. }
  347. // 锁外擦页: 页头只留自述页号 (Type=0、UseSize=页头长), 其余字节全 0。
  348. clean := make([]byte, PageSize)
  349. page.HdrOf(clean).Init(0, pid)
  350. _ = ff.WritePage(pid, clean)
  351. return pid
  352. }
  353. func (ff *File) ReadHeader() (*HeaderPage, error) {
  354. buf := make([]byte, PageSize4K)
  355. _, err := ff.f.ReadAt(buf, 0)
  356. if err != nil {
  357. return nil, err
  358. }
  359. h := &HeaderPage{}
  360. copy((*[PageSize4K]byte)(unsafe.Pointer(h))[:], buf)
  361. return h, nil
  362. }
  363. func (ff *File) TruncateSize(size int64) error {
  364. if ff.readOnly {
  365. return ErrReadOnly
  366. }
  367. return ff.f.Truncate(size)
  368. }