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

smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new d4eecad43 KNOX-3327: Support periodic refresh of gateway configuration 
(#1235)
d4eecad43 is described below

commit d4eecad43e74d6c31ce7d56bf6421a4d1bf745ae
Author: Sandor Molnar <[email protected]>
AuthorDate: Wed May 20 14:51:40 2026 +0200

    KNOX-3327: Support periodic refresh of gateway configuration (#1235)
---
 .../org/apache/knox/gateway/GatewayMessages.java   |  6 ++
 .../org/apache/knox/gateway/GatewayServer.java     | 41 ++++++++-
 .../gateway/config/impl/GatewayConfigImpl.java     | 12 ++-
 .../org/apache/knox/gateway/GatewayServerTest.java | 98 ++++++++++++++++++++++
 .../gateway/config/impl/GatewayConfigImplTest.java | 39 +++++++++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  5 ++
 .../apache/knox/gateway/config/GatewayConfig.java  |  6 ++
 7 files changed, 205 insertions(+), 2 deletions(-)

diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java 
b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index 7e8240879..21e0d7032 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -687,6 +687,12 @@ public interface GatewayMessages {
   @Message(level = MessageLevel.INFO, text = "Stopping service: {0}")
   void stoppingService(String serviceTypeName);
 
+  @Message(level = MessageLevel.INFO, text = "Refreshed gateway config")
+  void refreshedGatewayConfig();
+
+  @Message(level = MessageLevel.WARN, text = "Unable to refresh gateway 
config")
+  void unableToReloadGatewayConfig(@StackTrace(level = MessageLevel.DEBUG) 
Exception e);
+
   @Message(level = MessageLevel.INFO, text = "Redeploying topology {0} due to 
service definition change {1} / {2} / {3}")
   void redeployingTopologyOnServiceDefinitionChange(String topologyName, 
String serviceName, String role, String version);
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java 
b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 1106d0d9c..cf1e8f998 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -109,6 +109,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
@@ -126,9 +127,14 @@ import java.util.Properties;
 import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Pattern;
 
+import static 
org.apache.knox.gateway.config.impl.GatewayConfigImpl.RELOADABLE_CONFIG_FILENAME;
+
 public class GatewayServer {
   private static final GatewayResources res = 
ResourcesFactory.get(GatewayResources.class);
   private static final GatewayMessages log = 
MessagesFactory.get(GatewayMessages.class);
@@ -144,6 +150,9 @@ public class GatewayServer {
 
   private static Properties buildProperties;
 
+  private static final ScheduledExecutorService exec = 
Executors.newSingleThreadScheduledExecutor();
+  private static FileTime lastReloadTime;
+
   private Server jetty;
   private GatewayConfig config;
   private ContextHandlerCollection contexts;
@@ -171,12 +180,13 @@ public class GatewayServer {
         if (services == null) {
           log.failedToInstantiateGatewayServices();
         }
-        final GatewayConfig config = new GatewayConfigImpl();
+        final GatewayConfigImpl config = new GatewayConfigImpl();
         validateConfigurableGatewayDirectories(config);
         if (config.isHadoopKerberosSecured()) {
           validateKerberosConfig(config);
           configureKerberosSecurity( config );
         }
+        setupGatewayConfigRefresh(config);
         Map<String,String> options = new HashMap<>();
         options.put(GatewayCommandLine.PERSIST_LONG, 
Boolean.toString(cmd.hasOption(GatewayCommandLine.PERSIST_LONG)));
         services.init(config, options);
@@ -229,6 +239,35 @@ public class GatewayServer {
     return services;
   }
 
+  private static void setupGatewayConfigRefresh(GatewayConfigImpl config) {
+    Path resourcePath = Paths.get(config.getGatewayConfDir(), 
RELOADABLE_CONFIG_FILENAME);
+    int refreshInterval = config.getConfigRefreshInterval();
+    if(refreshInterval > 0) {
+      exec.scheduleAtFixedRate(() -> refreshGatewayConfig(config, 
resourcePath),
+              0, refreshInterval, TimeUnit.MILLISECONDS);
+    }
+  }
+
+  /**
+   * Check if the resourcePath has been modified since the last reload time
+   * If so then reload the configuration to pick up the new configs
+   * @param resourcePath resource path to check and reload
+   */
+  private static synchronized void refreshGatewayConfig(GatewayConfigImpl 
config, Path resourcePath) {
+    try {
+      if(Files.exists(resourcePath) && Files.isReadable(resourcePath)) {
+        FileTime lastModifiedTime = Files.getLastModifiedTime(resourcePath);
+        if (lastReloadTime == null || 
lastReloadTime.compareTo(lastModifiedTime) < 0) {
+          lastReloadTime = lastModifiedTime;
+          config.reloadConfiguration();
+          log.refreshedGatewayConfig();
+        }
+      }
+    } catch (IOException e) {
+      log.unableToReloadGatewayConfig(e);
+    }
+  }
+
   private static void logSysProp( String name ) {
     log.logSysProp( name, System.getProperty( name ) );
   }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index 11e58527b..79c67f5a2 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -104,7 +104,9 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
 
   private static final String DEFAULT_APPLICATIONS_DIR = "applications";
 
-  private static final String[] GATEWAY_CONFIG_FILENAMES = 
{GATEWAY_CONFIG_FILE_PREFIX + "-default.xml", GATEWAY_CONFIG_FILE_PREFIX + 
"-site.xml"};
+  public static final String RELOADABLE_CONFIG_FILENAME = 
GATEWAY_CONFIG_FILE_PREFIX + "-reloadable.xml";
+
+  private static final String[] GATEWAY_CONFIG_FILENAMES = 
{GATEWAY_CONFIG_FILE_PREFIX + "-default.xml", GATEWAY_CONFIG_FILE_PREFIX + 
"-site.xml", RELOADABLE_CONFIG_FILENAME};
 
   private static final String GATEWAY_SERVICE_PREFIX = 
GATEWAY_CONFIG_FILE_PREFIX + ".service.";
   public static final String HTTP_HOST = GATEWAY_CONFIG_FILE_PREFIX + ".host";
@@ -277,6 +279,9 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   static final String REMOTE_ALIAS_SERVICE_CONFIG_PREFIX = 
GATEWAY_CONFIG_FILE_PREFIX + ".remote.alias.service.config.prefix";
   static final String REMOTE_ALIAS_SERVICE_CONFIG_PREFIX_DEFAULT = 
GATEWAY_CONFIG_FILE_PREFIX + ".remote.alias.service.config.";
 
+  static final String CONFIG_REFRESH_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + 
".config.refresh.interval";
+  static final int CONFIG_REFRESH_INTERVAL_DEFAULT = 10000;
+
   private static final List<String> DEFAULT_GLOBAL_RULES_SERVICES = 
Arrays.asList(
       "NAMENODE", "JOBTRACKER", "WEBHDFS", "WEBHCAT",
       "OOZIE", "WEBHBASE", "HIVE", "RESOURCEMANAGER",
@@ -1332,6 +1337,11 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
     return set;
   }
 
+  @Override
+  public int getConfigRefreshInterval() {
+    return getInt(CONFIG_REFRESH_INTERVAL, CONFIG_REFRESH_INTERVAL_DEFAULT);
+  }
+
   @Override
   public long getClouderaManagerDescriptorsMonitoringInterval() {
     return getLong(CLOUDERA_MANAGER_DESCRIPTORS_MONITOR_INTERVAL, -1L);
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java 
b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
new file mode 100644
index 000000000..33d348190
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.knox.gateway;
+
+import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
+import org.easymock.EasyMock;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+
+import static org.junit.Assert.assertNotNull;
+
+public class GatewayServerTest {
+
+  @Rule
+  public TemporaryFolder folder = new TemporaryFolder();
+
+  @Test
+  public void testRefreshGatewayConfig() throws Exception {
+    GatewayConfigImpl config = 
EasyMock.createNiceMock(GatewayConfigImpl.class);
+
+    File configFile = folder.newFile("gateway-reloadable.xml");
+    try (PrintWriter writer = new PrintWriter(configFile, 
StandardCharsets.UTF_8)) {
+      writer.println("<configuration>");
+      writer.println("  <property>");
+      writer.println("    <name>test.prop</name>");
+      writer.println("    <value>test.val</value>");
+      writer.println("  </property>");
+      writer.println("</configuration>");
+    }
+
+    Method refreshMethod = 
GatewayServer.class.getDeclaredMethod("refreshGatewayConfig", 
GatewayConfigImpl.class, Path.class);
+    refreshMethod.setAccessible(true);
+
+    // Initial load
+    config.reloadConfiguration();
+    EasyMock.expectLastCall().once();
+    EasyMock.replay(config);
+
+    refreshMethod.invoke(null, config, configFile.toPath());
+
+    EasyMock.verify(config);
+
+    // Check lastReloadTime is set
+    Field lastReloadTimeField = 
GatewayServer.class.getDeclaredField("lastReloadTime");
+    lastReloadTimeField.setAccessible(true);
+    FileTime lastReloadTime = (FileTime) lastReloadTimeField.get(null);
+    assertNotNull(lastReloadTime);
+
+    // Second call without change should not trigger reload
+    EasyMock.reset(config);
+    EasyMock.replay(config);
+    refreshMethod.invoke(null, config, configFile.toPath());
+    EasyMock.verify(config);
+
+    // Update file to trigger reload
+    Thread.sleep(1000); // Ensure timestamp changes
+    try (PrintWriter writer = new PrintWriter(configFile, 
StandardCharsets.UTF_8)) {
+      writer.println("<configuration>");
+      writer.println("  <property>");
+      writer.println("    <name>test.prop</name>");
+      writer.println("    <value>test.val2</value>");
+      writer.println("  </property>");
+      writer.println("</configuration>");
+    }
+
+    EasyMock.reset(config);
+    config.reloadConfiguration();
+    EasyMock.expectLastCall().once();
+    EasyMock.replay(config);
+
+    refreshMethod.invoke(null, config, configFile.toPath());
+    EasyMock.verify(config);
+  }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/config/impl/GatewayConfigImplTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/config/impl/GatewayConfigImplTest.java
index 6791fe996..88b52a148 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/config/impl/GatewayConfigImplTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/config/impl/GatewayConfigImplTest.java
@@ -31,6 +31,9 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import java.io.File;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.security.KeyStore;
 import java.util.ArrayList;
@@ -45,10 +48,15 @@ import org.apache.knox.gateway.config.GatewayConfig;
 import 
org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
 import org.apache.knox.test.TestUtils;
 import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class GatewayConfigImplTest {
 
+  @Rule
+  public TemporaryFolder folder = new TemporaryFolder();
+
   @Test( timeout = TestUtils.SHORT_TIMEOUT )
   public void testHttpServerSettings() {
     GatewayConfigImpl config = new GatewayConfigImpl();
@@ -729,4 +737,35 @@ public class GatewayConfigImplTest {
     final List<String> result = new 
GatewayConfigImpl().getClouderaManagerClientSSLCiphers();
     assertNull(result);
   }
+
+  @Test
+  public void testGetConfigRefreshInterval() {
+    GatewayConfigImpl config = new GatewayConfigImpl();
+    assertThat(config.getConfigRefreshInterval(), is(10000));
+
+    config.setInt("gateway.config.refresh.interval", 5000);
+    assertThat(config.getConfigRefreshInterval(), is(5000));
+  }
+
+  @Test
+  public void testReloadableConfigLoading() throws Exception {
+    File dir = folder.newFolder("conf");
+    File reloadableFile = new File(dir, "gateway-reloadable.xml");
+    try (PrintWriter writer = new PrintWriter(reloadableFile, 
StandardCharsets.UTF_8)) {
+      writer.println("<configuration>");
+      writer.println("  <property>");
+      writer.println("    <name>test.reloadable.prop</name>");
+      writer.println("    <value>test.reloadable.val</value>");
+      writer.println("  </property>");
+      writer.println("</configuration>");
+    }
+
+    try {
+      System.setProperty("KNOX_GATEWAY_CONF_DIR", dir.getAbsolutePath());
+      GatewayConfigImpl config = new GatewayConfigImpl();
+      assertThat(config.get("test.reloadable.prop"), 
is("test.reloadable.val"));
+    } finally {
+      System.clearProperty("KNOX_GATEWAY_CONF_DIR");
+    }
+  }
 }
diff --git 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 7fbaa9a2a..8a55d3736 100644
--- 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -862,6 +862,11 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
     return null;
   }
 
+  @Override
+  public int getConfigRefreshInterval() {
+    return 0;
+  }
+
   @Override
   public long getClouderaManagerDescriptorsMonitoringInterval() {
     return 0;
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 75bc362a5..a3c62e011 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -746,6 +746,12 @@ public interface GatewayConfig {
    */
   Set<String> getServicesToIgnoreDoAs();
 
+  /**
+   * @return refresh interval in ms
+   * @since 3.0.0
+   */
+  int getConfigRefreshInterval();
+
   /**
    * @return the monitoring interval (in milliseconds) of Cloudera Manager 
descriptors
    */

Reply via email to