conditions = new ArrayList<>();
conditions.add(hostCondition);
SelectorData selectorData = buildSelectorData(selectorId, selectorName, conditions, upstreamList);
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java
index 9779ea3ac167..b12b50327b2b 100644
--- a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java
@@ -104,9 +104,44 @@ public Result reconcile(final Request request) {
* 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.
+ *
+ * Uses GatewayRouteCache as primary lookup for already-bound routes to avoid full-cluster scans
+ * in large deployments. Falls back to informer scanning only for routes not yet tracked in cache
+ * (e.g., when a Gateway is first created and no HTTPRoute has been successfully reconciled yet).
*/
private void requeueAffectedHTTPRoutes(final String gatewayNamespace, final String gatewayName) {
- // Search in the gateway's namespace (same-namespace reference)
+ GatewayRouteCache cache = GatewayRouteCache.getInstance();
+ List cachedRoutes = cache.getRoutesByGateway(gatewayNamespace, gatewayName);
+
+ if (CollectionUtils.isNotEmpty(cachedRoutes)) {
+ // Fast path: routes already bound in cache, re-queue by parsed keys
+ for (String routeKey : cachedRoutes) {
+ String[] parts = routeKey.split("/", 2);
+ if (parts.length >= 2) {
+ Request req = new Request(parts[0], parts[1]);
+ httpRouteWorkQueue.add(req);
+ LOG.info("Re-queued cached HTTPRoute {}/{} due to Gateway {}/{} reconciliation",
+ parts[0], parts[1], gatewayNamespace, gatewayName);
+ }
+ }
+ // Also scan for cross-namespace routes that may not be in cache yet
+ for (DynamicKubernetesObject route : httpRouteLister.list()) {
+ String routeNamespace = Objects.requireNonNull(route.getMetadata()).getNamespace();
+ if (routeNamespace.equals(gatewayNamespace)) {
+ 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);
+ }
+ }
+ return;
+ }
+
+ // Cache miss: Gateway just created, no routes reconciled yet. Fall back to scanning.
List localRoutes = httpRouteLister.namespace(gatewayNamespace).list();
for (DynamicKubernetesObject route : localRoutes) {
if (isBoundToGateway(route, gatewayNamespace, gatewayName)) {
@@ -117,7 +152,6 @@ private void requeueAffectedHTTPRoutes(final String gatewayNamespace, final Stri
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)) {
@@ -230,8 +264,7 @@ private void updateGatewayAcceptedStatus(final DynamicKubernetesObject gateway)
condition.addProperty("message", "Gateway has been accepted by the ShenYu controller");
condition.addProperty("lastTransitionTime", Instant.now().toString());
- JsonArray conditions = new JsonArray();
- conditions.add(condition);
+ JsonArray conditions = buildGatewayStatusConditions(gateway, condition);
JsonObject statusObj = new JsonObject();
statusObj.add("conditions", conditions);
@@ -268,4 +301,45 @@ private void updateGatewayAcceptedStatus(final DynamicKubernetesObject gateway)
}
}
+ /**
+ * Build the Gateway status conditions array for the patch body.
+ * Includes the accepted condition and preserves non-Accepted conditions
+ * already present in status. Ensures Programmed exists per Gateway API
+ * spec default (Unknown/Pending) if missing.
+ */
+ private JsonArray buildGatewayStatusConditions(final DynamicKubernetesObject gateway,
+ final JsonObject acceptedCondition) {
+ JsonArray conditions = new JsonArray();
+ conditions.add(acceptedCondition);
+
+ boolean hasProgrammed = false;
+ JsonObject raw = gateway.getRaw();
+ if (raw.has("status") && !raw.get("status").isJsonNull()) {
+ JsonObject status = raw.getAsJsonObject("status");
+ if (status.has("conditions") && !status.get("conditions").isJsonNull()) {
+ JsonArray existingConditions = status.getAsJsonArray("conditions");
+ for (JsonElement el : existingConditions) {
+ JsonObject existing = el.getAsJsonObject();
+ String existingType = existing.has("type") ? existing.get("type").getAsString() : null;
+ if ("Programmed".equals(existingType)) {
+ hasProgrammed = true;
+ conditions.add(existing);
+ } else if (!"Accepted".equals(existingType)) {
+ conditions.add(existing);
+ }
+ }
+ }
+ }
+ if (!hasProgrammed) {
+ JsonObject programmedDefault = new JsonObject();
+ programmedDefault.addProperty("type", "Programmed");
+ programmedDefault.addProperty("status", "Unknown");
+ programmedDefault.addProperty("reason", "Pending");
+ programmedDefault.addProperty("message", "Waiting for controller");
+ programmedDefault.addProperty("lastTransitionTime", "1970-01-01T00:00:00Z");
+ conditions.add(programmedDefault);
+ }
+ return conditions;
+ }
+
}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java
index 1545ce3cd0a3..a2e7b37a73af 100644
--- a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java
@@ -236,8 +236,11 @@ private void updateHTTPRouteStatus(final DynamicKubernetesObject httpRoute) {
}
/**
- * 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.
+ * Check if the HTTPRoute already has both Accepted=True and ResolvedRefs=True conditions
+ * from the ShenYu controller in its status.parents, to avoid unnecessary status patches
+ * that trigger infinite reconcile loops.
+ * Both conditions must be present because updateHTTPRouteStatus() always sets both together;
+ * checking only Accepted=True would leave routes with a partial status never repaired.
*/
private boolean isRouteStatusAlreadySet(final DynamicKubernetesObject httpRoute) {
JsonObject raw = httpRoute.getRaw();
@@ -258,13 +261,23 @@ private boolean isRouteStatusAlreadySet(final DynamicKubernetesObject httpRoute)
continue;
}
JsonArray conditions = parent.getAsJsonArray("conditions");
+ boolean hasAccepted = false;
+ boolean hasResolvedRefs = false;
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;
+ String type = cond.has("type") ? cond.get("type").getAsString() : null;
+ String condStatus = cond.has("status") ? cond.get("status").getAsString() : null;
+ if ("True".equals(condStatus)) {
+ if ("Accepted".equals(type)) {
+ hasAccepted = true;
+ } else if ("ResolvedRefs".equals(type)) {
+ hasResolvedRefs = true;
+ }
}
}
+ if (hasAccepted && hasResolvedRefs) {
+ return true;
+ }
}
return false;
}
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java
index fe6f7b6ae4e7..4e238285e3a8 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java
@@ -51,6 +51,7 @@
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
@@ -118,30 +119,48 @@ public SharedInformerFactory httpRouteSharedInformerFactory(final ApiClient apiC
return factory;
}
+ /**
+ * Shared ExecutorService for all ControllerManager beans, with a destroy method to
+ * ensure graceful shutdown and prevent thread leaks on context close.
+ *
+ * @return daemon cached thread pool executor
+ */
+ @Bean(destroyMethod = "shutdown")
+ public ExecutorService controllerExecutorService() {
+ return Executors.newCachedThreadPool(r -> {
+ Thread t = new Thread(r, "shenyu-k8s-controller");
+ t.setDaemon(true);
+ return t;
+ });
+ }
+
@Bean("gatewayclass-controller-manager")
public ControllerManager gatewayClassControllerManager(
@Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory,
- @Qualifier("gatewayclass-controller") final Controller gatewayClassController) {
+ @Qualifier("gatewayclass-controller") final Controller gatewayClassController,
+ final ExecutorService controllerExecutorService) {
ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController);
- Executors.newSingleThreadExecutor().submit(controllerManager);
+ controllerExecutorService.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) {
+ @Qualifier("gateway-controller") final Controller gatewayController,
+ final ExecutorService controllerExecutorService) {
ControllerManager controllerManager = new ControllerManager(gatewayFactory, gatewayController);
- Executors.newSingleThreadExecutor().submit(controllerManager);
+ controllerExecutorService.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) {
+ @Qualifier("httproute-controller") final Controller httpRouteController,
+ final ExecutorService controllerExecutorService) {
ControllerManager controllerManager = new ControllerManager(httpRouteFactory, httpRouteController);
- Executors.newSingleThreadExecutor().submit(controllerManager);
+ controllerExecutorService.submit(controllerManager);
return controllerManager;
}
From 6624c29265c3234eb220f1832d771b3a7bd85b76 Mon Sep 17 00:00:00 2001
From: eye-gu <734164350@qq.com>
Date: Mon, 1 Jun 2026 21:53:23 +0800
Subject: [PATCH 5/5] ci: trigger CI re-run