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

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 67ceed2eae1038cc92d18a7784ea1ffe3edf5936
Author: opencode <[email protected]>
AuthorDate: Wed Sep 30 11:18:51 2026 +0200

    Make FrameworkListener registration idempotent so repeated Server start 
events do not add duplicate container listeners or orphan previous per-context 
lifecycle listeners that can no longer be removed
---
 .../apache/catalina/core/FrameworkListener.java    | 28 +++++-
 .../catalina/core/TestFrameworkListener.java       | 99 ++++++++++++++++++++++
 2 files changed, 124 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/core/FrameworkListener.java 
b/java/org/apache/catalina/core/FrameworkListener.java
index c8b8c95cf3..e755cb325b 100644
--- a/java/org/apache/catalina/core/FrameworkListener.java
+++ b/java/org/apache/catalina/core/FrameworkListener.java
@@ -83,12 +83,29 @@ public abstract class FrameworkListener implements 
LifecycleListener, ContainerL
         for (Service service : server.findServices()) {
             Engine engine = service.getContainer();
             if (engine != null) {
-                engine.addContainerListener(this);
+                addContainerListenerIfAbsent(engine);
                 registerListenersForEngine(engine);
             }
         }
     }
 
+
+    /**
+     * Registers this as a container listener on the given container unless it 
is already registered, so that repeated
+     * Server start events do not result in duplicate registrations.
+     *
+     * @param container The container on which to register this listener
+     */
+    private void addContainerListenerIfAbsent(Container container) {
+        for (ContainerListener listener : container.findContainerListeners()) {
+            if (listener == this) {
+                return;
+            }
+        }
+        container.addContainerListener(this);
+    }
+
+
     /**
      * Registers listeners on all hosts of the given engine.
      *
@@ -97,11 +114,10 @@ public abstract class FrameworkListener implements 
LifecycleListener, ContainerL
     protected void registerListenersForEngine(Engine engine) {
         for (Container hostContainer : engine.findChildren()) {
             Host host = (Host) hostContainer;
-            host.addContainerListener(this);
+            addContainerListenerIfAbsent(host);
             registerListenersForHost(host);
         }
     }
-
     /**
      * Registers listeners on all contexts of the given host.
      *
@@ -120,6 +136,12 @@ public abstract class FrameworkListener implements 
LifecycleListener, ContainerL
      * @param context The context to register a listener for
      */
     protected void registerContextListener(Context context) {
+        // Reuse an existing listener if one is already registered, so that
+        // repeated Server start events do not create duplicate listeners that
+        // stay attached to the context after the map reference is replaced
+        if (contextListeners.containsKey(context)) {
+            return;
+        }
         LifecycleListener listener = createLifecycleListener(context);
         contextListeners.put(context, listener);
         context.addLifecycleListener(listener);
diff --git a/test/org/apache/catalina/core/TestFrameworkListener.java 
b/test/org/apache/catalina/core/TestFrameworkListener.java
new file mode 100644
index 0000000000..d8b59df1c6
--- /dev/null
+++ b/test/org/apache/catalina/core/TestFrameworkListener.java
@@ -0,0 +1,99 @@
+/*
+ * 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.catalina.core;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.ContainerListener;
+import org.apache.catalina.Context;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+
+public class TestFrameworkListener {
+
+    @Test
+    public void testRegistrationIsIdempotent() throws Exception {
+        StandardServer server = new StandardServer();
+        StandardService service = new StandardService();
+        service.setName("service");
+        StandardEngine engine = new StandardEngine();
+        engine.setName("engine");
+        service.setContainer(engine);
+        server.addService(service);
+
+        StandardHost host = new StandardHost();
+        host.setName("localhost");
+        engine.addChild(host);
+
+        StandardContext context = new StandardContext();
+        context.setPath("/ctx");
+        host.addChild(context);
+
+        TesterListener listener = new TesterListener();
+
+        LifecycleEvent beforeStart = new LifecycleEvent(server, 
Lifecycle.BEFORE_START_EVENT, null);
+
+        // Simulate repeated in-place Server starts
+        listener.lifecycleEvent(beforeStart);
+        listener.lifecycleEvent(beforeStart);
+        listener.lifecycleEvent(beforeStart);
+
+        Assert.assertEquals(1, countListener(engine.findContainerListeners(), 
listener));
+        Assert.assertEquals(1, countListener(host.findContainerListeners(), 
listener));
+        Assert.assertEquals(1, listener.getContextListenerCount());
+        Assert.assertEquals(1, listener.getCreatedListeners().size());
+        // Exactly one listener — the one created for this context — must be 
attached
+        LifecycleListener created = listener.getCreatedListeners().get(0);
+        long attached = Stream.of(context.findLifecycleListeners()).filter(l 
-> l == created).count();
+        Assert.assertEquals(1, attached);
+    }
+
+
+    private static long countListener(ContainerListener[] listeners, 
ContainerListener expected) {
+        return Stream.of(listeners).filter(l -> l == expected).count();
+    }
+
+
+    private static class TesterListener extends FrameworkListener {
+
+        private final List<LifecycleListener> createdListeners = new 
ArrayList<>();
+
+        @Override
+        protected LifecycleListener createLifecycleListener(Context context) {
+            // Return a new instance on each call, as OpenWebBeansListener does
+            LifecycleListener listener = event -> {
+                // No-Op
+            };
+            createdListeners.add(listener);
+            return listener;
+        }
+
+        int getContextListenerCount() {
+            return contextListeners.size();
+        }
+
+        List<LifecycleListener> getCreatedListeners() {
+            return createdListeners;
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to