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 }