You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2026/10/22 20:34:53 UTC

(knox) branch master updated: KNOX-3526: Redeploy topologies with reloadableListener on gateway-rel… (#1466)

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

moresandeep 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 e19e63e4f KNOX-3526: Redeploy topologies with reloadableListener on gateway-rel… (#1466)
e19e63e4f is described below

commit e19e63e4faade107cc3f944e41327e3b94e764ed
Author: hanicz <ha...@users.noreply.github.com>
AuthorDate: Thu Oct 22 22:34:48 2026 +0200

    KNOX-3526: Redeploy topologies with reloadableListener on gateway-rel… (#1466)
    
    * KNOX-3526: Redeploy topologies with reloadableListener on gateway-reloadable.xml changes
    
    * KNOX-3526: Keep notifying listeners in case of exceptions. Admin API now saves the reloadableListener flag.
---
 .../org/apache/knox/gateway/GatewayMessages.java   |  6 ++
 .../org/apache/knox/gateway/GatewayServer.java     | 12 ++-
 .../topology/impl/DefaultTopologyService.java      | 15 +++-
 .../builder/BeanPropertyTopologyBuilder.java       | 11 +++
 .../topology/xml/KnoxFormatXmlTopologyRules.java   |  2 +
 .../src/main/resources/conf/topology-v1.xsd        |  1 +
 .../org/apache/knox/gateway/GatewayServerTest.java | 34 ++++++++
 .../topology/DefaultTopologyServiceTest.java       | 90 ++++++++++++++++++++++
 .../simple/SimpleDescriptorFactoryTest.java        | 18 +++++
 .../simple/SimpleDescriptorHandlerTest.java        | 40 ++++++++++
 .../topology/validation/TopologyValidatorTest.java |  4 +
 .../topology-valid-reloadable-listener.xml         | 30 ++++++++
 .../gateway/service/admin/beans/BeanConverter.java |  2 +
 .../knox/gateway/service/admin/beans/Topology.java | 11 +++
 .../org/apache/knox/gateway/topology/Topology.java | 17 ++++
 .../knox/gateway/topology/topology_binding-xml.xml |  5 +-
 .../hadoop/xml/HadoopXmlResourceParser.java        |  4 +
 .../hadoop/xml/HadoopXmlResourceParserTest.java    |  2 +
 .../src/test/resources/testDescriptor.xml          |  1 +
 .../gateway/topology/simple/SimpleDescriptor.java  |  2 +
 .../topology/simple/SimpleDescriptorHandler.java   |  4 +
 .../topology/simple/SimpleDescriptorImpl.java      | 12 +++
 22 files changed, 320 insertions(+), 3 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 d4639b011..817e72b33 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
@@ -702,9 +702,15 @@ public interface GatewayMessages {
   @Message(level = MessageLevel.WARN, text = "Unable to refresh gateway config")
   void unableToReloadGatewayConfig(@StackTrace(level = MessageLevel.DEBUG) Exception e);
 
+  @Message(level = MessageLevel.ERROR, text = "Gateway config change listener {0} failed: {1}")
+  void gatewayConfigChangeListenerFailed(String listener, @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);
 
+  @Message(level = MessageLevel.INFO, text = "Redeploying topology {0} due to gateway reloadable configuration change")
+  void redeployingTopologyOnGatewayConfigChange(String topologyName);
+
   @Message(level = MessageLevel.INFO, text = "Saved service definition {0} / {1} / {2}")
   void savedServiceDefinitionChange(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 405604f63..9438ac364 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
@@ -277,7 +277,11 @@ public class GatewayServer {
           config.reloadConfiguration();
           log.refreshedGatewayConfig();
           for (GatewayConfigChangeListener listener : configChangeListeners) {
-            listener.onGatewayConfigChanged(config);
+            try {
+              listener.onGatewayConfigChanged(config);
+            } catch (RuntimeException e) {
+              log.gatewayConfigChangeListenerFailed(listener.getClass().getName(), e);
+            }
           }
         }
       }
@@ -725,6 +729,9 @@ public class GatewayServer {
 
     final ServiceDefinitionRegistry serviceDefinitionRegistry = services.getService(ServiceType.SERVICE_DEFINITION_REGISTRY);
     serviceDefinitionRegistry.addServiceDefinitionChangeListener(monitor);
+    if (monitor instanceof GatewayConfigChangeListener) {
+      registerConfigChangeListener((GatewayConfigChangeListener) monitor);
+    }
 
     final Map<String, Integer> topologyPortMap = config.getGatewayPortMappings();
 
@@ -820,6 +827,9 @@ public class GatewayServer {
         log.stoppingGateway();
         services.stop();
         monitor.stopMonitor();
+        if (monitor instanceof GatewayConfigChangeListener) {
+          unregisterConfigChangeListener((GatewayConfigChangeListener) monitor);
+        }
         jetty.stop();
         jetty.join();
         log.stoppedGateway();
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
index 0468e4aee..3437d678d 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java
@@ -33,6 +33,7 @@ import org.apache.knox.gateway.audit.api.Auditor;
 import org.apache.knox.gateway.audit.api.ResourceType;
 import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.config.GatewayConfigChangeListener;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.service.definition.ServiceDefinition;
 import org.apache.knox.gateway.service.definition.ServiceDefinitionChangeListener;
@@ -86,7 +87,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class DefaultTopologyService extends FileAlterationListenerAdaptor implements TopologyService, TopologyMonitor,
-    TopologyProvider, FileFilter, FileAlterationListener, ServiceDefinitionChangeListener {
+    TopologyProvider, FileFilter, FileAlterationListener, ServiceDefinitionChangeListener, GatewayConfigChangeListener {
 
   private static final JAXBContext jaxbContext = getJAXBContext();
   private static final String TOPOLOGY_CLOSING_XML_ELEMENT = "</topology>";
@@ -536,6 +537,18 @@ public class DefaultTopologyService extends FileAlterationListenerAdaptor implem
     });
   }
 
+  @Override
+  public void onGatewayConfigChanged(GatewayConfig config) {
+    getTopologies().stream().filter(Topology::isReloadableListener).forEach(topology -> {
+      try {
+        log.redeployingTopologyOnGatewayConfigChange(topology.getName());
+        redeployTopology(topology);
+      } catch (RuntimeException e) {
+        log.failedToRedeployTopology(topology.getName(), e);
+      }
+    });
+  }
+
   private boolean isRelevantService(Service service, String role, String name, String version) {
     return service.getRole().equalsIgnoreCase(role)
         && (service.getName() == null || service.getName().equalsIgnoreCase(name) && (service.getVersion() == null || service.getVersion().equals(new Version(version))));
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/topology/builder/BeanPropertyTopologyBuilder.java b/gateway-server/src/main/java/org/apache/knox/gateway/topology/builder/BeanPropertyTopologyBuilder.java
index 0e08f06e4..7c5902217 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/builder/BeanPropertyTopologyBuilder.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/topology/builder/BeanPropertyTopologyBuilder.java
@@ -30,6 +30,7 @@ public class BeanPropertyTopologyBuilder implements TopologyBuilder {
     private String defaultService;
     private boolean isGenerated;
     private long redeployTime;
+    private boolean reloadableListener;
     private List<Provider> providers;
     private List<Service> services;
     private List<Application> applications;
@@ -67,6 +68,15 @@ public class BeanPropertyTopologyBuilder implements TopologyBuilder {
       return redeployTime;
     }
 
+    public BeanPropertyTopologyBuilder reloadableListener(String reloadableListener) {
+      this.reloadableListener = Boolean.parseBoolean(reloadableListener);
+      return this;
+    }
+
+    public boolean isReloadableListener() {
+      return reloadableListener;
+    }
+
     public BeanPropertyTopologyBuilder defaultService(String defaultService) {
       this.defaultService = defaultService;
       return this;
@@ -110,6 +120,7 @@ public class BeanPropertyTopologyBuilder implements TopologyBuilder {
         topology.setDefaultServicePath(defaultService);
         topology.setGenerated(isGenerated);
         topology.setRedeployTime(redeployTime);
+        topology.setReloadableListener(reloadableListener);
 
         for (Provider provider : providers) {
             topology.addProvider(provider);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/topology/xml/KnoxFormatXmlTopologyRules.java b/gateway-server/src/main/java/org/apache/knox/gateway/topology/xml/KnoxFormatXmlTopologyRules.java
index becdd3dfb..865197c18 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/xml/KnoxFormatXmlTopologyRules.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/topology/xml/KnoxFormatXmlTopologyRules.java
@@ -37,6 +37,7 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
   private static final String DEFAULT_SERVICE_TAG = "path";
   private static final String GENERATED_TAG = "generated";
   private static final String REDEPLOY_TIME_TAG = "redeployTime";
+  private static final String RELOADABLE_LISTENER_TAG = "reloadableListener";
   private static final String APPLICATION_TAG = "application";
   private static final String SERVICE_TAG = "service";
   private static final String ROLE_TAG = "role";
@@ -66,6 +67,7 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
     forPattern( ROOT_TAG + "/" + DEFAULT_SERVICE_TAG ).callMethod("defaultService").usingElementBodyAsArgument();
     forPattern( ROOT_TAG + "/" + GENERATED_TAG ).callMethod("generated").usingElementBodyAsArgument();
     forPattern( ROOT_TAG + "/" + REDEPLOY_TIME_TAG ).callMethod("redeployTime").usingElementBodyAsArgument();
+    forPattern( ROOT_TAG + "/" + RELOADABLE_LISTENER_TAG ).callMethod("reloadableListener").usingElementBodyAsArgument();
 
     forPattern( ROOT_TAG + "/" + APPLICATION_TAG ).createObject().ofType( Application.class ).then().setNext( "addApplication" );
     forPattern( ROOT_TAG + "/" + APPLICATION_TAG + "/" + ROLE_TAG ).setBeanProperty();
diff --git a/gateway-server/src/main/resources/conf/topology-v1.xsd b/gateway-server/src/main/resources/conf/topology-v1.xsd
index a828e72db..382467233 100644
--- a/gateway-server/src/main/resources/conf/topology-v1.xsd
+++ b/gateway-server/src/main/resources/conf/topology-v1.xsd
@@ -23,6 +23,7 @@ limitations under the License.
                 <h:element name="name" minOccurs="0" maxOccurs="1"/>
                 <h:element name="path" minOccurs="0" maxOccurs="1"/>
                 <h:element name="generated" type="h:boolean" minOccurs="0"/>
+                <h:element name="reloadableListener" type="h:boolean" minOccurs="0"/>
 
                 <h:element name="gateway" minOccurs="0" maxOccurs="1">
                     <h:complexType>
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
index e8ad52e9c..a142c19e4 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
@@ -138,4 +138,38 @@ public class GatewayServerTest {
       GatewayServer.unregisterConfigChangeListener(listener);
     }
   }
+
+  @Test
+  public void testFailingConfigChangeListenerDoesNotBlockOthers() throws Exception {
+    GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class);
+    File configFile = folder.newFile("gateway-reloadable-failing-listener.xml");
+    try (PrintWriter writer = new PrintWriter(configFile, StandardCharsets.UTF_8)) {
+      writer.println("<configuration></configuration>");
+    }
+
+    final boolean[] notified = {false};
+    GatewayConfigChangeListener failing = (c) -> {
+      throw new IllegalStateException("listener failure");
+    };
+    GatewayConfigChangeListener listener = (c) -> notified[0] = true;
+
+    GatewayServer.registerConfigChangeListener(failing);
+    GatewayServer.registerConfigChangeListener(listener);
+
+    try {
+      Method refreshMethod = GatewayServer.class.getDeclaredMethod("refreshGatewayConfig", GatewayConfigImpl.class, Path.class);
+      refreshMethod.setAccessible(true);
+
+      Field lastReloadTimeField = GatewayServer.class.getDeclaredField("lastReloadTime");
+      lastReloadTimeField.setAccessible(true);
+      lastReloadTimeField.set(null, null);
+
+      refreshMethod.invoke(null, config, configFile.toPath());
+
+      assertTrue("Listener after the failing one should have been notified", notified[0]);
+    } finally {
+      GatewayServer.unregisterConfigChangeListener(failing);
+      GatewayServer.unregisterConfigChangeListener(listener);
+    }
+  }
 }
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/topology/DefaultTopologyServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/topology/DefaultTopologyServiceTest.java
index e428d1e84..bf61974e2 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/topology/DefaultTopologyServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/topology/DefaultTopologyServiceTest.java
@@ -39,6 +39,7 @@ import org.apache.knox.gateway.topology.Provider;
 import org.apache.knox.gateway.topology.Topology;
 import org.apache.knox.gateway.topology.TopologyEvent;
 import org.apache.knox.gateway.topology.TopologyListener;
+import org.apache.knox.gateway.topology.validation.TopologyValidator;
 import org.easymock.EasyMock;
 import org.junit.Test;
 
@@ -830,6 +831,95 @@ public class DefaultTopologyServiceTest {
     }
   }
 
+  @Test
+  public void testOnlyReloadableListenerTopologiesRedeployedOnGatewayConfigChange() throws Exception {
+    final File dir = createDir();
+    try {
+      final File topologyDir = new File(dir, "topologies");
+      final long time = topologyDir.lastModified();
+      final String listening = "<topology>\n    <reloadableListener>true</reloadableListener>\n"
+          + "    <service>\n        <role>WEBHDFS</role>\n        <url>http://host-one:80/webhdfs</url>\n    </service>\n</topology>\n";
+      final String notListening = "<topology>\n"
+          + "    <service>\n        <role>WEBHDFS</role>\n        <url>http://host-two:80/webhdfs</url>\n    </service>\n</topology>\n";
+      final File listeningFile = createFile(topologyDir, "listening.xml",
+          new ByteArrayInputStream(listening.getBytes(StandardCharsets.UTF_8)), time);
+      final File notListeningFile = createFile(topologyDir, "notlistening.xml",
+          new ByteArrayInputStream(notListening.getBytes(StandardCharsets.UTF_8)), time);
+
+      final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+      EasyMock.expect(config.getGatewayTopologyDir()).andReturn(topologyDir.getAbsolutePath()).anyTimes();
+      EasyMock.expect(config.isTopologyValidationEnabled()).andReturn(true).anyTimes();
+      EasyMock.replay(config);
+
+      final DefaultTopologyService topologyService = new DefaultTopologyService();
+      topologyService.init(config, new HashMap<>());
+      topologyService.reloadTopologies();
+
+      final Map<String, Boolean> flags = new HashMap<>();
+      topologyService.getTopologies().forEach(t -> flags.put(t.getName(), t.isReloadableListener()));
+      assertEquals(Boolean.TRUE, flags.get("listening"));
+      assertEquals(Boolean.FALSE, flags.get("notlistening"));
+
+      topologyService.onGatewayConfigChanged(config);
+
+      assertTrue(FileUtils.readFileToString(listeningFile, StandardCharsets.UTF_8).contains("<redeployTime>"));
+      assertEquals(notListening, FileUtils.readFileToString(notListeningFile, StandardCharsets.UTF_8));
+    } finally {
+      FileUtils.deleteQuietly(dir);
+    }
+  }
+
+  @Test
+  public void testDeployTopologyPersistsReloadableListener() throws Exception {
+    final File dir = createDir();
+    try {
+      final File topologyDir = new File(dir, "topologies");
+      topologyDir.mkdirs();
+
+      final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+      EasyMock.expect(config.getGatewayTopologyDir()).andReturn(topologyDir.getAbsolutePath()).anyTimes();
+      EasyMock.replay(config);
+
+      final DefaultTopologyService topologyService = new DefaultTopologyService();
+      topologyService.init(config, new HashMap<>());
+
+      topologyService.deployTopology(createTopologyForDeployment("listening", true));
+      topologyService.deployTopology(createTopologyForDeployment("notlistening", false));
+      topologyService.reloadTopologies();
+
+      final Map<String, Boolean> flags = new HashMap<>();
+      topologyService.getTopologies().forEach(t -> flags.put(t.getName(), t.isReloadableListener()));
+      assertEquals(Boolean.TRUE, flags.get("listening"));
+      assertEquals(Boolean.FALSE, flags.get("notlistening"));
+
+      final File listeningFile = new File(topologyDir, "listening.xml");
+      final File notListeningFile = new File(topologyDir, "notlistening.xml");
+      assertTrue(FileUtils.readFileToString(listeningFile, StandardCharsets.UTF_8).contains("<reloadableListener>true</reloadableListener>"));
+      assertFalse(FileUtils.readFileToString(notListeningFile, StandardCharsets.UTF_8).contains("reloadableListener"));
+
+      final TopologyValidator validator = new TopologyValidator(listeningFile.getAbsolutePath());
+      assertTrue(validator.getErrorString(), validator.validateTopology());
+    } finally {
+      FileUtils.deleteQuietly(dir);
+    }
+  }
+
+  private Topology createTopologyForDeployment(String name, boolean reloadableListener) {
+    final Topology topology = new Topology();
+    topology.setName(name);
+    topology.setReloadableListener(reloadableListener);
+    final Provider provider = new Provider();
+    provider.setRole("authentication");
+    provider.setName("Anonymous");
+    provider.setEnabled(true);
+    topology.addProvider(provider);
+    final org.apache.knox.gateway.topology.Service service = new org.apache.knox.gateway.topology.Service();
+    service.setRole("WEBHDFS");
+    service.addUrl("http://localhost:50070/webhdfs");
+    topology.addService(service);
+    return topology;
+  }
+
   private class TestTopologyListener implements TopologyListener {
     List<List<TopologyEvent>> events = new ArrayList<>();
 
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorFactoryTest.java
index 985aa0006..8507d31c9 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorFactoryTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorFactoryTest.java
@@ -34,6 +34,7 @@ import java.util.List;
 import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -103,6 +104,23 @@ public class SimpleDescriptorFactoryTest {
         testParseSimpleDescriptorWithServicesAndApplications(FileType.YAML);
     }
 
+    @Test
+    public void testParseReloadableListener() throws Exception {
+        assertTrue(parseDescriptorContent("json", "{\"provider-config-ref\":\"pc\",\"reloadable-listener\":true}").isReloadableListener());
+        assertFalse(parseDescriptorContent("json", "{\"provider-config-ref\":\"pc\"}").isReloadableListener());
+        assertTrue(parseDescriptorContent("yaml", "---\nprovider-config-ref: pc\nreloadable-listener: true\n").isReloadableListener());
+        assertFalse(parseDescriptorContent("yml", "---\nprovider-config-ref: pc\n").isReloadableListener());
+    }
+
+    private SimpleDescriptor parseDescriptorContent(String extension, String content) throws Exception {
+        final Path descriptor = Files.createTempFile("reloadable-listener-test", "." + extension);
+        try {
+            Files.write(descriptor, content.getBytes(StandardCharsets.UTF_8));
+            return SimpleDescriptorFactory.parse(descriptor.toString());
+        } finally {
+            Files.deleteIfExists(descriptor);
+        }
+    }
 
     private void testParseSimpleDescriptor(FileType type, boolean provisionEncryptQueryStringCredential) throws Exception {
         final String   discoveryType    = "AMBARI";
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandlerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandlerTest.java
index 9bab863e7..c3d40c591 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandlerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandlerTest.java
@@ -141,6 +141,46 @@ public class SimpleDescriptorHandlerTest {
         doTestDiscoveryConfig(null, null, null, null, null);
     }
 
+    @Test
+    public void testReloadableListenerWrittenToGeneratedTopology() throws Exception {
+        assertTrue(generateTopologyContent(true).contains("<reloadableListener>true</reloadableListener>"));
+        assertFalse(generateTopologyContent(false).contains("reloadableListener"));
+    }
+
+    private String generateTopologyContent(boolean reloadableListener) throws Exception {
+        GatewayConfig gc = EasyMock.createNiceMock(GatewayConfig.class);
+        EasyMock.expect(gc.getReadOnlyOverrideTopologyNames()).andReturn(Collections.emptyList()).anyTimes();
+        EasyMock.replay(gc);
+
+        File providerConfig = new File(System.getProperty("java.io.tmpdir"), "test-providers.xml");
+        FileUtils.write(providerConfig, TEST_PROVIDER_CONFIG, StandardCharsets.UTF_8);
+
+        SimpleDescriptor testDescriptor = EasyMock.createNiceMock(SimpleDescriptor.class);
+        EasyMock.expect(testDescriptor.getName()).andReturn("reloadablelistenerdescriptor").anyTimes();
+        EasyMock.expect(testDescriptor.getProviderConfig()).andReturn(providerConfig.getAbsolutePath()).anyTimes();
+        EasyMock.expect(testDescriptor.isReloadableListener()).andReturn(reloadableListener).anyTimes();
+        SimpleDescriptor.Service svc = EasyMock.createNiceMock(SimpleDescriptor.Service.class);
+        EasyMock.expect(svc.getName()).andReturn("KNOXTOKEN").anyTimes();
+        EasyMock.expect(svc.getURLs()).andReturn(Collections.emptyList()).anyTimes();
+        EasyMock.expect(svc.getParams()).andReturn(Collections.emptyMap()).anyTimes();
+        EasyMock.replay(svc);
+        EasyMock.expect(testDescriptor.getServices()).andReturn(Collections.singletonList(svc)).anyTimes();
+        EasyMock.replay(testDescriptor);
+
+        File destDir = new File(System.getProperty("java.io.tmpdir")).getCanonicalFile();
+        File topologyFile = null;
+        try {
+            Map<String, File> files = SimpleDescriptorHandler.handle(gc, testDescriptor, providerConfig.getParentFile(), destDir);
+            topologyFile = files.get("topology");
+            return FileUtils.readFileToString(topologyFile, StandardCharsets.UTF_8);
+        } finally {
+            providerConfig.delete();
+            if (topologyFile != null) {
+                topologyFile.delete();
+            }
+        }
+    }
+
     private void doTestDiscoveryConfig(final String discoveryType,
                                        final String address,
                                        final String clusterName,
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/topology/validation/TopologyValidatorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/topology/validation/TopologyValidatorTest.java
index 299027f27..bd22926d0 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/topology/validation/TopologyValidatorTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/topology/validation/TopologyValidatorTest.java
@@ -42,5 +42,9 @@ public class TopologyValidatorTest {
     url = TestUtils.getResourceUrl( TopologyValidatorTest.class, "topology-valid-with-name.xml" );
     validator = new TopologyValidator( url );
     assertThat( validator.getErrorString(), validator.validateTopology(), is( true ) );
+
+    url = TestUtils.getResourceUrl( TopologyValidatorTest.class, "topology-valid-reloadable-listener.xml" );
+    validator = new TopologyValidator( url );
+    assertThat( validator.getErrorString(), validator.validateTopology(), is( true ) );
   }
 }
\ No newline at end of file
diff --git a/gateway-server/src/test/resources/org/apache/knox/gateway/topology/validation/TopologyValidatorTest/topology-valid-reloadable-listener.xml b/gateway-server/src/test/resources/org/apache/knox/gateway/topology/validation/TopologyValidatorTest/topology-valid-reloadable-listener.xml
new file mode 100644
index 000000000..081392b7b
--- /dev/null
+++ b/gateway-server/src/test/resources/org/apache/knox/gateway/topology/validation/TopologyValidatorTest/topology-valid-reloadable-listener.xml
@@ -0,0 +1,30 @@
+<!--
+  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.
+-->
+<topology>
+    <generated>true</generated>
+    <reloadableListener>true</reloadableListener>
+    <gateway>
+        <provider>
+            <role>authentication</role>
+            <name>Anonymous</name>
+            <enabled>true</enabled>
+        </provider>
+    </gateway>
+    <service>
+        <role>KNOX</role>
+    </service>
+</topology>
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/BeanConverter.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/BeanConverter.java
index a06ba7abf..9cb4b45d0 100644
--- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/BeanConverter.java
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/BeanConverter.java
@@ -32,6 +32,7 @@ public class BeanConverter {
     topologyResource.setUri(topology.getUri());
     topologyResource.setGenerated(topology.isGenerated());
     topologyResource.setRedeployTime(topology.getRedeployTime());
+    topologyResource.setReloadableListener(topology.isReloadableListener());
     for ( org.apache.knox.gateway.topology.Provider provider : topology.getProviders() ) {
       topologyResource.getProviders().add( getProvider(provider) );
     }
@@ -52,6 +53,7 @@ public class BeanConverter {
     deploymentTopology.setUri(topology.getUri());
     deploymentTopology.setGenerated(topology.isGenerated());
     deploymentTopology.setRedeployTime(topology.getRedeployTime());
+    deploymentTopology.setReloadableListener(topology.isReloadableListener());
     for ( Provider provider : topology.getProviders() ) {
       deploymentTopology.addProvider( getProvider(provider) );
     }
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/Topology.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/Topology.java
index 00f9e7d1f..1cf4f25ae 100644
--- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/Topology.java
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/beans/Topology.java
@@ -45,6 +45,9 @@ public class Topology {
   @XmlElement(name="redeployTime")
   private long redeployTime;
 
+  @XmlElement(name="reloadableListener")
+  private boolean reloadableListener;
+
   @XmlElement(name="provider")
   @XmlElementWrapper(name="gateway")
   public List<Provider> providers;
@@ -106,6 +109,14 @@ public class Topology {
     this.redeployTime = redeployTime;
   }
 
+  public boolean isReloadableListener() {
+    return reloadableListener;
+  }
+
+  public void setReloadableListener(boolean reloadableListener) {
+    this.reloadableListener = reloadableListener;
+  }
+
   public List<Service> getServices() {
     if (services == null) {
       services = new ArrayList<>();
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/topology/Topology.java b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/Topology.java
index 003dd3965..f8d9176b5 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/topology/Topology.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/Topology.java
@@ -39,6 +39,7 @@ public class Topology {
   private long timestamp;
   private boolean isGenerated;
   private long redeployTime = -1L;
+  private boolean reloadableListener;
   public List<Provider> providerList = new ArrayList<>();
   private Map<String,Map<String,Provider>> providerMap = new HashMap<>();
   public List<Service> services = new ArrayList<>();
@@ -102,6 +103,22 @@ public class Topology {
     this.redeployTime = redeployTime;
   }
 
+  public boolean isReloadableListener() {
+    return reloadableListener;
+  }
+
+  public void setReloadableListener(boolean reloadableListener) {
+    this.reloadableListener = reloadableListener;
+  }
+
+  public Boolean getReloadableListenerElement() {
+    return reloadableListener ? Boolean.TRUE : null;
+  }
+
+  public void setReloadableListenerElement(Boolean reloadableListener) {
+    this.reloadableListener = Boolean.TRUE.equals(reloadableListener);
+  }
+
   public Collection<Service> getServices() {
     return services;
   }
diff --git a/gateway-spi/src/main/resources/org/apache/knox/gateway/topology/topology_binding-xml.xml b/gateway-spi/src/main/resources/org/apache/knox/gateway/topology/topology_binding-xml.xml
index 956387e29..52d115ad6 100644
--- a/gateway-spi/src/main/resources/org/apache/knox/gateway/topology/topology_binding-xml.xml
+++ b/gateway-spi/src/main/resources/org/apache/knox/gateway/topology/topology_binding-xml.xml
@@ -24,11 +24,14 @@ or more contributor license agreements.  See the NOTICE file
         element-form-default="QUALIFIED"/>
     <java-types>
         <java-type name="Topology" xml-accessor-type="NONE">
-            <xml-type prop-order="name defaultServicePath providers services applications"/>
+            <xml-type prop-order="name defaultServicePath reloadableListenerElement providers services applications"/>
             <xml-root-element/>
             <java-attributes>
                 <xml-element java-attribute="name" name="name"/>
                 <xml-element java-attribute="defaultServicePath" name="path"/>
+                <xml-element java-attribute="reloadableListenerElement" name="reloadableListener">
+                    <xml-access-methods get-method="getReloadableListenerElement" set-method="setReloadableListenerElement"/>
+                </xml-element>
                 <xml-elements java-attribute="providers">
                     <xml-element name="provider"/>
                     <xml-element-wrapper name="gateway"/>
diff --git a/gateway-topology-hadoop-xml/src/main/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParser.java b/gateway-topology-hadoop-xml/src/main/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParser.java
index 75f00fe62..c9869be30 100644
--- a/gateway-topology-hadoop-xml/src/main/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParser.java
+++ b/gateway-topology-hadoop-xml/src/main/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParser.java
@@ -64,6 +64,7 @@ public class HadoopXmlResourceParser {
   private static final String CONFIG_NAME_DISCOVERY_CLUSTER = "cluster";
   private static final String CONFIG_NAME_PROVIDER_CONFIG_REFERENCE = "providerConfigRef";
   private static final String CONFIG_NAME_PROVISION_ENCRYPT_QUERY_STRING_CREDENTIAL = "provisionEncryptQueryStringCredential";
+  private static final String CONFIG_NAME_RELOADABLE_LISTENER = "reloadableListener";
   private static final String CONFIG_NAME_APPLICATION_PREFIX = "app";
   private static final String CONFIG_NAME_SERVICE_URL = "url";
   private static final String CONFIG_NAME_SERVICE_VERSION = "version";
@@ -265,6 +266,9 @@ public class HadoopXmlResourceParser {
         case CONFIG_NAME_PROVISION_ENCRYPT_QUERY_STRING_CREDENTIAL:
           descriptor.setProvisionEncryptQueryStringCredential(Boolean.valueOf(parameterPairParts[1].trim()));
           break;
+        case CONFIG_NAME_RELOADABLE_LISTENER:
+          descriptor.setReloadableListener(Boolean.parseBoolean(parameterPairParts[1].trim()));
+          break;
         case CONFIG_NAME_SERVICES:
           Arrays.asList(parameterPairParts[1].trim().split(",", -1)).forEach(serviceName -> descriptor.addService(emptyService(serviceName)));
           break;
diff --git a/gateway-topology-hadoop-xml/src/test/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParserTest.java b/gateway-topology-hadoop-xml/src/test/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParserTest.java
index 587b3ceee..43ef191a2 100644
--- a/gateway-topology-hadoop-xml/src/test/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParserTest.java
+++ b/gateway-topology-hadoop-xml/src/test/java/org/apache/knox/gateway/topology/hadoop/xml/HadoopXmlResourceParserTest.java
@@ -218,6 +218,7 @@ public class HadoopXmlResourceParserTest {
     assertEquals("Cluster 1", descriptor.getCluster());
     assertEquals("topology1-provider", descriptor.getProviderConfig());
     assertTrue(descriptor.isProvisionEncryptQueryStringCredential());
+    assertFalse(descriptor.isReloadableListener());
     assertEquals(2, descriptor.getApplications().size());
 
     assertApplication(descriptor, "knoxauth", Collections.singletonMap("param1.name", "param1.value"));
@@ -244,6 +245,7 @@ public class HadoopXmlResourceParserTest {
     assertEquals("Cluster 2", descriptor.getCluster());
     assertEquals("topology2-provider", descriptor.getProviderConfig());
     assertFalse(descriptor.isProvisionEncryptQueryStringCredential());
+    assertTrue(descriptor.isReloadableListener());
     assertTrue(descriptor.getApplications().isEmpty());
 
     final Map<String, String> expectedServiceParameters = Stream.of(new String[][] { { "httpclient.connectionTimeout", "5m" }, { "httpclient.socketTimeout", "100m" }, })
diff --git a/gateway-topology-hadoop-xml/src/test/resources/testDescriptor.xml b/gateway-topology-hadoop-xml/src/test/resources/testDescriptor.xml
index bf35934fc..7530aab19 100644
--- a/gateway-topology-hadoop-xml/src/test/resources/testDescriptor.xml
+++ b/gateway-topology-hadoop-xml/src/test/resources/testDescriptor.xml
@@ -40,6 +40,7 @@ limitations under the License.
         cluster=Cluster 2#
         providerConfigRef=topology2-provider#
         provisionEncryptQueryStringCredential=false#
+        reloadableListener=true#
         ATLAS-API:url=http://localhost:456#
         ATLAS-API:httpclient.connectionTimeout=5m#
         ATLAS-API:httpclient.socketTimeout=100m#
diff --git a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java
index 2aa75b3c0..a37b806f1 100644
--- a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java
+++ b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java
@@ -44,6 +44,8 @@ public interface SimpleDescriptor {
 
     boolean isProvisionEncryptQueryStringCredential();
 
+    boolean isReloadableListener();
+
     List<Service> getServices();
 
     Service getService(String serviceName);
diff --git a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
index b6bbc3270..9581bc306 100644
--- a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
+++ b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java
@@ -468,6 +468,10 @@ public class SimpleDescriptorHandler {
             // KNOX-1105 Indicate that this topology was auto-generated
             sw.write("    <generated>true</generated>\n");
 
+            if (desc.isReloadableListener()) {
+                sw.write("    <reloadableListener>true</reloadableListener>\n");
+            }
+
             // Incorporate the externalized provider configuration content into the topology descriptor
             sw.write("    <gateway>\n");
             for (ProviderConfiguration.Provider provider : providerConfiguration.getProviders()) {
diff --git a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java
index 1feec15b9..68d5d1cfb 100644
--- a/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java
+++ b/gateway-topology-simple/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java
@@ -51,6 +51,9 @@ public class SimpleDescriptorImpl implements SimpleDescriptor {
     @JsonProperty("provision-encrypt-query-string-credential")
     private boolean provisionEncryptQueryStringCredential = true;
 
+    @JsonProperty("reloadable-listener")
+    private boolean reloadableListener;
+
     @JsonProperty("services")
     private List<Service> services;
 
@@ -140,6 +143,15 @@ public class SimpleDescriptorImpl implements SimpleDescriptor {
       this.provisionEncryptQueryStringCredential = provisionEncryptQueryStringCredential;
     }
 
+    @Override
+    public boolean isReloadableListener() {
+      return reloadableListener;
+    }
+
+    public void setReloadableListener(boolean reloadableListener) {
+      this.reloadableListener = reloadableListener;
+    }
+
     public void addService(Service service) {
       if (services == null) {
         services = new ArrayList<>();