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  }