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  }