blob_commit.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. package core
  2. import (
  3. "bytes"
  4. "errors"
  5. "io"
  6. "github.com/golang/snappy"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  8. )
  9. // BlobCommit 把 io.Reader 流式写入 blob 链, 一遍跑完即得到行内引用.
  10. //
  11. // 调用约定 (用户必须遵守):
  12. // 1. 必须在 Transaction 上下文中调用, 即 tx := db.BeginTransaction();
  13. // 2. 流可被任意次 Read 消费; 库内部 io.Copy(buf=32KB) 循环, 全程峰值 ~128KB;
  14. // 3. Snappy 压缩按块 (snappy.NewBufferedWriter) 自动分段, 调用方无感;
  15. // 4. 调用方必须保证 compress 与将来的 BlobRead 一致 (库不持久化压缩标志).
  16. //
  17. // 中断保护:
  18. // - 流中途断 (conn closed, EOF 提前, Read 返错): 函数返 err, 此时 NewPage 出来的页
  19. // 已在 BufferPool (脏页不刷盘) + tx.allocated 列表里;
  20. // - 调用方应 tx.Rollback(): 整链随 allocated 走"事务回滚归还空闲链"路径回收;
  21. // - 若 tx.Commit() 已被调用, 则链随提交落盘, 调用方需自管生命周期 (BlobFree).
  22. //
  23. // 内存峰值固定 (与 blob 大小无关):
  24. //
  25. // io.Copy buf 32KB + snappy 内部缓冲 ~64KB + 链适配器临时页 buf 16KB ≈ 128KB.
  26. //
  27. // 容量上限:
  28. //
  29. // BlobChainMax = 256K 页 ≈ 4 GB (原始数据); 超出返 ErrBlobTooLarge.
  30. func (tx *Transaction) BlobCommit(r io.Reader, compress types.FieldCompress) (BlobRef, error) {
  31. if tx == nil || tx.db == nil {
  32. return BlobRef{}, errors.New("BlobCommit: tx is nil")
  33. }
  34. return tx.db.blobCommit(r, compress)
  35. }
  36. // BlobCommit 无事务上下文版的 BlobCommit —— 让 *DB 直接满足 types.BlobCommitter。
  37. //
  38. // 用途: ORM 层 Table.Insert/Update/Delete 把 *core.DB 当 blobc 传给 structToFieldValues,
  39. // 用户不需要手动开事务也能写 Blob 列 (内容自动落链, 行内只存 8B 引用)。
  40. //
  41. // 事务语义: 调用时若恰有活跃事务 (db.currentTxn != nil), 链页经 DB.NewPage 自动登记进
  42. // 该事务的 allocated 列表, Rollback 一样能整链回收; 无事务时链页与普通脏页同命运,
  43. // 随 Commit / Close 落盘。因此对调用方而言语义与 tx.BlobCommit 一致。
  44. //
  45. // 千万不要为了拿这个入口回到"ORM 每行自动 begin-tx + 立即 Commit"的老路: tx.Commit
  46. // 只要缓冲池里有脏页 (单行插入必然有) 就走完整 WAL + fsync, ~37ms/行 —— 逐行插入
  47. // 10000 行要 6 分钟, SetCommitPreset 的防抖/阈值机制被整体绕过 (a56b71d 引入、本次
  48. // 修复的 1500 倍性能回退就是它)。
  49. func (db *DB) BlobCommit(r io.Reader, compress types.FieldCompress) (BlobRef, error) {
  50. if db == nil {
  51. return BlobRef{}, errors.New("BlobCommit: db is nil")
  52. }
  53. return db.blobCommit(r, compress)
  54. }
  55. // blobCommit 是 BlobCommit 的无事务版本: 同样的流式写入, 但不要求调用方持有 Transaction.
  56. //
  57. // 为什么需要它: FieldType_Blob 列的行内只有 8 字节 BlobRef, 而用户往这一列写的是
  58. // []byte / io.Reader 这种"内容本身" (见 Table.blobFieldRef). core 的 InsertRow / UpdateRow
  59. // 在无显式事务时也必须能落地 —— 否则"写一行 Blob 列"就得先手动开事务, 与
  60. // types/blob.go 里"引擎自动走链页"的设计目标背离.
  61. //
  62. // 与 tx.BlobCommit 的唯一差别在失败回收: 有 currentTxn 时新页登记进 tx.allocated, 回滚即回收;
  63. // 无事务时没有 allocated 可回滚 —— 函数返回 err 后, 已分配的页只能由调用方保证不再复用
  64. // (core 的列内联写入在出错时整行写入中止, 这批页成为孤儿, 与无事务下任何写入失败的语义一致).
  65. func (db *DB) blobCommit(r io.Reader, compress types.FieldCompress) (BlobRef, error) {
  66. if db == nil {
  67. return BlobRef{}, errors.New("BlobCommit: db is nil")
  68. }
  69. if !compress.IsValid() {
  70. return BlobRef{}, errors.New("BlobCommit: unknown compress option")
  71. }
  72. if r == nil {
  73. // 与"空流"等价: 给空 blob
  74. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  75. }
  76. // 0. 先探一个字节: 空输入连第一页都不分配 (否则空 blob 会白占一页 —— 提前开页是为了接住
  77. // snappy 的 stream header, 但空输入压根没有 header 要接).
  78. var head [1]byte
  79. n, rerr := r.Read(head[:])
  80. if rerr != nil && rerr != io.EOF {
  81. return BlobRef{}, rerr
  82. }
  83. if n == 0 {
  84. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  85. }
  86. src := io.MultiReader(bytes.NewReader(head[:n]), r)
  87. // 1. 预开第一页 (snappy.Writer 第一次 Write 会先写 stream header, 必须有页接住)
  88. w, err := newBlobChainWriter(db)
  89. if err != nil {
  90. return BlobRef{}, err
  91. }
  92. // 2. io.Copy 链: r -> [snappy?] -> blobChainWriter
  93. // io.Copy 内部 32KB buf, snappy.NewBufferedWriter 内部 ~64KB, 都是固定大小.
  94. var totalRaw int64
  95. if compress == types.CompressSnappy {
  96. sw := snappy.NewBufferedWriter(w)
  97. totalRaw, err = io.Copy(sw, src)
  98. if cerr := sw.Close(); cerr != nil && err == nil {
  99. err = cerr // Close 才能 flush snappy 末尾
  100. }
  101. } else {
  102. totalRaw, err = io.Copy(w, src)
  103. }
  104. if err != nil {
  105. // 流中断 / io 错误: 链已分配但未 Commit, 由 tx.Rollback() 走 allocated 路径回收
  106. return BlobRef{}, err
  107. }
  108. if totalRaw == 0 {
  109. // 有输入却一个字节都没落地 (压缩后为空的极端情况): 把预开的页还回去, 给空引用.
  110. db.deferFreeBlobChain(w.firstPage, w.tailPage, w.chainLen)
  111. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  112. }
  113. if uint64(totalRaw) > uint64(^uint32(0)) {
  114. return BlobRef{}, ErrBlobTooLarge // 单 blob 上限 4GB
  115. }
  116. return BlobRef{
  117. FirstPageID: w.firstPage,
  118. TotalLen: uint32(totalRaw),
  119. }, nil
  120. }