Copilot commented on code in PR #6347:
URL: https://github.com/apache/shenyu/pull/6347#discussion_r3272479044


##########
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java:
##########
@@ -0,0 +1,261 @@
+/*
+ * 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 org.apache.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.DefaultController;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import 
io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import 
org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api")
+public class GatewayApiControllerConfiguration {
+
+    /**
+     * GatewayClass SharedInformerFactory - only registers GatewayClass 
informer.
+     * Separate factory to avoid DynamicKubernetesObject class key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for GatewayClass resources
+     */
+    @Bean("gatewayclass-shared-informer-factory")
+    public SharedInformerFactory gatewayClassSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gatewayclasses",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayClassApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * Gateway SharedInformerFactory - only registers Gateway informer.
+     * Separate from other factories to avoid DynamicKubernetesObject class 
key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for Gateway resources
+     */
+    @Bean("gateway-shared-informer-factory")
+    public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient 
apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gateways",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints 
informers.
+     * Separate from gatewayFactory to avoid DynamicKubernetesObject class key 
collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for HTTPRoute and Endpoints resources
+     */
+    @Bean("httproute-shared-informer-factory")
+    public SharedInformerFactory httpRouteSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "httproutes",
+                apiClient);
+        factory.sharedIndexInformerFor(httpRouteApi, 
DynamicKubernetesObject.class, 0);
+
+        GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new 
GenericKubernetesApi<>(V1Endpoints.class,
+                V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+        factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0);
+        return factory;
+    }
+
+    @Bean("gatewayclass-controller-manager")
+    public ControllerManager gatewayClassControllerManager(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            @Qualifier("gatewayclass-controller") final Controller 
gatewayClassController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayClassFactory, gatewayClassController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;

Review Comment:
   These ControllerManager beans start a new single-thread executor but never 
shut it down (and each `@Bean` call creates a new executor). This can leak 
threads and also prevent clean Spring context shutdown. Consider injecting a 
shared ExecutorService bean with a destroyMethod (shutdown) or using a Spring 
TaskExecutor / Lifecycle-aware component to manage the controller threads.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java:
##########
@@ -0,0 +1,304 @@
+/*
+ * 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 org.apache.shenyu.k8s.parser;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1EndpointSubset;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.convert.rule.impl.DivideRuleHandle;
+import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.enums.LoadBalanceEnum;
+import org.apache.shenyu.common.enums.MatchModeEnum;
+import org.apache.shenyu.common.enums.OperatorEnum;
+import org.apache.shenyu.common.enums.ParamTypeEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.SelectorTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class HttpRouteParser {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(HttpRouteParser.class);
+
+    private final Lister<V1Endpoints> endpointsLister;
+
+    public HttpRouteParser(final Lister<V1Endpoints> endpointsLister) {
+        this.endpointsLister = endpointsLister;
+    }
+
+    public ShenyuMemoryConfig parse(final DynamicKubernetesObject httpRoute) {
+        ShenyuMemoryConfig res = new ShenyuMemoryConfig();
+        String namespace = 
Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+        String routeName = httpRoute.getMetadata().getName();
+
+        JsonObject raw = httpRoute.getRaw();
+        JsonObject spec = raw.getAsJsonObject("spec");
+        if (Objects.isNull(spec)) {
+            return res;
+        }
+
+        JsonArray hostnames = spec.getAsJsonArray("hostnames");
+        JsonArray rules = spec.getAsJsonArray("rules");
+        if (Objects.isNull(rules) || rules.isEmpty()) {
+            return res;
+        }
+
+        GatewayRouteCache cache = GatewayRouteCache.getInstance();
+        List<IngressConfiguration> routeConfigList = new ArrayList<>();
+
+        for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) {
+            processRule(rules.get(ruleIndex).getAsJsonObject(), hostnames, 
namespace, routeName, ruleIndex, cache, routeConfigList);
+        }
+
+        res.setRouteConfigList(routeConfigList);
+        return res;
+    }
+
+    private void processRule(final JsonObject rule, final JsonArray hostnames, 
final String namespace,
+                             final String routeName, final int ruleIndex, 
final GatewayRouteCache cache,
+                             final List<IngressConfiguration> routeConfigList) 
{
+        JsonArray backendRefs = rule.getAsJsonArray("backendRefs");
+        if (Objects.isNull(backendRefs) || backendRefs.isEmpty()) {
+            return;
+        }
+
+        List<DivideUpstream> upstreamList = parseBackendRefs(backendRefs, 
namespace);
+
+        // Build a list of individual hostname conditions.
+        // Each hostname generates a separate selector+rule to avoid AND logic 
contradiction
+        // (a request can only match one hostname at a time).
+        List<ConditionData> hostnameConditions = new ArrayList<>();
+        if (Objects.nonNull(hostnames) && !hostnames.isEmpty()) {
+            for (JsonElement hostname : hostnames) {
+                ConditionData hostCondition = new ConditionData();
+                hostCondition.setParamType(ParamTypeEnum.DOMAIN.getName());
+                hostCondition.setOperator(OperatorEnum.EQ.getAlias());
+                hostCondition.setParamValue(hostname.getAsString());
+                hostnameConditions.add(hostCondition);
+            }
+        }
+
+        JsonArray matches = rule.getAsJsonArray("matches");
+        if (Objects.nonNull(matches) && !matches.isEmpty()) {
+            for (JsonElement matchElement : matches) {
+                JsonObject match = matchElement.getAsJsonObject();
+                List<ConditionData> matchConditions = new ArrayList<>();
+                appendMatchConditions(matchConditions, match);
+
+                if (hostnameConditions.isEmpty()) {
+                    // No hostname: one selector+rule for this match
+                    String selectorId = cache.generateSelectorId();
+                    String selectorName = routeName + "-rule-" + ruleIndex;
+                    SelectorData selectorData = buildSelectorData(selectorId, 
selectorName, matchConditions, upstreamList);
+                    RuleData ruleData = buildRuleData(cache.generateRuleId(), 
selectorId, selectorName, matchConditions);
+                    cache.addRouteSelector(namespace, routeName, 
PluginEnum.DIVIDE.getName(), selectorId);
+                    routeConfigList.add(new IngressConfiguration(selectorData, 
List.of(ruleData), null));
+                } else {
+                    // One selector+rule per hostname to keep AND semantics 
correct
+                    for (ConditionData hostCondition : hostnameConditions) {
+                        List<ConditionData> conditions = new ArrayList<>();
+                        conditions.add(hostCondition);
+                        conditions.addAll(matchConditions);
+
+                        String selectorId = cache.generateSelectorId();
+                        String selectorName = routeName + "-rule-" + ruleIndex;
+                        SelectorData selectorData = 
buildSelectorData(selectorId, selectorName, conditions, upstreamList);
+                        RuleData ruleData = 
buildRuleData(cache.generateRuleId(), selectorId, selectorName, conditions);
+                        cache.addRouteSelector(namespace, routeName, 
PluginEnum.DIVIDE.getName(), selectorId);
+                        routeConfigList.add(new 
IngressConfiguration(selectorData, List.of(ruleData), null));

Review Comment:
   When hostnames are present, multiple selectors/rules are generated but 
selectorName is always routeName + "-rule-" + ruleIndex. This produces 
duplicate selector/rule names for different hostnames/matches, which makes 
logs/debugging and any name-based tooling harder. Consider including the 
hostname and/or match index in the generated names to keep them distinct.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:
##########
@@ -0,0 +1,358 @@
+/*
+ * 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 org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class HTTPRouteReconciler implements Reconciler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(HTTPRouteReconciler.class);
+
+    private final Lister<DynamicKubernetesObject> httpRouteLister;
+
+    private final Lister<DynamicKubernetesObject> gatewayLister;
+
+    private final HttpRouteParser httpRouteParser;
+
+    private final ShenyuCacheRepository shenyuCacheRepository;
+
+    private final ApiClient apiClient;
+
+    public HTTPRouteReconciler(final 
SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer,
+                               final 
SharedIndexInformer<DynamicKubernetesObject> gatewayInformer,
+                               final HttpRouteParser httpRouteParser,
+                               final ShenyuCacheRepository 
shenyuCacheRepository,
+                               final ApiClient apiClient) {
+        this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+        this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+        this.httpRouteParser = httpRouteParser;
+        this.shenyuCacheRepository = shenyuCacheRepository;
+        this.apiClient = apiClient;
+    }
+
+    @Override
+    public Result reconcile(final Request request) {
+        LOG.info("Starting to reconcile HTTPRoute {}", request);
+        try {
+            String namespace = request.getNamespace();
+            String routeName = request.getName();
+            DynamicKubernetesObject httpRoute = 
httpRouteLister.namespace(namespace).get(routeName);
+
+            if (Objects.isNull(httpRoute)) {
+                deleteConfig(namespace, routeName);
+                return new Result(false);
+            }
+
+            if (!isBoundToShenyuGateway(httpRoute)) {
+                LOG.info("HTTPRoute {} is not bound to a ShenYu Gateway, 
skipping", request);
+                return new Result(false);
+            }
+
+            deleteConfig(namespace, routeName);
+
+            ShenyuMemoryConfig config = httpRouteParser.parse(httpRoute);
+            applyConfig(config);
+
+            bindToGateway(httpRoute);
+
+            updateHTTPRouteStatus(httpRoute);
+
+            LOG.info("HTTPRoute {} reconciled successfully", request);
+            return new Result(false);
+        } catch (Exception e) {
+            LOG.error("Error reconciling HTTPRoute {}, will retry", request, 
e);
+            return new Result(true);
+        }
+    }
+
+    private boolean isBoundToShenyuGateway(final DynamicKubernetesObject 
httpRoute) {
+        JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+        if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+            return false;
+        }
+        JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+        if (Objects.isNull(parentRefs)) {
+            return false;
+        }
+        String routeNamespace = 
Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+        for (JsonElement element : parentRefs) {
+            JsonObject parentRef = element.getAsJsonObject();
+            String parentName = parentRef.has("name") ? 
parentRef.get("name").getAsString() : null;
+            String parentNamespace = parentRef.has("namespace") ? 
parentRef.get("namespace").getAsString() : routeNamespace;
+            String sectionName = parentRef.has("sectionName") ? 
parentRef.get("sectionName").getAsString() : null;
+            if (Objects.isNull(parentName)) {
+                continue;
+            }
+            DynamicKubernetesObject gateway = 
gatewayLister.namespace(parentNamespace).get(parentName);
+            if (Objects.nonNull(gateway) && 
GatewayReconciler.isShenyuGateway(gateway)) {
+                // If sectionName is specified, verify the Gateway has a 
matching listener
+                if (Objects.nonNull(sectionName) && 
!hasMatchingListener(gateway, sectionName)) {
+                    LOG.info("HTTPRoute references sectionName '{}' but 
Gateway {}/{} has no matching listener", sectionName, parentNamespace, 
parentName);
+                    continue;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasMatchingListener(final DynamicKubernetesObject gateway, 
final String sectionName) {
+        JsonObject spec = gateway.getRaw().getAsJsonObject("spec");
+        if (Objects.isNull(spec) || !spec.has("listeners")) {
+            return false;
+        }
+        JsonArray listeners = spec.getAsJsonArray("listeners");
+        if (Objects.isNull(listeners)) {
+            return false;
+        }
+        for (JsonElement listenerElement : listeners) {
+            JsonObject listener = listenerElement.getAsJsonObject();
+            if (listener.has("name") && 
sectionName.equals(listener.get("name").getAsString())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void deleteConfig(final String namespace, final String routeName) {
+        GatewayRouteCache cache = GatewayRouteCache.getInstance();
+        List<String> selectorIds = cache.removeRouteSelectors(namespace, 
routeName, PluginEnum.DIVIDE.getName());
+        if (CollectionUtils.isNotEmpty(selectorIds)) {
+            for (String selectorId : selectorIds) {
+                List<RuleData> rules = 
shenyuCacheRepository.findRuleDataList(selectorId);
+                if (CollectionUtils.isNotEmpty(rules)) {
+                    for (RuleData rule : new ArrayList<>(rules)) {
+                        
shenyuCacheRepository.deleteRuleData(PluginEnum.DIVIDE.getName(), selectorId, 
rule.getId());
+                    }
+                }
+                
shenyuCacheRepository.deleteSelectorData(PluginEnum.DIVIDE.getName(), 
selectorId);
+            }
+        }
+        cache.removeRouteGatewayBinding(namespace, routeName);
+    }
+
+    private void applyConfig(final ShenyuMemoryConfig config) {
+        List<IngressConfiguration> routeConfigs = config.getRouteConfigList();
+        if (CollectionUtils.isEmpty(routeConfigs)) {
+            return;
+        }
+        for (IngressConfiguration routeConfig : routeConfigs) {
+            SelectorData selectorData = routeConfig.getSelectorData();
+            shenyuCacheRepository.saveOrUpdateSelectorData(selectorData);
+            for (RuleData ruleData : routeConfig.getRuleDataList()) {
+                shenyuCacheRepository.saveOrUpdateRuleData(ruleData);
+            }
+        }
+    }
+
+    private void bindToGateway(final DynamicKubernetesObject httpRoute) {
+        JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+        if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+            return;
+        }
+        JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+        String routeNamespace = 
Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+        String routeName = httpRoute.getMetadata().getName();
+
+        for (JsonElement element : parentRefs) {
+            JsonObject parentRef = element.getAsJsonObject();
+            String parentName = parentRef.has("name") ? 
parentRef.get("name").getAsString() : null;
+            String parentNamespace = parentRef.has("namespace") ? 
parentRef.get("namespace").getAsString() : routeNamespace;
+            if (Objects.nonNull(parentName)) {
+                
GatewayRouteCache.getInstance().bindRouteToGateway(parentNamespace, parentName, 
routeNamespace, routeName);
+            }
+        }
+    }
+
+    /**
+     * Update HTTPRoute status with Accepted=True and ResolvedRefs=True for 
each ShenYu-managed parent.
+     * Uses merge-patch on the /status subresource, same approach as 
GatewayReconciler.
+     * Skips the patch if the status is already up-to-date to avoid triggering 
an infinite reconcile loop.
+     */
+    private void updateHTTPRouteStatus(final DynamicKubernetesObject 
httpRoute) {
+        if (isRouteStatusAlreadySet(httpRoute)) {
+            return;
+        }
+        try {
+            final String routeNamespace = 
Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+            final String routeName = httpRoute.getMetadata().getName();
+
+            JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec");
+            if (Objects.isNull(spec) || !spec.has("parentRefs")) {
+                return;
+            }
+            JsonArray parentRefs = spec.getAsJsonArray("parentRefs");
+            JsonArray parentsStatus = buildParentsStatus(parentRefs, 
routeNamespace);
+
+            if (parentsStatus.size() == 0) {
+                return;
+            }
+            sendStatusPatch(routeNamespace, routeName, parentsStatus);
+        } catch (Exception e) {
+            LOG.warn("Failed to update HTTPRoute status, will retry on next 
resync", e);
+        }
+    }
+
+    /**
+     * Check if the HTTPRoute already has Accepted=True condition from the 
ShenYu controller
+     * in its status.parents, to avoid unnecessary status patches that trigger 
infinite reconcile loops.
+     */
+    private boolean isRouteStatusAlreadySet(final DynamicKubernetesObject 
httpRoute) {
+        JsonObject raw = httpRoute.getRaw();
+        if (!raw.has("status") || raw.get("status").isJsonNull()) {
+            return false;
+        }
+        JsonObject status = raw.getAsJsonObject("status");
+        if (!status.has("parents") || status.get("parents").isJsonNull()) {
+            return false;
+        }
+        JsonArray parents = status.getAsJsonArray("parents");
+        for (JsonElement parentElement : parents) {
+            JsonObject parent = parentElement.getAsJsonObject();
+            if (!parent.has("controllerName") || 
!GatewayApiConstants.SHENYU_CONTROLLER_NAME.equals(parent.get("controllerName").getAsString()))
 {
+                continue;
+            }
+            if (!parent.has("conditions") || 
parent.get("conditions").isJsonNull()) {
+                continue;
+            }
+            JsonArray conditions = parent.getAsJsonArray("conditions");
+            for (JsonElement condElement : conditions) {
+                JsonObject cond = condElement.getAsJsonObject();
+                if ("Accepted".equals(cond.has("type") ? 
cond.get("type").getAsString() : null)
+                        && "True".equals(cond.has("status") ? 
cond.get("status").getAsString() : null)) {
+                    return true;
+                }

Review Comment:
   isRouteStatusAlreadySet() only checks for an Accepted=True condition, but 
updateHTTPRouteStatus() documents setting both Accepted=True and 
ResolvedRefs=True and intends to skip patching only when status is up-to-date. 
As written, a route with Accepted=True but missing/False ResolvedRefs will 
never be patched again. Consider verifying both conditions (and/or checking the 
exact parentRef entry) before skipping the status patch.



##########
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java:
##########
@@ -0,0 +1,261 @@
+/*
+ * 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 org.apache.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.DefaultController;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import 
io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import 
org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api")
+public class GatewayApiControllerConfiguration {
+
+    /**
+     * GatewayClass SharedInformerFactory - only registers GatewayClass 
informer.
+     * Separate factory to avoid DynamicKubernetesObject class key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for GatewayClass resources
+     */
+    @Bean("gatewayclass-shared-informer-factory")
+    public SharedInformerFactory gatewayClassSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gatewayclasses",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayClassApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * Gateway SharedInformerFactory - only registers Gateway informer.
+     * Separate from other factories to avoid DynamicKubernetesObject class 
key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for Gateway resources
+     */
+    @Bean("gateway-shared-informer-factory")
+    public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient 
apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gateways",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints 
informers.
+     * Separate from gatewayFactory to avoid DynamicKubernetesObject class key 
collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for HTTPRoute and Endpoints resources
+     */
+    @Bean("httproute-shared-informer-factory")
+    public SharedInformerFactory httpRouteSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "httproutes",
+                apiClient);
+        factory.sharedIndexInformerFor(httpRouteApi, 
DynamicKubernetesObject.class, 0);
+
+        GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new 
GenericKubernetesApi<>(V1Endpoints.class,
+                V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+        factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0);
+        return factory;
+    }
+
+    @Bean("gatewayclass-controller-manager")
+    public ControllerManager gatewayClassControllerManager(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            @Qualifier("gatewayclass-controller") final Controller 
gatewayClassController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayClassFactory, gatewayClassController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("gateway-controller-manager")
+    public ControllerManager gatewayControllerManager(
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            @Qualifier("gateway-controller") final Controller 
gatewayController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayFactory, gatewayController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("httproute-controller-manager")
+    public ControllerManager httpRouteControllerManager(
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory,
+            @Qualifier("httproute-controller") final Controller 
httpRouteController) {
+        ControllerManager controllerManager = new 
ControllerManager(httpRouteFactory, httpRouteController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("gatewayclass-controller")
+    public Controller gatewayClassController(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            final GatewayClassReconciler gatewayClassReconciler) {
+        DefaultControllerBuilder builder = 
ControllerBuilder.defaultBuilder(gatewayClassFactory);
+        builder = builder.watch(q -> 
ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+                .withResyncPeriod(Duration.ofMinutes(1))
+                .build());
+        builder.withWorkerCount(1);
+        return 
builder.withReconciler(gatewayClassReconciler).withName("gatewayClassController").build();
+    }
+
+    @Bean("gateway-controller")
+    public Controller gatewayController(
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            final GatewayReconciler gatewayReconciler) {
+        DefaultControllerBuilder builder = 
ControllerBuilder.defaultBuilder(gatewayFactory);
+        builder = builder.watch(q -> 
ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+                .withResyncPeriod(Duration.ofMinutes(1))
+                .build());
+        builder.withWorkerCount(2);
+        return 
builder.withReconciler(gatewayReconciler).withName("gatewayController").build();
+    }
+
+    @Bean("httproute-controller")
+    public Controller httpRouteController(
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory,
+            final HTTPRouteReconciler httpRouteReconciler) {
+        DefaultControllerBuilder builder = 
ControllerBuilder.defaultBuilder(httpRouteFactory);
+        builder = builder.watch(q -> 
ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q)
+                .withResyncPeriod(Duration.ofMinutes(1))
+                .build());
+        builder.withWorkerCount(2);
+        return 
builder.withReconciler(httpRouteReconciler).withName("httpRouteController").build();
+    }
+
+    @Bean
+    public GatewayClassReconciler gatewayClassReconciler(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            @Qualifier("gateway-controller") final Controller 
gatewayController,
+            final ApiClient apiClient) {
+        SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer =
+                
gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        SharedIndexInformer<DynamicKubernetesObject> gatewayInformer =
+                
gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        RateLimitingQueue<Request> gatewayWorkQueue = ((DefaultController) 
gatewayController).getWorkQueue();
+        return new GatewayClassReconciler(gatewayClassInformer, 
gatewayInformer, gatewayWorkQueue, apiClient);
+    }
+
+    @Bean
+    public GatewayReconciler gatewayReconciler(
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory,
+            @Qualifier("httproute-controller") final Controller 
httpRouteController,
+            final ShenyuCacheRepository shenyuCacheRepository,
+            final ApiClient apiClient) {
+        SharedIndexInformer<DynamicKubernetesObject> gatewayInformer =
+                
gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer =
+                
httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        RateLimitingQueue<Request> httpRouteWorkQueue = ((DefaultController) 
httpRouteController).getWorkQueue();
+        return new GatewayReconciler(gatewayInformer, httpRouteInformer, 
shenyuCacheRepository, httpRouteWorkQueue, apiClient);
+    }
+
+    @Bean
+    public HTTPRouteReconciler httpRouteReconciler(
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory,
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            final HttpRouteParser httpRouteParser,
+            final ShenyuCacheRepository shenyuCacheRepository,
+            final ApiClient apiClient) {
+        SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer =
+                
httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        SharedIndexInformer<DynamicKubernetesObject> gatewayInformer =
+                
gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class);
+        return new HTTPRouteReconciler(httpRouteInformer, gatewayInformer, 
httpRouteParser, shenyuCacheRepository, apiClient);
+    }
+
+    @Bean
+    public HttpRouteParser httpRouteParser(
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory) {
+        SharedIndexInformer<V1Endpoints> endpointsInformer =
+                
httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class);
+        Lister<V1Endpoints> endpointsLister = new 
Lister<>(endpointsInformer.getIndexer());
+        return new HttpRouteParser(endpointsLister);
+    }
+
+    @Bean
+    public ShenyuCacheRepository shenyuCacheRepository(final 
CommonPluginDataSubscriber pluginDataSubscriber,
+                                                       final 
CommonDiscoveryUpstreamDataSubscriber discoveryUpstreamDataSubscriber,
+                                                       final 
MetaDataCacheSubscriber metaDataSubscriber,
+                                                       final 
MetaDataCacheSubscriber metaDataCacheSubscriber) {
+        ShenyuCacheRepository repository = new 
ShenyuCacheRepository(pluginDataSubscriber, discoveryUpstreamDataSubscriber, 
metaDataSubscriber, metaDataCacheSubscriber);
+        enablePlugin(repository, PluginEnum.GLOBAL, null);

Review Comment:
   The ShenyuCacheRepository bean method takes two MetaDataCacheSubscriber 
parameters. With a single MetaDataCacheSubscriber bean on the classpath, Spring 
will inject the same instance twice, and ShenyuCacheRepository will call 
onSubscribe/unSubscribe twice for metadata operations. Prefer wiring a 
MetaDataSubscriber for the first parameter (or otherwise dedupe in 
ShenyuCacheRepository) to avoid duplicate work and to allow non-cache 
MetaDataSubscriber implementations when present.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:
##########
@@ -0,0 +1,271 @@
+/*
+ * 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 org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class GatewayReconciler implements Reconciler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GatewayReconciler.class);
+
+    private final Lister<DynamicKubernetesObject> gatewayLister;
+
+    private final Lister<DynamicKubernetesObject> httpRouteLister;
+
+    private final ShenyuCacheRepository shenyuCacheRepository;
+
+    private final RateLimitingQueue<Request> httpRouteWorkQueue;
+
+    private final ApiClient apiClient;
+
+    public GatewayReconciler(final 
SharedIndexInformer<DynamicKubernetesObject> gatewayInformer,
+                             final 
SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer,
+                             final ShenyuCacheRepository shenyuCacheRepository,
+                             final RateLimitingQueue<Request> 
httpRouteWorkQueue,
+                             final ApiClient apiClient) {
+        this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+        this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+        this.shenyuCacheRepository = shenyuCacheRepository;
+        this.httpRouteWorkQueue = httpRouteWorkQueue;
+        this.apiClient = apiClient;
+    }
+
+    @Override
+    public Result reconcile(final Request request) {
+        LOG.info("Starting to reconcile gateway {}", request);
+        try {
+            DynamicKubernetesObject gateway = 
gatewayLister.namespace(request.getNamespace()).get(request.getName());
+
+            if (Objects.isNull(gateway)) {
+                LOG.info("Gateway {} deleted, cleaning associated routes", 
request);
+                deleteAssociatedRoutes(request.getNamespace(), 
request.getName());
+                return new Result(false);
+            }
+
+            if (!isShenyuGateway(gateway)) {
+                LOG.info("Gateway {} is not managed by ShenYu, skipping", 
request);
+                return new Result(false);
+            }
+
+            updateGatewayAcceptedStatus(gateway);
+
+            // Re-queue HTTPRoutes that reference this Gateway but haven't 
been applied yet
+            requeueAffectedHTTPRoutes(request.getNamespace(), 
request.getName());
+
+            LOG.info("Gateway {} reconciled successfully", request);
+            return new Result(false);
+        } catch (Exception e) {
+            LOG.error("Error reconciling gateway {}, will retry", request, e);
+            return new Result(true);
+        }
+    }
+
+    /**
+     * When a ShenYu Gateway is created/updated, find HTTPRoutes whose 
parentRefs reference
+     * this Gateway and add them to the HTTPRoute controller's work queue for 
re-reconciliation.
+     * This handles the case where an HTTPRoute was created before the Gateway 
existed.
+     * Also handles cross-namespace references where HTTPRoute's parentRef 
specifies a different namespace.
+     */
+    private void requeueAffectedHTTPRoutes(final String gatewayNamespace, 
final String gatewayName) {
+        // Search in the gateway's namespace (same-namespace reference)
+        List<DynamicKubernetesObject> localRoutes = 
httpRouteLister.namespace(gatewayNamespace).list();
+        for (DynamicKubernetesObject route : localRoutes) {
+            if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
+                Request req = new Request(route.getMetadata().getNamespace(), 
route.getMetadata().getName());
+                httpRouteWorkQueue.add(req);
+                LOG.info("Re-queued HTTPRoute {}/{} due to Gateway {}/{} 
reconciliation",
+                        route.getMetadata().getNamespace(), 
route.getMetadata().getName(),
+                        gatewayNamespace, gatewayName);
+            }
+        }
+        // Also search all namespaces for cross-namespace references
+        for (DynamicKubernetesObject route : httpRouteLister.list()) {
+            String routeNamespace = 
Objects.requireNonNull(route.getMetadata()).getNamespace();
+            if (routeNamespace.equals(gatewayNamespace)) {
+                // Already handled in local routes search above
+                continue;
+            }
+            if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
+                Request req = new Request(route.getMetadata().getNamespace(), 
route.getMetadata().getName());
+                httpRouteWorkQueue.add(req);
+                LOG.info("Re-queued cross-namespace HTTPRoute {}/{} due to 
Gateway {}/{} reconciliation",
+                        route.getMetadata().getNamespace(), 
route.getMetadata().getName(),
+                        gatewayNamespace, gatewayName);
+            }
+        }

Review Comment:
   requeueAffectedHTTPRoutes() iterates over all HTTPRoutes cluster-wide on 
every Gateway reconcile (httpRouteLister.list()), which can be expensive in 
large clusters and may create unnecessary queue churn. Consider using an 
index/cached binding (e.g., GatewayRouteCache) to target only routes that 
reference the Gateway, or maintaining an index keyed by parentRef to avoid full 
scans.



##########
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java:
##########
@@ -0,0 +1,261 @@
+/*
+ * 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 org.apache.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.DefaultController;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import 
io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import 
org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api")
+public class GatewayApiControllerConfiguration {
+
+    /**
+     * GatewayClass SharedInformerFactory - only registers GatewayClass 
informer.
+     * Separate factory to avoid DynamicKubernetesObject class key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for GatewayClass resources
+     */
+    @Bean("gatewayclass-shared-informer-factory")
+    public SharedInformerFactory gatewayClassSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gatewayclasses",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayClassApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * Gateway SharedInformerFactory - only registers Gateway informer.
+     * Separate from other factories to avoid DynamicKubernetesObject class 
key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for Gateway resources
+     */
+    @Bean("gateway-shared-informer-factory")
+    public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient 
apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gateways",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints 
informers.
+     * Separate from gatewayFactory to avoid DynamicKubernetesObject class key 
collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for HTTPRoute and Endpoints resources
+     */
+    @Bean("httproute-shared-informer-factory")
+    public SharedInformerFactory httpRouteSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "httproutes",
+                apiClient);
+        factory.sharedIndexInformerFor(httpRouteApi, 
DynamicKubernetesObject.class, 0);
+
+        GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new 
GenericKubernetesApi<>(V1Endpoints.class,
+                V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+        factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0);
+        return factory;
+    }
+
+    @Bean("gatewayclass-controller-manager")
+    public ControllerManager gatewayClassControllerManager(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            @Qualifier("gatewayclass-controller") final Controller 
gatewayClassController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayClassFactory, gatewayClassController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("gateway-controller-manager")
+    public ControllerManager gatewayControllerManager(
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            @Qualifier("gateway-controller") final Controller 
gatewayController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayFactory, gatewayController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("httproute-controller-manager")
+    public ControllerManager httpRouteControllerManager(
+            @Qualifier("httproute-shared-informer-factory") final 
SharedInformerFactory httpRouteFactory,
+            @Qualifier("httproute-controller") final Controller 
httpRouteController) {
+        ControllerManager controllerManager = new 
ControllerManager(httpRouteFactory, httpRouteController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;

Review Comment:
   This ControllerManager bean starts a new single-thread executor but never 
shuts it down. Consider reusing a shared ExecutorService bean with a 
destroyMethod (shutdown) or making the controller manager lifecycle-managed to 
avoid thread leaks and improve graceful shutdown.



##########
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java:
##########
@@ -0,0 +1,261 @@
+/*
+ * 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 org.apache.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.DefaultController;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import 
io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.workqueue.RateLimitingQueue;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler;
+import org.apache.shenyu.k8s.reconciler.GatewayReconciler;
+import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import 
org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+@Configuration
+@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api")
+public class GatewayApiControllerConfiguration {
+
+    /**
+     * GatewayClass SharedInformerFactory - only registers GatewayClass 
informer.
+     * Separate factory to avoid DynamicKubernetesObject class key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for GatewayClass resources
+     */
+    @Bean("gatewayclass-shared-informer-factory")
+    public SharedInformerFactory gatewayClassSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gatewayclasses",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayClassApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * Gateway SharedInformerFactory - only registers Gateway informer.
+     * Separate from other factories to avoid DynamicKubernetesObject class 
key collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for Gateway resources
+     */
+    @Bean("gateway-shared-informer-factory")
+    public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient 
apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "gateways",
+                apiClient);
+        factory.sharedIndexInformerFor(gatewayApi, 
DynamicKubernetesObject.class, 0);
+        return factory;
+    }
+
+    /**
+     * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints 
informers.
+     * Separate from gatewayFactory to avoid DynamicKubernetesObject class key 
collision.
+     *
+     * @param apiClient the Kubernetes API client
+     * @return the SharedInformerFactory for HTTPRoute and Endpoints resources
+     */
+    @Bean("httproute-shared-informer-factory")
+    public SharedInformerFactory httpRouteSharedInformerFactory(final 
ApiClient apiClient) {
+        SharedInformerFactory factory = new SharedInformerFactory(apiClient);
+        DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi(
+                GatewayApiConstants.GATEWAY_API_GROUP,
+                GatewayApiConstants.GATEWAY_API_VERSION,
+                "httproutes",
+                apiClient);
+        factory.sharedIndexInformerFor(httpRouteApi, 
DynamicKubernetesObject.class, 0);
+
+        GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new 
GenericKubernetesApi<>(V1Endpoints.class,
+                V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+        factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0);
+        return factory;
+    }
+
+    @Bean("gatewayclass-controller-manager")
+    public ControllerManager gatewayClassControllerManager(
+            @Qualifier("gatewayclass-shared-informer-factory") final 
SharedInformerFactory gatewayClassFactory,
+            @Qualifier("gatewayclass-controller") final Controller 
gatewayClassController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayClassFactory, gatewayClassController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;
+    }
+
+    @Bean("gateway-controller-manager")
+    public ControllerManager gatewayControllerManager(
+            @Qualifier("gateway-shared-informer-factory") final 
SharedInformerFactory gatewayFactory,
+            @Qualifier("gateway-controller") final Controller 
gatewayController) {
+        ControllerManager controllerManager = new 
ControllerManager(gatewayFactory, gatewayController);
+        Executors.newSingleThreadExecutor().submit(controllerManager);
+        return controllerManager;

Review Comment:
   This ControllerManager bean starts a new single-thread executor but never 
shuts it down. Consider reusing a shared ExecutorService bean with a 
destroyMethod (shutdown) or making the controller manager lifecycle-managed to 
avoid thread leaks and improve graceful shutdown.



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

Reply via email to