github.com/kaydxh/golang@v0.0.131/pkg/discovery/etcd/etcd.go (about)

     1  /*
     2   *Copyright (c) 2022, kaydxh
     3   *
     4   *Permission is hereby granted, free of charge, to any person obtaining a copy
     5   *of this software and associated documentation files (the "Software"), to deal
     6   *in the Software without restriction, including without limitation the rights
     7   *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8   *copies of the Software, and to permit persons to whom the Software is
     9   *furnished to do so, subject to the following conditions:
    10   *
    11   *The above copyright notice and this permission notice shall be included in all
    12   *copies or substantial portions of the Software.
    13   *
    14   *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15   *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16   *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17   *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18   *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19   *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    20   *SOFTWARE.
    21   */
    22  package etcd
    23  
    24  import (
    25  	"context"
    26  	"fmt"
    27  	"path/filepath"
    28  	"sync"
    29  	"time"
    30  
    31  	clientv3 "go.etcd.io/etcd/client/v3"
    32  	"go.etcd.io/etcd/client/v3/concurrency"
    33  
    34  	time_ "github.com/kaydxh/golang/go/time"
    35  	"github.com/sirupsen/logrus"
    36  )
    37  
    38  type EventCallbackFunc func(ctx context.Context, key, value string)
    39  
    40  var (
    41  	etcdKV ETCDKV
    42  	mu     sync.Mutex
    43  )
    44  
    45  // Default values for Etcd.
    46  const (
    47  	DefaultDialTimeout = 5 * time.Second
    48  	DefaultLockTTL     = 15 * time.Second
    49  )
    50  
    51  type EtcdConfig struct {
    52  	Addresses []string
    53  	UserName  string
    54  	Password  string
    55  }
    56  
    57  type EtcdKVOptions struct {
    58  	DialTimeout        time.Duration
    59  	MaxCallSendMsgSize int
    60  	MaxCallRecvMsgSize int
    61  	AutoSyncInterval   time.Duration
    62  
    63  	LockPrefixPath     string
    64  	LockKey            string
    65  	LockTTL            time.Duration
    66  	WatchPaths         []string
    67  	CreateCallbackFunc EventCallbackFunc
    68  	DeleteCallbackFunc EventCallbackFunc
    69  }
    70  
    71  type EtcdKV struct {
    72  	Conf EtcdConfig
    73  	*clientv3.Client
    74  
    75  	session *concurrency.Session
    76  	mutex   *concurrency.Mutex
    77  
    78  	opts EtcdKVOptions
    79  }
    80  
    81  func NewEtcdKV(conf EtcdConfig, opts ...EtcdKVOption) *EtcdKV {
    82  	kv := &EtcdKV{
    83  		Conf: conf,
    84  	}
    85  	kv.ApplyOptions(opts...)
    86  	if kv.opts.DialTimeout == 0 {
    87  		kv.opts.DialTimeout = DefaultDialTimeout
    88  	}
    89  	if kv.opts.LockTTL == 0 {
    90  		kv.opts.LockTTL = DefaultLockTTL
    91  	}
    92  
    93  	return kv
    94  }
    95  
    96  func GetKV() *clientv3.Client {
    97  	return etcdKV.Load()
    98  }
    99  
   100  func CloseKV() error {
   101  	if etcdKV.Load() == nil {
   102  		return nil
   103  	}
   104  
   105  	return etcdKV.Load().Close()
   106  }
   107  
   108  func (d *EtcdKV) GetKV(ctx context.Context) (*clientv3.Client, error) {
   109  	if d.Client != nil {
   110  		return d.Client, nil
   111  	}
   112  
   113  	if len(d.Conf.Addresses) == 0 {
   114  		return nil, fmt.Errorf("invalid etcd address")
   115  	}
   116  	logrus.Infof("dialTimeout: %v", d.opts.DialTimeout)
   117  
   118  	kv, err := clientv3.New(clientv3.Config{
   119  		Endpoints:          d.Conf.Addresses,
   120  		Context:            ctx,
   121  		Username:           d.Conf.UserName,
   122  		Password:           d.Conf.Password,
   123  		MaxCallRecvMsgSize: d.opts.MaxCallRecvMsgSize,
   124  		MaxCallSendMsgSize: d.opts.MaxCallSendMsgSize,
   125  		AutoSyncInterval:   d.opts.AutoSyncInterval,
   126  		DialTimeout:        d.opts.DialTimeout,
   127  	})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	ctx, cancel := context.WithTimeout(ctx, d.opts.DialTimeout)
   133  	defer cancel()
   134  	status, err := kv.Status(ctx, d.Conf.Addresses[0])
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	logrus.Infof("kv status: %v", status)
   139  
   140  	d.Client = kv
   141  	etcdKV.Store(kv)
   142  
   143  	return kv, nil
   144  }
   145  
   146  func (d *EtcdKV) GetKVUntil(
   147  	ctx context.Context,
   148  	maxWaitInterval time.Duration,
   149  	failAfter time.Duration,
   150  ) (*clientv3.Client, error) {
   151  	var kv *clientv3.Client
   152  	exp := time_.NewExponentialBackOff(
   153  		time_.WithExponentialBackOffOptionMaxInterval(maxWaitInterval),
   154  		time_.WithExponentialBackOffOptionMaxElapsedTime(failAfter),
   155  	)
   156  	err := time_.BackOffUntilWithContext(ctx, func(ctx context.Context) (err_ error) {
   157  		kv, err_ = d.GetKV(ctx)
   158  		if err_ != nil {
   159  			return err_
   160  		}
   161  		return nil
   162  	}, exp, true, false)
   163  	if err != nil {
   164  		return nil, fmt.Errorf("get etcd fail after: %v", failAfter)
   165  	}
   166  
   167  	return kv, nil
   168  
   169  }
   170  
   171  func (d *EtcdKV) Watch(ctx context.Context) {
   172  	for _, path := range d.opts.WatchPaths {
   173  		fmt.Printf("watch path: %v\n", path)
   174  		Watch(ctx, d.Client, path, d.opts.CreateCallbackFunc, d.opts.DeleteCallbackFunc)
   175  	}
   176  }
   177  
   178  func (d *EtcdKV) Close() error {
   179  	if d.Client == nil {
   180  		return fmt.Errorf("no etcd pool")
   181  	}
   182  	return d.Client.Close()
   183  }
   184  
   185  func (d *EtcdKV) Lock(ctx context.Context, opts ...EtcdKVOption) error {
   186  	d.ApplyOptions(opts...)
   187  
   188  	lockTTL := d.opts.LockTTL
   189  	session, err := concurrency.NewSession(d.Client, concurrency.WithContext(ctx), concurrency.WithTTL(int(lockTTL.Seconds())))
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	keyPath := filepath.Join(d.opts.LockPrefixPath, d.opts.LockKey)
   195  	mutex := concurrency.NewMutex(session, keyPath)
   196  	if err := mutex.Lock(ctx); err != nil {
   197  		return err
   198  	}
   199  
   200  	go func() {
   201  		select {
   202  		case <-ctx.Done():
   203  			logrus.Infof("lock[%v]'s context is done, err: %v", keyPath, ctx.Err())
   204  		case <-session.Done():
   205  			logrus.Infof("lock[%v]'s session is done", keyPath)
   206  		}
   207  
   208  	}()
   209  
   210  	d.session = session
   211  	d.mutex = mutex
   212  
   213  	return nil
   214  }
   215  
   216  func (d *EtcdKV) Unlock(ctx context.Context) error {
   217  	if d.mutex == nil {
   218  		return fmt.Errorf("lock mutx is nil")
   219  	}
   220  	err := d.mutex.Unlock(ctx)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	return d.session.Close()
   226  }
   227  
   228  func (d *EtcdKV) TxPipelined(ctx context.Context, cmps []clientv3.Cmp, doOps, elOps []clientv3.Op) error {
   229  	txn := d.Client.Txn(ctx)
   230  	resp, err := txn.If(cmps...).Then(doOps...).Else(elOps...).Commit()
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	if resp.Succeeded {
   236  		return nil
   237  	}
   238  
   239  	return fmt.Errorf("condition not meet, run else ops in transaction")
   240  }