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 }