This is an automated email from the ASF dual-hosted git repository.

dmsolr pushed a commit to branch feat/7586-k8s-controller-tuning
in repository https://gitbox.apache.org/repos/asf/shenyu.git

commit d7a1c9dbc312e97cf25c2db770fa0e77e2208d06
Author: Luke Haochao Zhuang <[email protected]>
AuthorDate: Tue Oct 13 22:25:12 2026 +0800

    feat(k8s): configure ingress controller tuning (#7586)
---
 .../k8s/IngressControllerConfiguration.java        |  56 +++++++--
 .../k8s/ShenyuKubernetesControllerProperties.java  |  76 ++++++++++++
 .../k8s/IngressControllerConfigurationTest.java    | 135 +++++++++++++++++++++
 3 files changed, 255 insertions(+), 12 deletions(-)

diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
index 7548b9d524..43d4a4a97a 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
@@ -50,25 +50,29 @@ import 
org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.SmartLifecycle;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import reactor.netty.tcp.TcpSslContextSpec;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
-import java.time.Duration;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.Collectors;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 /**
  * The type shenyu ingress controller configuration.
  */
 @Configuration
 @ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "ingress", 
matchIfMissing = true)
+@EnableConfigurationProperties(ShenyuKubernetesControllerProperties.class)
 public class IngressControllerConfiguration {
 
     /**
@@ -83,9 +87,34 @@ public class IngressControllerConfiguration {
     public ControllerManager controllerManager(final SharedInformerFactory 
sharedInformerFactory,
                                                
@Qualifier("ingress-controller") final Controller ingressController,
                                                
@Qualifier("endpoints-controller") final Controller endpointsController) {
-        ControllerManager controllerManager = new 
ControllerManager(sharedInformerFactory, ingressController, 
endpointsController);
-        Executors.newSingleThreadExecutor().submit(controllerManager);
-        return controllerManager;
+        return new ControllerManager(sharedInformerFactory, ingressController, 
endpointsController);
+    }
+
+    /**
+     * Controller executor service.
+     *
+     * @return controller executor service
+     */
+    @Bean(destroyMethod = "shutdown")
+    public ExecutorService ingressControllerExecutorService() {
+        return Executors.newSingleThreadExecutor(runnable -> {
+            Thread thread = new Thread(runnable, 
"shenyu-k8s-ingress-controller");
+            thread.setDaemon(true);
+            return thread;
+        });
+    }
+
+    /**
+     * Controller lifecycle.
+     *
+     * @param controllerManager controller manager
+     * @param executorService controller executor service
+     * @return controller lifecycle
+     */
+    @Bean
+    public SmartLifecycle 
ingressControllerLifecycle(@Qualifier("controller-manager") final 
ControllerManager controllerManager,
+                                                      
@Qualifier("ingressControllerExecutorService") final ExecutorService 
executorService) {
+        return new 
ControllerManagerLifecycle(Collections.singletonList(controllerManager), 
executorService);
     }
 
     /**
@@ -93,16 +122,17 @@ public class IngressControllerConfiguration {
      *
      * @param sharedInformerFactory sharedInformerFactory
      * @param ingressReconciler ingressReconciler
+     * @param properties controller properties
      * @return Ingress Controller
      */
     @Bean("ingress-controller")
-    public Controller ingressController(final SharedInformerFactory 
sharedInformerFactory, final IngressReconciler ingressReconciler) {
+    public Controller ingressController(final SharedInformerFactory 
sharedInformerFactory, final IngressReconciler ingressReconciler,
+                                        final 
ShenyuKubernetesControllerProperties properties) {
         DefaultControllerBuilder builder = 
ControllerBuilder.defaultBuilder(sharedInformerFactory);
         builder = builder.watch(q -> 
ControllerBuilder.controllerWatchBuilder(V1Ingress.class, q)
-                                .withResyncPeriod(Duration.ofMinutes(1))
+                                .withResyncPeriod(properties.getResyncPeriod())
                                 .build());
-        // TODO support config in application.yaml
-        builder.withWorkerCount(2);
+        builder.withWorkerCount(properties.getWorkerCount());
         return 
builder.withReconciler(ingressReconciler).withName("ingressController").build();
     }
 
@@ -133,15 +163,17 @@ public class IngressControllerConfiguration {
      *
      * @param sharedInformerFactory sharedInformerFactory
      * @param endpointsReconciler endpointsReconciler
+     * @param properties controller properties
      * @return Endpoints Controller
      */
     @Bean("endpoints-controller")
-    public Controller endpointsController(final SharedInformerFactory 
sharedInformerFactory, final EndpointsReconciler endpointsReconciler) {
+    public Controller endpointsController(final SharedInformerFactory 
sharedInformerFactory, final EndpointsReconciler endpointsReconciler,
+                                          final 
ShenyuKubernetesControllerProperties properties) {
         DefaultControllerBuilder builder = 
ControllerBuilder.defaultBuilder(sharedInformerFactory);
         builder = builder.watch(q -> 
ControllerBuilder.controllerWatchBuilder(V1Endpoints.class, q)
-                .withResyncPeriod(Duration.ofMinutes(1))
+                .withResyncPeriod(properties.getResyncPeriod())
                 .build());
-        builder.withWorkerCount(2);
+        builder.withWorkerCount(properties.getWorkerCount());
         return 
builder.withReconciler(endpointsReconciler).withName("ingressController").build();
     }
 
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java
new file mode 100644
index 0000000000..b3033e0d01
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java
@@ -0,0 +1,76 @@
+/*
+ * 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 org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.time.Duration;
+
+/**
+ * Configuration properties for Kubernetes ingress and endpoints controllers.
+ */
+@ConfigurationProperties(prefix = "shenyu.k8s.controller")
+public class ShenyuKubernetesControllerProperties {
+
+    /**
+     * Number of workers used by each controller. Values greater than one rely 
on the
+     * ingress, selector, secret and service relation caches remaining 
thread-safe.
+     */
+    private int workerCount = 2;
+
+    /**
+     * Period between full informer resynchronizations.
+     */
+    private Duration resyncPeriod = Duration.ofMinutes(1);
+
+    /**
+     * Get worker count.
+     *
+     * @return worker count
+     */
+    public int getWorkerCount() {
+        return workerCount;
+    }
+
+    /**
+     * Set worker count.
+     *
+     * @param workerCount worker count
+     */
+    public void setWorkerCount(final int workerCount) {
+        this.workerCount = workerCount;
+    }
+
+    /**
+     * Get resync period.
+     *
+     * @return resync period
+     */
+    public Duration getResyncPeriod() {
+        return resyncPeriod;
+    }
+
+    /**
+     * Set resync period.
+     *
+     * @param resyncPeriod resync period
+     */
+    public void setResyncPeriod(final Duration resyncPeriod) {
+        this.resyncPeriod = resyncPeriod;
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java
new file mode 100644
index 0000000000..99a0ea8859
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1Ingress;
+import org.apache.shenyu.k8s.reconciler.EndpointsReconciler;
+import org.apache.shenyu.k8s.reconciler.IngressReconciler;
+import org.junit.jupiter.api.Test;
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for {@link IngressControllerConfiguration}.
+ */
+public final class IngressControllerConfigurationTest {
+
+    private final IngressControllerConfiguration configuration = new 
IngressControllerConfiguration();
+
+    @Test
+    public void testDefaultProperties() {
+        ShenyuKubernetesControllerProperties properties = new 
ShenyuKubernetesControllerProperties();
+
+        assertThat(properties.getWorkerCount(), is(2));
+        assertThat(properties.getResyncPeriod(), is(Duration.ofMinutes(1)));
+    }
+
+    @Test
+    public void testPropertiesBinding() {
+        new ApplicationContextRunner()
+                .withUserConfiguration(PropertiesConfiguration.class)
+                .withPropertyValues(
+                        "shenyu.k8s.controller.worker-count=4",
+                        "shenyu.k8s.controller.resync-period=30s")
+                .run(context -> {
+                    ShenyuKubernetesControllerProperties properties = 
context.getBean(ShenyuKubernetesControllerProperties.class);
+                    assertThat(properties.getWorkerCount(), is(4));
+                    assertThat(properties.getResyncPeriod(), 
is(Duration.ofSeconds(30)));
+                });
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testControllersUseConfiguredTuning() {
+        SharedInformerFactory sharedInformerFactory = 
mock(SharedInformerFactory.class);
+        SharedIndexInformer<V1Ingress> ingressInformer = 
mock(SharedIndexInformer.class);
+        SharedIndexInformer<V1Endpoints> endpointsInformer = 
mock(SharedIndexInformer.class);
+        
when(sharedInformerFactory.getExistingSharedIndexInformer(V1Ingress.class)).thenReturn(ingressInformer);
+        
when(sharedInformerFactory.getExistingSharedIndexInformer(V1Endpoints.class)).thenReturn(endpointsInformer);
+        ShenyuKubernetesControllerProperties properties = new 
ShenyuKubernetesControllerProperties();
+        properties.setWorkerCount(4);
+        properties.setResyncPeriod(Duration.ofSeconds(30));
+
+        Controller ingressController = 
configuration.ingressController(sharedInformerFactory, 
mock(IngressReconciler.class), properties);
+        Controller endpointsController = 
configuration.endpointsController(sharedInformerFactory, 
mock(EndpointsReconciler.class), properties);
+
+        assertThat(ingressController, instanceOf(DefaultController.class));
+        assertThat(endpointsController, instanceOf(DefaultController.class));
+        assertThat(((DefaultController) ingressController).getWorkerCount(), 
is(4));
+        assertThat(((DefaultController) endpointsController).getWorkerCount(), 
is(4));
+        verify(ingressInformer).addEventHandlerWithResyncPeriod(any(), 
eq(30_000L));
+        verify(endpointsInformer).addEventHandlerWithResyncPeriod(any(), 
eq(30_000L));
+    }
+
+    @Test
+    public void testControllerLifecycleAndExecutor() throws Exception {
+        ControllerManager controllerManager = mock(ControllerManager.class);
+        ExecutorService executorService = 
configuration.ingressControllerExecutorService();
+        try {
+            SmartLifecycle lifecycle = 
configuration.ingressControllerLifecycle(controllerManager, executorService);
+            assertThat(lifecycle, 
instanceOf(ControllerManagerLifecycle.class));
+            assertThat(executorService.submit(() -> 
Thread.currentThread().getName()).get(5, TimeUnit.SECONDS),
+                    is("shenyu-k8s-ingress-controller"));
+
+            lifecycle.start();
+            lifecycle.stop();
+
+            verify(controllerManager).run();
+            verify(controllerManager).shutdown();
+        } finally {
+            executorService.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testLifecycleDoesNotStartDuringCreation() {
+        ControllerManager controllerManager = mock(ControllerManager.class);
+        ExecutorService executorService = mock(ExecutorService.class);
+
+        configuration.ingressControllerLifecycle(controllerManager, 
executorService);
+
+        verifyNoInteractions(controllerManager, executorService);
+    }
+
+    @Configuration
+    @EnableConfigurationProperties(ShenyuKubernetesControllerProperties.class)
+    static class PropertiesConfiguration {
+    }
+}

Reply via email to