Use the opencloud reva from now on

This commit is contained in:
André Duffeck
2025-01-21 11:16:38 +01:00
parent 2c1afafb35
commit e8d35e1280
1007 changed files with 2988 additions and 27822 deletions
+532
View File
@@ -0,0 +1,532 @@
package etcd
import (
"context"
"encoding/json"
"strings"
"time"
"go-micro.dev/v4/store"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
)
const (
prefixNS = ".prefix"
suffixNS = ".suffix"
)
// Store is a store implementation which uses etcd to store the data
type Store struct {
options store.Options
client *clientv3.Client
}
// NewStore creates a new go-micro store backed by etcd
func NewStore(opts ...store.Option) store.Store {
es := &Store{}
_ = es.Init(opts...)
return es
}
func (es *Store) getCtx() (context.Context, context.CancelFunc) {
currentCtx := es.options.Context
if currentCtx == nil {
currentCtx = context.TODO()
}
ctx, cancel := context.WithTimeout(currentCtx, 10*time.Second)
return ctx, cancel
}
// Setup the etcd client based on the current options. The old client (if any)
// will be closed.
// Currently, only the etcd nodes are configurable. If no node is provided,
// it will use the "127.0.0.1:2379" node.
// Context timeout is setup to 10 seconds, and dial timeout to 2 seconds
func (es *Store) setupClient() {
if es.client != nil {
es.client.Close()
}
endpoints := []string{"127.0.0.1:2379"}
if len(es.options.Nodes) > 0 {
endpoints = es.options.Nodes
}
cli, _ := clientv3.New(clientv3.Config{
DialTimeout: 2 * time.Second,
Endpoints: endpoints,
})
es.client = cli
}
// Init initializes the go-micro store implementation.
// Currently, only the nodes are configurable, the rest of the options
// will be ignored.
func (es *Store) Init(opts ...store.Option) error {
optList := store.Options{}
for _, opt := range opts {
opt(&optList)
}
es.options = optList
es.setupClient()
return nil
}
// Options returns the store options
func (es *Store) Options() store.Options {
return es.options
}
// Get the effective TTL, as int64 number of seconds. It will prioritize
// the TTL set in the options, then the expiry time in the options, and
// finally the one set as part of the record
func getEffectiveTTL(r *store.Record, opts store.WriteOptions) int64 {
// set base ttl duration and expiration time based on the record
duration := r.Expiry
// overwrite ttl duration and expiration time based on options
if !opts.Expiry.IsZero() {
// options.Expiry is a time.Time, newRecord.Expiry is a time.Duration
duration = time.Until(opts.Expiry)
}
// TTL option takes precedence over expiration time
if opts.TTL != 0 {
duration = opts.TTL
}
// use milliseconds because it returns an int64 instead of a float64
return duration.Milliseconds() / 1000
}
// Write the record into the etcd. The record will be duplicated in order to
// find it by prefix or by suffix. This means that it will take double space.
// Note that this is an implementation detail and it will be handled
// transparently.
//
// Database and Table options will be used to provide a different prefix to
// the key. Each service using this store should use a different database+table
// combination in order to prevent key collisions.
//
// Due to how TTLs are implemented in etcd, the minimum valid TTL seems to
// be 2 secs. Using lower values or even negative values will force the etcd
// server to use the minimum value instead.
// In addition, getting a lease for the TTL and attach it to the target key
// are 2 different operations that can't be sent as part of a transaction.
// This means that it's possible to get a lease and have that lease expire
// before attaching it to the key. Errors are expected to happen if this is
// the case, and no key will be inserted.
// According to etcd documentation, the key is guaranteed to be available
// AT LEAST the TTL duration. This means that the key might be available for
// a longer period of time in special circumstances.
//
// It's recommended to use a minimum TTL of 10 secs or higher (or not to use
// TTL) in order to prevent problematic scenarios.
func (es *Store) Write(r *store.Record, opts ...store.WriteOption) error {
wopts := store.WriteOptions{}
for _, opt := range opts {
opt(&wopts)
}
prefix := buildPrefix(wopts.Database, wopts.Table, prefixNS)
suffix := buildPrefix(wopts.Database, wopts.Table, suffixNS)
kv := es.client.KV
jsonRecord, err := json.Marshal(r)
if err != nil {
return err
}
jsonStringRecord := string(jsonRecord)
effectiveTTL := getEffectiveTTL(r, wopts)
var opOpts []clientv3.OpOption
if effectiveTTL != 0 {
lease := es.client.Lease
ctx, cancel := es.getCtx()
gResp, gErr := lease.Grant(ctx, getEffectiveTTL(r, wopts))
cancel()
if gErr != nil {
return gErr
}
opOpts = []clientv3.OpOption{clientv3.WithLease(gResp.ID)}
} else {
opOpts = []clientv3.OpOption{clientv3.WithLease(0)}
}
ctx, cancel := es.getCtx()
_, err = kv.Txn(ctx).Then(
clientv3.OpPut(prefix+r.Key, jsonStringRecord, opOpts...),
clientv3.OpPut(suffix+reverseString(r.Key), jsonStringRecord, opOpts...),
).Commit()
cancel()
return err
}
// Process a Get response taking into account the provided offset
func processGetResponse(resp *clientv3.GetResponse, offset int64) ([]*store.Record, error) {
result := make([]*store.Record, 0, len(resp.Kvs))
for index, kvs := range resp.Kvs {
if int64(index) < offset {
// skip entries before the offset
continue
}
value := &store.Record{}
err := json.Unmarshal(kvs.Value, value)
if err != nil {
return nil, err
}
result = append(result, value)
}
return result, nil
}
// Process a List response taking into account the provided offset.
// The reverse flag will be used to reverse the keys found. For example,
// "zyxw" will be reversed to "wxyz". This is used for suffix searches,
// where the keys are stored reversed and need to be changed
func processListResponse(resp *clientv3.GetResponse, offset int64, reverse bool) ([]string, error) {
result := make([]string, 0, len(resp.Kvs))
for index, kvs := range resp.Kvs {
if int64(index) < offset {
// skip entries before the offset
continue
}
targetKey := string(kvs.Key)
if reverse {
targetKey = reverseString(targetKey)
}
result = append(result, targetKey)
}
return result, nil
}
// Perform an exact key read and return the result
func (es *Store) directRead(kv clientv3.KV, key string) ([]*store.Record, error) {
ctx, cancel := es.getCtx()
resp, err := kv.Get(ctx, key)
cancel()
if err != nil {
return nil, err
}
if len(resp.Kvs) == 0 {
return nil, store.ErrNotFound
}
return processGetResponse(resp, 0)
}
// Perform a prefix read with limit and offset. A limit of 0 will return all
// results. Usage of offset isn't recommended because those results must still
// be fethed from the server in order to be discarded.
func (es *Store) prefixRead(kv clientv3.KV, key string, limit, offset int64) ([]*store.Record, error) {
getOptions := []clientv3.OpOption{
clientv3.WithPrefix(),
}
if limit > 0 {
getOptions = append(getOptions, clientv3.WithLimit(limit+offset))
}
ctx, cancel := es.getCtx()
resp, err := kv.Get(ctx, key, getOptions...)
cancel()
if err != nil {
return nil, err
}
return processGetResponse(resp, offset)
}
// Perform a prefix + suffix read with limit and offset. A limit of 0 will
// return all results found. Usage of this function is discouraged because
// we'll have to request a prefix search and match the suffix manually. This
// means that even with a limit = 3 and offset = 0, there is no guarantee
// we'll find all the results we need within that range, and we'll likely
// need to request more data from the server. The number of requests we need
// to perform is unknown and might cause load.
func (es *Store) prefixSuffixRead(kv clientv3.KV, prefix, suffix string, limit, offset int64) ([]*store.Record, error) {
firstKeyOut := firstKeyOutOfPrefixString(prefix)
getOptions := []clientv3.OpOption{
clientv3.WithRange(firstKeyOut),
}
if limit > 0 {
// unlikely to find all the entries we need within offset + limit
getOptions = append(getOptions, clientv3.WithLimit((limit+offset)*2))
}
var currentRecordOffset int64
result := []*store.Record{}
initialKey := prefix
keepGoing := true
for keepGoing {
ctx, cancel := es.getCtx()
resp, respErr := kv.Get(ctx, initialKey, getOptions...)
cancel()
if respErr != nil {
return nil, respErr
}
records, err := processGetResponse(resp, 0)
if err != nil {
return nil, err
}
for _, record := range records {
if !strings.HasSuffix(record.Key, suffix) {
continue
}
if currentRecordOffset < offset {
currentRecordOffset++
continue
}
if !shouldFinish(int64(len(result)), limit) {
result = append(result, record)
if shouldFinish(int64(len(result)), limit) {
break
}
}
}
if !resp.More || shouldFinish(int64(len(result)), limit) {
keepGoing = false
} else {
initialKey = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0)) // append byte 0 (nul char) to the last key
}
}
return result, nil
}
// Read records from the etcd server based in the key. Database and Table
// options are highly recommended, otherwise we'll use a default one (which
// might not have the requested keys)
//
// If no prefix or suffix option is provided, we'll read the record matching
// the provided key. Note that a list of records will be provided anyway,
// likely with only one record (the one requested)
//
// Prefix and suffix options are supported and should perform fine even with
// a large amount of data. Note that the limit option should also be included
// in order to limit the amount of records we need to fetch.
//
// Note that using both prefix and suffix options at the same time is possible
// but discouraged. A prefix search will be send to the etcd server, and from
// there we'll manually pick the records matching the suffix. This might become
// very inefficient since we might need to request more data to the etcd
// multiple times in order to provide the results asked.
// Usage of the offset option is also discouraged because we'll have to request
// records that we'll have to skip manually on our side.
//
// Don't rely on any particular order of the keys. The records are expected to
// be sorted by key except if the suffix option (suffix without prefix) is
// used. In this case, the keys will be sorted based on the reversed key
func (es *Store) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
ropts := store.ReadOptions{}
for _, opt := range opts {
opt(&ropts)
}
prefix := buildPrefix(ropts.Database, ropts.Table, prefixNS)
suffix := buildPrefix(ropts.Database, ropts.Table, suffixNS)
kv := es.client.KV
preKv := namespace.NewKV(kv, prefix)
sufKv := namespace.NewKV(kv, suffix)
if ropts.Prefix && ropts.Suffix {
return es.prefixSuffixRead(preKv, key, key, int64(ropts.Limit), int64(ropts.Offset))
}
if ropts.Prefix {
return es.prefixRead(preKv, key, int64(ropts.Limit), int64(ropts.Offset))
}
if ropts.Suffix {
return es.prefixRead(sufKv, reverseString(key), int64(ropts.Limit), int64(ropts.Offset))
}
return es.directRead(preKv, key)
}
// Delete the record containing the key provided. Database and Table
// options are highly recommended, otherwise we'll use a default one (which
// might not have the requested keys)
//
// Since the Write method inserts 2 entries for a given key, those both
// entries will also be removed using the same key. This is handled
// transparently.
func (es *Store) Delete(key string, opts ...store.DeleteOption) error {
dopts := store.DeleteOptions{}
for _, opt := range opts {
opt(&dopts)
}
prefix := buildPrefix(dopts.Database, dopts.Table, prefixNS)
suffix := buildPrefix(dopts.Database, dopts.Table, suffixNS)
kv := es.client.KV
ctx, cancel := es.getCtx()
_, err := kv.Txn(ctx).Then(
clientv3.OpDelete(prefix+key),
clientv3.OpDelete(suffix+reverseString(key)),
).Commit()
cancel()
return err
}
// List the keys based on the provided prefix. Use the empty string (and no
// limit nor offset) to list all keys available.
// Limit and offset options are available to limit the keys we need to return.
// The reverse option will reverse the keys before returning them. Use it when
// listing the keys from the suffix KV.
//
// Note that values for the keys won't be requested to the etcd server, that's
// why the reverse option is important
func (es *Store) listKeys(kv clientv3.KV, prefixKey string, limit, offset int64, reverse bool) ([]string, error) {
getOptions := []clientv3.OpOption{
clientv3.WithKeysOnly(),
clientv3.WithPrefix(),
}
if limit > 0 {
getOptions = append(getOptions, clientv3.WithLimit(limit+offset))
}
ctx, cancel := es.getCtx()
resp, err := kv.Get(ctx, prefixKey, getOptions...)
cancel()
if err != nil {
return nil, err
}
return processListResponse(resp, offset, reverse)
}
// List the keys matching both prefix and suffix, with the provided limit and
// offset. Usage of this function is discouraged because we'll have to match
// the suffix manually on our side, which means we'll likely need to perform
// additional requests to the etcd server to get more results matching all the
// requirements.
func (es *Store) prefixSuffixList(kv clientv3.KV, prefix, suffix string, limit, offset int64) ([]string, error) {
firstKeyOut := firstKeyOutOfPrefixString(prefix)
getOptions := []clientv3.OpOption{
clientv3.WithKeysOnly(),
clientv3.WithRange(firstKeyOut),
}
if firstKeyOut == "" {
// could happen of all bytes are "\xff"
getOptions = getOptions[:1] // remove the WithRange option
}
if limit > 0 {
// unlikely to find all the entries we need within offset + limit
getOptions = append(getOptions, clientv3.WithLimit((limit+offset)*2))
}
var currentRecordOffset int64
result := []string{}
initialKey := prefix
keepGoing := true
for keepGoing {
ctx, cancel := es.getCtx()
resp, respErr := kv.Get(ctx, initialKey, getOptions...)
cancel()
if respErr != nil {
return nil, respErr
}
keys, err := processListResponse(resp, 0, false)
if err != nil {
return nil, err
}
for _, key := range keys {
if !strings.HasSuffix(key, suffix) {
continue
}
if currentRecordOffset < offset {
currentRecordOffset++
continue
}
if !shouldFinish(int64(len(result)), limit) {
result = append(result, key)
if shouldFinish(int64(len(result)), limit) {
break
}
}
}
if !resp.More || shouldFinish(int64(len(result)), limit) {
keepGoing = false
} else {
initialKey = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0)) // append byte 0 (nul char) to the last key
}
}
return result, nil
}
// List the keys available in the etcd server. Database and Table
// options are highly recommended, otherwise we'll use a default one (which
// might not have the requested keys)
//
// With the Database and Table options, all the keys returned will be within
// that database and table. Each service is expected to use a different
// database + table, so using those options will list only the keys used by
// that particular service.
//
// Prefix and suffix options are available along with the limit and offset
// ones.
//
// Using prefix and suffix options at the same time is discourage because
// the suffix matching will be done on our side, and we'll likely need to
// perform multiple requests to get the requested results. Note that using
// just the suffix option is fine.
// In addition, using the offset option is also discouraged because we'll
// need to request additional keys that will be skipped on our side.
func (es *Store) List(opts ...store.ListOption) ([]string, error) {
lopts := store.ListOptions{}
for _, opt := range opts {
opt(&lopts)
}
prefix := buildPrefix(lopts.Database, lopts.Table, prefixNS)
suffix := buildPrefix(lopts.Database, lopts.Table, suffixNS)
kv := es.client.KV
preKv := namespace.NewKV(kv, prefix)
sufKv := namespace.NewKV(kv, suffix)
if lopts.Prefix != "" && lopts.Suffix != "" {
return es.prefixSuffixList(preKv, lopts.Prefix, lopts.Suffix, int64(lopts.Limit), int64(lopts.Offset))
}
if lopts.Prefix != "" {
return es.listKeys(preKv, lopts.Prefix, int64(lopts.Limit), int64(lopts.Offset), false)
}
if lopts.Suffix != "" {
return es.listKeys(sufKv, reverseString(lopts.Suffix), int64(lopts.Limit), int64(lopts.Offset), true)
}
return es.listKeys(preKv, "", int64(lopts.Limit), int64(lopts.Offset), false)
}
// Close the client
func (es *Store) Close() error {
return es.client.Close()
}
// Return the service name
func (es *Store) String() string {
return "Etcd"
}
+65
View File
@@ -0,0 +1,65 @@
package etcd
import (
"strings"
)
// Returns true if the limit isn't 0 AND is greater or equal to the number
// of results.
// If the limit is 0 or the number of items is less than the number of items,
// it will return false
func shouldFinish(numberOfResults, limit int64) bool {
if limit == 0 || numberOfResults < limit {
return false
}
return true
}
// Return the first key out of the prefix represented by the parameter,
// as a byte sequence. Note that it applies to byte sequences and not
// rune sequences, so it might be ill-suited for multi-byte chars
func firstKeyOutOfPrefix(src []byte) []byte {
dst := make([]byte, len(src))
copy(dst, src)
var i int
for i = len(dst) - 1; i >= 0; i-- {
if dst[i] < 255 {
dst[i]++
break
}
}
return dst[:i+1]
}
// Return the first key out of the prefix represented by the parameter.
// This function relies on the firstKeyOutOfPrefix one, which uses a byte
// sequence, so it might be ill-suited if the string contains multi-byte chars.
func firstKeyOutOfPrefixString(src string) string {
srcBytes := []byte(src)
dstBytes := firstKeyOutOfPrefix(srcBytes)
return string(dstBytes)
}
// Reverse the string based on the containing runes
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)
}
// Build a string based on the parts, to be used as a prefix. Empty string is
// expected if no part is passed as parameter.
// The string will contain all the parts separated by '/'. The last char will
// also be '/'
//
// For example `buildPrefix(P1, P2, P3)` will return "P1/P2/P3/"
func buildPrefix(parts ...string) string {
var b strings.Builder
for _, part := range parts {
b.WriteString(part)
b.WriteRune('/')
}
return b.String()
}
+518
View File
@@ -0,0 +1,518 @@
package memory
import (
"container/list"
"context"
"strings"
"sync"
"time"
"github.com/armon/go-radix"
"go-micro.dev/v4/store"
)
// MemStore is a 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
// NewContext prepares 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)
}
// NewMemStore creates 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
}
// Init initializes 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
}
// Options returns 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
}
// Delete removes 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
}
// Close closes the store
func (m *MemStore) Close() error {
return nil
}
// String returns the name of the store implementation
func (m *MemStore) String() string {
return "RadixMemStore"
}
// Len returns the number of items in the store
func (m *MemStore) Len() (int, bool) {
eLen := m.evictionList.Len()
pLen := m.preRadix.Len()
sLen := m.sufRadix.Len()
if eLen == pLen && pLen == 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
}
}
@@ -0,0 +1,160 @@
package memory
import (
"sync"
"go-micro.dev/v4/store"
)
// MultiMemStore is a 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
}
// NewMultiMemStore creates 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
}
// Init initializes 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...)
}
// Options returns 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...)
}
// Close closes the store
func (m *MultiMemStore) Close() error {
return nil
}
// String returns the name of the store implementation
func (m *MultiMemStore) String() string {
return "MultiRadixMemStore"
}
+63
View File
@@ -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)
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package store
import (
"context"
"time"
"go-micro.dev/v4/store"
)
type typeContextKey struct{}
// Store determines the implementation:
// - "memory", for a in-memory implementation, which is also the default if noone matches
// - "noop", for a noop store (it does nothing)
// - "etcd", for etcd
// - "nats-js" for nats-js, needs to have TTL configured at creation
// - "redis", for redis
// - "redis-sentinel", for redis-sentinel
// - "ocmem", custom in-memory implementation, with fixed size and optimized prefix
// and suffix search
func Store(val string) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, typeContextKey{}, val)
}
}
type sizeContextKey struct{}
// Size configures the maximum capacity of the cache for the "ocmem" implementation,
// in number of items that the cache can hold per table.
// You can use 5000 to make the cache hold up to 5000 elements.
// The parameter only affects to the "ocmem" implementation, the rest will ignore it.
// If an invalid value is used, the default of 512 will be used instead.
func Size(val int) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, sizeContextKey{}, val)
}
}
type ttlContextKey struct{}
// TTL is the time to live for documents stored in the store
func TTL(val time.Duration) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, ttlContextKey{}, val)
}
}
type disablePersistanceContextKey struct{}
// DisablePersistence disables the persistence of the store by instructing it to use memory only.
// Only supported by the `natsjs` and `natsjskv` implementations.
func DisablePersistence(val bool) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, disablePersistanceContextKey{}, val)
}
}
type authenticationContextKey struct{}
// Authentication configures the username and password to use for authentication.
// Only supported by the `natsjskv` implementation.
func Authentication(username, password string) store.Option {
return func(o *store.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, authenticationContextKey{}, []string{username, password})
}
}
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package store
import (
"context"
"strings"
"time"
natsjs "github.com/go-micro/plugins/v4/store/nats-js"
natsjskv "github.com/go-micro/plugins/v4/store/nats-js-kv"
"github.com/go-micro/plugins/v4/store/redis"
redisopts "github.com/go-redis/redis/v8"
"github.com/nats-io/nats.go"
"github.com/opencloud-eu/reva/v2/pkg/store/etcd"
"github.com/opencloud-eu/reva/v2/pkg/store/memory"
"go-micro.dev/v4/logger"
microstore "go-micro.dev/v4/store"
)
var ocMemStore *microstore.Store
const (
// TypeMemory represents memory stores
TypeMemory = "memory"
// TypeNoop represents noop stores
TypeNoop = "noop"
// TypeEtcd represents etcd stores
TypeEtcd = "etcd"
// TypeRedis represents redis stores
TypeRedis = "redis"
// TypeRedisSentinel represents redis-sentinel stores
TypeRedisSentinel = "redis-sentinel"
// TypeOCMem represents ocmem stores
TypeOCMem = "ocmem"
// TypeNatsJS represents nats-js stores
TypeNatsJS = "nats-js"
// TypeNatsJSKV represents nats-js-kv stores
TypeNatsJSKV = "nats-js-kv"
)
// Create initializes a new store
func Create(opts ...microstore.Option) microstore.Store {
options := &microstore.Options{
Context: context.Background(),
}
for _, o := range opts {
o(options)
}
storeType, _ := options.Context.Value(typeContextKey{}).(string)
switch storeType {
case TypeNoop:
return microstore.NewNoopStore(opts...)
case TypeEtcd:
return etcd.NewStore(opts...)
case TypeRedis:
// FIXME redis plugin does not support redis cluster or ring -> needs upstream patch or our implementation
return redis.NewStore(opts...)
case TypeRedisSentinel:
redisMaster := ""
redisNodes := []string{}
for _, node := range options.Nodes {
parts := strings.SplitN(node, "/", 2)
if len(parts) != 2 {
return nil
}
// the first node is used to retrieve the redis master
redisNodes = append(redisNodes, parts[0])
if redisMaster == "" {
redisMaster = parts[1]
}
}
return redis.NewStore(
microstore.Database(options.Database),
microstore.Table(options.Table),
microstore.Nodes(redisNodes...),
redis.WithRedisOptions(redisopts.UniversalOptions{
MasterName: redisMaster,
}),
)
case TypeOCMem:
if ocMemStore == nil {
var memStore microstore.Store
sizeNum, _ := options.Context.Value(sizeContextKey{}).(int)
if sizeNum <= 0 {
memStore = memory.NewMultiMemStore()
} else {
memStore = memory.NewMultiMemStore(
microstore.WithContext(
memory.NewContext(
context.Background(),
map[string]interface{}{
"maxCap": sizeNum,
},
)),
)
}
ocMemStore = &memStore
}
return *ocMemStore
case TypeNatsJS:
ttl, _ := options.Context.Value(ttlContextKey{}).(time.Duration)
if mem, _ := options.Context.Value(disablePersistanceContextKey{}).(bool); mem {
opts = append(opts, natsjs.DefaultMemory())
}
// TODO nats needs a DefaultTTL option as it does not support per Write TTL ...
// FIXME nats has restrictions on the key, we cannot use slashes AFAICT
// host, port, clusterid
natsOptions := nats.GetDefaultOptions()
natsOptions.Name = "TODO" // we can pass in the service name to allow identifying the client, but that requires adding a custom context option
if auth, ok := options.Context.Value(authenticationContextKey{}).([]string); ok && len(auth) == 2 {
natsOptions.User = auth[0]
natsOptions.Password = auth[1]
}
return natsjs.NewStore(
append(opts,
natsjs.NatsOptions(natsOptions), // always pass in properly initialized default nats options
natsjs.DefaultTTL(ttl))...,
) // TODO test with ocis nats
case TypeNatsJSKV:
// NOTE: nats needs a DefaultTTL option as it does not support per Write TTL ...
ttl, _ := options.Context.Value(ttlContextKey{}).(time.Duration)
if mem, _ := options.Context.Value(disablePersistanceContextKey{}).(bool); mem {
opts = append(opts, natsjskv.DefaultMemory())
}
natsOptions := nats.GetDefaultOptions()
natsOptions.Name = "TODO" // we can pass in the service name to allow identifying the client, but that requires adding a custom context option
if auth, ok := options.Context.Value(authenticationContextKey{}).([]string); ok && len(auth) == 2 {
natsOptions.User = auth[0]
natsOptions.Password = auth[1]
}
return natsjskv.NewStore(
append(opts,
natsjskv.NatsOptions(natsOptions), // always pass in properly initialized default nats options
natsjskv.EncodeKeys(),
natsjskv.DefaultTTL(ttl))...,
)
case TypeMemory, "mem", "": // allow existing short form and use as default
return microstore.NewMemoryStore(opts...)
default:
// try to log an error
if options.Logger == nil {
options.Logger = logger.DefaultLogger
}
options.Logger.Logf(logger.ErrorLevel, "unknown store type: '%s', falling back to memory", storeType)
return microstore.NewMemoryStore(opts...)
}
}