github.com/TeaOSLab/EdgeNode@v1.3.8/internal/caches/list_file_db_sqlite.go (about) 1 // Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. 2 3 package caches 4 5 import ( 6 "errors" 7 "fmt" 8 teaconst "github.com/TeaOSLab/EdgeNode/internal/const" 9 "github.com/TeaOSLab/EdgeNode/internal/remotelogs" 10 "github.com/TeaOSLab/EdgeNode/internal/utils/dbs" 11 "github.com/TeaOSLab/EdgeNode/internal/utils/fasttime" 12 fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs" 13 memutils "github.com/TeaOSLab/EdgeNode/internal/utils/mem" 14 "github.com/iwind/TeaGo/logs" 15 "github.com/iwind/TeaGo/types" 16 "net" 17 "net/url" 18 "os" 19 "runtime" 20 "strings" 21 "time" 22 ) 23 24 type SQLiteFileListDB struct { 25 dbPath string 26 27 readDB *dbs.DB 28 writeDB *dbs.DB 29 30 hashMap *SQLiteFileListHashMap 31 32 itemsTableName string 33 34 isClosed bool // 是否已关闭 35 isReady bool // 是否已完成初始化 36 hashMapIsLoaded bool // Hash是否已加载 37 38 // cacheItems 39 existsByHashStmt *dbs.Stmt // 根据hash检查是否存在 40 41 insertStmt *dbs.Stmt // 写入数据 42 insertSQL string 43 44 selectByHashStmt *dbs.Stmt // 使用hash查询数据 45 46 selectHashListStmt *dbs.Stmt 47 48 deleteByHashStmt *dbs.Stmt // 根据hash删除数据 49 deleteByHashSQL string 50 51 statStmt *dbs.Stmt // 统计 52 purgeStmt *dbs.Stmt // 清理 53 deleteAllStmt *dbs.Stmt // 删除所有数据 54 listOlderItemsStmt *dbs.Stmt // 读取较早存储的缓存 55 } 56 57 func NewSQLiteFileListDB() *SQLiteFileListDB { 58 return &SQLiteFileListDB{ 59 hashMap: NewSQLiteFileListHashMap(), 60 } 61 } 62 63 func (this *SQLiteFileListDB) Open(dbPath string) error { 64 this.dbPath = dbPath 65 66 // 动态调整Cache值 67 var cacheSize = 512 68 var memoryGB = memutils.SystemMemoryGB() 69 if memoryGB >= 1 { 70 cacheSize = 256 * memoryGB 71 } 72 73 // write db 74 // 这里不能加 EXCLUSIVE 锁,不然异步事务可能会失败 75 writeDB, err := dbs.OpenWriter("file:" + dbPath + "?cache=private&mode=rwc&_journal_mode=WAL&_sync=" + dbs.SyncMode + "&_cache_size=" + types.String(cacheSize) + "&_secure_delete=FAST") 76 if err != nil { 77 return fmt.Errorf("open write database failed: %w", err) 78 } 79 80 writeDB.SetMaxOpenConns(1) 81 82 this.writeDB = writeDB 83 84 // TODO 耗时过长,暂时不整理数据库 85 // TODO 需要根据行数来判断是否VACUUM 86 // TODO 注意VACUUM反而可能让数据库文件变大 87 /**_, err = db.Exec("VACUUM") 88 if err != nil { 89 return err 90 }**/ 91 92 // 检查是否损坏 93 // TODO 暂时屏蔽,因为用时过长 94 95 var recoverEnv, _ = os.LookupEnv("EdgeRecover") 96 if len(recoverEnv) > 0 && this.shouldRecover() { 97 for _, indexName := range []string{"staleAt", "hash"} { 98 _, _ = this.writeDB.Exec(`REINDEX "` + indexName + `"`) 99 } 100 } 101 102 if teaconst.EnableDBStat { 103 this.writeDB.EnableStat(true) 104 } 105 106 // read db 107 readDB, err := dbs.OpenReader("file:" + dbPath + "?cache=private&mode=ro&_journal_mode=WAL&_sync=OFF&_cache_size=" + types.String(cacheSize)) 108 if err != nil { 109 return fmt.Errorf("open read database failed: %w", err) 110 } 111 112 readDB.SetMaxOpenConns(runtime.NumCPU()) 113 114 this.readDB = readDB 115 116 if teaconst.EnableDBStat { 117 this.readDB.EnableStat(true) 118 } 119 120 return nil 121 } 122 123 func (this *SQLiteFileListDB) Init() error { 124 this.itemsTableName = "cacheItems" 125 126 // 创建 127 var err = this.initTables(1) 128 if err != nil { 129 return fmt.Errorf("init tables failed: %w", err) 130 } 131 132 // 常用语句 133 this.existsByHashStmt, err = this.readDB.Prepare(`SELECT "expiredAt" FROM "` + this.itemsTableName + `" INDEXED BY "hash" WHERE "hash"=? AND expiredAt>? LIMIT 1`) 134 if err != nil { 135 return err 136 } 137 138 this.insertSQL = `INSERT INTO "` + this.itemsTableName + `" ("hash", "key", "headerSize", "bodySize", "metaSize", "expiredAt", "staleAt", "host", "serverId", "createdAt") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` 139 this.insertStmt, err = this.writeDB.Prepare(this.insertSQL) 140 if err != nil { 141 return err 142 } 143 144 this.selectByHashStmt, err = this.readDB.Prepare(`SELECT "key", "headerSize", "bodySize", "metaSize", "expiredAt" FROM "` + this.itemsTableName + `" WHERE "hash"=? LIMIT 1`) 145 if err != nil { 146 return err 147 } 148 149 this.selectHashListStmt, err = this.readDB.Prepare(`SELECT "id", "hash" FROM "` + this.itemsTableName + `" WHERE id>? ORDER BY id ASC LIMIT 2000`) 150 if err != nil { 151 return err 152 } 153 154 this.deleteByHashSQL = `DELETE FROM "` + this.itemsTableName + `" WHERE "hash"=?` 155 this.deleteByHashStmt, err = this.writeDB.Prepare(this.deleteByHashSQL) 156 if err != nil { 157 return err 158 } 159 160 this.statStmt, err = this.readDB.Prepare(`SELECT COUNT(*), IFNULL(SUM(headerSize+bodySize+metaSize), 0), IFNULL(SUM(headerSize+bodySize), 0) FROM "` + this.itemsTableName + `"`) 161 if err != nil { 162 return err 163 } 164 165 this.purgeStmt, err = this.readDB.Prepare(`SELECT "hash" FROM "` + this.itemsTableName + `" WHERE staleAt<=? LIMIT ?`) 166 if err != nil { 167 return err 168 } 169 170 this.deleteAllStmt, err = this.writeDB.Prepare(`DELETE FROM "` + this.itemsTableName + `"`) 171 if err != nil { 172 return err 173 } 174 175 this.listOlderItemsStmt, err = this.readDB.Prepare(`SELECT "hash" FROM "` + this.itemsTableName + `" ORDER BY "id" ASC LIMIT ?`) 176 if err != nil { 177 return err 178 } 179 180 this.isReady = true 181 182 // 加载HashMap 183 go this.loadHashMap() 184 185 return nil 186 } 187 188 func (this *SQLiteFileListDB) IsReady() bool { 189 return this.isReady 190 } 191 192 func (this *SQLiteFileListDB) Total() (int64, error) { 193 // 读取总数量 194 var row = this.readDB.QueryRow(`SELECT COUNT(*) FROM "` + this.itemsTableName + `"`) 195 if row.Err() != nil { 196 return 0, row.Err() 197 } 198 var total int64 199 err := row.Scan(&total) 200 return total, err 201 } 202 203 func (this *SQLiteFileListDB) AddSync(hash string, item *Item) error { 204 this.hashMap.Add(hash) 205 206 if item.StaleAt == 0 { 207 item.StaleAt = item.ExpiresAt 208 } 209 210 _, err := this.insertStmt.Exec(hash, item.Key, item.HeaderSize, item.BodySize, item.MetaSize, item.ExpiresAt, item.StaleAt, item.Host, item.ServerId, fasttime.Now().Unix()) 211 if err != nil { 212 return this.WrapError(err) 213 } 214 215 return nil 216 } 217 218 func (this *SQLiteFileListDB) DeleteSync(hash string) error { 219 this.hashMap.Delete(hash) 220 221 _, err := this.deleteByHashStmt.Exec(hash) 222 if err != nil { 223 return err 224 } 225 return nil 226 } 227 228 func (this *SQLiteFileListDB) ListExpiredItems(count int) (hashList []string, err error) { 229 if !this.isReady { 230 return nil, nil 231 } 232 233 if count <= 0 { 234 count = 100 235 } 236 237 rows, err := this.purgeStmt.Query(time.Now().Unix(), count) 238 if err != nil { 239 return nil, err 240 } 241 defer func() { 242 _ = rows.Close() 243 }() 244 245 for rows.Next() { 246 var hash string 247 err = rows.Scan(&hash) 248 if err != nil { 249 return nil, err 250 } 251 hashList = append(hashList, hash) 252 } 253 return hashList, nil 254 } 255 256 func (this *SQLiteFileListDB) ListLFUItems(count int) (hashList []string, err error) { 257 if !this.isReady { 258 return nil, nil 259 } 260 261 if count <= 0 { 262 count = 100 263 } 264 265 // 先找过期的 266 hashList, err = this.ListExpiredItems(count) 267 if err != nil { 268 return 269 } 270 var l = len(hashList) 271 272 // 从旧缓存中补充 273 if l < count { 274 oldHashList, err := this.listOlderItems(count - l) 275 if err != nil { 276 return nil, err 277 } 278 hashList = append(hashList, oldHashList...) 279 } 280 281 return hashList, nil 282 } 283 284 func (this *SQLiteFileListDB) ListHashes(lastId int64) (hashList []string, maxId int64, err error) { 285 rows, err := this.selectHashListStmt.Query(lastId) 286 if err != nil { 287 return nil, 0, err 288 } 289 var id int64 290 var hash string 291 for rows.Next() { 292 err = rows.Scan(&id, &hash) 293 if err != nil { 294 _ = rows.Close() 295 return 296 } 297 maxId = id 298 hashList = append(hashList, hash) 299 } 300 301 _ = rows.Close() 302 return 303 } 304 305 func (this *SQLiteFileListDB) IncreaseHitAsync(hash string) error { 306 // do nothing 307 return nil 308 } 309 310 func (this *SQLiteFileListDB) CleanPrefix(prefix string) error { 311 if !this.isReady { 312 return nil 313 } 314 var count = int64(10000) 315 var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的 316 for { 317 result, err := this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET expiredAt=0,staleAt=? WHERE id IN (SELECT id FROM "`+this.itemsTableName+`" WHERE expiredAt>0 AND createdAt<=? AND INSTR("key", ?)=1 LIMIT `+types.String(count)+`)`, unixTime+DefaultStaleCacheSeconds, unixTime, prefix) 318 if err != nil { 319 return this.WrapError(err) 320 } 321 affectedRows, err := result.RowsAffected() 322 if err != nil { 323 return err 324 } 325 if affectedRows < count { 326 return nil 327 } 328 } 329 } 330 331 func (this *SQLiteFileListDB) CleanMatchKey(key string) error { 332 if !this.isReady { 333 return nil 334 } 335 336 // 忽略 @GOEDGE_ 337 if strings.Contains(key, SuffixAll) { 338 return nil 339 } 340 341 u, err := url.Parse(key) 342 if err != nil { 343 return nil 344 } 345 346 var host = u.Host 347 hostPart, _, err := net.SplitHostPort(host) 348 if err == nil && len(hostPart) > 0 { 349 host = hostPart 350 } 351 if len(host) == 0 { 352 return nil 353 } 354 355 // 转义 356 var queryKey = strings.ReplaceAll(key, "%", "\\%") 357 queryKey = strings.ReplaceAll(queryKey, "_", "\\_") 358 queryKey = strings.Replace(queryKey, "*", "%", 1) 359 360 // TODO 检查大批量数据下的操作性能 361 var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的 362 363 _, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryKey) 364 if err != nil { 365 return err 366 } 367 368 _, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryKey+SuffixAll+"%") 369 if err != nil { 370 return err 371 } 372 373 return nil 374 } 375 376 func (this *SQLiteFileListDB) CleanMatchPrefix(prefix string) error { 377 if !this.isReady { 378 return nil 379 } 380 381 u, err := url.Parse(prefix) 382 if err != nil { 383 return nil 384 } 385 386 var host = u.Host 387 hostPart, _, err := net.SplitHostPort(host) 388 if err == nil && len(hostPart) > 0 { 389 host = hostPart 390 } 391 if len(host) == 0 { 392 return nil 393 } 394 395 // 转义 396 var queryPrefix = strings.ReplaceAll(prefix, "%", "\\%") 397 queryPrefix = strings.ReplaceAll(queryPrefix, "_", "\\_") 398 queryPrefix = strings.Replace(queryPrefix, "*", "%", 1) 399 queryPrefix += "%" 400 401 // TODO 检查大批量数据下的操作性能 402 var unixTime = fasttime.Now().Unix() // 只删除当前的,不删除新的 403 404 _, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+DefaultStaleCacheSeconds, host, "*."+host, queryPrefix) 405 return err 406 } 407 408 func (this *SQLiteFileListDB) CleanAll() error { 409 if !this.isReady { 410 return nil 411 } 412 413 _, err := this.deleteAllStmt.Exec() 414 if err != nil { 415 return this.WrapError(err) 416 } 417 418 this.hashMap.Clean() 419 420 return nil 421 } 422 423 func (this *SQLiteFileListDB) Close() error { 424 if this.isClosed { 425 return nil 426 } 427 428 this.isClosed = true 429 this.isReady = false 430 431 if this.existsByHashStmt != nil { 432 _ = this.existsByHashStmt.Close() 433 } 434 if this.insertStmt != nil { 435 _ = this.insertStmt.Close() 436 } 437 if this.selectByHashStmt != nil { 438 _ = this.selectByHashStmt.Close() 439 } 440 if this.selectHashListStmt != nil { 441 _ = this.selectHashListStmt.Close() 442 } 443 if this.deleteByHashStmt != nil { 444 _ = this.deleteByHashStmt.Close() 445 } 446 if this.statStmt != nil { 447 _ = this.statStmt.Close() 448 } 449 if this.purgeStmt != nil { 450 _ = this.purgeStmt.Close() 451 } 452 if this.deleteAllStmt != nil { 453 _ = this.deleteAllStmt.Close() 454 } 455 if this.listOlderItemsStmt != nil { 456 _ = this.listOlderItemsStmt.Close() 457 } 458 459 var errStrings []string 460 461 if this.readDB != nil { 462 err := this.readDB.Close() 463 if err != nil { 464 errStrings = append(errStrings, err.Error()) 465 } 466 } 467 468 if this.writeDB != nil { 469 err := this.writeDB.Close() 470 if err != nil { 471 errStrings = append(errStrings, err.Error()) 472 } 473 } 474 475 if len(errStrings) == 0 { 476 return nil 477 } 478 return errors.New("close database failed: " + strings.Join(errStrings, ", ")) 479 } 480 481 func (this *SQLiteFileListDB) WrapError(err error) error { 482 if err == nil { 483 return nil 484 } 485 return fmt.Errorf("%w (file: %s)", err, this.dbPath) 486 } 487 488 func (this *SQLiteFileListDB) HashMapIsLoaded() bool { 489 return this.hashMapIsLoaded 490 } 491 492 // 初始化 493 func (this *SQLiteFileListDB) initTables(times int) error { 494 { 495 // expiredAt - 过期时间,用来判断有无过期 496 // staleAt - 过时缓存最大时间,用来清理缓存 497 // 不对 hash 增加 unique 参数,是尽可能避免产生 malformed 错误 498 _, err := this.writeDB.Exec(`CREATE TABLE IF NOT EXISTS "` + this.itemsTableName + `" ( 499 "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 500 "hash" varchar(32), 501 "key" varchar(1024), 502 "tag" varchar(64), 503 "headerSize" integer DEFAULT 0, 504 "bodySize" integer DEFAULT 0, 505 "metaSize" integer DEFAULT 0, 506 "expiredAt" integer DEFAULT 0, 507 "staleAt" integer DEFAULT 0, 508 "createdAt" integer DEFAULT 0, 509 "host" varchar(128), 510 "serverId" integer 511 ); 512 513 DROP INDEX IF EXISTS "createdAt"; 514 DROP INDEX IF EXISTS "expiredAt"; 515 DROP INDEX IF EXISTS "serverId"; 516 517 CREATE INDEX IF NOT EXISTS "staleAt" 518 ON "` + this.itemsTableName + `" ( 519 "staleAt" ASC 520 ); 521 522 CREATE INDEX IF NOT EXISTS "hash" 523 ON "` + this.itemsTableName + `" ( 524 "hash" ASC 525 ); 526 `) 527 528 if err != nil { 529 // 忽略可以预期的错误 530 if strings.Contains(err.Error(), "duplicate column name") { 531 err = nil 532 } 533 534 // 尝试删除重建 535 if err != nil { 536 if times < 3 { 537 _, dropErr := this.writeDB.Exec(`DROP TABLE "` + this.itemsTableName + `"`) 538 if dropErr == nil { 539 return this.initTables(times + 1) 540 } 541 return this.WrapError(err) 542 } 543 544 return this.WrapError(err) 545 } 546 } 547 } 548 549 // 删除hits表 550 { 551 _, _ = this.writeDB.Exec(`DROP TABLE "hits"`) 552 } 553 554 return nil 555 } 556 557 func (this *SQLiteFileListDB) listOlderItems(count int) (hashList []string, err error) { 558 rows, err := this.listOlderItemsStmt.Query(count) 559 if err != nil { 560 return nil, err 561 } 562 defer func() { 563 _ = rows.Close() 564 }() 565 566 for rows.Next() { 567 var hash string 568 err = rows.Scan(&hash) 569 if err != nil { 570 return nil, err 571 } 572 hashList = append(hashList, hash) 573 } 574 575 return hashList, nil 576 } 577 578 func (this *SQLiteFileListDB) shouldRecover() bool { 579 result, err := this.writeDB.Query("pragma integrity_check;") 580 if err != nil { 581 logs.Println(result) 582 } 583 var errString = "" 584 var shouldRecover = false 585 if result.Next() { 586 _ = result.Scan(&errString) 587 if strings.TrimSpace(errString) != "ok" { 588 shouldRecover = true 589 } 590 } 591 _ = result.Close() 592 return shouldRecover 593 } 594 595 // 删除数据库文件 596 func (this *SQLiteFileListDB) deleteDB() { 597 _ = fsutils.Remove(this.dbPath) 598 _ = fsutils.Remove(this.dbPath + "-shm") 599 _ = fsutils.Remove(this.dbPath + "-wal") 600 } 601 602 // 加载Hash列表 603 func (this *SQLiteFileListDB) loadHashMap() { 604 this.hashMapIsLoaded = false 605 606 err := this.hashMap.Load(this) 607 if err != nil { 608 remotelogs.Error("LIST_FILE_DB", "load hash map failed: "+err.Error()+"(file: "+this.dbPath+")") 609 610 // 自动修复错误 611 // TODO 将来希望能尽可能恢复以往数据库中的内容 612 if strings.Contains(err.Error(), "database is closed") || strings.Contains(err.Error(), "database disk image is malformed") { 613 _ = this.Close() 614 this.deleteDB() 615 remotelogs.Println("LIST_FILE_DB", "recreating the database (file:"+this.dbPath+") ...") 616 err = this.Open(this.dbPath) 617 if err != nil { 618 remotelogs.Error("LIST_FILE_DB", "recreate the database failed: "+err.Error()+" (file:"+this.dbPath+")") 619 } else { 620 _ = this.Init() 621 } 622 } 623 return 624 } 625 626 this.hashMapIsLoaded = true 627 }