store.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. package main
  2. import (
  3. "crypto/sha256"
  4. "encoding/binary"
  5. "encoding/hex"
  6. "encoding/json"
  7. "log"
  8. "os"
  9. "path/filepath"
  10. "sync"
  11. "time"
  12. "git.ycl.pub/yl32940/TsDB2/DB"
  13. )
  14. // ===== 审计结论缓存库 =====
  15. //
  16. // 为什么要落盘
  17. // ------------
  18. // 全库审计在千万行级别要十几秒 CPU、数 GB 内存 (见 indexcheck.go 里那几张指纹集),
  19. // 而结论只在库内容真的变了以后才需要重算。把结论存下来, 下次打开同一个库直接取,
  20. // 既省掉一遍全库扫描, 也不用再把中间过程堆在内存里。
  21. //
  22. // 存在哪
  23. // ------
  24. // 被分析库旁边的独立库文件 "<db>.analyzer", 用**引擎自己的 ORM** 组织:
  25. // audit_cache 每个指纹一行 —— 摘要 (判决 / 页账 / 每表每索引统计 / 说明) 的 JSON
  26. // audit_issue 索引内容问题明细, 一行一条 → 可按页取, 不必整份读进内存
  27. // audit_unacc 孤儿页明细, 一行一条
  28. // 明细拆表就是为了分页: 库里出了问题可能有成千上万条, 一次性塞给前端既慢又占内存。
  29. //
  30. // 什么时候算"同一个库"
  31. // --------------------
  32. // 指纹 = sha256(文件大小 + 头部页 4096 字节)。头部里有 ModifyTime / TxnID / FreeStart /
  33. // NextFreePage / TablePage 等字段, 引擎每写一次都会动其中之一, 所以指纹一致基本等价于
  34. // "文件没被改过"; 不一致就老老实实重算 —— 宁可多算一次, 也不能拿旧结论糊弄人。
  35. //
  36. // 容错
  37. // ----
  38. // 缓存是纯粹的加速层。任何一步出错 (文件被占 / 引擎报错 / 结构不兼容) 都只记日志并
  39. // 退回"跑一遍全量审计", 绝不影响结论正确性。
  40. // auditCacheRow 一次审计的摘要。Payload 是 AuditResult 的 JSON (去掉了明细列表,
  41. // 明细在下面两张表里)。
  42. type auditCacheRow struct {
  43. DB.BaseModel
  44. FP string `vrtx:"maxlen:80,index"` // 库指纹
  45. Updated int64 // 写入时间 (Unix 秒)
  46. Payload []byte // 动态列: 存 JSON 本体
  47. }
  48. // auditIssueRow 索引内容问题明细, 供分页查看。
  49. type auditIssueRow struct {
  50. DB.BaseModel
  51. FP string `vrtx:"maxlen:80,index"`
  52. Table string `vrtx:"maxlen:64"`
  53. Index string `vrtx:"maxlen:64"`
  54. IType string `vrtx:"maxlen:24"`
  55. Kind string `vrtx:"maxlen:32"`
  56. Page uint32
  57. At int
  58. Detail string `vrtx:"maxlen:256"`
  59. }
  60. // auditUnaccRow 孤儿页明细 (既不属于任何结构、也不在空闲链上的页)。
  61. type auditUnaccRow struct {
  62. DB.BaseModel
  63. FP string `vrtx:"maxlen:80,index"`
  64. Page uint32
  65. }
  66. var (
  67. storeMu sync.Mutex // 引擎非线程安全 —— 缓存库的所有访问都串行化
  68. cacheFP string // 当前库指纹 (由 openH 设置)
  69. )
  70. // cachePath 缓存库路径。可用 TSDB_ANALYZER_CACHE 指定到别处 (比如非持久盘)。
  71. func cachePath(dbPath string) string {
  72. if v := os.Getenv("TSDB_ANALYZER_CACHE"); v != "" {
  73. return v
  74. }
  75. return dbPath + ".analyzer"
  76. }
  77. // setCacheFP 记录当前库指纹 (供分页查询定位缓存行)。
  78. func setCacheFP(fp string) {
  79. cacheMu.Lock()
  80. cacheFP = fp
  81. cacheMu.Unlock()
  82. }
  83. // fingerprint 当前库的指纹。必须在头部页已加载 (rHdr) 之后调用。
  84. func fingerprint() string {
  85. if dbF == "" || len(hdr) == 0 {
  86. return ""
  87. }
  88. h := sha256.New()
  89. var sz [8]byte
  90. if fi, err := os.Stat(dbF); err == nil {
  91. binary.LittleEndian.PutUint64(sz[:], uint64(fi.Size()))
  92. }
  93. h.Write(sz[:])
  94. h.Write(hdr[:])
  95. return hex.EncodeToString(h.Sum(nil)[:20])
  96. }
  97. // payloadOf 复制一份可落盘的审计结果: 明细列表与空闲页列表都摘出去。
  98. // 注意 Free / Reach 是**指针**, 必须先复制再改, 不能就地清空 —— 那份结果可能还在用。
  99. func payloadOf(a *AuditResult) *AuditResult {
  100. b := *a
  101. b.IdxIssues = nil
  102. if b.Free != nil {
  103. f := *b.Free
  104. f.Pages = nil // 空闲页由 /api/free-pages 现走链取, 不在这份里重复存
  105. b.Free = &f
  106. }
  107. if b.Reach != nil {
  108. r := *b.Reach
  109. r.UnaccountedIDs = nil
  110. b.Reach = &r
  111. }
  112. return &b
  113. }
  114. // cacheOpen 打开缓存库并关掉自动提交 (由我们显式 Commit)。
  115. func cacheOpen() (*DB.DB, error) {
  116. db, err := DB.Open(cachePath(dbF))
  117. if err != nil {
  118. return nil, err
  119. }
  120. db.SetAutoCommitDelay(0)
  121. return db, nil
  122. }
  123. func removeCache() {
  124. p := cachePath(dbF)
  125. _ = os.Remove(p)
  126. _ = os.Remove(p + ".wal")
  127. }
  128. // storeSave 把一份**完整**的审计结论落盘。
  129. // 整库重写: 缓存只服务于一个库路径, 全量重建比"按指纹删旧行"简单得多, 也不会留下
  130. // 历史指纹的垃圾行。
  131. func storeSave(fp string, a *AuditResult) {
  132. if fp == "" || a == nil || a.IdxPending || a.Reach == nil {
  133. return
  134. }
  135. payload, err := json.Marshal(payloadOf(a))
  136. if err != nil {
  137. log.Printf("[cache] 序列化失败: %v", err)
  138. return
  139. }
  140. if err := doSave(fp, payload, a); err != nil {
  141. // 结构不兼容 / 文件损坏: 删掉重建一次, 还不行就放弃缓存。
  142. removeCache()
  143. if err2 := doSave(fp, payload, a); err2 != nil {
  144. log.Printf("[cache] 写入失败, 本次不缓存: %v", err2)
  145. }
  146. }
  147. }
  148. func doSave(fp string, payload []byte, a *AuditResult) error {
  149. storeMu.Lock()
  150. defer storeMu.Unlock()
  151. removeCache() // 全量重建
  152. db, err := cacheOpen()
  153. if err != nil {
  154. return err
  155. }
  156. defer db.Close()
  157. meta, err := DB.UseTable[auditCacheRow](db, "audit_cache")
  158. if err != nil {
  159. return err
  160. }
  161. iss, err := DB.UseTable[auditIssueRow](db, "audit_issue")
  162. if err != nil {
  163. return err
  164. }
  165. una, err := DB.UseTable[auditUnaccRow](db, "audit_unacc")
  166. if err != nil {
  167. return err
  168. }
  169. if err := meta.Insert(&auditCacheRow{FP: fp, Updated: time.Now().Unix(), Payload: payload}); err != nil {
  170. return err
  171. }
  172. for i := range a.IdxIssues {
  173. is := &a.IdxIssues[i]
  174. if err := iss.Insert(&auditIssueRow{
  175. FP: fp, Table: is.Table, Index: is.Index, IType: is.IType, Kind: is.Kind,
  176. Page: is.Page, At: is.At, Detail: clip(is.Detail, 240),
  177. }); err != nil {
  178. return err
  179. }
  180. }
  181. for _, p := range a.Reach.UnaccountedIDs {
  182. if err := una.Insert(&auditUnaccRow{FP: fp, Page: p}); err != nil {
  183. return err
  184. }
  185. }
  186. return db.Commit()
  187. }
  188. // storeLoad 尝试取回同一指纹的审计结论。命中返回 (结果, true)。
  189. // gen 为调用时刻的切库代数 —— 期间用户换库就别往回装了。
  190. func storeLoad(fp string, gen uint64) (*AuditResult, bool) {
  191. if fp == "" {
  192. return nil, false
  193. }
  194. a, err := doLoad(fp)
  195. if err != nil || a == nil {
  196. if err != nil {
  197. log.Printf("[cache] 读取失败, 转为全量分析: %v", err)
  198. }
  199. return nil, false
  200. }
  201. a.IdxPending = false
  202. _ = gen
  203. return a, true
  204. }
  205. func doLoad(fp string) (*AuditResult, error) {
  206. storeMu.Lock()
  207. defer storeMu.Unlock()
  208. if _, err := os.Stat(cachePath(dbF)); err != nil {
  209. return nil, nil // 没有缓存文件 = 未命中, 不是错误
  210. }
  211. db, err := cacheOpen()
  212. if err != nil {
  213. return nil, err
  214. }
  215. defer db.Close()
  216. meta, err := DB.UseTable[auditCacheRow](db, "audit_cache")
  217. if err != nil {
  218. return nil, err
  219. }
  220. rows, err := meta.Where("FP", "=", fp).Find()
  221. if err != nil {
  222. return nil, err
  223. }
  224. if len(rows) == 0 {
  225. return nil, nil
  226. }
  227. a := &AuditResult{}
  228. if err := json.Unmarshal(rows[0].Payload, a); err != nil {
  229. return nil, err
  230. }
  231. if a.Reach == nil {
  232. a.Reach = &ReachResult{ByRole: map[string]int{}, UnaccountedByType: map[string]int{}}
  233. }
  234. if a.Free == nil {
  235. a.Free = &FreeChainInfo{}
  236. }
  237. if iss, err := DB.UseTable[auditIssueRow](db, "audit_issue"); err == nil {
  238. if rs, err := iss.Where("FP", "=", fp).Find(); err == nil {
  239. for _, r := range rs {
  240. a.IdxIssues = append(a.IdxIssues, IndexIssue{
  241. Table: r.Table, Index: r.Index, IType: r.IType, Kind: r.Kind,
  242. Page: r.Page, At: r.At, Detail: r.Detail,
  243. })
  244. }
  245. }
  246. }
  247. if una, err := DB.UseTable[auditUnaccRow](db, "audit_unacc"); err == nil {
  248. if rs, err := una.Where("FP", "=", fp).Find(); err == nil {
  249. for _, r := range rs {
  250. a.Reach.UnaccountedIDs = append(a.Reach.UnaccountedIDs, r.Page)
  251. }
  252. }
  253. }
  254. return a, nil
  255. }
  256. // ===== 明细分页查询 (从缓存库里按页取, 不整份读进内存) =====
  257. // cachePageIssues 分页取索引问题明细。第二个返回值是总数。
  258. func cachePageIssues(offset, limit int) ([]IndexIssue, int, error) {
  259. storeMu.Lock()
  260. defer storeMu.Unlock()
  261. cacheMu.Lock()
  262. fp := cacheFP
  263. cacheMu.Unlock()
  264. if fp == "" {
  265. return nil, 0, os.ErrNotExist
  266. }
  267. db, err := cacheOpen()
  268. if err != nil {
  269. return nil, 0, err
  270. }
  271. defer db.Close()
  272. t, err := DB.UseTable[auditIssueRow](db, "audit_issue")
  273. if err != nil {
  274. return nil, 0, err
  275. }
  276. total, err := t.Where("FP", "=", fp).Count()
  277. if err != nil {
  278. return nil, 0, err
  279. }
  280. rows, err := t.Where("FP", "=", fp).OrderBy("_id", false).Offset(offset).Limit(limit).Find()
  281. if err != nil {
  282. return nil, 0, err
  283. }
  284. out := make([]IndexIssue, 0, len(rows))
  285. for _, r := range rows {
  286. out = append(out, IndexIssue{
  287. Table: r.Table, Index: r.Index, IType: r.IType, Kind: r.Kind,
  288. Page: r.Page, At: r.At, Detail: r.Detail,
  289. })
  290. }
  291. return out, int(total), nil
  292. }
  293. // cachePageUnacc 分页取孤儿页明细。第二个返回值是总数。
  294. func cachePageUnacc(offset, limit int) ([]uint32, int, error) {
  295. storeMu.Lock()
  296. defer storeMu.Unlock()
  297. cacheMu.Lock()
  298. fp := cacheFP
  299. cacheMu.Unlock()
  300. if fp == "" {
  301. return nil, 0, os.ErrNotExist
  302. }
  303. db, err := cacheOpen()
  304. if err != nil {
  305. return nil, 0, err
  306. }
  307. defer db.Close()
  308. t, err := DB.UseTable[auditUnaccRow](db, "audit_unacc")
  309. if err != nil {
  310. return nil, 0, err
  311. }
  312. total, err := t.Where("FP", "=", fp).Count()
  313. if err != nil {
  314. return nil, 0, err
  315. }
  316. rows, err := t.Where("FP", "=", fp).OrderBy("_id", false).Offset(offset).Limit(limit).Find()
  317. if err != nil {
  318. return nil, 0, err
  319. }
  320. out := make([]uint32, 0, len(rows))
  321. for _, r := range rows {
  322. out = append(out, r.Page)
  323. }
  324. return out, int(total), nil
  325. }
  326. // clip 把字符串截到 n 字节以内 (按 rune 边界, 不切出半个汉字)。
  327. func clip(s string, n int) string {
  328. if len(s) <= n {
  329. return s
  330. }
  331. rs := []rune(s)
  332. for len(string(rs)) > n {
  333. rs = rs[:len(rs)-1]
  334. }
  335. return string(rs)
  336. }
  337. // cacheFileInfo 缓存库的位置与大小, 仅供日志/调试。
  338. func cacheFileInfo() (string, int64) {
  339. p := cachePath(dbF)
  340. if dbF == "" {
  341. return "", 0
  342. }
  343. if fi, err := os.Stat(p); err == nil {
  344. return filepath.Clean(p), fi.Size()
  345. }
  346. return filepath.Clean(p), 0
  347. }