Use go-micro store to cache the roles (#4337)
* Use go-micro store to cache the roles Add custom in-memory implementation * replace redis with custom etcd implementation * adjust table name for the cache in the roles manager * Fix tests * Fix sonarcloud issues * Refactor for sonarcloud * Allow configuration of cache per service * Reuse parent context in etcd implementation
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// In-memory store implementation using radix tree for fast prefix and suffix
|
||||
// searches.
|
||||
// Insertions are expected to be a bit slow due to the data structures, but
|
||||
// searches are expected to be fast, including exact key search, as well as
|
||||
// prefix and suffix searches (based on the number of elements to be returned).
|
||||
// Prefix+suffix search isn't optimized and will depend on how many items we
|
||||
// need to skip.
|
||||
// It's also recommended to use reasonable limits when using prefix or suffix
|
||||
// searches because we'll need to traverse the data structures to provide the
|
||||
// results. The traversal will stop a soon as we have the required number of
|
||||
// results, so it will be faster if we use a short limit.
|
||||
//
|
||||
// The overall performance will depend on how the radix trees are built.
|
||||
// The number of elements won't directly affect the performance but how the
|
||||
// keys are dispersed. The more dispersed the keys are, the faster the search
|
||||
// will be, regardless of the number of keys. This happens due to the number
|
||||
// of hops we need to do to reach the target element.
|
||||
// This also mean that if the keys are too similar, the performance might be
|
||||
// slower than expected even if the number of elements isn't too big.
|
||||
type MemStore struct {
|
||||
preRadix *radix.Tree
|
||||
sufRadix *radix.Tree
|
||||
evictionList *list.List
|
||||
|
||||
options store.Options
|
||||
|
||||
lockGlob sync.RWMutex
|
||||
lockEvicList sync.RWMutex // Read operation will modify the eviction list
|
||||
}
|
||||
|
||||
type storeRecord struct {
|
||||
Key string
|
||||
Value []byte
|
||||
Metadata map[string]interface{}
|
||||
Expiry time.Duration
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
var targetContextKey contextKey
|
||||
|
||||
// Prepare a context to be used with the memory implementation. The context
|
||||
// is used to set up custom parameters to the specific implementation.
|
||||
// In this case, you can configure the maximum capacity for the MemStore
|
||||
// implementation as shown below.
|
||||
// ```
|
||||
// cache := NewMemStore(
|
||||
// store.WithContext(
|
||||
// NewContext(
|
||||
// ctx,
|
||||
// map[string]interface{}{
|
||||
// "maxCap": 50,
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// ```
|
||||
//
|
||||
// Available options for the MemStore are:
|
||||
// * "maxCap" -> 512 (int) The maximum number of elements the cache will hold.
|
||||
// Adding additional elements will remove old elements to ensure we aren't over
|
||||
// the maximum capacity.
|
||||
//
|
||||
// For convenience, this can also be used for the MultiMemStore.
|
||||
func NewContext(ctx context.Context, storeParams map[string]interface{}) context.Context {
|
||||
return context.WithValue(ctx, targetContextKey, storeParams)
|
||||
}
|
||||
|
||||
// Create a new MemStore instance
|
||||
func NewMemStore(opts ...store.Option) store.Store {
|
||||
m := &MemStore{}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
|
||||
// Get the maximum capacity configured. If no maxCap has been configured
|
||||
// (via `NewContext`), 512 will be used as maxCap.
|
||||
func (m *MemStore) getMaxCap() int {
|
||||
maxCap := 512
|
||||
|
||||
ctx := m.options.Context
|
||||
if ctx == nil {
|
||||
return maxCap
|
||||
}
|
||||
|
||||
ctxValue := ctx.Value(targetContextKey)
|
||||
if ctxValue == nil {
|
||||
return maxCap
|
||||
}
|
||||
additionalOpts := ctxValue.(map[string]interface{})
|
||||
|
||||
confCap, exists := additionalOpts["maxCap"]
|
||||
if exists {
|
||||
maxCap = confCap.(int)
|
||||
}
|
||||
return maxCap
|
||||
}
|
||||
|
||||
// Initialize the MemStore. If the MemStore was used, this will reset
|
||||
// all the internal structures and the new options (passed as parameters)
|
||||
// will be used.
|
||||
func (m *MemStore) Init(opts ...store.Option) error {
|
||||
optList := store.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&optList)
|
||||
}
|
||||
|
||||
m.lockGlob.Lock()
|
||||
defer m.lockGlob.Unlock()
|
||||
|
||||
m.preRadix = radix.New()
|
||||
m.sufRadix = radix.New()
|
||||
m.evictionList = list.New()
|
||||
m.options = optList
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the options being used
|
||||
func (m *MemStore) Options() store.Options {
|
||||
m.lockGlob.RLock()
|
||||
defer m.lockGlob.RUnlock()
|
||||
|
||||
return m.options
|
||||
}
|
||||
|
||||
// Write the record in the MemStore.
|
||||
// Note that Database and Table options will be ignored.
|
||||
// Expiration options will take the following precedence:
|
||||
// TTL option > expiration option > TTL record
|
||||
//
|
||||
// New elements will take the last position in the eviction list. Updating
|
||||
// an element will also move the element to the last position.
|
||||
//
|
||||
// Although not recommended, new elements might be inserted with an
|
||||
// already-expired date
|
||||
func (m *MemStore) Write(r *store.Record, opts ...store.WriteOption) error {
|
||||
var element *list.Element
|
||||
|
||||
wopts := store.WriteOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&wopts)
|
||||
}
|
||||
cRecord := toStoreRecord(r, wopts)
|
||||
|
||||
m.lockGlob.Lock()
|
||||
defer m.lockGlob.Unlock()
|
||||
|
||||
ele, exists := m.preRadix.Get(cRecord.Key)
|
||||
if exists {
|
||||
element = ele.(*list.Element)
|
||||
element.Value = cRecord
|
||||
|
||||
m.evictionList.MoveToBack(element)
|
||||
} else {
|
||||
if m.evictionList.Len() >= m.getMaxCap() {
|
||||
elementToDelete := m.evictionList.Front()
|
||||
if elementToDelete != nil {
|
||||
recordToDelete := elementToDelete.Value.(*storeRecord)
|
||||
_, _ = m.preRadix.Delete(recordToDelete.Key)
|
||||
_, _ = m.sufRadix.Delete(recordToDelete.Key)
|
||||
m.evictionList.Remove(elementToDelete)
|
||||
}
|
||||
}
|
||||
element = m.evictionList.PushBack(cRecord)
|
||||
_, _ = m.preRadix.Insert(cRecord.Key, element)
|
||||
_, _ = m.sufRadix.Insert(reverseString(cRecord.Key), element)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the key from the MemStore. A list of records will be returned even if
|
||||
// you're asking for the exact key (only one record is expected in that case).
|
||||
//
|
||||
// Reading the exact element will move such element to the last position of
|
||||
// the eviction list. This WON'T apply for prefix and / or suffix reads.
|
||||
//
|
||||
// This method guarantees that no expired element will be returned. For the
|
||||
// case of exact read, the element will be removed and a "not found" error
|
||||
// will be returned.
|
||||
// For prefix and suffix reads, all the elements that we traverse through
|
||||
// will be removed. This includes the elements we need to skip as well as
|
||||
// the elements that might have gotten into the the result. Note that the
|
||||
// elements that are over the limit won't be touched
|
||||
//
|
||||
// All read options are supported except Database and Table.
|
||||
//
|
||||
// For prefix and prefix+suffix options, the records will be returned in
|
||||
// alphabetical order on the keys.
|
||||
// For the suffix option (just suffix, no prefix), the records will be
|
||||
// returned in alphabetical order after reversing the keys. This means,
|
||||
// reverse all the keys and then sort them alphabetically. This just affects
|
||||
// the sorting order; the keys will be returned as expected.
|
||||
// This means that ["aboz", "caaz", "ziuz"] will be sorted as ["caaz", "aboz", "ziuz"]
|
||||
// for the key "z" as suffix.
|
||||
//
|
||||
// Note that offset are supported but not recommended. There is no direct access
|
||||
// to the record X. We'd need to skip all the records until we reach the specified
|
||||
// offset, which could be problematic.
|
||||
// Performance for prefix and suffix searches should be good assuming we limit
|
||||
// the number of results we need to return.
|
||||
func (m *MemStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
var element *list.Element
|
||||
|
||||
ropts := store.ReadOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&ropts)
|
||||
}
|
||||
|
||||
if !ropts.Prefix && !ropts.Suffix {
|
||||
m.lockGlob.RLock()
|
||||
ele, exists := m.preRadix.Get(key)
|
||||
if !exists {
|
||||
m.lockGlob.RUnlock()
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
|
||||
element = ele.(*list.Element)
|
||||
record := element.Value.(*storeRecord)
|
||||
if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) {
|
||||
// record expired -> need to delete
|
||||
m.lockGlob.RUnlock()
|
||||
m.lockGlob.Lock()
|
||||
defer m.lockGlob.Unlock()
|
||||
|
||||
m.evictionList.Remove(element)
|
||||
_, _ = m.preRadix.Delete(key)
|
||||
_, _ = m.sufRadix.Delete(reverseString(key))
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
|
||||
m.lockEvicList.Lock()
|
||||
m.evictionList.MoveToBack(element)
|
||||
m.lockEvicList.Unlock()
|
||||
|
||||
foundRecords := []*store.Record{
|
||||
fromStoreRecord(record),
|
||||
}
|
||||
m.lockGlob.RUnlock()
|
||||
|
||||
return foundRecords, nil
|
||||
}
|
||||
|
||||
records := []*store.Record{}
|
||||
expiredElements := make(map[string]*list.Element)
|
||||
|
||||
m.lockGlob.RLock()
|
||||
if ropts.Prefix && ropts.Suffix {
|
||||
// if we need to check both prefix and suffix, go through the
|
||||
// prefix tree and skip elements without the right suffix. We
|
||||
// don't need to check the suffix tree because the elements
|
||||
// must be in both trees
|
||||
m.preRadix.WalkPrefix(key, m.radixTreeCallBackCheckSuffix(ropts.Offset, ropts.Limit, key, &records, expiredElements))
|
||||
} else {
|
||||
if ropts.Prefix {
|
||||
m.preRadix.WalkPrefix(key, m.radixTreeCallBack(ropts.Offset, ropts.Limit, &records, expiredElements))
|
||||
}
|
||||
if ropts.Suffix {
|
||||
m.sufRadix.WalkPrefix(reverseString(key), m.radixTreeCallBack(ropts.Offset, ropts.Limit, &records, expiredElements))
|
||||
}
|
||||
}
|
||||
m.lockGlob.RUnlock()
|
||||
|
||||
// if there are expired elements, get a write lock and delete the expired elements
|
||||
if len(expiredElements) > 0 {
|
||||
m.lockGlob.Lock()
|
||||
for key, element := range expiredElements {
|
||||
m.evictionList.Remove(element)
|
||||
_, _ = m.preRadix.Delete(key)
|
||||
_, _ = m.sufRadix.Delete(reverseString(key))
|
||||
}
|
||||
m.lockGlob.Unlock()
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// Remove the record based on the key. It won't return any error if it's missing
|
||||
//
|
||||
// Database and Table options aren't supported
|
||||
func (m *MemStore) Delete(key string, opts ...store.DeleteOption) error {
|
||||
m.lockGlob.Lock()
|
||||
defer m.lockGlob.Unlock()
|
||||
|
||||
ele, exists := m.preRadix.Get(key)
|
||||
if exists {
|
||||
element := ele.(*list.Element)
|
||||
m.evictionList.Remove(element)
|
||||
_, _ = m.preRadix.Delete(key)
|
||||
_, _ = m.sufRadix.Delete(reverseString(key))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List the keys currently used in the MemStore
|
||||
//
|
||||
// All options are supported except Database and Table
|
||||
//
|
||||
// For prefix and prefix+suffix options, the keys will be returned in
|
||||
// alphabetical order.
|
||||
// For the suffix option (just suffix, no prefix), the keys will be
|
||||
// returned in alphabetical order after reversing the keys. This means,
|
||||
// reverse all the keys and then sort them alphabetically. This just affects
|
||||
// the sorting order; the keys will be returned as expected.
|
||||
// This means that ["aboz", "caaz", "ziuz"] will be sorted as ["caaz", "aboz", "ziuz"]
|
||||
func (m *MemStore) List(opts ...store.ListOption) ([]string, error) {
|
||||
records := []string{}
|
||||
expiredElements := make(map[string]*list.Element)
|
||||
|
||||
lopts := store.ListOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&lopts)
|
||||
}
|
||||
|
||||
if lopts.Prefix == "" && lopts.Suffix == "" {
|
||||
m.lockGlob.RLock()
|
||||
m.preRadix.Walk(m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements))
|
||||
m.lockGlob.RUnlock()
|
||||
|
||||
// if there are expired elements, get a write lock and delete the expired elements
|
||||
if len(expiredElements) > 0 {
|
||||
m.lockGlob.Lock()
|
||||
for key, element := range expiredElements {
|
||||
m.evictionList.Remove(element)
|
||||
_, _ = m.preRadix.Delete(key)
|
||||
_, _ = m.sufRadix.Delete(reverseString(key))
|
||||
}
|
||||
m.lockGlob.Unlock()
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
m.lockGlob.RLock()
|
||||
if lopts.Prefix != "" && lopts.Suffix != "" {
|
||||
// if we need to check both prefix and suffix, go through the
|
||||
// prefix tree and skip elements without the right suffix. We
|
||||
// don't need to check the suffix tree because the elements
|
||||
// must be in both trees
|
||||
m.preRadix.WalkPrefix(lopts.Prefix, m.radixTreeCallBackKeysOnlyWithSuffix(lopts.Offset, lopts.Limit, lopts.Suffix, &records, expiredElements))
|
||||
} else {
|
||||
if lopts.Prefix != "" {
|
||||
m.preRadix.WalkPrefix(lopts.Prefix, m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements))
|
||||
}
|
||||
if lopts.Suffix != "" {
|
||||
m.sufRadix.WalkPrefix(reverseString(lopts.Suffix), m.radixTreeCallBackKeysOnly(lopts.Offset, lopts.Limit, &records, expiredElements))
|
||||
}
|
||||
}
|
||||
m.lockGlob.RUnlock()
|
||||
|
||||
// if there are expired elements, get a write lock and delete the expired elements
|
||||
if len(expiredElements) > 0 {
|
||||
m.lockGlob.Lock()
|
||||
for key, element := range expiredElements {
|
||||
m.evictionList.Remove(element)
|
||||
_, _ = m.preRadix.Delete(key)
|
||||
_, _ = m.sufRadix.Delete(reverseString(key))
|
||||
}
|
||||
m.lockGlob.Unlock()
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (m *MemStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemStore) String() string {
|
||||
return "RadixMemStore"
|
||||
}
|
||||
|
||||
func (m *MemStore) Len() (int, bool) {
|
||||
eLen := m.evictionList.Len()
|
||||
pLen := m.preRadix.Len()
|
||||
sLen := m.sufRadix.Len()
|
||||
if eLen == pLen && eLen == sLen {
|
||||
return eLen, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (m *MemStore) radixTreeCallBack(offset, limit uint, result *[]*store.Record, expiredElements map[string]*list.Element) radix.WalkFn {
|
||||
currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
*maxIndex = offset + limit
|
||||
return func(key string, value interface{}) bool {
|
||||
element := value.(*list.Element)
|
||||
record := element.Value.(*storeRecord)
|
||||
|
||||
if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) {
|
||||
// record has expired -> add element to the expiredElements map
|
||||
// and jump directly to the next element without increasing the index
|
||||
expiredElements[record.Key] = element
|
||||
return false
|
||||
}
|
||||
|
||||
if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) {
|
||||
// if it's within expected range, add a copy to the results
|
||||
*result = append(*result, fromStoreRecord(record))
|
||||
}
|
||||
|
||||
*currentIndex++
|
||||
|
||||
if *currentIndex < *maxIndex || *maxIndex == offset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemStore) radixTreeCallBackCheckSuffix(offset, limit uint, presuf string, result *[]*store.Record, expiredElements map[string]*list.Element) radix.WalkFn {
|
||||
currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
*maxIndex = offset + limit
|
||||
return func(key string, value interface{}) bool {
|
||||
if !strings.HasSuffix(key, presuf) {
|
||||
return false
|
||||
}
|
||||
|
||||
element := value.(*list.Element)
|
||||
record := element.Value.(*storeRecord)
|
||||
|
||||
if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) {
|
||||
// record has expired -> add element to the expiredElements map
|
||||
// and jump directly to the next element without increasing the index
|
||||
expiredElements[record.Key] = element
|
||||
return false
|
||||
}
|
||||
|
||||
if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) {
|
||||
*result = append(*result, fromStoreRecord(record))
|
||||
}
|
||||
|
||||
*currentIndex++
|
||||
|
||||
if *currentIndex < *maxIndex || *maxIndex == offset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemStore) radixTreeCallBackKeysOnly(offset, limit uint, result *[]string, expiredElements map[string]*list.Element) radix.WalkFn {
|
||||
currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
*maxIndex = offset + limit
|
||||
return func(key string, value interface{}) bool {
|
||||
element := value.(*list.Element)
|
||||
record := element.Value.(*storeRecord)
|
||||
|
||||
if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) {
|
||||
// record has expired -> add element to the expiredElements map
|
||||
// and jump directly to the next element without increasing the index
|
||||
expiredElements[record.Key] = element
|
||||
return false
|
||||
}
|
||||
|
||||
if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) {
|
||||
*result = append(*result, record.Key)
|
||||
}
|
||||
|
||||
*currentIndex++
|
||||
|
||||
if *currentIndex < *maxIndex || *maxIndex == offset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemStore) radixTreeCallBackKeysOnlyWithSuffix(offset, limit uint, presuf string, result *[]string, expiredElements map[string]*list.Element) radix.WalkFn {
|
||||
currentIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
maxIndex := new(uint) // needs to be a pointer so the value persist across callback calls
|
||||
*maxIndex = offset + limit
|
||||
return func(key string, value interface{}) bool {
|
||||
if !strings.HasSuffix(key, presuf) {
|
||||
return false
|
||||
}
|
||||
|
||||
element := value.(*list.Element)
|
||||
record := element.Value.(*storeRecord)
|
||||
|
||||
if record.Expiry != 0 && record.ExpiresAt.Before(time.Now()) {
|
||||
// record has expired -> add element to the expiredElements map
|
||||
// and jump directly to the next element without increasing the index
|
||||
expiredElements[record.Key] = element
|
||||
return false
|
||||
}
|
||||
|
||||
if *currentIndex >= offset && (*currentIndex < *maxIndex || *maxIndex == offset) {
|
||||
*result = append(*result, record.Key)
|
||||
}
|
||||
|
||||
*currentIndex++
|
||||
|
||||
if *currentIndex < *maxIndex || *maxIndex == offset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// In-memory store implementation using multiple MemStore to provide support
|
||||
// for multiple databases and tables.
|
||||
// Each table will be mapped to its own MemStore, which will be completely
|
||||
// isolated from the rest. In particular, each MemStore will have its own
|
||||
// capacity, so it's possible to have 10 MemStores with full capacity (512
|
||||
// by default)
|
||||
//
|
||||
// The options will be the same for all MemStores unless they're explicitly
|
||||
// initialized otherwise.
|
||||
//
|
||||
// Since each MemStore is isolated, the required synchronization caused by
|
||||
// concurrency will be minimal if the threads use different tables
|
||||
type MultiMemStore struct {
|
||||
storeMap map[string]*MemStore
|
||||
storeMapLock sync.RWMutex
|
||||
genOpts []store.Option
|
||||
}
|
||||
|
||||
// Create a new MultiMemStore. A new MemStore will be mapped based on the options.
|
||||
// A default MemStore will be mapped if no Database and Table aren't used.
|
||||
func NewMultiMemStore(opts ...store.Option) store.Store {
|
||||
m := &MultiMemStore{
|
||||
storeMap: make(map[string]*MemStore),
|
||||
genOpts: opts,
|
||||
}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultiMemStore) getMemStore(prefix string) *MemStore {
|
||||
m.storeMapLock.RLock()
|
||||
mStore, exists := m.storeMap[prefix]
|
||||
|
||||
if exists {
|
||||
m.storeMapLock.RUnlock()
|
||||
return mStore
|
||||
}
|
||||
|
||||
m.storeMapLock.RUnlock()
|
||||
|
||||
// if not exists
|
||||
newStore := NewMemStore(m.genOpts...).(*MemStore)
|
||||
|
||||
m.storeMapLock.Lock()
|
||||
m.storeMap[prefix] = newStore
|
||||
m.storeMapLock.Unlock()
|
||||
return newStore
|
||||
}
|
||||
|
||||
// Initialize the mapped MemStore based on the Database and Table values
|
||||
// from the options with the same options. The target MemStore will be
|
||||
// reinitialized if needed.
|
||||
func (m *MultiMemStore) Init(opts ...store.Option) error {
|
||||
optList := store.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&optList)
|
||||
}
|
||||
|
||||
prefix := optList.Database + "/" + optList.Table
|
||||
|
||||
mStore := m.getMemStore(prefix)
|
||||
return mStore.Init(opts...)
|
||||
}
|
||||
|
||||
// Get the options used to create the MultiMemStore.
|
||||
// Specific options for each MemStore aren't available
|
||||
func (m *MultiMemStore) Options() store.Options {
|
||||
optList := store.Options{}
|
||||
for _, opt := range m.genOpts {
|
||||
opt(&optList)
|
||||
}
|
||||
return optList
|
||||
}
|
||||
|
||||
// Write the record in the target MemStore based on the Database and Table
|
||||
// values from the options. A default MemStore will be used if no Database
|
||||
// and Table options are provided.
|
||||
// The write options will be forwarded to the target MemStore
|
||||
func (m *MultiMemStore) Write(r *store.Record, opts ...store.WriteOption) error {
|
||||
wopts := store.WriteOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&wopts)
|
||||
}
|
||||
|
||||
prefix := wopts.Database + "/" + wopts.Table
|
||||
|
||||
mStore := m.getMemStore(prefix)
|
||||
return mStore.Write(r, opts...)
|
||||
}
|
||||
|
||||
// Read the matching records in the target MemStore based on the Database and Table
|
||||
// values from the options. A default MemStore will be used if no Database
|
||||
// and Table options are provided.
|
||||
// The read options will be forwarded to the target MemStore.
|
||||
//
|
||||
// The expectations regarding the results (sort order, eviction policies, etc)
|
||||
// will be the same as the target MemStore
|
||||
func (m *MultiMemStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
ropts := store.ReadOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&ropts)
|
||||
}
|
||||
|
||||
prefix := ropts.Database + "/" + ropts.Table
|
||||
|
||||
mStore := m.getMemStore(prefix)
|
||||
return mStore.Read(key, opts...)
|
||||
}
|
||||
|
||||
// Delete the matching records in the target MemStore based on the Database and Table
|
||||
// values from the options. A default MemStore will be used if no Database
|
||||
// and Table options are provided.
|
||||
//
|
||||
// Matching records from other Tables won't be affected. In fact, we won't
|
||||
// access to other Tables
|
||||
func (m *MultiMemStore) Delete(key string, opts ...store.DeleteOption) error {
|
||||
dopts := store.DeleteOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&dopts)
|
||||
}
|
||||
|
||||
prefix := dopts.Database + "/" + dopts.Table
|
||||
|
||||
mStore := m.getMemStore(prefix)
|
||||
return mStore.Delete(key, opts...)
|
||||
}
|
||||
|
||||
// List the keys in the target MemStore based on the Database and Table
|
||||
// values from the options. A default MemStore will be used if no Database
|
||||
// and Table options are provided.
|
||||
// The list options will be forwarded to the target MemStore.
|
||||
func (m *MultiMemStore) List(opts ...store.ListOption) ([]string, error) {
|
||||
lopts := store.ListOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&lopts)
|
||||
}
|
||||
|
||||
prefix := lopts.Database + "/" + lopts.Table
|
||||
|
||||
mStore := m.getMemStore(prefix)
|
||||
return mStore.List(opts...)
|
||||
}
|
||||
|
||||
func (m *MultiMemStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MultiMemStore) String() string {
|
||||
return "MultiRadixMemStore"
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
func TestWriteReadTables(t *testing.T) {
|
||||
cache := NewMultiMemStore()
|
||||
|
||||
record1 := &store.Record{
|
||||
Key: "sameKey",
|
||||
Value: []byte("from record1"),
|
||||
}
|
||||
record2 := &store.Record{
|
||||
Key: "sameKey",
|
||||
Value: []byte("from record2"),
|
||||
}
|
||||
|
||||
_ = cache.Write(record1)
|
||||
_ = cache.Write(record2, store.WriteTo("DB02", "Table02"))
|
||||
|
||||
records1, _ := cache.Read("sameKey")
|
||||
if len(records1) != 1 {
|
||||
t.Fatalf("Wrong number of records, expected 1, got %d", len(records1))
|
||||
}
|
||||
if records1[0].Key != "sameKey" {
|
||||
t.Errorf("Wrong key, expected \"sameKey\", got %s", records1[0].Key)
|
||||
}
|
||||
if string(records1[0].Value) != "from record1" {
|
||||
t.Errorf("Wrong value, expected \"from record1\", got %s", string(records1[0].Value))
|
||||
}
|
||||
|
||||
records2, _ := cache.Read("sameKey", store.ReadFrom("DB02", "Table02"))
|
||||
if len(records2) != 1 {
|
||||
t.Fatalf("Wrong number of records, expected 1, got %d", len(records2))
|
||||
}
|
||||
if records2[0].Key != "sameKey" {
|
||||
t.Errorf("Wrong key, expected \"sameKey\", got %s", records2[0].Key)
|
||||
}
|
||||
if string(records2[0].Value) != "from record2" {
|
||||
t.Errorf("Wrong value, expected \"from record2\", got %s", string(records2[0].Value))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteTables(t *testing.T) {
|
||||
cache := NewMultiMemStore()
|
||||
|
||||
record1 := &store.Record{
|
||||
Key: "sameKey",
|
||||
Value: []byte("from record1"),
|
||||
}
|
||||
record2 := &store.Record{
|
||||
Key: "sameKey",
|
||||
Value: []byte("from record2"),
|
||||
}
|
||||
|
||||
_ = cache.Write(record1)
|
||||
_ = cache.Write(record2, store.WriteTo("DB02", "Table02"))
|
||||
|
||||
records1, _ := cache.Read("sameKey")
|
||||
if len(records1) != 1 {
|
||||
t.Fatalf("Wrong number of records, expected 1, got %d", len(records1))
|
||||
}
|
||||
if records1[0].Key != "sameKey" {
|
||||
t.Errorf("Wrong key, expected \"sameKey\", got %s", records1[0].Key)
|
||||
}
|
||||
if string(records1[0].Value) != "from record1" {
|
||||
t.Errorf("Wrong value, expected \"from record1\", got %s", string(records1[0].Value))
|
||||
}
|
||||
|
||||
records2, _ := cache.Read("sameKey", store.ReadFrom("DB02", "Table02"))
|
||||
if len(records2) != 1 {
|
||||
t.Fatalf("Wrong number of records, expected 1, got %d", len(records2))
|
||||
}
|
||||
if records2[0].Key != "sameKey" {
|
||||
t.Errorf("Wrong key, expected \"sameKey\", got %s", records2[0].Key)
|
||||
}
|
||||
if string(records2[0].Value) != "from record2" {
|
||||
t.Errorf("Wrong value, expected \"from record2\", got %s", string(records2[0].Value))
|
||||
}
|
||||
|
||||
_ = cache.Delete("sameKey")
|
||||
if _, err := cache.Read("sameKey"); err != store.ErrNotFound {
|
||||
t.Errorf("Key \"sameKey\" still exists after deletion")
|
||||
}
|
||||
|
||||
records2, _ = cache.Read("sameKey", store.ReadFrom("DB02", "Table02"))
|
||||
if len(records2) != 1 {
|
||||
t.Fatalf("Wrong number of records, expected 1, got %d", len(records2))
|
||||
}
|
||||
if records2[0].Key != "sameKey" {
|
||||
t.Errorf("Wrong key, expected \"sameKey\", got %s", records2[0].Key)
|
||||
}
|
||||
if string(records2[0].Value) != "from record2" {
|
||||
t.Errorf("Wrong value, expected \"from record2\", got %s", string(records2[0].Value))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTables(t *testing.T) {
|
||||
cache := NewMultiMemStore()
|
||||
|
||||
record1 := &store.Record{
|
||||
Key: "key001",
|
||||
Value: []byte("from record1"),
|
||||
}
|
||||
record2 := &store.Record{
|
||||
Key: "key002",
|
||||
Value: []byte("from record2"),
|
||||
}
|
||||
|
||||
_ = cache.Write(record1)
|
||||
_ = cache.Write(record2, store.WriteTo("DB02", "Table02"))
|
||||
|
||||
keys, _ := cache.List(store.ListFrom("DB02", "Table02"))
|
||||
expectedKeys := []string{"key002"}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("Wrong number of keys, expected 1, got %d", len(keys))
|
||||
}
|
||||
for index, key := range keys {
|
||||
if expectedKeys[index] != key {
|
||||
t.Errorf("Wrong key for index %d, expected %s, got %s", index, expectedKeys[index], key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSizeLimit(t *testing.T) {
|
||||
cache := NewMultiMemStore(
|
||||
store.WithContext(
|
||||
NewContext(
|
||||
context.Background(),
|
||||
map[string]interface{}{
|
||||
"maxCap": 2,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
record := &store.Record{}
|
||||
for i := 0; i < 4; i++ {
|
||||
v := strconv.Itoa(i)
|
||||
record.Key = v
|
||||
record.Value = []byte(v)
|
||||
_ = cache.Write(record)
|
||||
_ = cache.Write(record, store.WriteTo("DB02", "Table02"))
|
||||
}
|
||||
|
||||
keys1, _ := cache.List()
|
||||
expectedKeys1 := []string{"2", "3"}
|
||||
if len(keys1) != 2 {
|
||||
t.Fatalf("Wrong number of keys, expected 2, got %d", len(keys1))
|
||||
}
|
||||
for index, key := range keys1 {
|
||||
if expectedKeys1[index] != key {
|
||||
t.Errorf("Wrong key for index %d, expected %s, got %s", index, expectedKeys1[index], key)
|
||||
}
|
||||
}
|
||||
|
||||
keys2, _ := cache.List(store.ListFrom("DB02", "Table02"))
|
||||
expectedKeys2 := []string{"2", "3"}
|
||||
if len(keys2) != 2 {
|
||||
t.Fatalf("Wrong number of keys, expected 2, got %d", len(keys2))
|
||||
}
|
||||
for index, key := range keys2 {
|
||||
if expectedKeys2[index] != key {
|
||||
t.Errorf("Wrong key for index %d, expected %s, got %s", index, expectedKeys2[index], key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
func toStoreRecord(src *store.Record, options store.WriteOptions) *storeRecord {
|
||||
newRecord := &storeRecord{}
|
||||
newRecord.Key = src.Key
|
||||
newRecord.Value = make([]byte, len(src.Value))
|
||||
copy(newRecord.Value, src.Value)
|
||||
|
||||
// set base ttl duration and expiration time based on the record
|
||||
newRecord.Expiry = src.Expiry
|
||||
if src.Expiry != 0 {
|
||||
newRecord.ExpiresAt = time.Now().Add(src.Expiry)
|
||||
}
|
||||
|
||||
// overwrite ttl duration and expiration time based on options
|
||||
if !options.Expiry.IsZero() {
|
||||
// options.Expiry is a time.Time, newRecord.Expiry is a time.Duration
|
||||
newRecord.Expiry = time.Until(options.Expiry)
|
||||
newRecord.ExpiresAt = options.Expiry
|
||||
}
|
||||
|
||||
// TTL option takes precedence over expiration time
|
||||
if options.TTL != 0 {
|
||||
newRecord.Expiry = options.TTL
|
||||
newRecord.ExpiresAt = time.Now().Add(options.TTL)
|
||||
}
|
||||
|
||||
newRecord.Metadata = make(map[string]interface{})
|
||||
for k, v := range src.Metadata {
|
||||
newRecord.Metadata[k] = v
|
||||
}
|
||||
return newRecord
|
||||
}
|
||||
|
||||
func fromStoreRecord(src *storeRecord) *store.Record {
|
||||
newRecord := &store.Record{}
|
||||
newRecord.Key = src.Key
|
||||
newRecord.Value = make([]byte, len(src.Value))
|
||||
copy(newRecord.Value, src.Value)
|
||||
if src.Expiry != 0 {
|
||||
newRecord.Expiry = time.Until(src.ExpiresAt)
|
||||
}
|
||||
|
||||
newRecord.Metadata = make(map[string]interface{})
|
||||
for k, v := range src.Metadata {
|
||||
newRecord.Metadata[k] = v
|
||||
}
|
||||
return newRecord
|
||||
}
|
||||
|
||||
func reverseString(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
Reference in New Issue
Block a user