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 }