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

jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
     new 0debcb0b53 Do not restart an activator when nothing changed since it 
started (#2974)
0debcb0b53 is described below

commit 0debcb0b535cc779313bc61fac426f5d117d6dd8
Author: JB Onofré <[email protected]>
AuthorDate: Tue Oct 6 06:18:55 2026 +0200

    Do not restart an activator when nothing changed since it started (#2974)
    
    BaseActivator schedules a run (doStop + doStart) each time a tracked
    service or the configuration changes. When a tracked service showed up
    while a run was in progress, doStart could already see it and start the
    activator completely. The run scheduled for that same change then stopped
    and started everything again.
    
    For the features service, this happens when the mvn URL handler is
    registered while the activator processes its first run: the boot features
    are installed, BootFinished is published, then the FeaturesService and the
    region hooks are unregistered and registered again. Anything done at that
    moment can fail, and this is when the integration tests install their
    probe:
    - the resolver hook is registered before the probe is assigned to a
      region, so its resolution is prevented
    - the FeaturesService the test got has been stopped, so installing a
      feature is rejected
    
    doStart now works on the tracked services and the configuration read at
    the beginning of the run, and a run is skipped when they are the ones the
    activator has already been started with.
---
 .../apache/karaf/util/tracker/BaseActivator.java   |  85 ++++++++-
 .../karaf/util/tracker/BaseActivatorTest.java      | 192 +++++++++++++++++++++
 2 files changed, 269 insertions(+), 8 deletions(-)

diff --git 
a/util/src/main/java/org/apache/karaf/util/tracker/BaseActivator.java 
b/util/src/main/java/org/apache/karaf/util/tracker/BaseActivator.java
index de39cb0844..484e78a3cb 100644
--- a/util/src/main/java/org/apache/karaf/util/tracker/BaseActivator.java
+++ b/util/src/main/java/org/apache/karaf/util/tracker/BaseActivator.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -58,7 +59,12 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
     private final Queue<ServiceRegistration> registrations = new 
ConcurrentLinkedQueue<>();
     private Map<String, SingleServiceTracker> trackers = new HashMap<>();
     private ServiceRegistration managedServiceRegistration;
-    private Dictionary<String, ?> configuration;
+    private volatile Dictionary<String, ?> configuration;
+
+    /** What the activator has been started with, {@code null} if it is not 
started. */
+    private volatile State started;
+    /** What {@link #doStart()} is currently working with, {@code null} 
outside of {@link #doStart()}. */
+    private volatile State starting;
 
     private static final AtomicInteger poolNumber = new AtomicInteger(1);
     private final ThreadGroup group;
@@ -85,13 +91,13 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
         scheduled.set(true);
         doOpen();
         scheduled.set(false);
-        if (managedServiceRegistration == null
-                && trackers.values().stream()
-                    .allMatch(t -> t.getService() != null)) {
+        State state = new State();
+        if (managedServiceRegistration == null && state.hasAllServices()) {
             try {
-                doStart();
+                doStart(state);
             } catch (Throwable e) {
                 logger.warn("Error starting activator", e);
+                started = null;
                 doStop();
             }
         } else {
@@ -158,6 +164,10 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
 
             if (properties != null) {
                 this.configuration = properties;
+                State state = starting;
+                if (state != null) {
+                    state.configuration = properties;
+                }
                 return true;
             }
         }
@@ -307,15 +317,72 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
     @Override
     public void run() {
         scheduled.set(false);
+        // The state is read after the flag has been reset: a change happening 
from now on
+        // schedules another run, and a change that happened before is part of 
this state.
+        State state = new State();
+        if (state.sameAs(started)) {
+            // The change this run has been scheduled for was already visible 
to the previous
+            // start, so there is nothing to do. Restarting would needlessly 
unregister, and
+            // register again, everything that has just been started.
+            return;
+        }
         doStop();
         try {
-            doStart();
+            doStart(state);
         } catch (Exception e) {
             logger.warn("Error starting activator", e);
+            started = null;
             doStop();
         }
     }
 
+    private void doStart(State state) throws Exception {
+        started = state;
+        starting = state;
+        try {
+            doStart();
+        } finally {
+            starting = null;
+        }
+    }
+
+    /**
+     * The configuration and the tracked services at a given point in time.
+     * <p>
+     * {@link #doStart()} works on such a state rather than on the trackers, 
so that
+     * what the activator has been started with is known for sure.
+     */
+    private final class State {
+
+        private volatile Dictionary<String, ?> configuration = 
BaseActivator.this.configuration;
+        private final Map<String, ServiceReference> references = new 
HashMap<>();
+        private final Map<String, Object> services = new HashMap<>();
+
+        State() {
+            for (Map.Entry<String, SingleServiceTracker> entry : 
trackers.entrySet()) {
+                references.put(entry.getKey(), 
entry.getValue().getServiceReference());
+                services.put(entry.getKey(), entry.getValue().getService());
+            }
+        }
+
+        boolean hasAllServices() {
+            return services.values().stream().allMatch(Objects::nonNull);
+        }
+
+        boolean sameAs(State other) {
+            if (other == null || configuration != other.configuration) {
+                return false;
+            }
+            for (String name : services.keySet()) {
+                if (services.get(name) != other.services.get(name)
+                        || !Objects.equals(references.get(name), 
other.references.get(name))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
     /**
      * Called in {@link #doOpen()}.
      *
@@ -368,7 +435,8 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
         if (tracker == null) {
             throw new IllegalStateException("Service not tracked for class " + 
clazz);
         }
-        return clazz.cast(tracker.getService());
+        State state = starting;
+        return clazz.cast(state != null ? state.services.get(clazz.getName()) 
: tracker.getService());
     }
 
     protected <T> ServiceReference<T> getTrackedServiceRef(Class<T> clazz) {
@@ -376,7 +444,8 @@ public class BaseActivator implements BundleActivator, 
Runnable, ThreadFactory {
         if (tracker == null) {
             throw new IllegalStateException("Service not tracked for class " + 
clazz);
         }
-        return tracker.getServiceReference();
+        State state = starting;
+        return state != null ? state.references.get(clazz.getName()) : 
tracker.getServiceReference();
     }
 
     /**
diff --git 
a/util/src/test/java/org/apache/karaf/util/tracker/BaseActivatorTest.java 
b/util/src/test/java/org/apache/karaf/util/tracker/BaseActivatorTest.java
new file mode 100644
index 0000000000..9574508fe3
--- /dev/null
+++ b/util/src/test/java/org/apache/karaf/util/tracker/BaseActivatorTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.karaf.util.tracker;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.newCapture;
+import static org.easymock.EasyMock.niceMock;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import org.easymock.Capture;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+public class BaseActivatorTest {
+
+    private final Map<ServiceReference<?>, Runnable> services = new 
ConcurrentHashMap<>();
+    private volatile ServiceReference<?> registered;
+
+    private BundleContext bundleContext;
+    private Capture<ServiceListener> listener;
+    private TestActivator activator;
+
+    @Before
+    public void setUp() throws Exception {
+        Bundle bundle = niceMock(Bundle.class);
+        bundleContext = niceMock(BundleContext.class);
+        listener = newCapture();
+        expect(bundleContext.getBundle()).andStubReturn(bundle);
+        bundleContext.addServiceListener(capture(listener), anyString());
+        expectLastCall().asStub();
+        
expect(bundleContext.getServiceReferences(eq(Runnable.class.getName()), 
anyObject()))
+                .andStubAnswer(() -> registered != null ? new 
ServiceReference<?>[] { registered } : null);
+        expect(bundleContext.getService(anyObject()))
+                .andStubAnswer(() -> services.get(getCurrentArguments()[0]));
+        replay(bundle, bundleContext);
+
+        activator = new TestActivator();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        activator.stop(bundleContext);
+    }
+
+    @Test
+    public void startsOnceWhenServiceShowsUpWhileStarting() throws Exception {
+        // the tracked service is registered while the activator is processing 
a previous change
+        ServiceReference<?> reference = reference();
+        activator.onStop = () -> register(reference);
+
+        activator.start(bundleContext);
+        activator.await();
+
+        assertEquals(Collections.singletonList(reference), activator.starts);
+    }
+
+    @Test
+    public void startsWhenServiceShowsUpLater() throws Exception {
+        ServiceReference<?> reference = reference();
+
+        activator.start(bundleContext);
+        activator.await();
+        assertEquals(Collections.emptyList(), activator.starts);
+
+        register(reference);
+        activator.await();
+
+        assertEquals(Collections.singletonList(reference), activator.starts);
+    }
+
+    @Test
+    public void restartsWhenServiceIsReplaced() throws Exception {
+        ServiceReference<?> first = reference();
+        ServiceReference<?> second = reference();
+        registered = first;
+
+        activator.start(bundleContext);
+        activator.await();
+        assertEquals(Collections.singletonList(first), activator.starts);
+
+        registered = second;
+        listener.getValue().serviceChanged(new 
ServiceEvent(ServiceEvent.UNREGISTERING, first));
+        activator.await();
+
+        assertEquals(Arrays.asList(first, second), activator.starts);
+    }
+
+    @Test
+    public void restartsWhenConfigurationIsUpdated() throws Exception {
+        ServiceReference<?> reference = reference();
+        registered = reference;
+
+        activator.start(bundleContext);
+        activator.await();
+        assertEquals(Collections.singletonList(reference), activator.starts);
+
+        activator.updated(new Hashtable<>());
+        activator.await();
+
+        assertEquals(Arrays.asList(reference, reference), activator.starts);
+    }
+
+    private ServiceReference<?> reference() {
+        ServiceReference<?> reference = niceMock(ServiceReference.class);
+        replay(reference);
+        services.put(reference, () -> { });
+        return reference;
+    }
+
+    private void register(ServiceReference<?> reference) {
+        registered = reference;
+        listener.getValue().serviceChanged(new 
ServiceEvent(ServiceEvent.REGISTERED, reference));
+    }
+
+    /**
+     * Tracks a {@link Runnable} service and records the references it has 
been started with.
+     */
+    private static class TestActivator extends BaseActivator {
+
+        final List<ServiceReference<?>> starts = 
Collections.synchronizedList(new ArrayList<>());
+        volatile Runnable onStop;
+
+        @Override
+        protected void doOpen() throws Exception {
+            trackService(Runnable.class);
+        }
+
+        @Override
+        protected void doStart() throws Exception {
+            if (getTrackedService(Runnable.class) != null) {
+                starts.add(getTrackedServiceRef(Runnable.class));
+            }
+        }
+
+        @Override
+        protected void doStop() {
+            Runnable action = onStop;
+            onStop = null;
+            if (action != null) {
+                action.run();
+            }
+            super.doStop();
+        }
+
+        /**
+         * Waits for the changes notified so far to be processed.
+         */
+        void await() throws Exception {
+            // a change notified while another one is processed is queued 
behind the first marker
+            for (int i = 0; i < 2; i++) {
+                executor.submit(() -> { }).get(10, TimeUnit.SECONDS);
+            }
+        }
+    }
+
+}

Reply via email to