github.heygears.com/openimsdk/tools@v0.0.49/discovery/etcd/etcd.go (about)

     1  package etcd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/pkg/errors"
     7  	clientv3 "go.etcd.io/etcd/client/v3"
     8  	"go.etcd.io/etcd/client/v3/naming/endpoints"
     9  	"go.etcd.io/etcd/client/v3/naming/resolver"
    10  	"go.uber.org/zap"
    11  	"go.uber.org/zap/zapcore"
    12  	"google.golang.org/grpc"
    13  	gresolver "google.golang.org/grpc/resolver"
    14  	"io"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  )
    19  
    20  // ZkOption defines a function type for modifying clientv3.Config
    21  type ZkOption func(*clientv3.Config)
    22  
    23  // SvcDiscoveryRegistryImpl implementation
    24  type SvcDiscoveryRegistryImpl struct {
    25  	client            *clientv3.Client
    26  	resolver          gresolver.Builder
    27  	dialOptions       []grpc.DialOption
    28  	serviceKey        string
    29  	endpointMgr       endpoints.Manager
    30  	leaseID           clientv3.LeaseID
    31  	rpcRegisterTarget string
    32  
    33  	rootDirectory string
    34  
    35  	mu      sync.RWMutex
    36  	connMap map[string][]*grpc.ClientConn
    37  }
    38  
    39  func createNoOpLogger() *zap.Logger {
    40  	// Create a no-op write syncer
    41  	noOpWriter := zapcore.AddSync(io.Discard)
    42  
    43  	// Create a basic zap core with the no-op writer
    44  	core := zapcore.NewCore(
    45  		zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
    46  		noOpWriter,
    47  		zapcore.InfoLevel, // You can set this to any level that suits your needs
    48  	)
    49  
    50  	// Create the logger using the core
    51  	return zap.New(core)
    52  }
    53  
    54  // NewSvcDiscoveryRegistry creates a new service discovery registry implementation
    55  func NewSvcDiscoveryRegistry(rootDirectory string, endpoints []string, options ...ZkOption) (*SvcDiscoveryRegistryImpl, error) {
    56  	cfg := clientv3.Config{
    57  		Endpoints:   endpoints,
    58  		DialTimeout: 5 * time.Second,
    59  		// Increase keep-alive queue capacity and message size
    60  		PermitWithoutStream: true,
    61  		Logger:              createNoOpLogger(),
    62  		MaxCallSendMsgSize:  10 * 1024 * 1024, // 10 MB
    63  	}
    64  
    65  	// Apply provided options to the config
    66  	for _, opt := range options {
    67  		opt(&cfg)
    68  	}
    69  
    70  	client, err := clientv3.New(cfg)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	r, err := resolver.NewBuilder(client)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	s := &SvcDiscoveryRegistryImpl{
    80  		client:        client,
    81  		resolver:      r,
    82  		rootDirectory: rootDirectory,
    83  		connMap:       make(map[string][]*grpc.ClientConn),
    84  	}
    85  
    86  	go s.watchServiceChanges()
    87  	return s, nil
    88  }
    89  
    90  // initializeConnMap fetches all existing endpoints and populates the local map
    91  func (r *SvcDiscoveryRegistryImpl) initializeConnMap() error {
    92  	fullPrefix := fmt.Sprintf("%s/", r.rootDirectory)
    93  	resp, err := r.client.Get(context.Background(), fullPrefix, clientv3.WithPrefix())
    94  	if err != nil {
    95  		return err
    96  	}
    97  	r.connMap = make(map[string][]*grpc.ClientConn)
    98  	for _, kv := range resp.Kvs {
    99  		prefix, addr := r.splitEndpoint(string(kv.Key))
   100  		conn, err := grpc.DialContext(context.Background(), addr, append(r.dialOptions, grpc.WithResolvers(r.resolver))...)
   101  		if err != nil {
   102  			continue
   103  		}
   104  		r.connMap[prefix] = append(r.connMap[prefix], conn)
   105  	}
   106  	return nil
   107  }
   108  
   109  // WithDialTimeout sets a custom dial timeout for the etcd client
   110  func WithDialTimeout(timeout time.Duration) ZkOption {
   111  	return func(cfg *clientv3.Config) {
   112  		cfg.DialTimeout = timeout
   113  	}
   114  }
   115  
   116  // WithMaxCallSendMsgSize sets a custom max call send message size for the etcd client
   117  func WithMaxCallSendMsgSize(size int) ZkOption {
   118  	return func(cfg *clientv3.Config) {
   119  		cfg.MaxCallSendMsgSize = size
   120  	}
   121  }
   122  
   123  // WithUsernameAndPassword sets a username and password for the etcd client
   124  func WithUsernameAndPassword(username, password string) ZkOption {
   125  	return func(cfg *clientv3.Config) {
   126  		cfg.Username = username
   127  		cfg.Password = password
   128  	}
   129  }
   130  
   131  // GetUserIdHashGatewayHost returns the gateway host for a given user ID hash
   132  func (r *SvcDiscoveryRegistryImpl) GetUserIdHashGatewayHost(ctx context.Context, userId string) (string, error) {
   133  	return "", nil
   134  }
   135  
   136  // GetConns returns gRPC client connections for a given service name
   137  func (r *SvcDiscoveryRegistryImpl) GetConns(ctx context.Context, serviceName string, opts ...grpc.DialOption) ([]*grpc.ClientConn, error) {
   138  	fullServiceKey := fmt.Sprintf("%s/%s", r.rootDirectory, serviceName)
   139  	r.mu.RLock()
   140  	defer r.mu.RUnlock()
   141  	if len(r.connMap) == 0 {
   142  		r.initializeConnMap()
   143  	}
   144  	return r.connMap[fullServiceKey], nil
   145  }
   146  
   147  // GetConn returns a single gRPC client connection for a given service name
   148  func (r *SvcDiscoveryRegistryImpl) GetConn(ctx context.Context, serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
   149  	target := fmt.Sprintf("etcd:///%s/%s", r.rootDirectory, serviceName)
   150  	return grpc.DialContext(ctx, target, append(append(r.dialOptions, opts...), grpc.WithResolvers(r.resolver))...)
   151  }
   152  
   153  // GetSelfConnTarget returns the connection target for the current service
   154  func (r *SvcDiscoveryRegistryImpl) GetSelfConnTarget() string {
   155  	return r.rpcRegisterTarget
   156  }
   157  
   158  // AddOption appends gRPC dial options to the existing options
   159  func (r *SvcDiscoveryRegistryImpl) AddOption(opts ...grpc.DialOption) {
   160  	r.mu.Lock()
   161  	defer r.mu.Unlock()
   162  	r.connMap = make(map[string][]*grpc.ClientConn)
   163  	r.dialOptions = append(r.dialOptions, opts...)
   164  }
   165  
   166  // CloseConn closes a given gRPC client connection
   167  func (r *SvcDiscoveryRegistryImpl) CloseConn(conn *grpc.ClientConn) {
   168  	conn.Close()
   169  }
   170  
   171  // Register registers a new service endpoint with etcd
   172  func (r *SvcDiscoveryRegistryImpl) Register(serviceName, host string, port int, opts ...grpc.DialOption) error {
   173  	r.serviceKey = fmt.Sprintf("%s/%s/%s:%d", r.rootDirectory, serviceName, host, port)
   174  	em, err := endpoints.NewManager(r.client, r.rootDirectory+"/"+serviceName)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	r.endpointMgr = em
   179  
   180  	leaseResp, err := r.client.Grant(context.Background(), 30) //
   181  	if err != nil {
   182  		return err
   183  	}
   184  	r.leaseID = leaseResp.ID
   185  
   186  	r.rpcRegisterTarget = fmt.Sprintf("%s:%d", host, port)
   187  	endpoint := endpoints.Endpoint{Addr: r.rpcRegisterTarget}
   188  
   189  	err = em.AddEndpoint(context.TODO(), r.serviceKey, endpoint, clientv3.WithLease(leaseResp.ID))
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	go r.keepAliveLease(r.leaseID)
   195  	return nil
   196  }
   197  
   198  // keepAliveLease maintains the lease alive by sending keep-alive requests
   199  func (r *SvcDiscoveryRegistryImpl) keepAliveLease(leaseID clientv3.LeaseID) {
   200  	ch, err := r.client.KeepAlive(context.Background(), leaseID)
   201  	if err != nil {
   202  		return
   203  	}
   204  	for ka := range ch {
   205  		if ka != nil {
   206  		} else {
   207  			return
   208  		}
   209  	}
   210  }
   211  
   212  // watchServiceChanges watches for changes in the service directory
   213  func (r *SvcDiscoveryRegistryImpl) watchServiceChanges() {
   214  	watchChan := r.client.Watch(context.Background(), r.rootDirectory, clientv3.WithPrefix())
   215  	for range watchChan {
   216  		r.mu.RLock()
   217  		r.initializeConnMap()
   218  		r.mu.RUnlock()
   219  	}
   220  }
   221  
   222  // refreshConnMap fetches the latest endpoints and updates the local map
   223  func (r *SvcDiscoveryRegistryImpl) refreshConnMap(prefix string) {
   224  	r.mu.Lock()
   225  	defer r.mu.Unlock()
   226  
   227  	fullPrefix := fmt.Sprintf("%s/", prefix)
   228  	resp, err := r.client.Get(context.Background(), fullPrefix, clientv3.WithPrefix())
   229  	if err != nil {
   230  		return
   231  	}
   232  	r.connMap[prefix] = []*grpc.ClientConn{} // Update the connMap with new connections
   233  	for _, kv := range resp.Kvs {
   234  		_, addr := r.splitEndpoint(string(kv.Key))
   235  		conn, err := grpc.DialContext(context.Background(), addr, append(r.dialOptions, grpc.WithResolvers(r.resolver))...)
   236  		if err != nil {
   237  			continue
   238  		}
   239  		r.connMap[prefix] = append(r.connMap[prefix], conn)
   240  	}
   241  }
   242  
   243  // splitEndpoint splits the endpoint string into prefix and address
   244  func (r *SvcDiscoveryRegistryImpl) splitEndpoint(input string) (string, string) {
   245  	lastSlashIndex := strings.LastIndex(input, "/")
   246  	if lastSlashIndex != -1 {
   247  		part1 := input[:lastSlashIndex]
   248  		part2 := input[lastSlashIndex+1:]
   249  		return part1, part2
   250  	}
   251  	return input, ""
   252  }
   253  
   254  // UnRegister removes the service endpoint from etcd
   255  func (r *SvcDiscoveryRegistryImpl) UnRegister() error {
   256  	if r.endpointMgr == nil {
   257  		return fmt.Errorf("endpoint manager is not initialized")
   258  	}
   259  	err := r.endpointMgr.DeleteEndpoint(context.TODO(), r.serviceKey)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	return nil
   264  }
   265  
   266  // Close closes the etcd client connection
   267  func (r *SvcDiscoveryRegistryImpl) Close() {
   268  	if r.client != nil {
   269  		_ = r.client.Close()
   270  	}
   271  
   272  	r.mu.Lock()
   273  	defer r.mu.Unlock()
   274  }
   275  
   276  // Check verifies if etcd is running by checking the existence of the root node and optionally creates it with a lease
   277  func Check(ctx context.Context, etcdServers []string, etcdRoot string, createIfNotExist bool, options ...ZkOption) error {
   278  	cfg := clientv3.Config{
   279  		Endpoints: etcdServers,
   280  	}
   281  	for _, opt := range options {
   282  		opt(&cfg)
   283  	}
   284  	client, err := clientv3.New(cfg)
   285  	if err != nil {
   286  		return errors.Wrap(err, "failed to connect to etcd")
   287  	}
   288  	defer client.Close()
   289  
   290  	var opCtx context.Context
   291  	var cancel context.CancelFunc
   292  	if cfg.DialTimeout != 0 {
   293  		opCtx, cancel = context.WithTimeout(ctx, cfg.DialTimeout)
   294  	} else {
   295  		opCtx, cancel = context.WithTimeout(ctx, 10*time.Second)
   296  	}
   297  	defer cancel()
   298  
   299  	resp, err := client.Get(opCtx, etcdRoot)
   300  	if err != nil {
   301  		return errors.Wrap(err, "failed to get the root node from etcd")
   302  	}
   303  
   304  	if len(resp.Kvs) == 0 {
   305  		if createIfNotExist {
   306  			var leaseTTL int64 = 10
   307  			var leaseResp *clientv3.LeaseGrantResponse
   308  			if leaseTTL > 0 {
   309  				leaseResp, err = client.Grant(opCtx, leaseTTL)
   310  				if err != nil {
   311  					return errors.Wrap(err, "failed to create lease in etcd")
   312  				}
   313  			}
   314  			putOpts := []clientv3.OpOption{}
   315  			if leaseResp != nil {
   316  				putOpts = append(putOpts, clientv3.WithLease(leaseResp.ID))
   317  			}
   318  
   319  			_, err := client.Put(opCtx, etcdRoot, "", putOpts...)
   320  			if err != nil {
   321  				return errors.Wrap(err, "failed to create the root node in etcd")
   322  			}
   323  		} else {
   324  			return fmt.Errorf("root node %s does not exist in etcd", etcdRoot)
   325  		}
   326  	}
   327  	return nil
   328  }