github.com/aaabigfish/gopkg@v1.1.0/etcdv3/client.go (about) 1 // etcdV3 rebuild on go kit sd etcd 2 // add some more operator with etcd 3 package etcdv3 4 5 import ( 6 "context" 7 "crypto/tls" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "time" 12 13 "go.etcd.io/etcd/client/pkg/v3/transport" 14 clientv3 "go.etcd.io/etcd/client/v3" 15 "google.golang.org/grpc" 16 ) 17 18 var ( 19 // ErrNoKey indicates a client method needs a key but receives none. 20 ErrNoKey = errors.New("no key provided") 21 22 // ErrNoValue indicates a client method needs a value but receives none. 23 ErrNoValue = errors.New("no value provided") 24 25 // ErrNotfound 26 ErrNotfound = errors.New("not found") 27 ) 28 29 // KvEntry 30 type KvEntry struct { 31 Key string 32 Value string 33 } 34 35 type WatchEvent struct { 36 OpType int32 37 Kv *KvEntry 38 } 39 40 // Client is a wrapper around the etcd client. 41 type Client interface { 42 GetEtcdClient() *clientv3.Client 43 44 GetEtcdKV() clientv3.KV 45 // GetEntries queries the given prefix in etcd and returns a slice 46 // containing the values of all keys found, recursively, underneath that 47 // prefix. 48 GetEntries(prefix string) ([]*KvEntry, error) 49 50 // Put save data 51 Put(key, val string) (int64, int64, error) 52 53 // PutJSON save interface 54 PutJSON(key string, obj any) (int64, int64, error) 55 56 // Get query data 57 Get(key string) ([]string, error) 58 59 Delete(key string, opts ...clientv3.OpOption) (int64, error) 60 61 BatchDelete(keys []string, opts ...clientv3.OpOption) error 62 63 // WatchPrefix watches the given prefix in etcd for changes. When a change 64 // is detected, it will signal on the passed channel. Clients are expected 65 // to call GetEntries to update themselves with the latest set of complete 66 // values. WatchPrefix will always send an initial sentinel value on the 67 // channel after establishing the watch, to ensure that clients always 68 // receive the latest set of values. WatchPrefix will block until the 69 // context passed to the NewClient constructor is terminated. 70 WatchPrefix(prefix string, ev chan []*WatchEvent) 71 72 // Register a service with etcd. 73 Register(s Service, servCheckChan chan struct{}) error 74 75 // Deregister a service with etcd. 76 Deregister(s Service) error 77 78 // LeaseID returns the lease id created for this service instance 79 LeaseID() int64 80 } 81 82 const minHeartBeatTime = 500 * time.Millisecond 83 84 type EtcdEntity interface { 85 Key() string 86 Value() string 87 } 88 89 // Service holds the instance identifying data you want to publish to etcd. Key 90 // must be unique, and value is the string returned to subscribers, typically 91 // called the "instance" string in other parts of package sd. 92 type Service struct { 93 Key string // unique key, e.g. "/service/foobar/1.2.3.4:8080" 94 Value string // returned to subscribers, e.g. "http://1.2.3.4:8080" 95 TTL *TTLOption 96 } 97 98 // TTLOption allow setting a key with a TTL. This option will be used by a loop 99 // goroutine which regularly refreshes the lease of the key. 100 type TTLOption struct { 101 heartbeat time.Duration // e.g. time.Second * 3 102 ttl time.Duration // e.g. time.Second * 10 103 } 104 105 // NewTTLOption returns a TTLOption that contains proper TTL settings. Heartbeat 106 // is used to refresh the lease of the key periodically; its value should be at 107 // least 500ms. TTL defines the lease of the key; its value should be 108 // significantly greater than heartbeat. 109 // 110 // Good default values might be 3s heartbeat, 10s TTL. 111 func NewTTLOption(heartbeat, ttl time.Duration) *TTLOption { 112 if heartbeat <= minHeartBeatTime { 113 heartbeat = minHeartBeatTime 114 } 115 if ttl <= heartbeat { 116 ttl = 3 * heartbeat 117 } 118 return &TTLOption{ 119 heartbeat: heartbeat, 120 ttl: ttl, 121 } 122 } 123 124 type client struct { 125 cli *clientv3.Client 126 ctx context.Context 127 128 kv clientv3.KV 129 130 // Watcher interface instance, used to leverage Watcher.Close() 131 watcher clientv3.Watcher 132 // watcher context 133 wctx context.Context 134 // watcher cancel func 135 wcf context.CancelFunc 136 137 // leaseID will be 0 (clientv3.NoLease) if a lease was not created 138 leaseID clientv3.LeaseID 139 140 hbch <-chan *clientv3.LeaseKeepAliveResponse 141 // Lease interface instance, used to leverage Lease.Close() 142 leaser clientv3.Lease 143 } 144 145 // ClientOptions defines options for the etcd client. All values are optional. 146 // If any duration is not specified, a default of 3 seconds will be used. 147 type ClientOptions struct { 148 Cert string 149 Key string 150 CACert string 151 DialTimeout time.Duration 152 DialKeepAlive time.Duration 153 154 // DialOptions is a list of dial options for the gRPC client (e.g., for interceptors). 155 // For example, pass grpc.WithBlock() to block until the underlying connection is up. 156 // Without this, Dial returns immediately and connecting the server happens in background. 157 DialOptions []grpc.DialOption 158 159 Username string 160 Password string 161 } 162 163 // NewClient returns Client with a connection to the named machines. It will 164 // return an error if a connection to the cluster cannot be made. 165 func NewClient(ctx context.Context, machines []string, options ClientOptions) (Client, error) { 166 if options.DialTimeout == 0 { 167 options.DialTimeout = 3 * time.Second 168 } 169 if options.DialKeepAlive == 0 { 170 options.DialKeepAlive = 3 * time.Second 171 } 172 173 var err error 174 var tlscfg *tls.Config 175 176 if options.Cert != "" && options.Key != "" { 177 tlsInfo := transport.TLSInfo{ 178 CertFile: options.Cert, 179 KeyFile: options.Key, 180 TrustedCAFile: options.CACert, 181 } 182 tlscfg, err = tlsInfo.ClientConfig() 183 if err != nil { 184 return nil, err 185 } 186 } 187 188 cli, err := clientv3.New(clientv3.Config{ 189 Context: ctx, 190 Endpoints: machines, 191 DialTimeout: options.DialTimeout, 192 DialKeepAliveTime: options.DialKeepAlive, 193 DialOptions: options.DialOptions, 194 TLS: tlscfg, 195 Username: options.Username, 196 Password: options.Password, 197 }) 198 if err != nil { 199 return nil, err 200 } 201 202 return &client{ 203 cli: cli, 204 ctx: ctx, 205 kv: clientv3.NewKV(cli), 206 }, nil 207 } 208 209 func (c *client) GetEtcdClient() *clientv3.Client { return c.cli } 210 211 func (c *client) GetEtcdKV() clientv3.KV { return c.kv } 212 213 // LeaseID implements the etcd Client interface. 214 func (c *client) LeaseID() int64 { return int64(c.leaseID) } 215 216 // Put implements the etcd Client interface. 217 func (c *client) Put(key, val string) (int64, int64, error) { 218 resp, err := c.kv.Put(c.ctx, key, val, clientv3.WithPrevKV()) 219 if err != nil { 220 return 0, 0, err 221 } 222 223 if resp.PrevKv == nil { 224 return 0, 0, nil 225 } 226 return resp.PrevKv.Version, resp.PrevKv.ModRevision, nil 227 } 228 229 func (c *client) PutJSON(key string, obj any) (int64, int64, error) { 230 val, err := json.Marshal(obj) 231 if err != nil { 232 return 0, 0, err 233 } 234 return c.Put(key, string(val)) 235 } 236 237 func (c *client) Delete(key string, opts ...clientv3.OpOption) (int64, error) { 238 if key == "" { 239 return 0, ErrNoKey 240 } 241 242 resp, err := c.cli.Delete(c.ctx, key, opts...) 243 if err != nil { 244 return 0, err 245 } 246 return resp.Deleted, nil 247 } 248 249 func (c *client) BatchDelete(keys []string, opts ...clientv3.OpOption) error { 250 for i := range keys { 251 if _, err := c.Delete(keys[i], opts...); err != nil { 252 return fmt.Errorf("delete etcd key[%s] failed: %s", keys[i], err) 253 } 254 } 255 return nil 256 } 257 258 // Get implements the etcd Client interface. 259 func (c *client) Get(key string) ([]string, error) { 260 resp, err := c.kv.Get(c.ctx, key) 261 if err != nil { 262 return nil, err 263 } 264 sli := []string{} 265 for _, o := range resp.Kvs { 266 sli = append(sli, string(o.Value)) 267 } 268 return sli, nil 269 } 270 271 // GetEntries implements the etcd Client interface. 272 func (c *client) GetEntries(key string) ([]*KvEntry, error) { 273 resp, err := c.kv.Get(c.ctx, key, clientv3.WithPrefix()) 274 if err != nil { 275 return nil, err 276 } 277 278 entries := make([]*KvEntry, len(resp.Kvs)) 279 for i, kv := range resp.Kvs { 280 entry := &KvEntry{ 281 Key: string(kv.Key), 282 Value: string(kv.Value), 283 } 284 entries[i] = entry 285 } 286 return entries, nil 287 } 288 289 // WatchPrefix implements the etcd Client interface. 290 func (c *client) WatchPrefix(prefix string, ch chan []*WatchEvent) { 291 c.wctx, c.wcf = context.WithCancel(c.ctx) 292 c.watcher = clientv3.NewWatcher(c.cli) 293 294 wch := c.watcher.Watch(c.wctx, prefix, clientv3.WithPrefix(), clientv3.WithRev(0)) 295 296 for wr := range wch { 297 if wr.Canceled { 298 return 299 } 300 evSlice := []*WatchEvent{} 301 for _, ev := range wr.Events { 302 wev := &WatchEvent{ 303 OpType: int32(ev.Type), 304 Kv: &KvEntry{ 305 Key: string(ev.Kv.Key), 306 Value: string(ev.Kv.Value), 307 }, 308 } 309 evSlice = append(evSlice, wev) 310 } 311 ch <- evSlice 312 } 313 } 314 315 func (c *client) Register(s Service, servCheckChan chan struct{}) error { 316 if s.Key == "" { 317 return ErrNoKey 318 } 319 if s.Value == "" { 320 return ErrNoValue 321 } 322 323 if c.leaser != nil { 324 c.leaser.Close() 325 } 326 c.leaser = clientv3.NewLease(c.cli) 327 328 if c.watcher != nil { 329 c.watcher.Close() 330 } 331 c.watcher = clientv3.NewWatcher(c.cli) 332 if c.kv == nil { 333 c.kv = clientv3.NewKV(c.cli) 334 } 335 336 if s.TTL == nil { 337 s.TTL = NewTTLOption(time.Second*3, time.Second*10) 338 } 339 340 grantResp, err := c.leaser.Grant(c.ctx, int64(s.TTL.ttl.Seconds())) 341 if err != nil { 342 return err 343 } 344 c.leaseID = grantResp.ID 345 346 _, err = c.kv.Put( 347 c.ctx, 348 s.Key, 349 s.Value, 350 clientv3.WithLease(c.leaseID), 351 ) 352 if err != nil { 353 return err 354 } 355 356 // this will keep the key alive 'forever' or until we revoke it or 357 // the context is canceled 358 c.hbch, err = c.leaser.KeepAlive(c.ctx, c.leaseID) 359 if err != nil { 360 return err 361 } 362 363 // discard the keepalive response, make etcd library not to complain 364 // fix bug #799 365 go func() { 366 for { 367 select { 368 case r := <-c.hbch: 369 // avoid dead loop when channel was closed 370 if r == nil { 371 servCheckChan <- struct{}{} 372 return 373 } 374 case <-c.ctx.Done(): 375 return 376 } 377 } 378 }() 379 return nil 380 } 381 382 func (c *client) Deregister(s Service) error { 383 defer c.close() 384 385 if s.Key == "" { 386 return ErrNoKey 387 } 388 if _, err := c.cli.Delete(c.ctx, s.Key, clientv3.WithIgnoreLease()); err != nil { 389 return err 390 } 391 392 return nil 393 } 394 395 // close will close any open clients and call 396 // the watcher cancel func 397 func (c *client) close() { 398 if c.leaser != nil { 399 c.leaser.Close() 400 } 401 if c.watcher != nil { 402 c.watcher.Close() 403 } 404 if c.wcf != nil { 405 c.wcf() 406 } 407 }