Copilot commented on code in PR #1106:
URL: 
https://github.com/apache/incubator-seata-go/pull/1106#discussion_r3132211418


##########
pkg/discovery/naming_server.go:
##########
@@ -0,0 +1,673 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package discovery
+
+import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       gostnet "github.com/dubbogo/gost/net"
+       "go.uber.org/zap"
+       "net/http"
+       "net/url"
+       "seata.apache.org/seata-go/v2/pkg/util/rand"
+       "strconv"
+       "strings"
+       "sync"
+       "sync/atomic"
+       "time"
+)
+
+const (
+       httpPrefix               = "http://";
+       healthCheckThreshold     = 3
+       longPollTimeoutPeriod    = 28 * time.Second
+       authorizationHeader      = "Authorization"
+       contentTypeJSON          = "application/json"
+       namingAddrCacheTTL       = 30 * time.Second
+       failureRecoveryThreshold = 3
+       maxRetryAttempts         = 3
+       retryDelayMs             = 1000
+)
+
+type MetaResponse struct {
+       Term        int64     `json:"term"`
+       ClusterList []Cluster `json:"clusterList"`
+}
+
+type Cluster struct {
+       ClusterName string `json:"clusterName"`
+       ClusterType string `json:"clusterType"`
+       UnitData    []Unit `json:"unitData"`
+}
+
+type Unit struct {
+       UnitName           string             `json:"unitName"`
+       NamingInstanceList []NamingServerNode `json:"namingInstanceList"`
+}
+
+type NamingServerNode struct {
+       Role        ClusterRole            `json:"role"`
+       Term        int64                  `json:"term"`
+       Transaction Endpoint               `json:"transaction"`
+       Control     Endpoint               `json:"control"`
+       Internal    Endpoint               `json:"internal"`
+       Group       string                 `json:"group"`
+       Version     string                 `json:"version"`
+       Metadata    map[string]interface{} `json:"metadata"`
+       TimeStamp   int64                  `json:"timeStamp"`
+       Weight      float64                `json:"weight"`
+       Healthy     bool                   `json:"healthy"`
+       Unit        string                 `json:"unit"`
+}
+
+type Endpoint struct {
+       Host     string `json:"host"`
+       Port     int    `json:"port"`
+       Protocol string `json:"protocol"`
+}
+
+type ExternalEndpoint struct {
+       Host            string `json:"host"`
+       ControlPort     int    `json:"controlPort"`
+       TransactionPort int    `json:"transactionPort"`
+}
+
+type ClusterRole string
+
+const (
+       ClusterRoleLeader ClusterRole = "LEADER"
+       ClusterRoleMember ClusterRole = "MEMBER"
+)
+
+type NamingServerClient struct {
+       config                   *NamingServerConfig
+       logger                   *zap.Logger
+       mu                       sync.Mutex
+       instance                 *NamingServerClient
+       term                     int64
+       jwtToken                 string
+       tokenTimeStamp           int64
+       isSubscribed             bool
+       namingAddrCache          string
+       namingAddrCacheTimestamp int64
+
+       availableNamingMap sync.Map
+       vgroupAddressMap   sync.Map
+       listenerServiceMap sync.Map
+
+       healthCheckTicker *time.Ticker
+       closeChan         chan struct{}
+       wg                sync.WaitGroup
+
+       httpClient     *http.Client
+       longPollClient *http.Client
+}
+
+type NamingListener interface {
+       OnEvent(vGroup string) error
+}
+
+type NamingServerRegistryService struct {
+       client *NamingServerClient
+}
+
+var _ NamingserverRegistry = (*NamingServerRegistryService)(nil)
+
+func (n *NamingServerRegistryService) Lookup(key string) ([]*ServiceInstance, 
error) {
+       return n.client.Lookup(key)
+}
+
+func (n *NamingServerRegistryService) Close() {
+       n.client.Close()
+}
+
+func newNamingServerRegistryService(_ *ServiceConfig, cfg *NamingServerConfig) 
RegistryService {
+       client := GetInstance(cfg)
+       return &NamingServerRegistryService{
+               client: client,
+       }
+}
+
+var (
+       namingServerInstance *NamingServerClient
+       namingServerOnce     sync.Once
+)
+
+func GetInstance(config *NamingServerConfig) *NamingServerClient {
+       namingServerOnce.Do(func() {
+               namingServerInstance = &NamingServerClient{
+                       config:            config,
+                       logger:            
zap.L().Named("naming-server-client"),
+                       closeChan:         make(chan struct{}),
+                       healthCheckTicker: 
time.NewTicker(time.Duration(config.HeartbeatPeriod) * time.Millisecond),
+                       httpClient:        &http.Client{Timeout: 3 * 
time.Second},
+                       longPollClient:    &http.Client{Timeout: 30 * 
time.Second},
+               }
+               // Initialize available naming server addresses from config
+               namingServerInstance.initNamingAddrs()
+               namingServerInstance.initHealthCheck()
+       })
+       return namingServerInstance
+}
+
+func resetInstance() {
+       if namingServerInstance != nil {
+               namingServerInstance.Close()
+               namingServerInstance.mu.Lock()
+               namingServerInstance.clearNamingAddrCache()
+               namingServerInstance.mu.Unlock()
+       }
+       namingServerInstance = nil
+       namingServerOnce = sync.Once{}
+}
+
+func (c *NamingServerClient) initNamingAddrs() {
+       addrs := c.getNamingAddrs()
+       for _, addr := range addrs {
+               c.availableNamingMap.Store(addr, int32(0))
+       }
+}
+
+func (c *NamingServerClient) initHealthCheck() {
+       c.wg.Add(1)
+       go func() {
+               defer c.wg.Done()
+               for {
+                       select {
+                       case <-c.healthCheckTicker.C:
+                               urlList := c.getNamingAddrs()
+                               c.checkAvailableNamingAddr(urlList)
+                       case <-c.closeChan:
+                               return
+                       }
+               }
+       }()
+}
+
+func (c *NamingServerClient) checkAvailableNamingAddr(urlList []string) {
+       for _, addr := range urlList {
+               isHealthy := c.doHealthCheck(addr)
+
+               val, _ := c.availableNamingMap.LoadOrStore(addr, int32(0))
+               failCount := val.(int32)
+
+               if !isHealthy {
+                       newFailCount := atomic.AddInt32(&failCount, 1)
+                       c.availableNamingMap.Store(addr, newFailCount)
+                       if newFailCount == 1 {
+                               c.logger.Warn("naming server check failed", 
zap.String("addr", addr), zap.Int32("failCount", newFailCount))
+                       } else if newFailCount >= healthCheckThreshold {
+                               c.logger.Error("naming server offline", 
zap.String("addr", addr), zap.Int32("failCount", newFailCount))
+                               c.mu.Lock()
+                               if c.namingAddrCache == addr {
+                                       c.clearNamingAddrCache()
+                               }
+                               c.mu.Unlock()
+                       }
+               } else {
+                       if failCount > 0 {
+                               c.availableNamingMap.Store(addr, int32(0))
+                               c.logger.Info("naming server recovered", 
zap.String("addr", addr), zap.Int32("previousFailCount", failCount))
+                       }
+               }
+       }
+}
+
+func (c *NamingServerClient) doHealthCheck(addr string) bool {
+       checkURL := fmt.Sprintf("%s%s/naming/v1/health", httpPrefix, addr)
+
+       req, err := http.NewRequest(http.MethodGet, checkURL, nil)
+       if err != nil {
+               c.logger.Error("create health check request failed", 
zap.Error(err))
+               return false
+       }
+       req.Header.Set("Content-Type", contentTypeJSON)
+
+       resp, err := c.httpClient.Do(req)
+       if err != nil {
+               c.logger.Error("health check failed", zap.String("addr", addr), 
zap.Error(err))
+               return false
+       }
+       defer resp.Body.Close()
+       return resp.StatusCode == http.StatusOK
+}
+
+func (c *NamingServerClient) getNamingAddrs() []string {
+       return strings.Split(c.config.ServerAddr, ",")
+}
+
+func (c *NamingServerClient) Lookup(vGroup string) ([]*ServiceInstance, error) 
{
+       if !c.isSubscribed {
+               if err := c.RefreshGroup(vGroup); err != nil {
+                       return nil, fmt.Errorf("refresh group failed: %w", err)
+               }
+               listener := &RefreshListener{client: c}
+               if err := c.Subscribe(vGroup, listener); err != nil {
+                       return nil, fmt.Errorf("subscribe failed: %w", err)
+               }
+       }
+
+       val, ok := c.vgroupAddressMap.Load(vGroup)
+       if !ok {
+               if err := c.RefreshGroup(vGroup); err != nil {
+                       return nil, fmt.Errorf("refresh group failed: %w", err)
+               }
+               val, ok = c.vgroupAddressMap.Load(vGroup)
+               if !ok {
+                       return nil, errors.New("no nodes found for vgroup")
+               }
+       }
+
+       nodes := val.([]NamingServerNode)
+
+       var instances []*ServiceInstance
+       for _, node := range nodes {
+               if !node.Healthy {
+                       continue
+               }
+
+               if node.Transaction.Host == "" || node.Transaction.Port <= 0 || 
node.Transaction.Port > 65535 {
+                       c.logger.Warn("invalid node address", 
zap.String("host", node.Transaction.Host), zap.Int("port", 
node.Transaction.Port))
+                       continue
+               }
+
+               instances = append(instances, &ServiceInstance{
+                       Addr: node.Transaction.Host,
+                       Port: node.Transaction.Port,
+               })
+       }
+       return instances, nil
+}
+
+func (c *NamingServerClient) RefreshGroup(vGroup string) error {
+       namingAddr, err := c.getNamingAddr()
+       if err != nil {
+               return err
+       }
+
+       if c.isTokenExpired() {
+               if err := c.RefreshToken(namingAddr); err != nil {
+                       return err
+               }
+       }
+
+       params := url.Values{}
+       params.Add("vGroup", vGroup)
+       params.Add("namespace", c.config.Namespace)
+
+       discoveryURL := fmt.Sprintf("%s%s/naming/v1/discovery?%s", httpPrefix, 
namingAddr, params.Encode())
+       req, err := http.NewRequest(http.MethodGet, discoveryURL, nil)
+       if err != nil {
+               return err
+       }
+       if c.jwtToken != "" {
+               req.Header.Set(authorizationHeader, c.jwtToken)
+       }
+       req.Header.Set("Content-Type", contentTypeJSON)
+
+       resp, err := c.httpClient.Do(req)
+       if err != nil {
+               return fmt.Errorf("discovery request failed: %w", err)
+       }
+       defer resp.Body.Close()
+
+       if resp.StatusCode != http.StatusOK {
+               if resp.StatusCode == http.StatusUnauthorized {
+                       return fmt.Errorf("discovery failed: unauthorized 
(401), authentication required. please configure username and password in the 
naming server config")
+               }
+               return fmt.Errorf("discovery failed, status: %d", 
resp.StatusCode)
+       }
+
+       var metaResp MetaResponse
+       if err := json.NewDecoder(resp.Body).Decode(&metaResp); err != nil {
+               return fmt.Errorf("decode meta response failed: %w", err)
+       }
+
+       return c.handleMetadata(&metaResp, vGroup)
+}
+
+func (c *NamingServerClient) getNamingAddr() (string, error) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+
+       now := time.Now().UnixMilli()
+
+       if c.namingAddrCache != "" &&
+               now-c.namingAddrCacheTimestamp < 
namingAddrCacheTTL.Milliseconds() {
+
+               if val, ok := c.availableNamingMap.Load(c.namingAddrCache); ok {
+                       failCount := val.(int32)
+                       if failCount < healthCheckThreshold {
+                               return c.namingAddrCache, nil
+                       }
+               }
+
+               c.clearNamingAddrCache()
+       }
+
+       var availableAddrs []string
+       c.availableNamingMap.Range(func(key, value interface{}) bool {
+               addr := key.(string)
+               failCount := value.(int32)
+               if failCount < healthCheckThreshold {
+                       availableAddrs = append(availableAddrs, addr)
+               }
+               return true
+       })
+
+       if len(availableAddrs) == 0 {
+               return "", errors.New("no available naming server")
+       }
+
+       addr := availableAddrs[rand.RandIntn(len(availableAddrs))]
+       c.namingAddrCache = addr
+       c.namingAddrCacheTimestamp = now
+       return addr, nil
+}
+
+func (c *NamingServerClient) clearNamingAddrCache() {
+       c.namingAddrCache = ""
+       c.namingAddrCacheTimestamp = 0
+}
+
+func (c *NamingServerClient) isTokenExpired() bool {
+       if c.config.Username == "" || c.config.Password == "" {
+               return false
+       }
+
+       ts := atomic.LoadInt64(&c.tokenTimeStamp)
+       if ts == 0 {
+               return true
+       }
+
+       return time.Now().UnixMilli() >= ts+c.config.TokenValidityInMilliseconds
+}
+
+func (c *NamingServerClient) RefreshToken(addr string) error {
+       if c.config.Username == "" || c.config.Password == "" {
+               return nil
+       }
+
+       var lastErr error
+       for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
+               err := c.doRefreshToken(addr)
+               if err == nil {
+                       return nil
+               }
+
+               lastErr = err
+               if attempt < maxRetryAttempts {
+                       c.logger.Warn("token refresh failed, retrying",
+                               zap.String("addr", addr),
+                               zap.Int("attempt", attempt),
+                               zap.Error(err))
+                       time.Sleep(time.Duration(retryDelayMs*attempt) * 
time.Millisecond)
+               }
+       }
+
+       c.logger.Error("token refresh failed after all retries",
+               zap.String("addr", addr),
+               zap.Int("maxAttempts", maxRetryAttempts),
+               zap.Error(lastErr))
+       return fmt.Errorf("token refresh failed after %d attempts: %w", 
maxRetryAttempts, lastErr)
+}
+
+func (c *NamingServerClient) doRefreshToken(addr string) error {
+       loginURL := fmt.Sprintf("%s%s/api/v1/auth/login", httpPrefix, addr)
+
+       loginReq := struct {
+               Username string `json:"username"`
+               Password string `json:"password"`
+       }{
+               Username: c.config.Username,
+               Password: c.config.Password,
+       }
+
+       body, err := json.Marshal(loginReq)
+       if err != nil {
+               return fmt.Errorf("marshal login request failed: %w", err)
+       }
+
+       req, err := http.NewRequest(http.MethodPost, loginURL, 
strings.NewReader(string(body)))
+       if err != nil {
+               return err
+       }
+       req.Header.Set("Content-Type", contentTypeJSON)
+
+       resp, err := c.httpClient.Do(req)
+       if err != nil {
+               return fmt.Errorf("login request failed: %w", err)
+       }
+       defer resp.Body.Close()
+
+       if resp.StatusCode != http.StatusOK {
+               c.logger.Error("login request failed",
+                       zap.String("addr", addr),
+                       zap.Int("statusCode", resp.StatusCode),
+                       zap.String("username", c.config.Username))
+               return fmt.Errorf("authentication failed: status %d", 
resp.StatusCode)
+       }
+
+       var loginResp struct {
+               Success bool        `json:"success"`
+               Data    string      `json:"data"`
+               Code    interface{} `json:"code"`
+               Msg     string      `json:"msg"`
+       }
+       if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
+               return fmt.Errorf("decode login response failed: %w", err)
+       }
+
+       c.logger.Debug("login response received",
+               zap.Bool("success", loginResp.Success),
+               zap.String("code", fmt.Sprint(loginResp.Code)),
+               zap.String("msg", loginResp.Msg),
+               zap.String("data", loginResp.Data))
+
+       if !loginResp.Success {
+               errMsg := fmt.Sprintf("authentication failed: success=false, 
msg=%s", loginResp.Msg)
+               c.logger.Error(errMsg)
+               return fmt.Errorf(errMsg)
+       }
+       if loginResp.Data == "" {
+               return fmt.Errorf("authentication failed: token is empty")
+       }
+
+       c.jwtToken = loginResp.Data
+       atomic.StoreInt64(&c.tokenTimeStamp, time.Now().UnixMilli())
+       c.logger.Info("token refreshed successfully", zap.String("addr", addr))
+       return nil
+}
+
+func (c *NamingServerClient) handleMetadata(metaResp *MetaResponse, vGroup 
string) error {
+       if metaResp.Term > 0 {
+               atomic.StoreInt64(&c.term, metaResp.Term)
+       }
+
+       var newNodes []NamingServerNode
+       for _, cluster := range metaResp.ClusterList {
+               for _, unit := range cluster.UnitData {
+                       for _, node := range unit.NamingInstanceList {
+                               if (node.Role == ClusterRoleLeader && node.Term 
>= atomic.LoadInt64(&c.term)) ||
+                                       node.Role == ClusterRoleMember {
+                                       newNodes = append(newNodes, node)
+                               }
+                       }
+               }
+       }
+
+       c.vgroupAddressMap.Store(vGroup, newNodes)
+       return nil
+}
+
+type RefreshListener struct {
+       client *NamingServerClient
+}
+
+func (l *RefreshListener) OnEvent(vGroup string) error {
+       return l.client.RefreshGroup(vGroup)
+}
+
+func (c *NamingServerClient) Subscribe(vGroup string, listener NamingListener) 
error {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+
+       val, _ := c.listenerServiceMap.LoadOrStore(vGroup, []NamingListener{})
+       listeners := append(val.([]NamingListener), listener)
+       c.listenerServiceMap.Store(vGroup, listeners)
+
+       if !c.isSubscribed {
+               c.isSubscribed = true
+               c.wg.Add(1)
+               go c.watchLoop(vGroup)
+       }
+       return nil
+}
+
+func (c *NamingServerClient) watchLoop(vGroup string) {
+       defer c.wg.Done()
+       var retryCount int
+
+       for {
+               select {
+               case <-c.closeChan:
+                       return
+               default:
+               }
+
+               changed, err := c.Watch(vGroup)
+               if err != nil {
+                       retryCount++
+                       if retryCount > failureRecoveryThreshold {
+                               c.logger.Error("watch failed continuously, will 
retry",
+                                       zap.Error(err), zap.Int("retryCount", 
retryCount))
+                               select {
+                               case <-time.After(time.Duration(retryDelayMs) * 
time.Millisecond):
+                               case <-c.closeChan:
+                                       return
+                               }
+                       } else {
+                               c.logger.Warn("watch error, will retry 
immediately", zap.Error(err))
+                       }
+                       continue
+               }
+
+               retryCount = 0
+
+               if changed {
+                       if err := c.RefreshGroup(vGroup); err != nil {
+                               c.logger.Error("refresh group failed in watch", 
zap.Error(err))
+                               continue
+                       }
+                       val, ok := c.listenerServiceMap.Load(vGroup)
+                       if !ok {
+                               continue
+                       }
+                       for _, listener := range val.([]NamingListener) {
+                               if err := listener.OnEvent(vGroup); err != nil {
+                                       c.logger.Warn("listener callback 
failed", zap.Error(err))
+                               }
+                       }

Review Comment:
   watchLoop() refreshes the vGroup via RefreshGroup() and then invokes each 
listener's OnEvent(). The built-in RefreshListener.OnEvent() also calls 
RefreshGroup(), so a single change event triggers duplicate discovery calls. 
Consider making watchLoop either (a) only notify listeners and let listeners 
decide whether to refresh, or (b) keep the refresh in watchLoop and ensure 
listeners are pure notifications (and adjust RefreshListener accordingly).
   ```suggestion
                        val, ok := c.listenerServiceMap.Load(vGroup)
                        if ok {
                                for _, listener := range val.([]NamingListener) 
{
                                        if err := listener.OnEvent(vGroup); err 
!= nil {
                                                c.logger.Warn("listener 
callback failed", zap.Error(err))
                                        }
                                }
                                continue
                        }
   
                        if err := c.RefreshGroup(vGroup); err != nil {
                                c.logger.Error("refresh group failed in watch", 
zap.Error(err))
                                continue
                        }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to