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 }