diff --git a/framework.tck/pom.xml b/framework.tck/pom.xml
index 35cd65a10d..f5ca8d41fd 100644
--- a/framework.tck/pom.xml
+++ b/framework.tck/pom.xml
@@ -105,7 +105,7 @@
net.bytebuddy
byte-buddy
- 1.17.5
+ 1.18.0
test
@@ -132,12 +132,6 @@
1.12.1
test
-
- org.junit.platform
- junit-platform-launcher
- 1.12.1
- test
-
org.osgi
org.osgi.test.cases.framework
diff --git a/framework.tck/tck.bndrun b/framework.tck/tck.bndrun
index 541ec5ddb6..4adf75de7c 100644
--- a/framework.tck/tck.bndrun
+++ b/framework.tck/tck.bndrun
@@ -30,7 +30,7 @@
junit-platform-engine;version='[1.12.1,1.12.2)',\
org.opentest4j;version='[1.3.0,1.3.1)',\
junit-platform-launcher;version='[1.12.1,1.12.2)',\
- assertj-core;version='[3.27.3,3.27.4)',\
biz.aQute.junit;version='[6.4.1,6.4.2)',\
junit-vintage-engine;version='[5.7.1,5.7.2)',\
- net.bytebuddy.byte-buddy;version='[1.17.5,1.17.6)'
\ No newline at end of file
+ assertj-core;version='[3.27.7,3.27.8)',\
+ net.bytebuddy.byte-buddy;version='[1.18.0,1.18.1)'
\ No newline at end of file
diff --git a/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java b/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java
index 2e86fe35cf..63a3e167b5 100644
--- a/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java
+++ b/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java
@@ -69,12 +69,22 @@ public class EventDispatcher
private final static String m_threadLock = "thread lock";
private static int m_references = 0;
private static volatile boolean m_stopping = false;
+ private static int m_totalRestarts = 0;
+ private static final int MAX_RESTARTS = 3;
+ private static long m_lastRestartTime = 0;
+ private static final long RESTART_COOLDOWN_MS = 10000; // 10 seconds
+ private static final long RESTART_BACKOFF_MS = 1000; // 1 second backoff
+ private static boolean m_limitLogged = false;
// List of requests.
private static final List m_requestList = new ArrayList<>();
// Pooled requests to avoid memory allocation.
private static final List m_requestPool = new ArrayList<>();
+ // Track listener failures and blocklist after threshold is reached.
+ private static final Map m_badListeners = new java.util.concurrent.ConcurrentHashMap<>();
+ private static final int MAX_LISTENER_FAILURES = 3;
+
private static final SecureAction m_secureAction = new SecureAction();
public EventDispatcher(Logger logger, ServiceRegistry registry)
@@ -763,16 +773,107 @@ else if (eh instanceof org.osgi.framework.hooks.bundle.EventHook)
return whitelist;
}
+ private static void triggerRecovery(EventDispatcher dispatcher)
+ {
+ synchronized (m_threadLock)
+ {
+ // Recheck state inside lock
+ if (m_stopping || (m_thread != null && m_thread.isAlive()))
+ {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ if (m_totalRestarts >= MAX_RESTARTS)
+ {
+ if (!m_limitLogged)
+ {
+ dispatcher.m_logger.log((org.osgi.framework.Bundle) null, Logger.LOG_ERROR,
+ "EventDispatcher: FelixDispatchQueue thread has crashed repeatedly. Maximum restart limit (" + MAX_RESTARTS + ") reached. Auto-recovery disabled.");
+ m_limitLogged = true;
+ }
+ return;
+ }
+
+ if (now - m_lastRestartTime < RESTART_COOLDOWN_MS)
+ {
+ // Within cooldown period, do not trigger restart to prevent loops
+ return;
+ }
+
+ // Update state immediately to prevent other threads from triggering a restart
+ m_totalRestarts++;
+ m_lastRestartTime = now;
+ }
+
+ // Perform backoff sleep OUTSIDE the lock to avoid holding m_threadLock
+ try
+ {
+ Thread.sleep(RESTART_BACKOFF_MS);
+ }
+ catch (InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+
+ synchronized (m_threadLock)
+ {
+ // Re-verify after sleep
+ if (!m_stopping && (m_thread == null || !m_thread.isAlive()))
+ {
+ dispatcher.m_logger.log((org.osgi.framework.Bundle) null, Logger.LOG_WARNING,
+ "EventDispatcher: FelixDispatchQueue thread died unexpectedly. Restarting (Attempt " + m_totalRestarts + "/" + MAX_RESTARTS + ")...");
+
+ m_stopping = false;
+ m_thread = new Thread(new Runnable() {
+ @Override
+ public void run()
+ {
+ try
+ {
+ EventDispatcher.run();
+ }
+ finally
+ {
+ synchronized (m_threadLock)
+ {
+ m_thread = null;
+ m_stopping = false;
+ m_references = 0;
+ m_threadLock.notifyAll();
+ }
+ }
+ }
+ }, "FelixDispatchQueue");
+ m_thread.start();
+
+ // Ensure reference count is at least 1 since we are actively dispatching events
+ if (m_references <= 0)
+ {
+ m_references = 1;
+ }
+ }
+ }
+ }
+
private static void fireEventAsynchronously(
EventDispatcher dispatcher, int type,
Map> listeners,
EventObject event)
{
- //TODO: should possibly check this within thread lock, seems to be ok though without
- // If dispatch thread is stopped, then ignore dispatch request.
- if (m_stopping || m_thread == null)
+ // Check if the thread is dead unexpectedly
+ if (!m_stopping && (m_thread == null || !m_thread.isAlive()))
{
- return;
+ triggerRecovery(dispatcher);
+ }
+
+ synchronized (m_threadLock)
+ {
+ // If the dispatch thread is legitimately stopped/stopping, ignore the request.
+ if (m_stopping || m_thread == null)
+ {
+ return;
+ }
}
// First get a request from the pool or create one if necessary.
@@ -822,6 +923,12 @@ private static void fireEventImmediately(
Filter filter = info.getParsedFilter();
Object acc = info.getSecurityContext();
+ // Skip if the listener has been blocklisted
+ if (m_badListeners.getOrDefault(l, 0) >= MAX_LISTENER_FAILURES)
+ {
+ continue;
+ }
+
try
{
if (type == Request.FRAMEWORK_EVENT)
@@ -840,6 +947,17 @@ else if (type == Request.SERVICE_EVENT)
}
catch (Throwable th)
{
+ // Track failure and blocklist if needed
+ int failures = m_badListeners.getOrDefault(l, 0) + 1;
+ m_badListeners.put(l, failures);
+ if (failures >= MAX_LISTENER_FAILURES)
+ {
+ dispatcher.m_logger.log(bundle,
+ Logger.LOG_ERROR,
+ "EventDispatcher: Listener " + l.getClass().getName() +
+ " has failed " + failures + " times. Blacklisting it from further events.", th);
+ }
+
if ((type != Request.FRAMEWORK_EVENT)
|| (((FrameworkEvent) event).getType() != FrameworkEvent.ERROR))
{
@@ -1140,12 +1258,21 @@ private static void run()
// Deliver event outside of synchronized block
// so that we don't block other requests from being
// queued during event processing.
- // NOTE: We don't catch any exceptions here, because
- // the invoked method shields us from exceptions by
- // catching Throwables when it invokes callbacks.
- fireEventImmediately(
- req.m_dispatcher, req.m_type, req.m_listeners,
- req.m_event, null);
+ try
+ {
+ fireEventImmediately(
+ req.m_dispatcher, req.m_type, req.m_listeners,
+ req.m_event, null);
+ }
+ catch (Throwable th)
+ {
+ if (req.m_dispatcher != null)
+ {
+ req.m_dispatcher.m_logger.log((org.osgi.framework.Bundle) null,
+ Logger.LOG_ERROR,
+ "EventDispatcher: Unexpected error in dispatch loop.", th);
+ }
+ }
// Put dispatch request in cache.
synchronized (m_requestPool)