Copilot commented on code in PR #892:
URL: https://github.com/apache/dubbo-go-pixiu/pull/892#discussion_r2934586885


##########
pkg/server/router_manager.go:
##########
@@ -46,6 +46,19 @@ func (rm *RouterManager) AddRouterListener(l RouterListener) 
{
        rm.rls = append(rm.rls, l)
 }
 
+func (rm *RouterManager) RemoveRouterListener(l RouterListener) {
+       for i, listener := range rm.rls {
+               if listener == l {
+                       rm.rls = append(rm.rls[:i], rm.rls[i+1:]...)
+                       return
+               }
+       }
+}
+
+func (rm *RouterManager) ClearRouterListeners() {
+       rm.rls = nil
+}

Review Comment:
   RouterManager listener list (rm.rls) is mutated/cleared with no 
synchronization, but it’s also iterated in AddRouter/DeleteRouter/UpdateRoutes. 
Calling RemoveRouterListener/ClearRouterListeners concurrently with route 
updates can cause data races and potentially panics. Consider adding a mutex 
(or copy-on-write snapshot) to protect rm.rls across add/remove/clear and all 
iterations.



##########
pkg/hotreload/route_reloader.go:
##########
@@ -56,16 +54,44 @@ func (r *RouteReloader) CheckUpdate(oldConfig, newConfig 
*model.Bootstrap) bool
 
 // HotReload applies the new route configuration.
 func (r *RouteReloader) HotReload(oldConfig, newConfig *model.Bootstrap) error 
{
-       oldRoutes := extractRoutes(oldConfig)
-       newRoutes := extractRoutes(newConfig)
+       logger.Info("Starting route hot reload")
+
+       srv := server.GetServer()
+       if srv == nil {
+               logger.Error("Server instance is nil")
+               return errors.New("server instance is nil")
+       }
+       logger.Info("Got server instance")
+
+       logger.Info("Reinitializing server components...")
+
+       listenerManager := srv.GetListenerManager()
+       if listenerManager == nil {
+               logger.Error("Listener manager is nil")
+               return errors.New("listener manager is nil")
+       }
+
+       refreshed := 0
+       for _, listener := range newConfig.StaticResources.Listeners {
+               logger.Infof("Refreshing listener: name=%s, protocol=%s", 
listener.Name, listener.ProtocolStr)
+
+               srv.GetRouterManager().ClearRouterListeners()
+
+               if err := listenerManager.UpdateListener(listener); err != nil {
+                       logger.Errorf("Failed to refresh listener %s: %v", 
listener.Name, err)
+                       return errors.Wrapf(err, "failed to refresh listener 
%s", listener.Name)
+               }

Review Comment:
   `ClearRouterListeners()` is called inside the listener refresh loop. This 
will drop listeners registered by earlier refreshed listeners, so only the last 
refreshed listener’s RouterCoordinator remains subscribed (breaking dynamic 
route updates for multiple listeners). If the goal is to avoid duplicate 
registrations, clear once before the loop (and then re-register all 
coordinators) or remove only the stale listener(s) being replaced.



##########
pkg/hotreload/http_handler.go:
##########
@@ -0,0 +1,212 @@
+/*
+ * 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 hotreload
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "sync"
+       "time"
+)
+
+import (
+       "gopkg.in/yaml.v3"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/config"
+       "github.com/apache/dubbo-go-pixiu/pkg/logger"
+       "github.com/apache/dubbo-go-pixiu/pkg/model"
+)
+
+var (
+       reloadMutex sync.Mutex
+       configPath  string
+)
+
+func SetConfigPath(path string) {
+       configPath = path
+}
+
+// ReloadHandler handles HTTP reload requests
+type ReloadHandler struct{}
+
+// ServeHTTP handles the reload HTTP request
+func (h *ReloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       if r.Method != http.MethodPost {
+               http.Error(w, "Method not allowed, use POST", 
http.StatusMethodNotAllowed)
+               return
+       }
+
+       logger.Info("Received reload request via HTTP")
+
+       var err error
+       if r.ContentLength > 0 {
+               err = triggerConfigReloadFromBody(r)
+       } else {
+               err = triggerConfigReload()
+       }
+
+       if err != nil {
+               logger.Errorf("Reload failed: %v", err)
+               http.Error(w, fmt.Sprintf("Reload failed: %v", err), 
http.StatusInternalServerError)
+               return
+       }
+
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(http.StatusOK)
+       writeJSONResponse(w, "success", "Configuration reloaded successfully")
+       logger.Info("Reload completed successfully")
+}
+
+func writeJSONResponse(w http.ResponseWriter, status, message string) {
+       response := map[string]string{
+               "status":  status,
+               "message": message,
+               "time":    time.Now().Format(time.RFC3339),
+       }
+       if err := json.NewEncoder(w).Encode(response); err != nil {
+               logger.Errorf("Failed to encode response: %v", err)
+       }
+}
+
+// HealthHandler handles health check requests
+type HealthHandler struct{}
+
+// ServeHTTP handles the health check HTTP request
+func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(http.StatusOK)
+       writeJSONResponse(w, "healthy", "")
+}
+
+// triggerConfigReload reloads configuration from file
+func triggerConfigReload() error {
+       reloadMutex.Lock()
+       defer reloadMutex.Unlock()
+
+       if configPath == "" {
+               return fmt.Errorf("config path not set")
+       }
+
+       logger.Infof("Reloading configuration from: %s", configPath)
+
+       content, err := os.ReadFile(configPath)
+       if err != nil {
+               return fmt.Errorf("failed to read config file: %w", err)
+       }
+
+       return reloadFromYAML(content)
+}
+
+// triggerConfigReloadFromBody reloads configuration from HTTP request body
+func triggerConfigReloadFromBody(r *http.Request) error {
+       reloadMutex.Lock()
+       defer reloadMutex.Unlock()
+
+       logger.Info("Reloading configuration from request body")
+
+       content, err := io.ReadAll(r.Body)
+       if err != nil {
+               return fmt.Errorf("failed to read request body: %w", err)
+       }
+       defer func() {
+               if err := r.Body.Close(); err != nil {
+                       logger.Errorf("Failed to close request body: %v", err)
+               }
+       }()
+
+       return reloadFromYAML(content)
+}
+
+// reloadFromYAML performs the actual reload from YAML content
+func reloadFromYAML(content []byte) error {
+       newConfig := &model.Bootstrap{}
+       if err := yaml.Unmarshal(content, newConfig); err != nil {
+               return fmt.Errorf("failed to parse config: %w", err)
+       }
+
+       if err := config.Adapter(newConfig); err != nil {
+               return fmt.Errorf("failed to adapt config: %w", err)
+       }
+
+       oldConfig := config.GetBootstrap()
+       if oldConfig == nil {
+               return fmt.Errorf("current config is nil")
+       }
+
+       logger.Infof("Old config has %d listeners, new config has %d listeners",
+               len(oldConfig.StaticResources.Listeners), 
len(newConfig.StaticResources.Listeners))
+
+       wg := &sync.WaitGroup{}
+       var reloadErrors []error
+       errorMutex := &sync.Mutex{}
+
+       for _, reloader := range coordinator.reloaders {
+               logger.Infof("Triggering reload for %T", reloader)
+               wg.Add(1)
+               go func(r HotReloader) {
+                       defer wg.Done()
+                       if err := r.HotReload(oldConfig, newConfig); err != nil 
{
+                               logger.Errorf("Hot reload failed for %T: %v", 
r, err)
+                               errorMutex.Lock()
+                               reloadErrors = append(reloadErrors, err)
+                               errorMutex.Unlock()
+                       }
+               }(reloader)
+       }
+
+       wg.Wait()
+
+       if len(reloadErrors) > 0 {
+               return fmt.Errorf("reload completed with %d errors", 
len(reloadErrors))
+       }
+
+       config.SetBootstrap(newConfig)
+       logger.Info("Configuration reloaded successfully and global config 
updated")
+       return nil
+}
+
+// StartReloadServer starts the HTTP server for reload endpoint
+func StartReloadServer(port int) error {
+       mux := http.NewServeMux()
+
+       mux.Handle("/-/reload", &ReloadHandler{})
+       mux.Handle("/-/health", &HealthHandler{})
+
+       addr := fmt.Sprintf(":%d", port)
+       logger.Infof("Starting reload HTTP server on %s", addr)
+
+       server := &http.Server{
+               Addr:         addr,
+               Handler:      mux,
+               ReadTimeout:  10 * time.Second,
+               WriteTimeout: 10 * time.Second,
+       }

Review Comment:
   `StartReloadServer` binds to `:<port>` and exposes `/-/reload` without any 
authentication/authorization. This allows anyone with network access to the pod 
to push arbitrary config and trigger reloads. Please restrict the bind address 
(e.g., loopback by default), and/or require an auth token / mTLS (and make it 
configurable) before enabling this endpoint by default.



##########
controllers/internal/controller/gateway_controller.go:
##########
@@ -729,12 +805,20 @@ func (r *GatewayReconciler) ensureDataPlane(ctx 
context.Context, gateway *gatewa
                needsUpdate := false
                existingHash := 
existingDeployment.Spec.Template.Annotations["pixiu.apache.org/config-hash"]
                if existingHash != configHash {
-                       r.Log.Info("config hash changed, triggering deployment 
update",
+                       r.Log.Info("config hash changed, triggering hot reload",
                                "gateway", gateway.GetName(),
                                "deployment", deploymentName,
                                "oldHash", existingHash,
                                "newHash", configHash)
-                       needsUpdate = true
+
+                       if err := r.triggerHotReload(ctx, gateway, 
deploymentName); err != nil {
+                               r.Log.Error(err, "failed to trigger hot reload, 
will update deployment", "gateway", gateway.GetName())
+                               needsUpdate = true
+                       } else {
+                               // Update annotation only after successful hot 
reload
+                               
existingDeployment.Spec.Template.Annotations["pixiu.apache.org/config-hash"] = 
configHash
+                               needsUpdate = true
+                       }

Review Comment:
   Even when `triggerHotReload` succeeds, the code still updates 
`existingDeployment.Spec.Template.Annotations["pixiu.apache.org/config-hash"]` 
and later patches `existingDeployment.Spec` to the new pod template. Any change 
to the pod template annotations triggers a Deployment rollout/restart, which 
largely negates the benefit of hot reload. If hot reload is meant to avoid pod 
restarts, consider keeping the hash outside the pod template (e.g., Deployment 
metadata) and skipping spec/template updates on successful reload.



##########
controllers/internal/converter/policy_applier.go:
##########
@@ -154,8 +154,23 @@ func ApplyClusterConfig(cluster *Cluster, clusterConfig 
*v1alpha1.ClusterConfig)
                cluster.Type = clusterConfig.Type
        }
 
-       // Endpoints are already resolved in gateway_controller.go
-       // This function is called after endpoints are resolved
+       if len(clusterConfig.Endpoints) > 0 {
+               cluster.Endpoints = []*Endpoint{}
+               for i, ep := range clusterConfig.Endpoints {
+                       endpoint := &Endpoint{
+                               SocketAddress: SocketAddress{
+                                       Address: ep.Address,
+                                       Port:    int(ep.Port),
+                               },
+                       }
+                       if ep.ID != nil {
+                               endpoint.ID = int(*ep.ID)
+                       } else {
+                               endpoint.ID = i + 1
+                       }
+                       cluster.Endpoints = append(cluster.Endpoints, endpoint)
+               }
+       }

Review Comment:
   ApplyClusterConfig now overwrites `cluster.Endpoints` from 
`clusterConfig.Endpoints`. In ensureGatewayConfigMap, endpoints are resolved 
first via resolveClusterEndpoints() (to Pod IPs / ClusterIP), but this call 
then replaces the resolved endpoints with the original policy endpoints (often 
DNS), effectively undoing the resolution. Consider not mutating endpoints here 
(only apply LB/type), or pass/apply the resolved endpoints instead.



##########
pkg/common/router/router.go:
##########
@@ -65,7 +75,23 @@ func CreateRouterCoordinator(routeConfig 
*model.RouteConfiguration) *RouterCoord
        return rc
 }
 
+func (rm *RouterCoordinator) Close() {
+       if rm.dynamic {
+               routerMgr := server.GetRouterManager()
+               if routerMgr != nil {
+                       routerMgr.RemoveRouterListener(rm)
+               }
+       }
+}
+
 func (rm *RouterCoordinator) Route(hc *http.HttpContext) (*model.RouteAction, 
error) {
+       if rm.needsRegistration && rm.dynamic {
+               routerMgr := server.GetRouterManager()
+               if routerMgr != nil {
+                       routerMgr.AddRouterListener(rm)
+                       rm.needsRegistration = false
+               }

Review Comment:
   The delayed RouterListener registration in Route() mutates needsRegistration 
without synchronization. Since Route() can be called concurrently, this 
introduces a Go data race and can also register the same coordinator multiple 
times. Use sync.Once / atomic CAS (or guard with rm.mu) around the registration 
and flag update.
   



##########
pkg/common/http/manager.go:
##########
@@ -66,6 +66,10 @@ func CreateHttpConnectionManager(hcmc 
*model.HttpConnectionManagerConfig) *HttpC
                        route.Match.Methods = []string{}
                }
        }
+
+       // Force enable dynamic routing for hot reload
+       hcmc.RouteConfig.Dynamic = true
+

Review Comment:
   Forcing `hcmc.RouteConfig.Dynamic = true` changes routing behavior globally 
(even when config sets dynamic=false) and can also cause RouterListener 
registrations to accumulate across listener refreshes. This should be driven by 
configuration/feature-flag (or limited to hot-reload mode) rather than 
unconditionally overriding user config.
   



##########
pkg/cmd/gateway.go:
##########
@@ -116,6 +116,14 @@ func (d *DefaultDeployer) initialize() error {
 
        hotreload.StartHotReload(d.configManger, d.bootstrap)
 
+       // Set config path for hot reload
+       hotreload.SetConfigPath(configPath)
+
+       // Start HTTP reload endpoint on port 18380
+       if err := hotreload.StartReloadServer(18380); err != nil {
+               logger.Warnf("[startGatewayCmd] failed to start reload server: 
%s", err.Error())
+       }
+

Review Comment:
   Starting the reload HTTP server unconditionally on a hard-coded port (18380) 
makes this feature always-on and not configurable/disable-able, which is risky 
operationally (port conflicts in non-container runs, unexpected exposed surface 
area). Consider wiring the port/bind address and an enable flag through 
CLI/env/config, and defaulting to disabled in production.
   



##########
pkg/config/config_load.go:
##########
@@ -58,6 +58,11 @@ func GetBootstrap() *model.Bootstrap {
        return config
 }
 
+// SetBootstrap set config global
+func SetBootstrap(cfg *model.Bootstrap) {
+       config = cfg
+}

Review Comment:
   SetBootstrap updates the global `config` pointer without synchronization. 
With hot-reload now calling SetBootstrap at runtime, concurrent reads via 
GetBootstrap() can race. Consider storing the bootstrap in an `atomic.Pointer` 
or guarding Get/Set with an RWMutex.



##########
controllers/internal/controller/gateway_controller.go:
##########
@@ -618,6 +682,13 @@ func (r *GatewayReconciler) ensureGatewayConfigMap(ctx 
context.Context, gateway
                                return "", fmt.Errorf("failed to update 
configmap: %w", err)
                        }
                        r.Log.Info("updated gateway configmap", "gateway", 
gateway.GetName(), "configmap", configMapName)
+
+                       deploymentName := fmt.Sprintf("%s-%s", 
gateway.GetName(), string(gateway.GetUID())[:8])
+                       if err := r.triggerHotReload(ctx, gateway, 
deploymentName); err != nil {
+                               r.Log.Error(err, "failed to trigger hot reload 
after configmap update", "gateway", gateway.GetName())
+                       } else {
+                               r.Log.Info("hot reload triggered successfully 
after configmap update", "gateway", gateway.GetName())
+                       }

Review Comment:
   Hot reload is triggered immediately after updating the ConfigMap here, and 
then (on the same reconcile) ensureDataPlane() also triggers hot reload when it 
sees the config-hash change. This can result in duplicate reload attempts per 
change. Consider triggering reload in only one place (or short-circuiting the 
later check when reload already succeeded).
   



-- 
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