This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/main by this push:
new 1f081a7719 Do not restart an activator when nothing changed since it
started (#2973)
1f081a7719 is described below
commit 1f081a77192647286519b47eed3f47bef80e402e
Author: JB Onofré <[email protected]>
AuthorDate: Sun Oct 4 17:37:59 2026 +0200
Do not restart an activator when nothing changed since it started (#2973)
---
.../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);
+ }
+ }
+ }
+
+}