github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/store/etcdv3/meta/etcd.go (about) 1 package meta 2 3 import ( 4 "context" 5 "crypto/tls" 6 "fmt" 7 "strconv" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/cockroachdb/errors" 13 14 "github.com/projecteru2/core/lock" 15 "github.com/projecteru2/core/lock/etcdlock" 16 "github.com/projecteru2/core/log" 17 embedded "github.com/projecteru2/core/store/etcdv3/embedded" 18 "github.com/projecteru2/core/types" 19 20 "go.etcd.io/etcd/api/v3/mvccpb" 21 "go.etcd.io/etcd/client/pkg/v3/transport" 22 clientv3 "go.etcd.io/etcd/client/v3" 23 "go.etcd.io/etcd/client/v3/namespace" 24 ) 25 26 const ( 27 cmpVersion = "version" 28 cmpValue = "value" 29 ) 30 31 // ETCDClientV3 . 32 type ETCDClientV3 interface { 33 clientv3.KV 34 clientv3.Lease 35 clientv3.Watcher 36 } 37 38 // ETCD . 39 type ETCD struct { 40 cliv3 ETCDClientV3 41 config types.EtcdConfig 42 } 43 44 // ETCDTxn wraps a group of Cmp with Op 45 type ETCDTxn struct { 46 If []clientv3.Cmp 47 Then []clientv3.Op 48 Else []clientv3.Op 49 } 50 51 // ETCDTxnResp wraps etcd response with error 52 type ETCDTxnResp struct { 53 resp *clientv3.TxnResponse 54 err error 55 } 56 57 // NewETCD initailizes a new ETCD instance. 58 func NewETCD(config types.EtcdConfig, t *testing.T) (*ETCD, error) { 59 var cliv3 *clientv3.Client 60 var err error 61 var tlsConfig *tls.Config 62 63 switch { 64 case t != nil: 65 embededETCD := embedded.NewCluster(t, config.Prefix) 66 cliv3 = embededETCD.RandClient() 67 log.WithFunc("store.etcdv3.meta.NewETCD").Info(nil, "use embedded cluster") //nolint 68 default: 69 if config.Ca != "" && config.Key != "" && config.Cert != "" { 70 tlsInfo := transport.TLSInfo{ 71 TrustedCAFile: config.Ca, 72 KeyFile: config.Key, 73 CertFile: config.Cert, 74 } 75 tlsConfig, err = tlsInfo.ClientConfig() 76 if err != nil { 77 return nil, err 78 } 79 } 80 if cliv3, err = clientv3.New(clientv3.Config{ 81 Endpoints: config.Machines, 82 Username: config.Auth.Username, 83 Password: config.Auth.Password, 84 TLS: tlsConfig, 85 }); err != nil { 86 return nil, err 87 } 88 cliv3.KV = namespace.NewKV(cliv3.KV, config.Prefix) 89 cliv3.Watcher = namespace.NewWatcher(cliv3.Watcher, config.Prefix) 90 cliv3.Lease = namespace.NewLease(cliv3.Lease, config.Prefix) 91 } 92 return &ETCD{cliv3: cliv3, config: config}, nil 93 } 94 95 // CreateLock create a lock instance 96 func (e *ETCD) CreateLock(key string, ttl time.Duration) (lock.DistributedLock, error) { 97 lockKey := fmt.Sprintf("%s/%s", e.config.LockPrefix, key) 98 mutex, err := etcdlock.New(e.cliv3.(*clientv3.Client), lockKey, ttl) 99 return mutex, err 100 } 101 102 // Get get results or noting 103 func (e *ETCD) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { 104 return e.cliv3.Get(ctx, key, opts...) 105 } 106 107 // GetOne get one result or noting 108 func (e *ETCD) GetOne(ctx context.Context, key string, opts ...clientv3.OpOption) (*mvccpb.KeyValue, error) { 109 resp, err := e.Get(ctx, key, opts...) 110 if err != nil { 111 return nil, err 112 } 113 if resp.Count != 1 { 114 return nil, errors.Wrapf(types.ErrInvaildCount, "key: %s", key) 115 } 116 return resp.Kvs[0], nil 117 } 118 119 // GetMulti gets several results 120 func (e *ETCD) GetMulti(ctx context.Context, keys []string, _ ...clientv3.OpOption) (kvs []*mvccpb.KeyValue, err error) { 121 var txnResponse *clientv3.TxnResponse 122 if len(keys) == 0 { 123 return 124 } 125 if txnResponse, err = e.batchGet(ctx, keys); err != nil { 126 return 127 } 128 for idx, responseOp := range txnResponse.Responses { 129 resp := responseOp.GetResponseRange() 130 if resp.Count != 1 { 131 return nil, errors.Wrapf(types.ErrInvaildCount, "key: %s", keys[idx]) 132 } 133 kvs = append(kvs, resp.Kvs[0]) 134 } 135 if len(kvs) != len(keys) { 136 err = errors.Wrapf(types.ErrInvaildCount, "keys: %+v", keys) 137 } 138 return 139 } 140 141 // Delete delete key 142 func (e *ETCD) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { 143 return e.cliv3.Delete(ctx, key, opts...) 144 } 145 146 // Put save a key value 147 func (e *ETCD) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { 148 return e.cliv3.Put(ctx, key, val, opts...) 149 } 150 151 // Create create a key if not exists 152 func (e *ETCD) Create(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 153 return e.batchCreate(ctx, map[string]string{key: val}, opts...) 154 } 155 156 // Update update a key if exists 157 func (e *ETCD) Update(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 158 return e.batchUpdate(ctx, map[string]string{key: val}, opts...) 159 } 160 161 // Watch . 162 func (e *ETCD) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan { 163 return e.watch(ctx, key, opts...) 164 } 165 166 // Watch wath a key 167 func (e *ETCD) watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan { 168 return e.cliv3.Watch(ctx, key, opts...) 169 } 170 171 func (e *ETCD) batchGet(ctx context.Context, keys []string, opt ...clientv3.OpOption) (txnResponse *clientv3.TxnResponse, err error) { 172 txn := ETCDTxn{} 173 for _, key := range keys { 174 op := clientv3.OpGet(key, opt...) 175 txn.Then = append(txn.Then, op) 176 } 177 return e.doBatchOp(ctx, []ETCDTxn{txn}) 178 } 179 180 // BatchDelete . 181 func (e *ETCD) BatchDelete(ctx context.Context, keys []string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 182 return e.batchDelete(ctx, keys, opts...) 183 } 184 185 func (e *ETCD) batchDelete(ctx context.Context, keys []string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 186 txn := ETCDTxn{} 187 for _, key := range keys { 188 op := clientv3.OpDelete(key, opts...) 189 txn.Then = append(txn.Then, op) 190 } 191 192 return e.doBatchOp(ctx, []ETCDTxn{txn}) 193 } 194 195 func (e *ETCD) batchPut(ctx context.Context, data map[string]string, limit map[string]map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 196 txnes := []ETCDTxn{} 197 for key, val := range data { 198 txn := ETCDTxn{} 199 op := clientv3.OpPut(key, val, opts...) 200 txn.Then = append(txn.Then, op) 201 if v, ok := limit[key]; ok { 202 for method, condition := range v { 203 switch method { 204 case cmpVersion: 205 cond := clientv3.Compare(clientv3.Version(key), condition, 0) 206 txn.If = append(txn.If, cond) 207 case cmpValue: 208 cond := clientv3.Compare(clientv3.Value(key), condition, val) 209 txn.Else = append(txn.Else, clientv3.OpGet(key)) 210 txn.If = append(txn.If, cond) 211 } 212 } 213 } 214 txnes = append(txnes, txn) 215 } 216 return e.doBatchOp(ctx, txnes) 217 } 218 219 // BatchCreate . 220 func (e *ETCD) BatchCreate(ctx context.Context, data map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 221 return e.batchCreate(ctx, data, opts...) 222 } 223 224 func (e *ETCD) batchCreate(ctx context.Context, data map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 225 limit := map[string]map[string]string{} 226 for key := range data { 227 limit[key] = map[string]string{cmpVersion: "="} 228 } 229 resp, err := e.batchPut(ctx, data, limit, opts...) 230 if err != nil { 231 return resp, err 232 } 233 if !resp.Succeeded { 234 return resp, types.ErrKeyExists 235 } 236 return resp, nil 237 } 238 239 // BatchUpdate . 240 func (e *ETCD) BatchUpdate(ctx context.Context, data map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 241 return e.batchUpdate(ctx, data, opts...) 242 } 243 244 // BatchPut . 245 func (e *ETCD) BatchPut(ctx context.Context, data map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 246 return e.batchPut(ctx, data, nil, opts...) 247 } 248 249 // isTTLChanged returns true if there is a lease with a different ttl bound to the key 250 func (e *ETCD) isTTLChanged(ctx context.Context, key string, ttl int64) (bool, error) { 251 resp, err := e.GetOne(ctx, key) 252 if err != nil { 253 if errors.Is(err, types.ErrInvaildCount) { 254 return ttl != 0, nil 255 } 256 return false, err 257 } 258 259 leaseID := clientv3.LeaseID(resp.Lease) 260 if leaseID == 0 { 261 return ttl != 0, nil 262 } 263 264 getTTLResp, err := e.cliv3.TimeToLive(ctx, leaseID) 265 if err != nil { 266 return false, err 267 } 268 269 changed := getTTLResp.GrantedTTL != ttl 270 if changed { 271 log.WithFunc("store.etcdv3.meta.isTTLChanged").Infof(ctx, "key %+v ttl changed from %+v to %+v", key, getTTLResp.GrantedTTL, ttl) 272 } 273 274 return changed, nil 275 } 276 277 // BindStatus keeps on a lease alive. 278 func (e *ETCD) BindStatus(ctx context.Context, entityKey, statusKey, statusValue string, ttl int64) error { 279 if ttl == 0 { 280 return e.bindStatusWithoutTTL(ctx, statusKey, statusValue) 281 } 282 return e.bindStatusWithTTL(ctx, entityKey, statusKey, statusValue, ttl) 283 } 284 285 func (e *ETCD) bindStatusWithTTL(ctx context.Context, entityKey, statusKey, statusValue string, ttl int64) error { 286 lease, err := e.Grant(ctx, ttl) 287 if err != nil { 288 return err 289 } 290 291 leaseID := lease.ID 292 updateStatus := []clientv3.Op{clientv3.OpPut(statusKey, statusValue, clientv3.WithLease(lease.ID))} 293 logger := log.WithFunc("store.etcdv3.meta.bindStatusWithTTL") 294 295 ttlChanged, err := e.isTTLChanged(ctx, statusKey, ttl) 296 if err != nil { 297 return err 298 } 299 300 var entityTxn *clientv3.TxnResponse 301 302 if ttlChanged { 303 entityTxn, err = e.cliv3.Txn(ctx). 304 If(clientv3.Compare(clientv3.Version(entityKey), "!=", 0)). 305 Then(updateStatus...). // making sure there's an exists entity kv-pair. 306 Commit() 307 } else { 308 entityTxn, err = e.cliv3.Txn(ctx). 309 If(clientv3.Compare(clientv3.Version(entityKey), "!=", 0)). 310 Then( // making sure there's an exists entity kv-pair. 311 clientv3.OpTxn( 312 []clientv3.Cmp{clientv3.Compare(clientv3.Version(statusKey), "!=", 0)}, // Is the status exists? 313 []clientv3.Op{clientv3.OpTxn( // there's an exists status 314 []clientv3.Cmp{clientv3.Compare(clientv3.LeaseValue(statusKey), "!=", 0)}, // 315 []clientv3.Op{clientv3.OpTxn( // there has been a lease bound to the status 316 []clientv3.Cmp{clientv3.Compare(clientv3.Value(statusKey), "=", statusValue)}, // Is the status changed? 317 []clientv3.Op{clientv3.OpGet(statusKey)}, // The status hasn't been changed. 318 updateStatus, // The status had been changed. 319 )}, 320 updateStatus, // there is no lease bound to the status 321 )}, 322 updateStatus, // there isn't a status 323 ), 324 ).Commit() 325 } 326 327 if err != nil { 328 e.revokeLease(ctx, leaseID) 329 return err 330 } 331 332 // There isn't the entity kv pair. 333 if !entityTxn.Succeeded { 334 e.revokeLease(ctx, leaseID) 335 return types.ErrInvaildCount 336 } 337 338 // if ttl is changed, replace with the new lease 339 if ttlChanged { 340 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 341 return nil 342 } 343 344 // There isn't a status bound to the entity. 345 statusTxn := entityTxn.Responses[0].GetResponseTxn() 346 if !statusTxn.Succeeded { 347 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 348 return nil 349 } 350 351 // There is no lease bound to the status yet 352 leaseTxn := statusTxn.Responses[0].GetResponseTxn() 353 if !leaseTxn.Succeeded { 354 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 355 return nil 356 } 357 358 // There is a status bound to the entity yet but its value isn't same as the expected one. 359 valueTxn := leaseTxn.Responses[0].GetResponseTxn() 360 if !valueTxn.Succeeded { 361 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 362 return nil 363 } 364 365 // Gets the lease ID which binds onto the status, and renew it one round. 366 origLeaseID := clientv3.LeaseID(valueTxn.Responses[0].GetResponseRange().Kvs[0].Lease) 367 368 if origLeaseID != leaseID { 369 e.revokeLease(ctx, leaseID) 370 } 371 372 _, err = e.cliv3.KeepAliveOnce(ctx, origLeaseID) 373 return err 374 } 375 376 // bindStatusWithoutTTL sets status without TTL. 377 // When dealing with status of 0 TTL, we don't use lease, 378 // also we don't check the existence of the entity key since 379 // agent may report status earlier when core has not recorded the entity. 380 func (e *ETCD) bindStatusWithoutTTL(ctx context.Context, statusKey, statusValue string) error { 381 updateStatus := []clientv3.Op{clientv3.OpPut(statusKey, statusValue)} 382 logger := log.WithFunc("store.etcdv3.etcd.bindStatusWithoutTTL") 383 384 ttlChanged, err := e.isTTLChanged(ctx, statusKey, 0) 385 if err != nil { 386 return err 387 } 388 if ttlChanged { 389 _, err := e.Put(ctx, statusKey, statusValue) 390 if err != nil { 391 return err 392 } 393 394 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 395 return nil 396 } 397 398 resp, err := e.cliv3.Txn(ctx). 399 If(clientv3.Compare(clientv3.Version(statusKey), "!=", 0)). // if there's an existing status key 400 Then(clientv3.OpTxn( // deal with existing status key 401 []clientv3.Cmp{clientv3.Compare(clientv3.Value(statusKey), "!=", statusValue)}, // if the new value != the old value 402 updateStatus, // then the status has been changed. 403 []clientv3.Op{}, // otherwise do nothing. 404 )). 405 Else(updateStatus...). // otherwise deal with non-existing status key 406 Commit() 407 if err != nil { 408 return err 409 } 410 if !resp.Succeeded || resp.Responses[0].GetResponseTxn().Succeeded { 411 logger.Infof(ctx, "put: key %s value %s", statusKey, statusValue) 412 } 413 return nil 414 } 415 416 func (e *ETCD) revokeLease(ctx context.Context, leaseID clientv3.LeaseID) { 417 if leaseID == 0 { 418 return 419 } 420 if _, err := e.cliv3.Revoke(ctx, leaseID); err != nil { 421 log.WithFunc("store.etcdv3.etcd.revokeLease").Error(ctx, err, "revoke lease failed") 422 } 423 } 424 425 // Grant creates a new lease. 426 func (e *ETCD) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGrantResponse, error) { 427 return e.cliv3.Grant(ctx, ttl) 428 } 429 430 func (e *ETCD) batchUpdate(ctx context.Context, data map[string]string, opts ...clientv3.OpOption) (*clientv3.TxnResponse, error) { 431 limit := map[string]map[string]string{} 432 for key := range data { 433 limit[key] = map[string]string{cmpVersion: "!="} // check existence 434 } 435 resp, err := e.batchPut(ctx, data, limit, opts...) 436 if err != nil { 437 return resp, err 438 } 439 if !resp.Succeeded { 440 return resp, types.ErrKeyNotExists 441 } 442 return resp, nil 443 } 444 445 func (e *ETCD) doBatchOp(ctx context.Context, transactions []ETCDTxn) (resp *clientv3.TxnResponse, err error) { 446 if len(transactions) == 0 { 447 return nil, types.ErrNoOps 448 } 449 450 const txnLimit = 125 451 452 // split transactions into smaller pieces 453 txnes := []ETCDTxn{} 454 for _, txn := range transactions { 455 // TODO@zc: split if and else 456 if len(txn.Then) <= txnLimit { 457 txnes = append(txnes, txn) 458 continue 459 } 460 461 n, m := len(txn.Then)/txnLimit, len(txn.Then)%txnLimit 462 for i := 0; i < n; i++ { 463 txnes = append(txnes, ETCDTxn{ 464 If: txn.If, 465 Then: txn.Then[i*txnLimit : (i+1)*txnLimit], 466 Else: txn.Else, 467 }) 468 } 469 if m > 0 { 470 txnes = append(txnes, ETCDTxn{ 471 If: txn.If, 472 Then: txn.Then[n*txnLimit:], 473 Else: txn.Else, 474 }) 475 } 476 } 477 478 wg := sync.WaitGroup{} 479 respChan := make(chan ETCDTxnResp) 480 doOp := func(from, to int) { 481 defer wg.Done() 482 conds, thens, elses := []clientv3.Cmp{}, []clientv3.Op{}, []clientv3.Op{} 483 for i := from; i < to; i++ { 484 conds = append(conds, txnes[i].If...) 485 thens = append(thens, txnes[i].Then...) 486 elses = append(elses, txnes[i].Else...) 487 } 488 resp, err := e.cliv3.Txn(ctx).If(conds...).Then(thens...).Else(elses...).Commit() 489 respChan <- ETCDTxnResp{resp: resp, err: err} 490 } 491 492 lastIdx := 0 // last uncommit index 493 lenIf, lenThen, lenElse := 0, 0, 0 494 for i := 0; i < len(txnes); i++ { 495 if lenIf+len(txnes[i].If) > txnLimit || 496 lenThen+len(txnes[i].Then) > txnLimit || 497 lenElse+len(txnes[i].Else) > txnLimit { 498 wg.Add(1) 499 go doOp(lastIdx, i) // [lastIdx, i) 500 501 lastIdx = i 502 lenIf, lenThen, lenElse = 0, 0, 0 503 } 504 505 lenIf += len(txnes[i].If) 506 lenThen += len(txnes[i].Then) 507 lenElse += len(txnes[i].Else) 508 } 509 wg.Add(1) 510 go doOp(lastIdx, len(txnes)) 511 512 go func() { 513 wg.Wait() 514 close(respChan) 515 }() 516 517 resps := []ETCDTxnResp{} 518 for resp := range respChan { 519 resps = append(resps, resp) 520 if resp.err != nil { 521 err = resp.err 522 } 523 } 524 if err != nil { 525 return resp, err 526 } 527 528 if len(resps) == 0 { 529 return &clientv3.TxnResponse{}, nil 530 } 531 532 resp = resps[0].resp 533 // TODO@zc: should rollback all for any unsucceed txn 534 for i := 1; i < len(resps); i++ { 535 resp.Succeeded = resp.Succeeded && resps[i].resp.Succeeded 536 resp.Responses = append(resp.Responses, resps[i].resp.Responses...) 537 } 538 return resp, nil 539 } 540 541 // BatchCreateAndDecr used to decr processing and add workload 542 func (e *ETCD) BatchCreateAndDecr(ctx context.Context, data map[string]string, decrKey string) (err error) { 543 resp, err := e.Get(ctx, decrKey) 544 if err != nil { 545 return err 546 } 547 if len(resp.Kvs) == 0 { 548 return errors.Wrap(types.ErrKeyNotExists, decrKey) 549 } 550 551 decrKv := resp.Kvs[0] 552 putOps := []clientv3.Op{} 553 for key, value := range data { 554 putOps = append(putOps, clientv3.OpPut(key, value)) 555 } 556 557 for { 558 cnt, err := strconv.Atoi(string(decrKv.Value)) 559 if err != nil { 560 return err 561 } 562 563 txn := ETCDTxn{ 564 If: []clientv3.Cmp{ 565 clientv3.Compare(clientv3.Value(decrKey), "=", string(decrKv.Value)), 566 }, 567 Then: append(putOps, 568 clientv3.OpPut(decrKey, strconv.Itoa(cnt-1)), 569 ), 570 Else: []clientv3.Op{ 571 clientv3.OpGet(decrKey), 572 }, 573 } 574 txnResp, err := e.doBatchOp(ctx, []ETCDTxn{txn}) 575 if err != nil { 576 return err 577 } 578 if txnResp.Succeeded { 579 break 580 } 581 decrKv = txnResp.Responses[0].GetResponseRange().Kvs[0] 582 } 583 584 return nil 585 }