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  }