github.com/TeaOSLab/EdgeNode@v1.3.8/internal/caches/list_file_kv_store.go (about)

     1  // Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
     2  
     3  package caches
     4  
     5  import (
     6  	"errors"
     7  	"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
     8  	"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
     9  	"github.com/cockroachdb/pebble"
    10  	"regexp"
    11  	"strings"
    12  	"testing"
    13  )
    14  
    15  type KVListFileStore struct {
    16  	path     string
    17  	rawStore *kvstore.Store
    18  
    19  	// tables
    20  	itemsTable *kvstore.Table[*Item]
    21  
    22  	rawIsReady bool
    23  }
    24  
    25  func NewKVListFileStore(path string) *KVListFileStore {
    26  	return &KVListFileStore{
    27  		path: path,
    28  	}
    29  }
    30  
    31  func (this *KVListFileStore) Open() error {
    32  	var reg = regexp.MustCompile(`^(.+)/([\w-]+)(\.store)$`)
    33  	var matches = reg.FindStringSubmatch(this.path)
    34  	if len(matches) != 4 {
    35  		return errors.New("invalid path '" + this.path + "'")
    36  	}
    37  	var dir = matches[1]
    38  	var name = matches[2]
    39  
    40  	rawStore, err := kvstore.OpenStoreDir(dir, name)
    41  	if err != nil {
    42  		return err
    43  	}
    44  	this.rawStore = rawStore
    45  
    46  	db, err := rawStore.NewDB("cache")
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	{
    52  		table, tableErr := kvstore.NewTable[*Item]("items", NewItemKVEncoder[*Item]())
    53  		if tableErr != nil {
    54  			return tableErr
    55  		}
    56  
    57  		err = table.AddFields("staleAt", "key", "wildKey", "createdAt")
    58  		if err != nil {
    59  			return err
    60  		}
    61  
    62  		db.AddTable(table)
    63  		this.itemsTable = table
    64  	}
    65  
    66  	this.rawIsReady = true
    67  
    68  	return nil
    69  }
    70  
    71  func (this *KVListFileStore) Path() string {
    72  	return this.path
    73  }
    74  
    75  func (this *KVListFileStore) AddItem(hash string, item *Item) error {
    76  	if !this.isReady() {
    77  		return nil
    78  	}
    79  
    80  	var currentTime = fasttime.Now().Unix()
    81  	if item.ExpiresAt <= currentTime {
    82  		return nil
    83  	}
    84  	if item.CreatedAt <= 0 {
    85  		item.CreatedAt = currentTime
    86  	}
    87  	if item.StaleAt <= 0 {
    88  		item.StaleAt = item.ExpiresAt + DefaultStaleCacheSeconds
    89  	}
    90  	return this.itemsTable.Set(hash, item)
    91  }
    92  
    93  func (this *KVListFileStore) ExistItem(hash string) (bool, int64, error) {
    94  	if !this.isReady() {
    95  		return false, -1, nil
    96  	}
    97  
    98  	item, err := this.itemsTable.Get(hash)
    99  	if err != nil {
   100  		if kvstore.IsNotFound(err) {
   101  			return false, -1, nil
   102  		}
   103  		return false, -1, err
   104  	}
   105  	if item == nil {
   106  		return false, -1, nil
   107  	}
   108  
   109  	return item.ExpiresAt > fasttime.Now().Unix(), item.HeaderSize + item.BodySize, nil
   110  }
   111  
   112  func (this *KVListFileStore) ExistQuickItem(hash string) (bool, error) {
   113  	if !this.isReady() {
   114  		return false, nil
   115  	}
   116  
   117  	return this.itemsTable.Exist(hash)
   118  }
   119  
   120  func (this *KVListFileStore) RemoveItem(hash string) error {
   121  	if !this.isReady() {
   122  		return nil
   123  	}
   124  
   125  	return this.itemsTable.Delete(hash)
   126  }
   127  
   128  func (this *KVListFileStore) RemoveAllItems() error {
   129  	if !this.isReady() {
   130  		return nil
   131  	}
   132  
   133  	return this.itemsTable.Truncate()
   134  }
   135  
   136  func (this *KVListFileStore) PurgeItems(count int, callback func(hash string) error) (int, error) {
   137  	if !this.isReady() {
   138  		return 0, nil
   139  	}
   140  
   141  	var countFound int
   142  	var currentTime = fasttime.Now().Unix()
   143  	var hashList []string
   144  	err := this.itemsTable.
   145  		Query().
   146  		FieldAsc("staleAt").
   147  		Limit(count).
   148  		FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   149  			if item.Value == nil {
   150  				return true, nil
   151  			}
   152  			if item.Value.StaleAt < currentTime {
   153  				countFound++
   154  				hashList = append(hashList, item.Key)
   155  				return true, nil
   156  			}
   157  			return false, nil
   158  		})
   159  	if err != nil {
   160  		return 0, err
   161  	}
   162  
   163  	// delete items
   164  	if len(hashList) > 0 {
   165  		txErr := this.itemsTable.WriteTx(func(tx *kvstore.Tx[*Item]) error {
   166  			for _, hash := range hashList {
   167  				deleteErr := tx.Delete(hash)
   168  				if deleteErr != nil {
   169  					return deleteErr
   170  				}
   171  			}
   172  			return nil
   173  		})
   174  		if txErr != nil {
   175  			return 0, txErr
   176  		}
   177  
   178  		for _, hash := range hashList {
   179  			callbackErr := callback(hash)
   180  			if callbackErr != nil {
   181  				return 0, callbackErr
   182  			}
   183  		}
   184  	}
   185  
   186  	return countFound, nil
   187  }
   188  
   189  func (this *KVListFileStore) PurgeLFUItems(count int, callback func(hash string) error) error {
   190  	if !this.isReady() {
   191  		return nil
   192  	}
   193  
   194  	var hashList []string
   195  	err := this.itemsTable.
   196  		Query().
   197  		FieldAsc("createdAt").
   198  		Limit(count).
   199  		FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   200  			if item.Value != nil {
   201  				hashList = append(hashList, item.Key)
   202  			}
   203  			return true, nil
   204  		})
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	// delete items
   210  	if len(hashList) > 0 {
   211  		txErr := this.itemsTable.WriteTx(func(tx *kvstore.Tx[*Item]) error {
   212  			for _, hash := range hashList {
   213  				deleteErr := tx.Delete(hash)
   214  				if deleteErr != nil {
   215  					return deleteErr
   216  				}
   217  			}
   218  			return nil
   219  		})
   220  		if txErr != nil {
   221  			return txErr
   222  		}
   223  
   224  		for _, hash := range hashList {
   225  			callbackErr := callback(hash)
   226  			if callbackErr != nil {
   227  				return callbackErr
   228  			}
   229  		}
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  func (this *KVListFileStore) CleanItemsWithPrefix(prefix string) error {
   236  	if !this.isReady() {
   237  		return nil
   238  	}
   239  
   240  	if len(prefix) == 0 {
   241  		return nil
   242  	}
   243  
   244  	var currentTime = fasttime.Now().Unix()
   245  
   246  	var fieldOffset []byte
   247  	const size = 1000
   248  	for {
   249  		var count int
   250  		err := this.itemsTable.
   251  			Query().
   252  			FieldPrefix("key", prefix).
   253  			FieldOffset(fieldOffset).
   254  			Limit(size).
   255  			ForUpdate().
   256  			FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   257  				if item.Value == nil {
   258  					return true, nil
   259  				}
   260  
   261  				count++
   262  				fieldOffset = item.FieldKey
   263  
   264  				if item.Value.CreatedAt >= currentTime {
   265  					return true, nil
   266  				}
   267  				if item.Value.ExpiresAt == 0 {
   268  					return true, nil
   269  				}
   270  
   271  				item.Value.ExpiresAt = 0
   272  				item.Value.StaleAt = 0
   273  
   274  				setErr := tx.Set(item.Key, item.Value) // TODO improve performance
   275  				if setErr != nil {
   276  					return false, setErr
   277  				}
   278  
   279  				return true, nil
   280  			})
   281  		if err != nil {
   282  			return err
   283  		}
   284  
   285  		if count < size {
   286  			break
   287  		}
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func (this *KVListFileStore) CleanItemsWithWildcardPrefix(prefix string) error {
   294  	if !this.isReady() {
   295  		return nil
   296  	}
   297  
   298  	if len(prefix) == 0 {
   299  		return nil
   300  	}
   301  
   302  	var currentTime = fasttime.Now().Unix()
   303  
   304  	var fieldOffset []byte
   305  	const size = 1000
   306  	for {
   307  		var count int
   308  		err := this.itemsTable.
   309  			Query().
   310  			FieldPrefix("wildKey", prefix).
   311  			FieldOffset(fieldOffset).
   312  			Limit(size).
   313  			ForUpdate().
   314  			FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   315  				if item.Value == nil {
   316  					return true, nil
   317  				}
   318  
   319  				count++
   320  				fieldOffset = item.FieldKey
   321  
   322  				if item.Value.CreatedAt >= currentTime {
   323  					return true, nil
   324  				}
   325  				if item.Value.ExpiresAt == 0 {
   326  					return true, nil
   327  				}
   328  
   329  				item.Value.ExpiresAt = 0
   330  				item.Value.StaleAt = 0
   331  
   332  				setErr := tx.Set(item.Key, item.Value) // TODO improve performance
   333  				if setErr != nil {
   334  					return false, setErr
   335  				}
   336  
   337  				return true, nil
   338  			})
   339  		if err != nil {
   340  			return err
   341  		}
   342  
   343  		if count < size {
   344  			break
   345  		}
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func (this *KVListFileStore) CleanItemsWithWildcardKey(key string) error {
   352  	if !this.isReady() {
   353  		return nil
   354  	}
   355  
   356  	if len(key) == 0 {
   357  		return nil
   358  	}
   359  
   360  	var currentTime = fasttime.Now().Unix()
   361  
   362  	for _, realKey := range []string{key, key + SuffixAll} {
   363  		var fieldOffset = append(this.itemsTable.FieldKey("wildKey"), '$')
   364  		fieldOffset = append(fieldOffset, realKey...)
   365  		const size = 1000
   366  
   367  		var wildKey string
   368  		if !strings.HasSuffix(realKey, SuffixAll) {
   369  			wildKey = string(append([]byte(realKey), 0, 0))
   370  		} else {
   371  			wildKey = realKey
   372  		}
   373  
   374  		for {
   375  			var count int
   376  			err := this.itemsTable.
   377  				Query().
   378  				FieldPrefix("wildKey", wildKey).
   379  				FieldOffset(fieldOffset).
   380  				Limit(size).
   381  				ForUpdate().
   382  				FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   383  					if item.Value == nil {
   384  						return true, nil
   385  					}
   386  
   387  					count++
   388  					fieldOffset = item.FieldKey
   389  
   390  					if item.Value.CreatedAt >= currentTime {
   391  						return true, nil
   392  					}
   393  					if item.Value.ExpiresAt == 0 {
   394  						return true, nil
   395  					}
   396  
   397  					item.Value.ExpiresAt = 0
   398  					item.Value.StaleAt = 0
   399  
   400  					setErr := tx.Set(item.Key, item.Value) // TODO improve performance
   401  					if setErr != nil {
   402  						return false, setErr
   403  					}
   404  
   405  					return true, nil
   406  				})
   407  			if err != nil {
   408  				return err
   409  			}
   410  
   411  			if count < size {
   412  				break
   413  			}
   414  		}
   415  	}
   416  
   417  	return nil
   418  }
   419  
   420  func (this *KVListFileStore) CountItems() (int64, error) {
   421  	if !this.isReady() {
   422  		return 0, nil
   423  	}
   424  
   425  	return this.itemsTable.Count()
   426  }
   427  
   428  func (this *KVListFileStore) StatItems() (*Stat, error) {
   429  	if !this.isReady() {
   430  		return &Stat{}, nil
   431  	}
   432  
   433  	var stat = &Stat{}
   434  
   435  	err := this.itemsTable.
   436  		Query().
   437  		FindAll(func(tx *kvstore.Tx[*Item], item kvstore.Item[*Item]) (goNext bool, err error) {
   438  			if item.Value != nil {
   439  				stat.Size += item.Value.Size()
   440  				stat.ValueSize += item.Value.BodySize
   441  				stat.Count++
   442  			}
   443  			return true, nil
   444  		})
   445  	return stat, err
   446  }
   447  
   448  func (this *KVListFileStore) TestInspect(t *testing.T) error {
   449  	if !this.isReady() {
   450  		return nil
   451  	}
   452  
   453  	it, err := this.rawStore.RawDB().NewIter(&pebble.IterOptions{})
   454  	if err != nil {
   455  		return err
   456  	}
   457  	defer func() {
   458  		_ = it.Close()
   459  	}()
   460  
   461  	for it.First(); it.Valid(); it.Next() {
   462  		valueBytes, valueErr := it.ValueAndErr()
   463  		if valueErr != nil {
   464  			return valueErr
   465  		}
   466  		t.Log(string(it.Key()), "=>", string(valueBytes))
   467  	}
   468  	return nil
   469  }
   470  
   471  func (this *KVListFileStore) Close() error {
   472  	if this.rawStore != nil {
   473  		return this.rawStore.Close()
   474  	}
   475  
   476  	return nil
   477  }
   478  
   479  func (this *KVListFileStore) isReady() bool {
   480  	return this.rawIsReady && !this.rawStore.IsClosed()
   481  }