github.com/cs3org/reva/v2@v2.27.7/pkg/store/etcd/etcd.go (about)

     1  package etcd
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"strings"
     7  	"time"
     8  
     9  	"go-micro.dev/v4/store"
    10  	clientv3 "go.etcd.io/etcd/client/v3"
    11  	"go.etcd.io/etcd/client/v3/namespace"
    12  )
    13  
    14  const (
    15  	prefixNS = ".prefix"
    16  	suffixNS = ".suffix"
    17  )
    18  
    19  // Store is a store implementation which uses etcd to store the data
    20  type Store struct {
    21  	options store.Options
    22  	client  *clientv3.Client
    23  }
    24  
    25  // NewStore creates a new go-micro store backed by etcd
    26  func NewStore(opts ...store.Option) store.Store {
    27  	es := &Store{}
    28  	_ = es.Init(opts...)
    29  	return es
    30  }
    31  
    32  func (es *Store) getCtx() (context.Context, context.CancelFunc) {
    33  	currentCtx := es.options.Context
    34  	if currentCtx == nil {
    35  		currentCtx = context.TODO()
    36  	}
    37  	ctx, cancel := context.WithTimeout(currentCtx, 10*time.Second)
    38  	return ctx, cancel
    39  }
    40  
    41  // Setup the etcd client based on the current options. The old client (if any)
    42  // will be closed.
    43  // Currently, only the etcd nodes are configurable. If no node is provided,
    44  // it will use the "127.0.0.1:2379" node.
    45  // Context timeout is setup to 10 seconds, and dial timeout to 2 seconds
    46  func (es *Store) setupClient() {
    47  	if es.client != nil {
    48  		es.client.Close()
    49  	}
    50  
    51  	endpoints := []string{"127.0.0.1:2379"}
    52  	if len(es.options.Nodes) > 0 {
    53  		endpoints = es.options.Nodes
    54  	}
    55  
    56  	cli, _ := clientv3.New(clientv3.Config{
    57  		DialTimeout: 2 * time.Second,
    58  		Endpoints:   endpoints,
    59  	})
    60  
    61  	es.client = cli
    62  }
    63  
    64  // Init initializes the go-micro store implementation.
    65  // Currently, only the nodes are configurable, the rest of the options
    66  // will be ignored.
    67  func (es *Store) Init(opts ...store.Option) error {
    68  	optList := store.Options{}
    69  	for _, opt := range opts {
    70  		opt(&optList)
    71  	}
    72  
    73  	es.options = optList
    74  	es.setupClient()
    75  	return nil
    76  }
    77  
    78  // Options returns the store options
    79  func (es *Store) Options() store.Options {
    80  	return es.options
    81  }
    82  
    83  // Get the effective TTL, as int64 number of seconds. It will prioritize
    84  // the TTL set in the options, then the expiry time in the options, and
    85  // finally the one set as part of the record
    86  func getEffectiveTTL(r *store.Record, opts store.WriteOptions) int64 {
    87  	// set base ttl duration and expiration time based on the record
    88  	duration := r.Expiry
    89  
    90  	// overwrite ttl duration and expiration time based on options
    91  	if !opts.Expiry.IsZero() {
    92  		// options.Expiry is a time.Time, newRecord.Expiry is a time.Duration
    93  		duration = time.Until(opts.Expiry)
    94  	}
    95  
    96  	// TTL option takes precedence over expiration time
    97  	if opts.TTL != 0 {
    98  		duration = opts.TTL
    99  	}
   100  
   101  	// use milliseconds because it returns an int64 instead of a float64
   102  	return duration.Milliseconds() / 1000
   103  }
   104  
   105  // Write the record into the etcd. The record will be duplicated in order to
   106  // find it by prefix or by suffix. This means that it will take double space.
   107  // Note that this is an implementation detail and it will be handled
   108  // transparently.
   109  //
   110  // Database and Table options will be used to provide a different prefix to
   111  // the key. Each service using this store should use a different database+table
   112  // combination in order to prevent key collisions.
   113  //
   114  // Due to how TTLs are implemented in etcd, the minimum valid TTL seems to
   115  // be 2 secs. Using lower values or even negative values will force the etcd
   116  // server to use the minimum value instead.
   117  // In addition, getting a lease for the TTL and attach it to the target key
   118  // are 2 different operations that can't be sent as part of a transaction.
   119  // This means that it's possible to get a lease and have that lease expire
   120  // before attaching it to the key. Errors are expected to happen if this is
   121  // the case, and no key will be inserted.
   122  // According to etcd documentation, the key is guaranteed to be available
   123  // AT LEAST the TTL duration. This means that the key might be available for
   124  // a longer period of time in special circumstances.
   125  //
   126  // It's recommended to use a minimum TTL of 10 secs or higher (or not to use
   127  // TTL) in order to prevent problematic scenarios.
   128  func (es *Store) Write(r *store.Record, opts ...store.WriteOption) error {
   129  	wopts := store.WriteOptions{}
   130  	for _, opt := range opts {
   131  		opt(&wopts)
   132  	}
   133  
   134  	prefix := buildPrefix(wopts.Database, wopts.Table, prefixNS)
   135  	suffix := buildPrefix(wopts.Database, wopts.Table, suffixNS)
   136  
   137  	kv := es.client.KV
   138  
   139  	jsonRecord, err := json.Marshal(r)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	jsonStringRecord := string(jsonRecord)
   144  
   145  	effectiveTTL := getEffectiveTTL(r, wopts)
   146  	var opOpts []clientv3.OpOption
   147  
   148  	if effectiveTTL != 0 {
   149  		lease := es.client.Lease
   150  		ctx, cancel := es.getCtx()
   151  		gResp, gErr := lease.Grant(ctx, getEffectiveTTL(r, wopts))
   152  		cancel()
   153  		if gErr != nil {
   154  			return gErr
   155  		}
   156  		opOpts = []clientv3.OpOption{clientv3.WithLease(gResp.ID)}
   157  	} else {
   158  		opOpts = []clientv3.OpOption{clientv3.WithLease(0)}
   159  	}
   160  
   161  	ctx, cancel := es.getCtx()
   162  	_, err = kv.Txn(ctx).Then(
   163  		clientv3.OpPut(prefix+r.Key, jsonStringRecord, opOpts...),
   164  		clientv3.OpPut(suffix+reverseString(r.Key), jsonStringRecord, opOpts...),
   165  	).Commit()
   166  	cancel()
   167  
   168  	return err
   169  }
   170  
   171  // Process a Get response taking into account the provided offset
   172  func processGetResponse(resp *clientv3.GetResponse, offset int64) ([]*store.Record, error) {
   173  	result := make([]*store.Record, 0, len(resp.Kvs))
   174  	for index, kvs := range resp.Kvs {
   175  		if int64(index) < offset {
   176  			// skip entries before the offset
   177  			continue
   178  		}
   179  
   180  		value := &store.Record{}
   181  		err := json.Unmarshal(kvs.Value, value)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		result = append(result, value)
   186  	}
   187  	return result, nil
   188  }
   189  
   190  // Process a List response taking into account the provided offset.
   191  // The reverse flag will be used to reverse the keys found. For example,
   192  // "zyxw" will be reversed to "wxyz". This is used for suffix searches,
   193  // where the keys are stored reversed and need to be changed
   194  func processListResponse(resp *clientv3.GetResponse, offset int64, reverse bool) ([]string, error) {
   195  	result := make([]string, 0, len(resp.Kvs))
   196  	for index, kvs := range resp.Kvs {
   197  		if int64(index) < offset {
   198  			// skip entries before the offset
   199  			continue
   200  		}
   201  
   202  		targetKey := string(kvs.Key)
   203  		if reverse {
   204  			targetKey = reverseString(targetKey)
   205  		}
   206  		result = append(result, targetKey)
   207  	}
   208  	return result, nil
   209  }
   210  
   211  // Perform an exact key read and return the result
   212  func (es *Store) directRead(kv clientv3.KV, key string) ([]*store.Record, error) {
   213  	ctx, cancel := es.getCtx()
   214  	resp, err := kv.Get(ctx, key)
   215  	cancel()
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	if len(resp.Kvs) == 0 {
   221  		return nil, store.ErrNotFound
   222  	}
   223  
   224  	return processGetResponse(resp, 0)
   225  }
   226  
   227  // Perform a prefix read with limit and offset. A limit of 0 will return all
   228  // results. Usage of offset isn't recommended because those results must still
   229  // be fethed from the server in order to be discarded.
   230  func (es *Store) prefixRead(kv clientv3.KV, key string, limit, offset int64) ([]*store.Record, error) {
   231  	getOptions := []clientv3.OpOption{
   232  		clientv3.WithPrefix(),
   233  	}
   234  	if limit > 0 {
   235  		getOptions = append(getOptions, clientv3.WithLimit(limit+offset))
   236  	}
   237  
   238  	ctx, cancel := es.getCtx()
   239  	resp, err := kv.Get(ctx, key, getOptions...)
   240  	cancel()
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	return processGetResponse(resp, offset)
   245  }
   246  
   247  // Perform a prefix + suffix read with limit and offset. A limit of 0 will
   248  // return all results found. Usage of this function is discouraged because
   249  // we'll have to request a prefix search and match the suffix manually. This
   250  // means that even with a limit = 3 and offset = 0, there is no guarantee
   251  // we'll find all the results we need within that range, and we'll likely
   252  // need to request more data from the server. The number of requests we need
   253  // to perform is unknown and might cause load.
   254  func (es *Store) prefixSuffixRead(kv clientv3.KV, prefix, suffix string, limit, offset int64) ([]*store.Record, error) {
   255  	firstKeyOut := firstKeyOutOfPrefixString(prefix)
   256  	getOptions := []clientv3.OpOption{
   257  		clientv3.WithRange(firstKeyOut),
   258  	}
   259  
   260  	if limit > 0 {
   261  		// unlikely to find all the entries we need within offset + limit
   262  		getOptions = append(getOptions, clientv3.WithLimit((limit+offset)*2))
   263  	}
   264  
   265  	var currentRecordOffset int64
   266  	result := []*store.Record{}
   267  	initialKey := prefix
   268  
   269  	keepGoing := true
   270  	for keepGoing {
   271  		ctx, cancel := es.getCtx()
   272  		resp, respErr := kv.Get(ctx, initialKey, getOptions...)
   273  		cancel()
   274  		if respErr != nil {
   275  			return nil, respErr
   276  		}
   277  
   278  		records, err := processGetResponse(resp, 0)
   279  		if err != nil {
   280  			return nil, err
   281  		}
   282  		for _, record := range records {
   283  			if !strings.HasSuffix(record.Key, suffix) {
   284  				continue
   285  			}
   286  
   287  			if currentRecordOffset < offset {
   288  				currentRecordOffset++
   289  				continue
   290  			}
   291  
   292  			if !shouldFinish(int64(len(result)), limit) {
   293  				result = append(result, record)
   294  				if shouldFinish(int64(len(result)), limit) {
   295  					break
   296  				}
   297  			}
   298  		}
   299  		if !resp.More || shouldFinish(int64(len(result)), limit) {
   300  			keepGoing = false
   301  		} else {
   302  			initialKey = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0)) // append byte 0 (nul char) to the last key
   303  		}
   304  	}
   305  	return result, nil
   306  }
   307  
   308  // Read records from the etcd server based in the key. Database and Table
   309  // options are highly recommended, otherwise we'll use a default one (which
   310  // might not have the requested keys)
   311  //
   312  // If no prefix or suffix option is provided, we'll read the record matching
   313  // the provided key. Note that a list of records will be provided anyway,
   314  // likely with only one record (the one requested)
   315  //
   316  // Prefix and suffix options are supported and should perform fine even with
   317  // a large amount of data. Note that the limit option should also be included
   318  // in order to limit the amount of records we need to fetch.
   319  //
   320  // Note that using both prefix and suffix options at the same time is possible
   321  // but discouraged. A prefix search will be send to the etcd server, and from
   322  // there we'll manually pick the records matching the suffix. This might become
   323  // very inefficient since we might need to request more data to the etcd
   324  // multiple times in order to provide the results asked.
   325  // Usage of the offset option is also discouraged because we'll have to request
   326  // records that we'll have to skip manually on our side.
   327  //
   328  // Don't rely on any particular order of the keys. The records are expected to
   329  // be sorted by key except if the suffix option (suffix without prefix) is
   330  // used. In this case, the keys will be sorted based on the reversed key
   331  func (es *Store) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
   332  	ropts := store.ReadOptions{}
   333  	for _, opt := range opts {
   334  		opt(&ropts)
   335  	}
   336  
   337  	prefix := buildPrefix(ropts.Database, ropts.Table, prefixNS)
   338  	suffix := buildPrefix(ropts.Database, ropts.Table, suffixNS)
   339  
   340  	kv := es.client.KV
   341  	preKv := namespace.NewKV(kv, prefix)
   342  	sufKv := namespace.NewKV(kv, suffix)
   343  
   344  	if ropts.Prefix && ropts.Suffix {
   345  		return es.prefixSuffixRead(preKv, key, key, int64(ropts.Limit), int64(ropts.Offset))
   346  	}
   347  
   348  	if ropts.Prefix {
   349  		return es.prefixRead(preKv, key, int64(ropts.Limit), int64(ropts.Offset))
   350  	}
   351  
   352  	if ropts.Suffix {
   353  		return es.prefixRead(sufKv, reverseString(key), int64(ropts.Limit), int64(ropts.Offset))
   354  	}
   355  
   356  	return es.directRead(preKv, key)
   357  }
   358  
   359  // Delete the record containing the key provided. Database and Table
   360  // options are highly recommended, otherwise we'll use a default one (which
   361  // might not have the requested keys)
   362  //
   363  // Since the Write method inserts 2 entries for a given key, those both
   364  // entries will also be removed using the same key. This is handled
   365  // transparently.
   366  func (es *Store) Delete(key string, opts ...store.DeleteOption) error {
   367  	dopts := store.DeleteOptions{}
   368  	for _, opt := range opts {
   369  		opt(&dopts)
   370  	}
   371  
   372  	prefix := buildPrefix(dopts.Database, dopts.Table, prefixNS)
   373  	suffix := buildPrefix(dopts.Database, dopts.Table, suffixNS)
   374  
   375  	kv := es.client.KV
   376  
   377  	ctx, cancel := es.getCtx()
   378  	_, err := kv.Txn(ctx).Then(
   379  		clientv3.OpDelete(prefix+key),
   380  		clientv3.OpDelete(suffix+reverseString(key)),
   381  	).Commit()
   382  	cancel()
   383  
   384  	return err
   385  }
   386  
   387  // List the keys based on the provided prefix. Use the empty string (and no
   388  // limit nor offset) to list all keys available.
   389  // Limit and offset options are available to limit the keys we need to return.
   390  // The reverse option will reverse the keys before returning them. Use it when
   391  // listing the keys from the suffix KV.
   392  //
   393  // Note that values for the keys won't be requested to the etcd server, that's
   394  // why the reverse option is important
   395  func (es *Store) listKeys(kv clientv3.KV, prefixKey string, limit, offset int64, reverse bool) ([]string, error) {
   396  	getOptions := []clientv3.OpOption{
   397  		clientv3.WithKeysOnly(),
   398  		clientv3.WithPrefix(),
   399  	}
   400  	if limit > 0 {
   401  		getOptions = append(getOptions, clientv3.WithLimit(limit+offset))
   402  	}
   403  
   404  	ctx, cancel := es.getCtx()
   405  	resp, err := kv.Get(ctx, prefixKey, getOptions...)
   406  	cancel()
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	return processListResponse(resp, offset, reverse)
   412  }
   413  
   414  // List the keys matching both prefix and suffix, with the provided limit and
   415  // offset. Usage of this function is discouraged because we'll have to match
   416  // the suffix manually on our side, which means we'll likely need to perform
   417  // additional requests to the etcd server to get more results matching all the
   418  // requirements.
   419  func (es *Store) prefixSuffixList(kv clientv3.KV, prefix, suffix string, limit, offset int64) ([]string, error) {
   420  	firstKeyOut := firstKeyOutOfPrefixString(prefix)
   421  	getOptions := []clientv3.OpOption{
   422  		clientv3.WithKeysOnly(),
   423  		clientv3.WithRange(firstKeyOut),
   424  	}
   425  	if firstKeyOut == "" {
   426  		// could happen of all bytes are "\xff"
   427  		getOptions = getOptions[:1] // remove the WithRange option
   428  	}
   429  
   430  	if limit > 0 {
   431  		// unlikely to find all the entries we need within offset + limit
   432  		getOptions = append(getOptions, clientv3.WithLimit((limit+offset)*2))
   433  	}
   434  
   435  	var currentRecordOffset int64
   436  	result := []string{}
   437  	initialKey := prefix
   438  
   439  	keepGoing := true
   440  	for keepGoing {
   441  		ctx, cancel := es.getCtx()
   442  		resp, respErr := kv.Get(ctx, initialKey, getOptions...)
   443  		cancel()
   444  		if respErr != nil {
   445  			return nil, respErr
   446  		}
   447  
   448  		keys, err := processListResponse(resp, 0, false)
   449  		if err != nil {
   450  			return nil, err
   451  		}
   452  		for _, key := range keys {
   453  			if !strings.HasSuffix(key, suffix) {
   454  				continue
   455  			}
   456  
   457  			if currentRecordOffset < offset {
   458  				currentRecordOffset++
   459  				continue
   460  			}
   461  
   462  			if !shouldFinish(int64(len(result)), limit) {
   463  				result = append(result, key)
   464  				if shouldFinish(int64(len(result)), limit) {
   465  					break
   466  				}
   467  			}
   468  		}
   469  		if !resp.More || shouldFinish(int64(len(result)), limit) {
   470  			keepGoing = false
   471  		} else {
   472  			initialKey = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0)) // append byte 0 (nul char) to the last key
   473  		}
   474  	}
   475  	return result, nil
   476  }
   477  
   478  // List the keys available in the etcd server. Database and Table
   479  // options are highly recommended, otherwise we'll use a default one (which
   480  // might not have the requested keys)
   481  //
   482  // With the Database and Table options, all the keys returned will be within
   483  // that database and table. Each service is expected to use a different
   484  // database + table, so using those options will list only the keys used by
   485  // that particular service.
   486  //
   487  // Prefix and suffix options are available along with the limit and offset
   488  // ones.
   489  //
   490  // Using prefix and suffix options at the same time is discourage because
   491  // the suffix matching will be done on our side, and we'll likely need to
   492  // perform multiple requests to get the requested results. Note that using
   493  // just the suffix option is fine.
   494  // In addition, using the offset option is also discouraged because we'll
   495  // need to request additional keys that will be skipped on our side.
   496  func (es *Store) List(opts ...store.ListOption) ([]string, error) {
   497  	lopts := store.ListOptions{}
   498  	for _, opt := range opts {
   499  		opt(&lopts)
   500  	}
   501  
   502  	prefix := buildPrefix(lopts.Database, lopts.Table, prefixNS)
   503  	suffix := buildPrefix(lopts.Database, lopts.Table, suffixNS)
   504  
   505  	kv := es.client.KV
   506  	preKv := namespace.NewKV(kv, prefix)
   507  	sufKv := namespace.NewKV(kv, suffix)
   508  
   509  	if lopts.Prefix != "" && lopts.Suffix != "" {
   510  		return es.prefixSuffixList(preKv, lopts.Prefix, lopts.Suffix, int64(lopts.Limit), int64(lopts.Offset))
   511  	}
   512  
   513  	if lopts.Prefix != "" {
   514  		return es.listKeys(preKv, lopts.Prefix, int64(lopts.Limit), int64(lopts.Offset), false)
   515  	}
   516  
   517  	if lopts.Suffix != "" {
   518  		return es.listKeys(sufKv, reverseString(lopts.Suffix), int64(lopts.Limit), int64(lopts.Offset), true)
   519  	}
   520  
   521  	return es.listKeys(preKv, "", int64(lopts.Limit), int64(lopts.Offset), false)
   522  }
   523  
   524  // Close the client
   525  func (es *Store) Close() error {
   526  	return es.client.Close()
   527  }
   528  
   529  // Return the service name
   530  func (es *Store) String() string {
   531  	return "Etcd"
   532  }