wal_recovery.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package core
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "hash/crc32"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  7. )
  8. // =============================================================================
  9. // WAL 崩溃恢复
  10. //
  11. // 在 DB.Open 时调用 RecoverWAL:
  12. // 1. 从 FirstWALPage 遍历 WAL 链
  13. // 2. 读首页: TxnID + TotalEntries + Committed
  14. // 3. 收集全部 Entry (按 TotalEntries 计数)
  15. // 4. 每条 Entry 验证 CRC32
  16. // 5. Committed==1 -> 重放所有页镜像到实际位置
  17. // 6. Committed==0 -> 丢弃 (事务未完成)
  18. // 7. 回收所有 WAL 页, 重置 HeaderPage
  19. // =============================================================================
  20. // RecoverWAL 恢复未完成的 WAL 事务
  21. // 每个条目都显式带 OpType:
  22. //
  23. // WALOpFullPage (0): 整页镜像, 直接写回实际页
  24. // Insert/Delete/Update: 行级记录, 用 applyRowToPage 应用到页
  25. func RecoverWAL(ff *File, header *HeaderPage) error {
  26. if header.FirstWALPage == 0 {
  27. return nil // 无待恢复 WAL
  28. }
  29. // 读取首页 (直接读盘: 避免读到 BufferPool 里的陈旧缓存)
  30. firstBuf := make([]byte, PageSize)
  31. if err := ff.readPageDisk(header.FirstWALPage, firstBuf); err != nil {
  32. return err
  33. }
  34. // 解析首页元数据
  35. firstHdr := page.WalHdrOf(firstBuf)
  36. totalEntries := firstHdr.TotalEntries
  37. committed := firstHdr.State
  38. // 收集所有 entry
  39. type recoveryEntry struct {
  40. pageID uint32
  41. data []byte // 整页镜像 (旧格式)
  42. baseLen uint32 // 整页镜像入链长度 (补尾记录按它定位覆盖偏移)
  43. isRow bool // 是否为行级记录 (新格式)
  44. opType byte // 行级操作类型 / WALOpPageRest
  45. storeRow []byte // 行级数据 / 补尾数据
  46. }
  47. var entries []recoveryEntry
  48. var crcFailed bool // 链上任意一条条目 CRC 损坏即置位
  49. // malformed: 条目 dataLen 越出页容量 —— 长度字段本身不可信, 后续条目偏移
  50. // 全部无从解析, 与 CRC 失败一样停止收集。
  51. var malformed bool
  52. // chainBreakErr: 链中途页读失败 / 页类型非法。旧实现只 break, 会把"已提交却
  53. // 残缺"的链当完整事务重放 —— 与 CRC 失败拒绝重放的策略矛盾; 已提交链在循环外
  54. // 据此返回错误并保留链。未提交链维持丢弃语义: 事务从未发布, 残缺无害。
  55. var chainBreakErr error
  56. pid := header.FirstWALPage
  57. remaining := int(totalEntries)
  58. walTail := uint32(0) // 已遍历到的最后一页 (链正常读完时即真实链尾)
  59. walPages := uint32(0)
  60. for pid != 0 && remaining > 0 {
  61. buf := make([]byte, PageSize)
  62. if err := ff.readPageDisk(pid, buf); err != nil {
  63. chainBreakErr = fmt.Errorf("WAL 页 %d 读失败: %w", pid, err)
  64. break
  65. }
  66. hdr := page.HdrOf(buf)
  67. if hdr.Type != page.WALIndex {
  68. chainBreakErr = fmt.Errorf("WAL 页 %d 类型非法 (%d, 期望 %d)", pid, hdr.Type, page.WALIndex)
  69. break
  70. }
  71. walTail = pid
  72. walPages++
  73. // 首页与续页头部等长 —— 条目区起点与 EntryCount 位置一致, 无需按 isFirst 分叉
  74. walHdr := page.WalHdrOf(buf)
  75. off := page.WALPageHdrSize
  76. entryCount := walHdr.EntryCount
  77. for i := uint16(0); i < entryCount && remaining > 0; i++ {
  78. if off+page.WALEntryMeta > PageSize {
  79. break
  80. }
  81. targetPID := binary.LittleEndian.Uint32(buf[off:])
  82. dataLen := binary.LittleEndian.Uint32(buf[off+4:])
  83. checksum := binary.LittleEndian.Uint32(buf[off+8:])
  84. opType := buf[off+12]
  85. // uint64 提升: dataLen 被损坏/恶意写成超大值 (如 0xFFFFFFFF) 时, uint32
  86. // 加法回绕会绕过越界检查, 下一行切片直接 panic。越界即停止收集, 已提交链
  87. // 在循环外按"拒绝部分重放"返回错误。
  88. if uint64(off)+page.WALEntryMeta+uint64(dataLen) > uint64(PageSize) {
  89. malformed = true
  90. remaining = 0
  91. break
  92. }
  93. payload := buf[off+page.WALEntryMeta : off+page.WALEntryMeta+dataLen]
  94. // CRC32 覆盖 OpType + Data
  95. csBuf := make([]byte, 1+dataLen)
  96. csBuf[0] = opType
  97. copy(csBuf[1:], payload)
  98. if crc32.ChecksumIEEE(csBuf) != checksum {
  99. // 校验失败: 后续内容不可信, 停止收集
  100. crcFailed = true
  101. remaining = 0
  102. break
  103. }
  104. if opType == WALOpFullPage {
  105. // 整页镜像: 零填充到整页长度, 保证写回时文件偏移一致
  106. data := make([]byte, PageSize)
  107. copy(data, payload)
  108. entries = append(entries, recoveryEntry{pageID: targetPID, data: data, baseLen: dataLen})
  109. } else if opType == WALOpPageRest {
  110. // 整页镜像的页尾补片: 记录目标页与数据, 重放阶段按 baseLen 定位偏移
  111. rest := make([]byte, dataLen)
  112. copy(rest, payload)
  113. entries = append(entries, recoveryEntry{pageID: targetPID, opType: opType, storeRow: rest})
  114. } else {
  115. storeRow := make([]byte, dataLen)
  116. copy(storeRow, payload)
  117. entries = append(entries, recoveryEntry{
  118. pageID: targetPID,
  119. isRow: true,
  120. opType: opType,
  121. storeRow: storeRow,
  122. })
  123. }
  124. off += page.WALEntryMeta + dataLen
  125. remaining--
  126. }
  127. pid = readNextWalPage(buf)
  128. }
  129. // 已提交链出现 CRC 损坏 / 条目越界 / 链中途断裂: 不能静默部分重放 (撕裂提交)
  130. // 或回收整链 (销毁唯一完好的副本) —— 报告错误让 Open 失败, WAL 链原样保留,
  131. // 由用户决定修复方式 (从备份恢复 / 手工截断 WAL 链放弃这笔事务)。
  132. // 未提交链维持丢弃语义: 事务从未发布, 残缺/损坏的链丢弃回收即可。
  133. if committed == WALStateCommitted {
  134. switch {
  135. case crcFailed:
  136. return fmt.Errorf("WAL 恢复: 已提交事务 (TxnID=%d) 的条目 CRC 校验失败, "+
  137. "拒绝部分重放或回收 —— WAL 链已原样保留, 请修复后重试", header.NextTxnID)
  138. case malformed:
  139. return fmt.Errorf("WAL 恢复: 已提交事务 (TxnID=%d) 含越界条目 (dataLen 超出页容量), "+
  140. "拒绝部分重放或回收 —— WAL 链已原样保留, 请修复后重试", header.NextTxnID)
  141. case chainBreakErr != nil:
  142. return fmt.Errorf("WAL 恢复: 已提交事务 (TxnID=%d) 的 WAL 链中途断裂 (%v), "+
  143. "拒绝部分重放或回收 —— WAL 链已原样保留, 请修复后重试", header.NextTxnID, chainBreakErr)
  144. }
  145. }
  146. // 根据 Committed 标记决定是否重放
  147. if committed == WALStateCommitted && len(entries) > 0 {
  148. // Phase 1: 先处理整页条目 (恢复页结构: B+树节点, IndexMeta, DataPage 等)。
  149. //
  150. // 头页镜像 (pageID==0) 推迟到数据页全部写回之后: 头页记着 WAL 链指针与空闲链,
  151. // 若先写头页、中途断电, 会出现"头页说 WAL 已清空、数据页却还是旧内容"的窗口
  152. // (与提交路径步骤 4/5 钉死的顺序同一道理)。当前提交端头页恒为最后一条
  153. // RecordPage, 本不会先写 —— 恢复侧不依赖这个巧合。
  154. var headerImage []byte
  155. covered := make(map[uint32]uint32, len(entries)) // pageID → 已写回的字节数 (补尾定位用)
  156. for _, e := range entries {
  157. if e.isRow {
  158. continue
  159. }
  160. if e.pageID == 0 {
  161. headerImage = e.data // RecordPage 已按页去重, 至多一条头页镜像
  162. continue
  163. }
  164. if e.opType == WALOpPageRest {
  165. // 页尾补片: Data = [覆盖偏移 u32][页尾原文]。必须跟在同页整页镜像之后
  166. // (写端 packEntries 保证); 缺前置镜像 / 偏移越界都说明链被篡改或损坏,
  167. // 按拒绝部分重放返回错误。
  168. if _, ok := covered[e.pageID]; !ok {
  169. return fmt.Errorf("WAL 恢复: 已提交事务含孤儿补尾记录 (页 %d 缺少前置整页镜像), "+
  170. "拒绝部分重放 —— WAL 链已原样保留, 请修复后重试", e.pageID)
  171. }
  172. if len(e.storeRow) < 4 {
  173. return fmt.Errorf("WAL 恢复: 已提交事务的补尾记录过短 (页 %d, %d 字节), "+
  174. "拒绝部分重放 —— WAL 链已原样保留, 请修复后重试", e.pageID, len(e.storeRow))
  175. }
  176. off := binary.LittleEndian.Uint32(e.storeRow[:4])
  177. rest := e.storeRow[4:]
  178. if off+uint32(len(rest)) > PageSize {
  179. return fmt.Errorf("WAL 恢复: 已提交事务的补尾记录越界 (页 %d: %d+%d > %d), "+
  180. "拒绝部分重放 —— WAL 链已原样保留, 请修复后重试", e.pageID, off, len(rest), PageSize)
  181. }
  182. restBuf := make([]byte, PageSize)
  183. if err := ff.readPageDisk(e.pageID, restBuf); err != nil {
  184. return err
  185. }
  186. copy(restBuf[off:], rest)
  187. if err := ff.WritePageDirect(e.pageID, restBuf); err != nil {
  188. return err
  189. }
  190. continue
  191. }
  192. if err := ff.WritePageDirect(e.pageID, e.data); err != nil {
  193. return err
  194. }
  195. covered[e.pageID] = e.baseLen
  196. }
  197. if headerImage != nil {
  198. if err := recoverHeader(ff, headerImage); err != nil {
  199. return err
  200. }
  201. }
  202. // Phase 2: 处理行级条目 (用 applyRowToPage 应用到 DataPage)
  203. //
  204. // 去重: 若同一页已有整页镜像 (Phase 1 已完整恢复), 则跳过它的行级条目。
  205. // 否则整页镜像里的新行 + 行级 INSERT 会被应用两次, 造成行重复。
  206. fullPageApplied := make(map[uint32]bool, len(entries))
  207. for _, e := range entries {
  208. if !e.isRow {
  209. fullPageApplied[e.pageID] = true
  210. }
  211. }
  212. for _, e := range entries {
  213. if !e.isRow || fullPageApplied[e.pageID] {
  214. continue
  215. }
  216. pageBuf := make([]byte, PageSize)
  217. if err := ff.readPageDisk(e.pageID, pageBuf); err != nil {
  218. continue // 页不存在, 跳过
  219. }
  220. applyRowToPage(pageBuf, e.opType, e.storeRow)
  221. if err := ff.WritePageDirect(e.pageID, pageBuf); err != nil {
  222. return err
  223. }
  224. }
  225. }
  226. // committed != Committed: 事务未完成, 丢弃所有 entry
  227. // 回收整条 WAL 链
  228. //
  229. // 正常路径: 上面的收集循环正好把链读完 (pid == 0), 此时 walTail / walPages 即真实
  230. // 链尾 / 链长 —— 一次拼接 O(1) 还链, 与链长无关。
  231. // 异常路径 (CRC 失败或页类型不符而提前 break, pid != 0): 真实链尾未知, 退化为逐页回收,
  232. // 既不会漏页, 也避免把"半截链"拼进空闲链 —— 那截链尾的 NextPage 仍指向后继页, 会让
  233. // 空闲链遍历窜进未回收的 WAL 页, 造成链错乱。
  234. if pid == 0 && walPages > 0 {
  235. if err := recycleChainWithCache(ff, header, header.FirstWALPage, walTail, walPages); err != nil {
  236. return err
  237. }
  238. } else {
  239. walPID := header.FirstWALPage
  240. for walPID != 0 {
  241. buf := make([]byte, PageSize)
  242. if err := ff.readPageDisk(walPID, buf); err != nil {
  243. break
  244. }
  245. nextPID := readNextWalPage(buf)
  246. header.recyclePage(walPID, ff.WritePageDirect)
  247. walPID = nextPID
  248. }
  249. }
  250. ff.withHeader(func() {
  251. header.FirstWALPage = 0
  252. header.LastWALPage = 0
  253. })
  254. if err := ff.WriteHeader(header); err != nil {
  255. return err
  256. }
  257. // 恢复收尾 fsync: 保证已重放的真实页与"WAL 已清空"的 Header 一起落盘,
  258. // 否则恢复过程本身再断电会重复重放
  259. return ff.sync()
  260. }
  261. // readNextWalPage 读取 WAL 页的链指针 (统一存放在 PageHdr.NextPage, 与空闲链同字段)
  262. func readNextWalPage(buf []byte) uint32 {
  263. return page.HdrOf(buf).NextPage
  264. }
  265. // recoverHeader 恢复 HeaderPage —— WAL 整页镜像重放阶段最后一步, 把头页写回磁盘。
  266. //
  267. // 关键: 写盘后必须同步 BufferPool 页 0 缓存。本函数用裸 WriteAt(0) 绕过 BufferPool,
  268. // 如果不调 bp.UpdatePage(0, ...), Close 时 bp.Flush 会把 BufferPool 里陈旧的头页
  269. // 缓存写回去 —— 刚好覆盖掉 recoverHeader 刚落盘的干净头页 (和 writeHeaderLocked 之前
  270. // 犯的是同一个 bug, 之前 grep 只扫到了 file.go 的两个 WriteAt(0), 漏了这里)。
  271. func recoverHeader(ff *File, data []byte) error {
  272. if ff.bp != nil {
  273. defer func() {
  274. if len(data) >= PageSize4K {
  275. ff.bp.UpdatePage(0, data[:PageSize4K])
  276. }
  277. }()
  278. }
  279. if len(data) < PageSize4K {
  280. _, err := ff.f.WriteAt(data, 0)
  281. return err
  282. }
  283. _, err := ff.f.WriteAt(data[:PageSize4K], 0)
  284. return err
  285. }
  286. // healOrphanWALPages 孤儿 WAL 链自愈。
  287. //
  288. // 背景: 正常提交里 WAL 页写入与头页指针发布是同句柄程序序, kill 拷出的文件
  289. // 要么指针指向完整链 (RecoverWAL 重放), 要么链根本没写过 —— 但直写路径/旧版本
  290. // 引擎可能留下"头页说没有 WAL、分配区间里却躺着类型=WALIndex 的链页"的中间态,
  291. // 这些页不在空闲链上, 只能永久泄漏 (表现为 Analyzer 的 unaccounted/WAL 残留)。
  292. //
  293. // 判定刻意保守, 宁漏勿错:
  294. // - 只在 FirstWALPage==0 (没有待恢复事务) 时动手;
  295. // - 先实走空闲链, 必须与 header.FreePageCount 完全一致 (不成环、不越界、计数
  296. // 相符) —— 这保证"不在空闲链上"这个判定本身可信, 不会把"其实在空闲链上、
  297. // 只是头页版本错位走不到"的页二次回收 (二次回收会弄断空闲链);
  298. // - 链首 = TotalEntries>0 的 WAL 页 (续页该字段恒 0), 整链必须全部由
  299. // WALIndex 页组成、且不与空闲链/分配区间相交, 否则整条放弃;
  300. // - 回收不重放镜像: 若链属已提交事务, 其数据页回写 (wal.Commit 步骤 3) 先于
  301. // 指针清零 (步骤 5) 落盘, 镜像已在盘上, 重放只是幂等覆盖; 若属未提交事务,
  302. // 丢弃才是正确语义。统一 recycleChainWithCache 整条还链。
  303. func healOrphanWALPages(ff *File, header *HeaderPage) error {
  304. if header.FirstWALPage != 0 {
  305. return nil // 有待恢复事务, 归 RecoverWAL 全权处理
  306. }
  307. // 空闲链自洽性: 实走长度必须与声明值一致
  308. freeSet := make(map[uint32]bool, header.FreePageCount)
  309. for pid := header.FreePageStart; pid != 0; {
  310. if freeSet[pid] {
  311. return nil // 成环: 空闲链已不可信, 放弃自愈
  312. }
  313. freeSet[pid] = true
  314. buf := make([]byte, PageSize)
  315. if err := ff.readPageDisk(pid, buf); err != nil {
  316. return nil // 链中途读失败: 同样放弃
  317. }
  318. pid = page.HdrOf(buf).NextPage
  319. }
  320. if uint32(len(freeSet)) != header.FreePageCount {
  321. return nil // 计数不符: 头页与链内容版本错位, freeSet 不可信
  322. }
  323. // 分配区间 = [2, NextFreePage), 与文件实际页数取交集兜底
  324. fi, err := ff.f.Stat()
  325. if err != nil {
  326. return nil
  327. }
  328. total := (fi.Size() - int64(PageSize4K)) / int64(PageSize)
  329. limit := int64(header.NextFreePage)
  330. if limit <= 2 || limit > total+1 {
  331. limit = total + 1 // 游标不可信时用文件大小
  332. }
  333. buf := make([]byte, PageSize)
  334. for p := uint32(2); int64(p) < limit; p++ {
  335. if freeSet[p] {
  336. continue
  337. }
  338. if err := ff.readPageDisk(p, buf); err != nil {
  339. continue
  340. }
  341. if page.HdrOf(buf).Type != page.WALIndex {
  342. continue
  343. }
  344. if page.WalHdrOf(buf).TotalEntries == 0 {
  345. continue // 续页/游离页, 跟着链首一并回收
  346. }
  347. // 找到一个链首: 先走完整链, 校验全部由 WAL 页组成且不越界
  348. // (cnt 上限防循环链: 链页互不相交, 正常不会到达 limit)
  349. head, tail, cnt := p, p, uint32(0)
  350. ok := true
  351. for q := head; q != 0; {
  352. if freeSet[q] || int64(q) >= limit || int64(cnt) >= limit {
  353. ok = false // 与空闲链/分配区间相交, 或链成环: 不是完整孤儿链
  354. break
  355. }
  356. tail = q
  357. if err := ff.readPageDisk(q, buf); err != nil {
  358. ok = false
  359. break
  360. }
  361. if page.HdrOf(buf).Type != page.WALIndex {
  362. ok = false
  363. break
  364. }
  365. cnt++
  366. q = page.HdrOf(buf).NextPage
  367. }
  368. if !ok || cnt == 0 {
  369. continue
  370. }
  371. var recErr error
  372. ff.withHeader(func() {
  373. recErr = recycleChainWithCache(ff, header, head, tail, cnt)
  374. if recErr == nil {
  375. recErr = ff.writeHeaderLocked(header)
  376. }
  377. })
  378. if recErr != nil {
  379. return fmt.Errorf("孤儿 WAL 链回收失败 (head=%d, %d 页): %w", head, cnt, recErr)
  380. }
  381. }
  382. return ff.sync()
  383. }