github.com/benz9527/xboot@v0.0.0-20240504061247-c23f15593274/dlock/etcd_dlock.go (about)

     1  package dlock
     2  
     3  // References:
     4  // https://etcd.io/docs/v3.5/dev-guide/api_reference_v3/
     5  // https://github.com/etcd-io/etcd
     6  
     7  import (
     8  	"context"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	clientv3 "go.etcd.io/etcd/client/v3"
    13  	concv3 "go.etcd.io/etcd/client/v3/concurrency"
    14  	"go.uber.org/multierr"
    15  
    16  	"github.com/benz9527/xboot/lib/infra"
    17  )
    18  
    19  var _ DLocker = (*etcdDLock)(nil)
    20  
    21  type etcdDLock struct {
    22  	*etcdDLockOptions
    23  	session   *concv3.Session
    24  	mutexes   []*concv3.Mutex
    25  	startTime time.Time
    26  }
    27  
    28  func (dl *etcdDLock) Lock() error {
    29  	if len(dl.mutexes) < 1 {
    30  		return infra.NewErrorStack("etcd dlock is not initialized")
    31  	}
    32  
    33  	var (
    34  		merr                error
    35  		fallbackUnlockIndex int
    36  		retry               = dl.strategy
    37  		ticker              *time.Ticker
    38  	)
    39  	// TODO pay attention to the context has been cancelled.
    40  	for {
    41  		for i, mu := range dl.mutexes {
    42  			if err := /* reentrant */ mu.TryLock(*dl.ctx.Load()); err != nil {
    43  				merr = multierr.Append(merr, err)
    44  				fallbackUnlockIndex = i
    45  				break
    46  			}
    47  		}
    48  
    49  		if merr == nil {
    50  			return noErr
    51  		} else {
    52  			for i := fallbackUnlockIndex - 1; i >= 0; i-- {
    53  				if err := dl.mutexes[i].Unlock(*dl.ctx.Load()); err != nil {
    54  					return err
    55  				}
    56  			}
    57  		}
    58  		// Retry.
    59  		backoff := retry.Next()
    60  		if backoff.Milliseconds() < 1 {
    61  			if ticker != nil {
    62  				return infra.WrapErrorStackWithMessage(multierr.Combine(merr, ErrDLockAcquireFailed), "etcd dlock lock retry reach to max")
    63  			}
    64  			// No retry strategy.
    65  			return noErr
    66  		}
    67  
    68  		if ticker == nil {
    69  			ticker = time.NewTicker(backoff)
    70  			defer ticker.Stop() // Avoid ticker leak.
    71  		} else {
    72  			ticker.Reset(backoff)
    73  		}
    74  
    75  		select {
    76  		case <-(*dl.ctx.Load()).Done():
    77  			return infra.WrapErrorStack((*dl.ctx.Load()).Err())
    78  		case <-ticker.C:
    79  			// continue
    80  		}
    81  	}
    82  }
    83  
    84  func (dl *etcdDLock) Renewal(newTTL time.Duration) error {
    85  	return infra.NewErrorStack("etcd dlock not support to refresh ttl")
    86  }
    87  
    88  func (dl *etcdDLock) TTL() (time.Duration, error) {
    89  	if dl.startTime.IsZero() {
    90  		return 0, infra.NewErrorStack("etcd dlock is not initialized or has been  closed")
    91  	}
    92  	diff := time.Now().Sub(dl.startTime)
    93  	return dl.ttl - diff, nil
    94  }
    95  
    96  func (dl *etcdDLock) Unlock() error {
    97  	if len(dl.mutexes) < 1 {
    98  		return infra.NewErrorStack("etcd dlock is not initialized")
    99  	}
   100  	var merr error
   101  	// TODO pay attention to the context has been cancelled.
   102  	for _, mu := range dl.mutexes {
   103  		if err := mu.Unlock(*dl.ctx.Load()); err != nil {
   104  			merr = multierr.Append(merr, err)
   105  		}
   106  	}
   107  	if merr == nil && dl.session != nil {
   108  		merr = multierr.Append(merr, dl.session.Close())
   109  		if cancelPtr := dl.ctxCancel.Load(); cancelPtr != nil {
   110  			(*cancelPtr)()
   111  		}
   112  		dl.startTime = time.Time{} // Zero time
   113  	}
   114  	return merr
   115  }
   116  
   117  type etcdDLockOptions struct {
   118  	client    *clientv3.Client
   119  	ctx       atomic.Pointer[context.Context]
   120  	ctxCancel atomic.Pointer[context.CancelFunc]
   121  	strategy  RetryStrategy
   122  	keys      []string
   123  	ttl       time.Duration
   124  }
   125  
   126  func EtcdDLockBuilder(ctx context.Context, client *clientv3.Client) *etcdDLockOptions {
   127  	if ctx == nil {
   128  		ctx = context.Background()
   129  	}
   130  	opts := &etcdDLockOptions{client: client}
   131  	opts.ctx.Store(&ctx)
   132  	return opts
   133  }
   134  
   135  func (opt *etcdDLockOptions) TTL(ttl time.Duration) *etcdDLockOptions {
   136  	opt.ttl = ttl
   137  	return opt
   138  }
   139  
   140  func (opt *etcdDLockOptions) Keys(keys ...string) *etcdDLockOptions {
   141  	opt.keys = make([]string, len(keys))
   142  	for i, key := range keys {
   143  		opt.keys[i] = key
   144  	}
   145  	return opt
   146  }
   147  
   148  func (opt *etcdDLockOptions) Retry(strategy RetryStrategy) *etcdDLockOptions {
   149  	opt.strategy = strategy
   150  	return opt
   151  }
   152  
   153  func (opt *etcdDLockOptions) Build() (DLocker, error) {
   154  	if opt.client == nil {
   155  		return nil, infra.NewErrorStack("etcd dlock client is nil")
   156  	}
   157  	if opt.ttl.Seconds() < 1 {
   158  		return nil, infra.NewErrorStack("etcd dlock with zero second TTL")
   159  	}
   160  	if len(opt.keys) <= 0 {
   161  		return nil, infra.NewErrorStack("etcd dlock with zero keys")
   162  	}
   163  	if opt.strategy == nil {
   164  		opt.strategy = NoRetry()
   165  	}
   166  
   167  	var (
   168  		ctx    context.Context
   169  		cancel context.CancelFunc
   170  	)
   171  	ctx, cancel = context.WithTimeout(*opt.ctx.Load(), opt.ttl)
   172  	if ctx == nil || cancel == nil {
   173  		return nil, infra.NewErrorStack("etcd dlock build with nil context or nil context cancel function")
   174  	}
   175  	startTime := time.Now()
   176  	opt.ctx.Store(&ctx)
   177  	opt.ctxCancel.Store(&cancel)
   178  
   179  	session, err := concv3.NewSession(opt.client,
   180  		concv3.WithTTL(int(opt.ttl.Seconds())),
   181  		concv3.WithLease(clientv3.NoLease), // Grant new lease ID by new session.
   182  	)
   183  	if err != nil {
   184  		return nil, infra.WrapErrorStack(err)
   185  	}
   186  	var mutexes = make([]*concv3.Mutex, 0, len(opt.keys))
   187  	for _, prefix := range opt.keys {
   188  		mutexes = append(mutexes, concv3.NewMutex(session, prefix))
   189  	}
   190  	return &etcdDLock{
   191  		etcdDLockOptions: opt,
   192  		session:          session,
   193  		startTime:        startTime,
   194  		mutexes:          mutexes,
   195  	}, nil
   196  }
   197  
   198  type EtcdDLockOption func(opt *etcdDLockOptions)
   199  
   200  func WithEtcdDLockTTL(ttl time.Duration) EtcdDLockOption {
   201  	return func(opt *etcdDLockOptions) {
   202  		opt.TTL(ttl)
   203  	}
   204  }
   205  
   206  func WithEtcdDLockKeys(keys ...string) EtcdDLockOption {
   207  	return func(opt *etcdDLockOptions) {
   208  		opt.Keys(keys...)
   209  	}
   210  }
   211  
   212  func WithEtcdDLockRetry(strategy RetryStrategy) EtcdDLockOption {
   213  	return func(opt *etcdDLockOptions) {
   214  		opt.Retry(strategy)
   215  	}
   216  }
   217  
   218  func EtcdDLock(ctx context.Context, client *clientv3.Client, opts ...EtcdDLockOption) (DLocker, error) {
   219  	builderOpts := EtcdDLockBuilder(ctx, client)
   220  	for _, o := range opts {
   221  		o(builderOpts)
   222  	}
   223  	return builderOpts.Build()
   224  }