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  }