blob_commit.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package core
  2. import (
  3. "bytes"
  4. "errors"
  5. "io"
  6. "github.com/golang/snappy"
  7. "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 的无事务版本: 同样的流式写入, 但不要求调用方持有 Transaction.
  37. //
  38. // 为什么需要它: FieldType_Blob 列的行内只有 8 字节 BlobRef, 而用户往这一列写的是
  39. // []byte / io.Reader 这种"内容本身" (见 Table.blobFieldRef). core 的 InsertRow / UpdateRow
  40. // 在无显式事务时也必须能落地 —— 否则"写一行 Blob 列"就得先手动开事务, 与
  41. // types/blob.go 里"引擎自动走链页"的设计目标背离.
  42. //
  43. // 与 tx.BlobCommit 的唯一差别在失败回收: 有 currentTxn 时新页登记进 tx.allocated, 回滚即回收;
  44. // 无事务时没有 allocated 可回滚 —— 函数返回 err 后, 已分配的页只能由调用方保证不再复用
  45. // (core 的列内联写入在出错时整行写入中止, 这批页成为孤儿, 与无事务下任何写入失败的语义一致).
  46. func (db *DB) blobCommit(r io.Reader, compress types.FieldCompress) (BlobRef, error) {
  47. if db == nil {
  48. return BlobRef{}, errors.New("BlobCommit: db is nil")
  49. }
  50. if !compress.IsValid() {
  51. return BlobRef{}, errors.New("BlobCommit: unknown compress option")
  52. }
  53. if r == nil {
  54. // 与"空流"等价: 给空 blob
  55. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  56. }
  57. // 0. 先探一个字节: 空输入连第一页都不分配 (否则空 blob 会白占一页 —— 提前开页是为了接住
  58. // snappy 的 stream header, 但空输入压根没有 header 要接).
  59. var head [1]byte
  60. n, rerr := r.Read(head[:])
  61. if rerr != nil && rerr != io.EOF {
  62. return BlobRef{}, rerr
  63. }
  64. if n == 0 {
  65. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  66. }
  67. src := io.MultiReader(bytes.NewReader(head[:n]), r)
  68. // 1. 预开第一页 (snappy.Writer 第一次 Write 会先写 stream header, 必须有页接住)
  69. w, err := newBlobChainWriter(db)
  70. if err != nil {
  71. return BlobRef{}, err
  72. }
  73. // 2. io.Copy 链: r -> [snappy?] -> blobChainWriter
  74. // io.Copy 内部 32KB buf, snappy.NewBufferedWriter 内部 ~64KB, 都是固定大小.
  75. var totalRaw int64
  76. if compress == types.CompressSnappy {
  77. sw := snappy.NewBufferedWriter(w)
  78. totalRaw, err = io.Copy(sw, src)
  79. if cerr := sw.Close(); cerr != nil && err == nil {
  80. err = cerr // Close 才能 flush snappy 末尾
  81. }
  82. } else {
  83. totalRaw, err = io.Copy(w, src)
  84. }
  85. if err != nil {
  86. // 流中断 / io 错误: 链已分配但未 Commit, 由 tx.Rollback() 走 allocated 路径回收
  87. return BlobRef{}, err
  88. }
  89. if totalRaw == 0 {
  90. // 有输入却一个字节都没落地 (压缩后为空的极端情况): 把预开的页还回去, 给空引用.
  91. db.deferFreeBlobChain(w.firstPage, w.tailPage, w.chainLen)
  92. return BlobRef{FirstPageID: 0, TotalLen: 0}, nil
  93. }
  94. if uint64(totalRaw) > uint64(^uint32(0)) {
  95. return BlobRef{}, ErrBlobTooLarge // 单 blob 上限 4GB
  96. }
  97. return BlobRef{
  98. FirstPageID: w.firstPage,
  99. TotalLen: uint32(totalRaw),
  100. }, nil
  101. }