main.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. package main
  2. // =============================================================================
  3. // wal_crash_chaos — 随机断电压测
  4. //
  5. // 父进程循环 N 轮:
  6. // 1. 起子进程, 子进程向全新数据库按 "7 行一个显式事务" 循环提交
  7. // (负载为按行号确定性生成的 128 字节 + XXH32 校验和);
  8. // 2. 随机 20~250ms 后强杀子进程 (不做任何清理, 脏页随进程蒸发, 模拟断电);
  9. // 3. 父进程重开数据库 —— WAL 恢复 + 头页 CRC 校验在这一步发生 —— 然后断言:
  10. // - 库能正常打开, 无 panic / 无结构损坏;
  11. // - 已提交前缀 1..K 连续完整, 每行校验和对得上 (无丢页 / 无半条);
  12. // - 半截事务 (K 之后至多 6 行 + 在途) 完全不可见 (提交边界);
  13. // - 表元数据 TotalRows 与实际可见行一致。
  14. //
  15. // 默认 120 轮 (env TSDB_CRASH_ROUNDS 可调), 固定种子 (env TSDB_CRASH_SEED,
  16. // 默认 1) 保证可复现 —— 失败时输出复现命令。任何一轮失败都以非零退出,
  17. // 并把现场数据库留在临时目录路径打印出来。
  18. //
  19. // 这是 WAL 正确性的真实故障注入: 杀点覆盖 提交前 / 提交中(WAL 写链) /
  20. // 提交后(数据页回写) / 两 fsync 之间 的全部时序窗口。
  21. // =============================================================================
  22. import (
  23. "encoding/binary"
  24. "fmt"
  25. "io"
  26. "math/rand"
  27. "os"
  28. "os/exec"
  29. "path/filepath"
  30. "strconv"
  31. "time"
  32. "git.ycl.pub/yl32940/TsDB2/DB/core"
  33. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  34. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  35. )
  36. const (
  37. envChild = "TSDB_CRASH_CHAOS_CHILD"
  38. envDB = "TSDB_CRASH_DB"
  39. envRounds = "TSDB_CRASH_ROUNDS"
  40. envSeed = "TSDB_CRASH_SEED"
  41. payloadLen = 128
  42. batchSize = 7 // 每个显式事务提交的行数; K(已提交前缀) 必须是它的整数倍
  43. probeExtra = 8 // 已提交前缀之后必须不存在的探测行数 (>= 半截事务最大 6 行 + 余量)
  44. )
  45. func main() {
  46. if os.Getenv(envChild) == "1" {
  47. runChild()
  48. return
  49. }
  50. runParent()
  51. }
  52. // =============================================================================
  53. // 确定性负载: 父进程按行号重新生成, 不需要子进程留任何痕迹
  54. // =============================================================================
  55. func payloadFor(id uint64) []byte {
  56. buf := make([]byte, payloadLen)
  57. s := id*0x9E3779B97F4A7C15 + 0xD1B54A32D192ED03
  58. for i := 0; i < payloadLen; i += 8 {
  59. // splitmix64 步进: 纯位运算, 与平台无关
  60. s ^= s >> 12
  61. s ^= s << 25
  62. s ^= s >> 27
  63. binary.LittleEndian.PutUint64(buf[i:], s*0x2545F4914F6CDD1D)
  64. }
  65. return buf
  66. }
  67. func checksumFor(id uint64) uint64 {
  68. return uint64(core.XXH32(payloadFor(id)))
  69. }
  70. // =============================================================================
  71. // 子进程: 建库, 然后无限循环 "插 7 行 → Commit", 直到被强杀
  72. // =============================================================================
  73. func runChild() {
  74. path := os.Getenv(envDB)
  75. db, err := core.Open(path)
  76. if err != nil {
  77. os.Exit(2)
  78. }
  79. db.SetAutoCommitDelay(0) // 关掉防抖: 提交边界完全由显式 Commit 决定
  80. tbl, err := db.CreateTable("t", []types.FieldInfo{
  81. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  82. {FieldType: types.FieldType_Uint64, FieldName: []byte("c")},
  83. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(payloadLen), FieldName: []byte("p")},
  84. })
  85. if err != nil {
  86. os.Exit(2)
  87. }
  88. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  89. os.Exit(2)
  90. }
  91. for i := uint64(1); ; i++ {
  92. if _, err := tbl.InsertRowWithID(i, []types.FieldValue{
  93. {FieldName: []byte("id"), Value: i},
  94. {FieldName: []byte("c"), Value: checksumFor(i)},
  95. {FieldName: []byte("p"), Value: string(payloadFor(i))},
  96. }); err != nil {
  97. os.Exit(2)
  98. }
  99. if i%batchSize == 0 {
  100. if err := db.Commit(); err != nil { // WAL 写链 + fsync ×2 发生在这里
  101. os.Exit(2)
  102. }
  103. // 干净模式: 写满 N 批后正常关闭 (校验压测装置本身: 走 Close 路径必须零损伤)
  104. if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 && int(i/batchSize) == cleanBatches {
  105. if err := db.Close(); err != nil {
  106. os.Exit(2)
  107. }
  108. os.Exit(0)
  109. }
  110. }
  111. }
  112. }
  113. // =============================================================================
  114. // 父进程: 循环 起子进程 → 随机时刻强杀 → 重开校验
  115. // =============================================================================
  116. func runParent() {
  117. rounds := envInt(envRounds, 120)
  118. baseSeed := int64(envInt(envSeed, 1))
  119. rng := rand.New(rand.NewSource(baseSeed))
  120. fmt.Printf("随机断电压测: %d 轮, 种子 %d (复现: %s=<%s> %s=轮数 go run main.go)\n",
  121. rounds, baseSeed, envSeed, envRounds, envSeed)
  122. for r := 1; r <= rounds; r++ {
  123. if err := oneRound(rng); err != nil {
  124. fmt.Printf("❌ 第 %d 轮失败 (种子 %d): %v\n", r, baseSeed, err)
  125. fmt.Printf(" 复现: %s=%d %s=%d go run main.go\n", envSeed, baseSeed, envRounds, r)
  126. os.Exit(1)
  127. }
  128. if r%20 == 0 {
  129. fmt.Printf(" 进度 %d/%d 通过\n", r, rounds)
  130. }
  131. }
  132. fmt.Printf("✅ 随机断电压测通过: %d 轮, 每轮随机时刻强杀子进程后 —— 已提交前缀完整、校验和全对、未提交事务不可见\n", rounds)
  133. }
  134. func oneRound(rng *rand.Rand) error {
  135. dir, err := os.MkdirTemp("", "tsdb-chaos-")
  136. if err != nil {
  137. return err
  138. }
  139. path := filepath.Join(dir, "chaos.db")
  140. exe, err := os.Executable()
  141. if err != nil {
  142. os.RemoveAll(dir)
  143. return err
  144. }
  145. cmd := exec.Command(exe)
  146. cmd.Env = append(os.Environ(), envChild+"=1", envDB+"="+path)
  147. if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 {
  148. // 干净模式: 子进程写满 N 批后自行 Close 退出, 不杀 —— 校验压测装置本身
  149. cmd.Env = append(cmd.Env, "TSDB_CRASH_CLEAN_BATCHES="+strconv.Itoa(cleanBatches))
  150. }
  151. if err := cmd.Start(); err != nil {
  152. os.RemoveAll(dir)
  153. return err
  154. }
  155. if envInt("TSDB_CRASH_CLEAN_BATCHES", 0) > 0 {
  156. if err := cmd.Wait(); err != nil {
  157. os.RemoveAll(dir)
  158. return fmt.Errorf("干净模式子进程异常退出: %v", err)
  159. }
  160. } else {
  161. // 随机存活 20~250ms: 杀点均匀覆盖 提交前 / WAL 写链中 / 页回写中 / 两 fsync 之间
  162. time.Sleep(time.Duration(20+rng.Intn(230)) * time.Millisecond)
  163. _ = cmd.Process.Kill()
  164. _ = cmd.Wait() // 回收句柄, 确保文件锁释放后再重开
  165. }
  166. if _, err := os.Stat(path); os.IsNotExist(err) {
  167. os.RemoveAll(dir)
  168. return nil // 子进程在建文件前被杀, 本轮无对象可验
  169. }
  170. // 先留一份原始现场再校验: core.Open 会触发 WAL 重放 + Close 刷盘,
  171. // 直接开在原件上就把崩溃现场改掉了
  172. evidence := filepath.Join(dir, "chaos_evidence.db")
  173. if err := copyFile(path, evidence); err != nil {
  174. return err
  175. }
  176. if err := verify(path, dir); err != nil {
  177. return err // 现场保留在 dir (chaos.db=重放后, chaos_evidence.db=原始)
  178. }
  179. return os.RemoveAll(dir)
  180. }
  181. func copyFile(src, dst string) error {
  182. in, err := os.Open(src)
  183. if err != nil {
  184. return err
  185. }
  186. defer in.Close()
  187. out, err := os.Create(dst)
  188. if err != nil {
  189. return err
  190. }
  191. defer out.Close()
  192. _, err = io.Copy(out, in)
  193. return err
  194. }
  195. // =============================================================================
  196. // 恢复后校验: 已提交前缀完整 + 校验和 + 未提交不可见 + 元数据一致
  197. // =============================================================================
  198. func verify(path, dir string) error {
  199. db, err := core.Open(path) // RecoverWAL + HeaderCrc 在这里跑
  200. if err != nil {
  201. return fmt.Errorf("崩溃后重开失败: %w (现场: %s)", err, dir)
  202. }
  203. defer db.Close()
  204. tbl, ok := db.FindTable("t")
  205. if !ok {
  206. // 子进程在建表窗口内被杀: 库结构完好即算过, 后续断言无对象
  207. return nil
  208. }
  209. // 已提交前缀: id 从 1 开始连续存在, 第一个洞即是边界 K
  210. var k uint64
  211. for k = 0; ; k++ {
  212. if _, err := tbl.FindByPK(k + 1); err != nil {
  213. break
  214. }
  215. }
  216. if total := tbl.TotalRows(); total != k {
  217. return fmt.Errorf("TotalRows=%d 但实际连续前缀=%d (存在不可达的洞行/幽灵行) (现场: %s)", total, k, dir)
  218. }
  219. // 提交边界: K 之后必须完全不可见 (半截事务 + 在途行)
  220. for id := k + 1; id <= k+probeExtra; id++ {
  221. if _, err := tbl.FindByPK(id); err == nil {
  222. return fmt.Errorf("未提交行 id=%d 在恢复后可见, 提交边界被破坏 (现场: %s)", id, dir)
  223. }
  224. }
  225. // 已提交行逐行校验: 负载长度 + XXH32 校验和, 抓半条/错位/旧值
  226. for id := uint64(1); id <= k; id++ {
  227. vals, err := tbl.FindByPK(id)
  228. if err != nil {
  229. return fmt.Errorf("已提交行 id=%d 读取失败: %v (现场: %s)", id, err, dir)
  230. }
  231. var c uint64
  232. var p []byte
  233. for _, fv := range vals {
  234. switch string(fv.FieldName) {
  235. case "c":
  236. c, _ = fv.Value.(uint64)
  237. case "p":
  238. // core 反序列化字符串返回 []byte (非 string), 两种都接
  239. switch v := fv.Value.(type) {
  240. case string:
  241. p = []byte(v)
  242. case []byte:
  243. p = v
  244. }
  245. }
  246. }
  247. if len(p) != payloadLen || c != checksumFor(id) || uint64(core.XXH32(p)) != c {
  248. return fmt.Errorf("已提交行 id=%d 数据损坏 (len=%d, c=%d, want=%d) (现场: %s)",
  249. id, len(p), c, checksumFor(id), dir)
  250. }
  251. }
  252. return nil
  253. }
  254. func envInt(key string, def int) int {
  255. if v := os.Getenv(key); v != "" {
  256. var n int
  257. if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 {
  258. return n
  259. }
  260. }
  261. return def
  262. }