github.com/cs3org/reva/v2@v2.27.7/pkg/store/memory/memstore.go (about) 1 package memory 2 3 import ( 4 "container/list" 5 "context" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/armon/go-radix" 11 "go-micro.dev/v4/store" 12 ) 13 14 // MemStore is a in-memory store implementation using radix tree for fast 15 // prefix and suffix searches. 16 // Insertions are expected to be a bit slow due to the data structures, but 17 // searches are expected to be fast, including exact key search, as well as 18 // prefix and suffix searches (based on the number of elements to be returned). 19 // Prefix+suffix search isn't optimized and will depend on how many items we 20 // need to skip. 21 // It's also recommended to use reasonable limits when using prefix or suffix 22 // searches because we'll need to traverse the data structures to provide the 23 // results. The traversal will stop a soon as we have the required number of 24 // results, so it will be faster if we use a short limit. 25 // 26 // The overall performance will depend on how the radix trees are built. 27 // The number of elements won't directly affect the performance but how the 28 // keys are dispersed. The more dispersed the keys are, the faster the search 29 // will be, regardless of the number of keys. This happens due to the number 30 // of hops we need to do to reach the target element. 31 // This also mean that if the keys are too similar, the performance might be 32 // slower than expected even if the number of elements isn't too big. 33 type MemStore struct { 34 preRadix *radix.Tree 35 sufRadix *radix.Tree 36 evictionList *list.List 37 38 options store.Options 39 40 lockGlob sync.RWMutex 41 lockEvicList sync.RWMutex // Read operation will modify the eviction list 42 } 43 44 type storeRecord struct { 45 Key string 46 Value []byte 47 Metadata map[string]interface{} 48 Expiry time.Duration 49 ExpiresAt time.Time 50 } 51 52 type contextKey string 53 54 var targetContextKey contextKey 55 56 // NewContext prepares a context to be used with the memory implementation. 57 // The context is used to set up custom parameters to the specific implementation. 58 // In this case, you can configure the maximum capacity for the MemStore 59 // implementation as shown below. 60 // ``` 61 // cache := NewMemStore( 62 // 63 // store.WithContext( 64 // NewContext( 65 // ctx, 66 // map[string]interface{}{ 67 // "maxCap": 50, 68 // }, 69 // ), 70 // ), 71 // 72 // ) 73 // ``` 74 // 75 // Available options for the MemStore are: 76 // * "maxCap" -> 512 (int) The maximum number of elements the cache will hold. 77 // Adding additional elements will remove old elements to ensure we aren't over 78 // the maximum capacity. 79 // 80 // For convenience, this can also be used for the MultiMemStore. 81 func NewContext(ctx context.Context, storeParams map[string]interface{}) context.Context { 82 return context.WithValue(ctx, targetContextKey, storeParams) 83 } 84 85 // NewMemStore creates a new MemStore instance 86 func NewMemStore(opts ...store.Option) store.Store { 87 m := &MemStore{} 88 _ = m.Init(opts...) 89 return m 90 } 91 92 // Get the maximum capacity configured. If no maxCap has been configured 93 // (via `NewContext`), 512 will be used as maxCap. 94 func (m *MemStore) getMaxCap() int { 95 maxCap := 512 96 97 ctx := m.options.Context 98 if ctx == nil { 99 return maxCap 100 } 101 102 ctxValue := ctx.Value(targetContextKey) 103 if ctxValue == nil { 104 return maxCap 105 } 106 additionalOpts := ctxValue.(map[string]interface{}) 107 108 confCap, exists := additionalOpts["maxCap"] 109 if exists { 110 maxCap = confCap.(int) 111 } 112 return maxCap 113 } 114 115 // Init initializes the MemStore. If the MemStore was used, this will reset 116 // all the internal structures and the new options (passed as parameters) 117 // will be used. 118 func (m *MemStore) Init(opts ...store.Option) error { 119 optList := store.Options{} 120 for _, opt := range opts { 121 opt(&optList) 122 } 123 124 m.lockGlob.Lock() 125 defer m.lockGlob.Unlock() 126 127 m.preRadix = radix.New() 128 m.sufRadix = radix.New() 129 m.evictionList = list.New() 130 m.options = optList 131 132 return nil 133 } 134 135 // Options returns the options being used 136 func (m *MemStore) Options() store.Options { 137 m.lockGlob.RLock() 138 defer m.lockGlob.RUnlock() 139 140 return m.options 141 } 142 143 // Write the record in the MemStore. 144 // Note that Database and Table options will be ignored. 145 // Expiration options will take the following precedence: 146 // TTL option > expiration option > TTL record 147 // 148 // New elements will take the last position in the eviction list. Updating 149 // an element will also move the element to the last position. 150 // 151 // Although not recommended, new elements might be inserted with an 152 // already-expired date 153 func (m *MemStore) Write(r *store.Record, opts ...store.WriteOption) error { 154 var element *list.Element 155 156 wopts := store.WriteOptions{} 157 for _, opt := range opts { 158 opt(&wopts) 159 } 160 cRecord := toStoreRecord(r, wopts) 161 162 m.lockGlob.Lock() 163 defer m.lockGlob.Unlock() 164 165 ele, exists := m.preRadix.Get(cRecord.Key) 166 if exists { 167 element = ele.(*list.Element) 168 element.Value = cRecord 169 170 m.evictionList.MoveToBack(element) 171 } else { 172 if m.evictionList.Len() >= m.getMaxCap() { 173 elementToDelete := m.evictionList.Front() 174 if elementToDelete != nil { 175 recordToDelete := elementToDelete.Value.(*storeRecord) 176 _, _ = m.preRadix.Delete(recordToDelete.Key) 177 _, _ = m.sufRadix.Delete(recordToDelete.Key) 178 m.evictionList.Remove(elementToDelete) 179 } 180 } 181 element = m.evictionList.PushBack(cRecord) 182 _, _ = m.preRadix.Insert(cRecord.Key, element) 183 _, _ = m.sufRadix.Insert(reverseString(cRecord.Key), element) 184 } 185 return nil 186 } 187 188 // Read the key from the MemStore. A list of records will be returned even if 189 // you're asking for the exact key (only one record is expected in that case). 190 // 191 // Reading the exact element will move such element to the last position of 192 // the eviction list. This WON'T apply for prefix and / or suffix reads. 193 // 194 // This method guarantees that no expired element will be returned. For the 195 // case of exact read, the element will be removed and a "not found" error 196 // will be returned. 197 // For prefix and suffix reads, all the elements that we traverse through 198 // will be removed. This includes the elements we need to skip as well as 199 // the elements that might have gotten into the the result. Note that the 200 // elements that are over the limit won't be touched 201 // 202 // All read options are supported except Database and Table. 203 // 204 // For prefix and prefix+suffix options, the records will be returned in 205 // alphabetical order on the keys. 206 // For the suffix option (just suffix, no prefix), the records will be 207 // returned in alphabetical order after reversing the keys. This means, 208 // reverse all the keys and then sort them alphabetically. This just affects 209 // the sorting order; the keys will be returned as expected. 210 // This means that ["aboz", "caaz", "ziuz"] will be sorted as ["caaz", "aboz", "ziuz"] 211 // for the key "z" as suffix. 212 // 213 // Note that offset are supported but not recommended. There is no direct access 214 // to the record X. We'd need to skip all the records until we reach the specified 215 // offset, which could be problematic. 216 // Performance for prefix and suffix searches should be good assuming we limit 217 // the number of results we need to return. 218 func (m *MemStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { 219 var element *list.Element 220 221 ropts := store.ReadOptions{} 222 for _, opt := range opts { 223 opt(&ropts) 224 } 225 226 if !ropts.Prefix && !ropts.Suffix { 227 m.lockGlob.RLock() 228 ele, exists := m.preRadix.Get(key) 229 if !exists { 230 m.lockGlob.RUnlock() 231 return nil, store.ErrNotFound 232 } 233 234 element = ele.(*list.Element) 235 record := element.Value.(*storeRecord) 236 if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) { 237 // record expired -> need to delete 238 m.lockGlob.RUnlock() 239 m.lockGlob.Lock() 240 defer m.lockGlob.Unlock() 241 242 m.evictionList.Remove(element) 243 _, _ = m.preRadix.Delete(key) 244 _, _ = m.sufRadix.Delete(reverseString(key)) 245 return nil, store.ErrNotFound 246 } 247 248 m.lockEvicList.Lock() 249 m.evictionList.MoveToBack(element) 250 m.lockEvicList.Unlock() 251 252 foundRecords := []*store.Record{ 253 fromStoreRecord(record), 254 } 255 m.lockGlob.RUnlock() 256 257 return foundRecords, nil 258 } 259 260 records := []*store.Record{} 261 expiredElements := make(map[string]*list.Element) 262 263 m.lockGlob.RLock() 264 if ropts.Prefix && ropts.Suffix { 265 // if we need to check both prefix and suffix, go through the 266 // prefix tree and skip elements without the right suffix. We 267 // don't need to check the suffix tree because the elements 268 // must be in both trees 269 m.preRadix.WalkPrefix(key, m.radixTreeCallBackCheckSuffix(ropts.Offset, ropts.Limit, key, &records, expiredElements)) 270 } else { 271 if ropts.Prefix { 272 m.preRadix.WalkPrefix(key, m.radixTreeCallBack(ropts.Offset, ropts.Limit, &records, expiredElements)) 273 } 274 if ropts.Suffix { 275 m.sufRadix.WalkPrefix(reverseString(key), m.radixTreeCallBack(ropts.Offset, ropts.Limit, &records, expiredElements)) 276 } 277 } 278 m.lockGlob.RUnlock() 279 280 // if there are expired elements, get a write lock and delete the expired elements 281 if len(expiredElements) > 0 { 282 m.lockGlob.Lock() 283 for key, element := range expiredElements { 284 m.evictionList.Remove(element) 285 _, _ = m.preRadix.Delete(key) 286 _, _ = m.sufRadix.Delete(reverseString(key)) 287 } 288 m.lockGlob.Unlock() 289 } 290 return records, nil 291 } 292 293 // Delete removes the record based on the key. It won't return any error if it's missing 294 // 295 // Database and Table options aren't supported 296 func (m *MemStore) Delete(key string, opts ...store.DeleteOption) error { 297 m.lockGlob.Lock() 298 defer m.lockGlob.Unlock() 299 300 ele, exists := m.preRadix.Get(key) 301 if exists { 302 element := ele.(*list.Element) 303 m.evictionList.Remove(element) 304 _, _ = m.preRadix.Delete(key) 305 _, _ = m.sufRadix.Delete(reverseString(key)) 306 } 307 return nil 308 } 309 310 // List the keys currently used in the MemStore 311 // 312 // # All options are supported except Database and Table 313 // 314 // For prefix and prefix+suffix options, the keys will be returned in 315 // alphabetical order. 316 // For the suffix option (just suffix, no prefix), the keys will be 317 // returned in alphabetical order after reversing the keys. This means, 318 // reverse all the keys and then sort them alphabetically. This just affects 319 // the sorting order; the keys will be returned as expected. 320 // This means that ["aboz", "caaz", "ziuz"] will be sorted as ["caaz", "aboz", "ziuz"] 321 func (m *MemStore) List(opts ...store.ListOption) ([]string, error) { 322 records := []string{} 323 expiredElements := make(map[string]*list.Element) 324 325 lopts := store.ListOptions{} 326 for _, opt := range opts { 327 opt(&lopts) 328 } 329 330 if lopts.Prefix == "" && lopts.Suffix == "" { 331 m.lockGlob.RLock() 332 m.preRadix.Walk(m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements)) 333 m.lockGlob.RUnlock() 334 335 // if there are expired elements, get a write lock and delete the expired elements 336 if len(expiredElements) > 0 { 337 m.lockGlob.Lock() 338 for key, element := range expiredElements { 339 m.evictionList.Remove(element) 340 _, _ = m.preRadix.Delete(key) 341 _, _ = m.sufRadix.Delete(reverseString(key)) 342 } 343 m.lockGlob.Unlock() 344 } 345 return records, nil 346 } 347 348 m.lockGlob.RLock() 349 if lopts.Prefix != "" && lopts.Suffix != "" { 350 // if we need to check both prefix and suffix, go through the 351 // prefix tree and skip elements without the right suffix. We 352 // don't need to check the suffix tree because the elements 353 // must be in both trees 354 m.preRadix.WalkPrefix(lopts.Prefix, m.radixTreeCallBackKeysOnlyWithSuffix(lopts.Offset, lopts.Limit, lopts.Suffix, &records, expiredElements)) 355 } else { 356 if lopts.Prefix != "" { 357 m.preRadix.WalkPrefix(lopts.Prefix, m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements)) 358 } 359 if lopts.Suffix != "" { 360 m.sufRadix.WalkPrefix(reverseString(lopts.Suffix), m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements)) 361 } 362 } 363 m.lockGlob.RUnlock() 364 365 // if there are expired elements, get a write lock and delete the expired elements 366 if len(expiredElements) > 0 { 367 m.lockGlob.Lock() 368 for key, element := range expiredElements { 369 m.evictionList.Remove(element) 370 _, _ = m.preRadix.Delete(key) 371 _, _ = m.sufRadix.Delete(reverseString(key)) 372 } 373 m.lockGlob.Unlock() 374 } 375 return records, nil 376 } 377 378 // Close closes the store 379 func (m *MemStore) Close() error { 380 return nil 381 } 382 383 // String returns the name of the store implementation 384 func (m *MemStore) String() string { 385 return "RadixMemStore" 386 } 387 388 // Len returns the number of items in the store 389 func (m *MemStore) Len() (int, bool) { 390 eLen := m.evictionList.Len() 391 pLen := m.preRadix.Len() 392 sLen := m.sufRadix.Len() 393 if eLen == pLen && pLen == sLen { 394 return eLen, true 395 } 396 return 0, false 397 } 398 399 func (m *MemStore) radixTreeCallBack(offset, limit uint, result *[]*store.Record, expiredElements map[string]*list.Element) radix.WalkFn { 400 currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls 401 maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls 402 *maxIndex = offset + limit 403 return func(key string, value interface{}) bool { 404 element := value.(*list.Element) 405 record := element.Value.(*storeRecord) 406 407 if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) { 408 // record has expired -> add element to the expiredElements map 409 // and jump directly to the next element without increasing the index 410 expiredElements[record.Key] = element 411 return false 412 } 413 414 if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) { 415 // if it's within expected range, add a copy to the results 416 *result = append(*result, fromStoreRecord(record)) 417 } 418 419 *currentIndex++ 420 421 if *currentIndex < *maxIndex || *maxIndex == offset { 422 return false 423 } 424 return true 425 } 426 } 427 428 func (m *MemStore) radixTreeCallBackCheckSuffix(offset, limit uint, presuf string, result *[]*store.Record, expiredElements map[string]*list.Element) radix.WalkFn { 429 currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls 430 maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls 431 *maxIndex = offset + limit 432 return func(key string, value interface{}) bool { 433 if !strings.HasSuffix(key, presuf) { 434 return false 435 } 436 437 element := value.(*list.Element) 438 record := element.Value.(*storeRecord) 439 440 if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) { 441 // record has expired -> add element to the expiredElements map 442 // and jump directly to the next element without increasing the index 443 expiredElements[record.Key] = element 444 return false 445 } 446 447 if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) { 448 *result = append(*result, fromStoreRecord(record)) 449 } 450 451 *currentIndex++ 452 453 if *currentIndex < *maxIndex || *maxIndex == offset { 454 return false 455 } 456 return true 457 } 458 } 459 460 func (m *MemStore) radixTreeCallBackKeysOnly(offset, limit uint, result *[]string, expiredElements map[string]*list.Element) radix.WalkFn { 461 currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls 462 maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls 463 *maxIndex = offset + limit 464 return func(key string, value interface{}) bool { 465 element := value.(*list.Element) 466 record := element.Value.(*storeRecord) 467 468 if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) { 469 // record has expired -> add element to the expiredElements map 470 // and jump directly to the next element without increasing the index 471 expiredElements[record.Key] = element 472 return false 473 } 474 475 if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) { 476 *result = append(*result, record.Key) 477 } 478 479 *currentIndex++ 480 481 if *currentIndex < *maxIndex || *maxIndex == offset { 482 return false 483 } 484 return true 485 } 486 } 487 488 func (m *MemStore) radixTreeCallBackKeysOnlyWithSuffix(offset, limit uint, presuf string, result *[]string, expiredElements map[string]*list.Element) radix.WalkFn { 489 currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls 490 maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls 491 *maxIndex = offset + limit 492 return func(key string, value interface{}) bool { 493 if !strings.HasSuffix(key, presuf) { 494 return false 495 } 496 497 element := value.(*list.Element) 498 record := element.Value.(*storeRecord) 499 500 if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) { 501 // record has expired -> add element to the expiredElements map 502 // and jump directly to the next element without increasing the index 503 expiredElements[record.Key] = element 504 return false 505 } 506 507 if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) { 508 *result = append(*result, record.Key) 509 } 510 511 *currentIndex++ 512 513 if *currentIndex < *maxIndex || *maxIndex == offset { 514 return false 515 } 516 return true 517 } 518 }