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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-jcs.git

commit 85b906cf4611bc4837c05d69eabfe406a414b047
Author: Thomas Vandahl <[email protected]>
AuthorDate: Wed Aug 26 13:12:42 2026 +0200

    Unify the handling of thread pools and their lifecycle. Fixes JCS-248
---
 .../commons/jcs4/engine/CacheEventQueue.java       |  10 +-
 .../commons/jcs4/engine/PooledCacheEventQueue.java |  31 +--
 .../engine/control/event/ElementEventQueue.java    |   9 +-
 .../jcs4/utils/discovery/UDPDiscoveryReceiver.java |   5 +-
 .../jcs4/utils/threadpool/PoolConfiguration.java   |  29 ++-
 .../jcs4/utils/threadpool/ThreadPoolManager.java   | 232 ++++++++++++++++-----
 .../control/event/ElementEventQueueUnitTest.java   |  87 ++++++++
 7 files changed, 312 insertions(+), 91 deletions(-)

diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/CacheEventQueue.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/CacheEventQueue.java
index 0dc68083..e9a75bb9 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/CacheEventQueue.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/CacheEventQueue.java
@@ -56,22 +56,20 @@ public class CacheEventQueue<K, V>
     public CacheEventQueue( final ICacheListener<K, V> listener, final long 
listenerId, final String cacheName, final int maxFailure,
                             final int waitBeforeRetry )
     {
-        super( listener, listenerId, cacheName, maxFailure, waitBeforeRetry, 
null );
+        super( listener, listenerId, cacheName, maxFailure, waitBeforeRetry, 
"CacheEventQueue.QProcessor-" + cacheName);
     }
 
     /**
      * Create the thread pool.
      *
-     * @param threadPoolName
      * @since 3.1
      */
     @Override
-    protected ExecutorService createPool(final String threadPoolName)
+    protected ExecutorService createPool()
     {
         // create a default pool with one worker thread to mimic the SINGLE 
queue behavior
-        return ThreadPoolManager.getInstance().createPool(
-                new PoolConfiguration(false, 0, 1, 1, getWaitToDie(), 
WhenBlockedPolicy.RUN, 1),
-                "CacheEventQueue.QProcessor-" + getCacheName());
+        return ThreadPoolManager.getInstance().getExecutorService(poolName,
+                new PoolConfiguration(false, 0, 1, 1, getWaitToDie(), 
WhenBlockedPolicy.RUN, 1));
     }
 
     /**
diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/PooledCacheEventQueue.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/PooledCacheEventQueue.java
index 1f8178e8..b27914af 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/PooledCacheEventQueue.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/PooledCacheEventQueue.java
@@ -24,7 +24,6 @@ import java.time.Duration;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.jcs4.engine.behavior.ICacheListener;
 import org.apache.commons.jcs4.engine.stats.Stats;
@@ -51,6 +50,9 @@ public class PooledCacheEventQueue<K, V>
     /** The Thread Pool to execute events with. */
     protected ExecutorService pool;
 
+    /** The Thread Pool name in ThreadPoolManager. */
+    protected String poolName;
+
     /** The Thread Pool queue */
     protected BlockingQueue<Runnable> queue;
 
@@ -73,14 +75,12 @@ public class PooledCacheEventQueue<K, V>
     /**
      * Create the thread pool.
      *
-     * @param threadPoolName
      * @since 3.1
      */
-    protected ExecutorService createPool(final String threadPoolName)
+    protected ExecutorService createPool()
     {
         // this will share the same pool with other event queues by default.
-        return ThreadPoolManager.getInstance().getExecutorService(
-                threadPoolName == null ? "cache_event_queue" : threadPoolName 
);
+        return ThreadPoolManager.getInstance().getExecutorService(poolName);
     }
 
     /**
@@ -94,23 +94,7 @@ public class PooledCacheEventQueue<K, V>
         if ( isWorking() )
         {
             setWorking(false);
-            pool.shutdown();
-
-            if (wait.toSeconds() > 0)
-            {
-                try
-                {
-                    if (!pool.awaitTermination(wait.toSeconds(), 
TimeUnit.SECONDS))
-                    {
-                        log.info( "No longer waiting for event queue to 
finish: {0}",
-                                this::getStatistics);
-                    }
-                }
-                catch (final InterruptedException e)
-                {
-                    // ignore
-                }
-            }
+            ThreadPoolManager.getInstance().disposeExecutorService(poolName, 
wait);
             log.info( "Cache event queue destroyed: {0}", this );
         }
     }
@@ -160,7 +144,8 @@ public class PooledCacheEventQueue<K, V>
     {
         super.initialize(listener, listenerId, cacheName, maxFailure, 
waitBeforeRetry);
 
-        pool = createPool(threadPoolName);
+        poolName = threadPoolName == null ? "cache_event_queue" : 
threadPoolName;
+        pool = createPool();
 
         if (pool instanceof ThreadPoolExecutor tpe)
         {
diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueue.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueue.java
index 0a0948a9..cffc420f 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueue.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueue.java
@@ -38,7 +38,7 @@ import 
org.apache.commons.jcs4.utils.threadpool.ThreadPoolManager;
 public class ElementEventQueue
     implements IElementEventQueue
 {
-    private static final String THREAD_PREFIX = "JCS-ElementEventQueue-";
+    protected static final String POOL_NAME = "ElementEventQueue";
 
     /** The logger */
     private static final Log log = Log.getLog( ElementEventQueue.class );
@@ -54,8 +54,8 @@ public class ElementEventQueue
      */
     public ElementEventQueue()
     {
-        queueProcessor = ThreadPoolManager.getInstance().createPool(
-                       new PoolConfiguration(false, 0, 1, 1, Duration.ZERO, 
WhenBlockedPolicy.RUN, 1), THREAD_PREFIX);
+        queueProcessor = 
ThreadPoolManager.getInstance().getExecutorService(POOL_NAME,
+                       new PoolConfiguration(false, 0, 1, 1, Duration.ZERO, 
WhenBlockedPolicy.RUN, 1));
 
         log.debug( "Constructed: {0}", this );
     }
@@ -91,8 +91,7 @@ public class ElementEventQueue
     {
         if (destroyed.compareAndSet(false, true))
         {
-            // Pool will be shut down by the ThreadPoolManager
-            // queueProcessor.shutdownNow();
+            ThreadPoolManager.getInstance().disposeExecutorService(POOL_NAME);
             log.info( "Element event queue destroyed: {0}", this );
         }
     }
diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/discovery/UDPDiscoveryReceiver.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/discovery/UDPDiscoveryReceiver.java
index 51d41191..eb217107 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/discovery/UDPDiscoveryReceiver.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/discovery/UDPDiscoveryReceiver.java
@@ -105,10 +105,9 @@ public class UDPDiscoveryReceiver
         setService(service);
 
         // create a small thread pool to handle a barrage
-        this.pooledExecutor = ThreadPoolManager.getInstance().createPool(
+        this.pooledExecutor = 
ThreadPoolManager.getInstance().getExecutorService("UDPDiscoveryReceiver",
                 new PoolConfiguration(false, 0, maxPoolSize, maxPoolSize, 
Duration.ZERO,
-                        WhenBlockedPolicy.DISCARDOLDEST, maxPoolSize),
-                "JCS-UDPDiscoveryReceiver-", Thread.MIN_PRIORITY);
+                        WhenBlockedPolicy.DISCARDOLDEST, maxPoolSize, 
Thread.MIN_PRIORITY));
 
         log.info( "Constructing listener, [{0}:{1}]", multicastAddress, 
multicastPort );
         createSocket( multicastInterfaceString, multicastAddress, 
multicastPort );
diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/PoolConfiguration.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/PoolConfiguration.java
index aa88096b..c022080b 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/PoolConfiguration.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/PoolConfiguration.java
@@ -47,7 +47,10 @@ public record PoolConfiguration(
         WhenBlockedPolicy whenBlockedPolicy,
 
         /** The number of threads to create on startup */
-        int startUpSize
+        int startUpSize,
+
+        /** The thread priority */
+        int threadPriority
 ) implements Cloneable
 {
     public enum WhenBlockedPolicy
@@ -88,12 +91,15 @@ public record PoolConfiguration(
     /** Default startup size */
     private static final int DEFAULT_STARTUP_SIZE = DEFAULT_MINIMUM_POOL_SIZE;
 
+    /** Default thread priority */
+    private static final int DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY;
+
     /**
      * Default
      */
     private static PoolConfiguration DEFAULT = new 
PoolConfiguration(DEFAULT_USE_BOUNDARY,
             DEFAULT_BOUNDARY_SIZE, DEFAULT_MAXIMUM_POOL_SIZE, 
DEFAULT_MINIMUM_POOL_SIZE,
-            DEFAULT_KEEPALIVE_TIME, DEFAULT_WHEN_BLOCKED_POLICY, 
DEFAULT_STARTUP_SIZE);
+            DEFAULT_KEEPALIVE_TIME, DEFAULT_WHEN_BLOCKED_POLICY, 
DEFAULT_STARTUP_SIZE, DEFAULT_THREAD_PRIORITY);
 
     /**
      * @return An object containing the default settings
@@ -103,6 +109,22 @@ public record PoolConfiguration(
         return DEFAULT;
     }
 
+    /**
+     * Convenience constructor
+     *
+     * @param useBoundary Should we bound the queue
+     * @param boundarySize If the queue is bounded, how big can it get
+     * @param maximumPoolSize Only has meaning if a boundary is used
+     * @param minimumPoolSize the exact number that will be used in a 
boundless queue
+     * @param keepAliveTime How long idle threads above the minimum should be 
kept alive
+     * @param whenBlockedPolicy Should be ABORT, BLOCK, RUN, WAIT, 
DISCARDOLDEST
+     * @param startUpSize The number of threads to create on startup
+     */
+    public PoolConfiguration(boolean useBoundary, int boundarySize, int 
maximumPoolSize, int minimumPoolSize, Duration keepAliveTime,
+            WhenBlockedPolicy whenBlockedPolicy, int startUpSize)
+    {
+        this(useBoundary, boundarySize, maximumPoolSize, minimumPoolSize, 
keepAliveTime, whenBlockedPolicy, startUpSize, DEFAULT_THREAD_PRIORITY);
+    }
 
     /**
      * To string for debugging purposes.
@@ -118,7 +140,8 @@ public record PoolConfiguration(
         buf.append("minimumPoolSize = [").append(minimumPoolSize()).append("] 
");
         buf.append("keepAliveTime = [").append(keepAliveTime()).append("] ");
         buf.append("whenBlockedPolicy = 
[").append(whenBlockedPolicy()).append("] ");
-        buf.append("startUpSize = [").append(startUpSize()).append("]" );
+        buf.append("startUpSize = [").append(startUpSize()).append("] " );
+        buf.append("threadPriority = [").append(threadPriority()).append("]" );
         return buf.toString();
     }
 }
diff --git 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/ThreadPoolManager.java
 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/ThreadPoolManager.java
index ddbf7aeb..b9e807ea 100644
--- 
a/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/ThreadPoolManager.java
+++ 
b/commons-jcs4-core/src/main/java/org/apache/commons/jcs4/utils/threadpool/ThreadPoolManager.java
@@ -1,5 +1,7 @@
 package org.apache.commons.jcs4.utils.threadpool;
 
+import java.time.Duration;
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -34,6 +36,24 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.jcs4.log.Log;
 import org.apache.commons.jcs4.utils.config.ConfigurationBuilder;
+/*
+ * 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
+ *
+ *   https://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.
+ */
 
 /**
  * This manages threadpools for an application
@@ -68,6 +88,9 @@ public class ThreadPoolManager
     /** The logger */
     private static final Log log = Log.getLog( ThreadPoolManager.class );
 
+    /** The common prefix for all thread names managed by the 
ThreadPoolManager */
+    public static final String JCS_THREAD_POOL_MANAGER_PREFIX = 
"JCS-ThreadPoolManager-";
+
     /** The root property name */
     private static final String PROP_NAME_ROOT = "thread_pool";
 
@@ -81,46 +104,154 @@ public class ThreadPoolManager
     private static final String DEFAULT_PROP_NAME_SCHEDULER_ROOT = 
"scheduler_pool.default";
 
     /**
-         * You can specify the properties to be used to configure the thread 
pool. Setting this post
-         * initialization will have no effect.
-         */
-        private static volatile Properties props;
+     * You can specify the properties to be used to configure the thread pool. 
Setting this post
+     * initialization will have no effect.
+     */
+    private static volatile Properties props;
 
-   /**
- * Dispose of the instance of the ThreadPoolManger and shut down all thread 
pools
- */
-public static void dispose()
-{
-    for ( final Iterator<Map.Entry<String, ExecutorService>> i =
-            getInstance().pools.entrySet().iterator(); i.hasNext(); )
+    /**
+     * Dispose of the instance of the ThreadPoolManger and shut down all 
thread pools
+     */
+    public static void dispose()
     {
-        final Map.Entry<String, ExecutorService> entry = i.next();
-        try
+        for ( final Iterator<Map.Entry<String, ExecutorService>> i =
+                getInstance().pools.entrySet().iterator(); i.hasNext(); )
         {
-            entry.getValue().shutdownNow();
+            final Map.Entry<String, ExecutorService> entry = i.next();
+            try
+            {
+                entry.getValue().shutdownNow();
+            }
+            catch (final Throwable t)
+            {
+                log.warn("Failed to close pool {0}", entry.getKey(), t);
+            }
+            i.remove();
         }
-        catch (final Throwable t)
+
+        for ( final Iterator<Map.Entry<String, ScheduledExecutorService>> i =
+                getInstance().schedulerPools.entrySet().iterator(); 
i.hasNext(); )
         {
-            log.warn("Failed to close pool {0}", entry.getKey(), t);
+            final Map.Entry<String, ScheduledExecutorService> entry = i.next();
+            try
+            {
+                entry.getValue().shutdownNow();
+            }
+            catch (final Throwable t)
+            {
+                log.warn("Failed to close pool {0}", entry.getKey(), t);
+            }
+            i.remove();
         }
-        i.remove();
     }
 
-    for ( final Iterator<Map.Entry<String, ScheduledExecutorService>> i =
-            getInstance().schedulerPools.entrySet().iterator(); i.hasNext(); )
+    /**
+     * Dispose of a thread pool
+     *
+     * @param poolName the name of the pool
+     */
+    public void disposeExecutorService(String poolName)
+    {
+        disposeExecutorService(poolName, Duration.ZERO);
+    }
+
+    /**
+     * Dispose of a thread pool
+     *
+     * @param poolName the name of the pool
+     * @param wait Duration to wait for termination
+     */
+    public void disposeExecutorService(String poolName, Duration wait)
     {
-        final Map.Entry<String, ScheduledExecutorService> entry = i.next();
-        try
+        ExecutorService pool = pools.remove(poolName);
+        if (pool == null)
         {
-            entry.getValue().shutdownNow();
+            log.warn("Failed to close non-existing pool {0}", poolName);
         }
-        catch (final Throwable t)
+        else
         {
-            log.warn("Failed to close pool {0}", entry.getKey(), t);
+            try
+            {
+                if (wait == null || wait.isZero())
+                {
+                    pool.shutdownNow();
+                }
+                else
+                {
+                    pool.shutdown();
+                    try
+                    {
+                        if (!pool.awaitTermination(wait.toSeconds(), 
TimeUnit.SECONDS))
+                        {
+                            log.info( "No longer waiting for pool {0} to 
terminate", poolName);
+                        }
+                    }
+                    catch (final InterruptedException e)
+                    {
+                        // ignore
+                    }
+                }
+            }
+            catch (final Throwable t)
+            {
+                log.warn("Failed to close pool {0}", poolName, t);
+            }
+        }
+    }
+
+    /**
+     * Dispose of a scheduler thread pool
+     *
+     * @param poolName the name of the pool
+     */
+    public void disposeSchedulerPool(String poolName)
+    {
+        disposeSchedulerPool(poolName, Duration.ZERO);
+    }
+
+    /**
+     * Dispose of a scheduler thread pool
+     *
+     * @param poolName the name of the pool
+     * @param wait Duration to wait for termination
+     */
+    public void disposeSchedulerPool(String poolName, Duration wait)
+    {
+        ExecutorService pool = schedulerPools.remove(poolName);
+        if (pool == null)
+        {
+            log.warn("Failed to close non-existing pool {0}", poolName);
+        }
+        else
+        {
+            try
+            {
+                if (wait == null || wait.isZero())
+                {
+                    pool.shutdownNow();
+                }
+                else
+                {
+                    pool.shutdown();
+                    try
+                    {
+                        if (!pool.awaitTermination(wait.toSeconds(), 
TimeUnit.SECONDS))
+                        {
+                            log.info( "No longer waiting for pool {0} to 
terminate", poolName);
+                        }
+                    }
+                    catch (final InterruptedException e)
+                    {
+                        // ignore
+                    }
+                }
+            }
+            catch (final Throwable t)
+            {
+                log.warn("Failed to close pool {0}", poolName, t);
+            }
         }
-        i.remove();
     }
-}
 
     /**
      * Returns a configured instance of the ThreadPoolManger To specify a 
configuration file or
@@ -210,20 +341,7 @@ public static void dispose()
      * @param threadNamePrefix prefix for the thread names of the pool
      * @return A ThreadPool wrapper
      */
-    public ExecutorService createPool( final PoolConfiguration config, final 
String threadNamePrefix)
-    {
-       return createPool(config, threadNamePrefix, Thread.NORM_PRIORITY);
-    }
-
-    /**
-     * Creates a pool based on the configuration info.
-     *
-     * @param config The pool configuration
-     * @param threadNamePrefix prefix for the thread names of the pool
-     * @param threadPriority The priority of the created threads
-     * @return A ThreadPool wrapper
-     */
-    public ExecutorService createPool( final PoolConfiguration config, final 
String threadNamePrefix, final int threadPriority )
+    private ExecutorService createPool( final PoolConfiguration config, final 
String threadNamePrefix)
     {
         BlockingQueue<Runnable> queue = null;
         if ( config.useBoundary() )
@@ -243,7 +361,7 @@ public static void dispose()
             config.keepAliveTime().toMillis(),
             TimeUnit.MILLISECONDS,
             queue,
-            new DaemonThreadFactory(threadNamePrefix, threadPriority));
+            new DaemonThreadFactory(threadNamePrefix, 
config.threadPriority()));
 
         // when blocked policy
         switch (config.whenBlockedPolicy())
@@ -275,15 +393,13 @@ public static void dispose()
      *
      * @param config The pool configuration
      * @param threadNamePrefix prefix for the thread names of the pool
-     * @param threadPriority The priority of the created threads
      * @return A ScheduledExecutorService
      */
-    public ScheduledExecutorService createSchedulerPool( final 
PoolConfiguration config, final String threadNamePrefix, final int 
threadPriority )
+    private ScheduledExecutorService createSchedulerPool(final 
PoolConfiguration config, final String threadNamePrefix)
     {
-
         return Executors.newScheduledThreadPool(
                 config.maximumPoolSize(),
-                new DaemonThreadFactory(threadNamePrefix, threadPriority));
+                new DaemonThreadFactory(threadNamePrefix, 
config.threadPriority()));
     }
 
     /**
@@ -297,11 +413,25 @@ public static void dispose()
      */
     public ExecutorService getExecutorService( final String name )
     {
-       return pools.computeIfAbsent(name, key -> {
-            log.debug( "Creating pool for name [{0}]", key );
-            final PoolConfiguration config = loadConfig( PROP_NAME_ROOT + "." 
+ key, defaultConfig );
-            return createPool( config, "JCS-ThreadPoolManager-" + key + "-" );
-       });
+       return getExecutorService(name, loadConfig( PROP_NAME_ROOT + "." + 
name, defaultConfig ));
+    }
+
+    /**
+     * Returns an executor service by name. If a service by this name does not 
exist in the configuration file or
+     * properties, one will be created using the default values.
+     * <p>
+     * Services are lazily created.
+     *
+     * @param name
+     * @param config The pool configuration
+     * @return The executor service configured for the name.
+     */
+    public ExecutorService getExecutorService(final String name, final 
PoolConfiguration config)
+    {
+        return pools.computeIfAbsent(name, key -> {
+            log.debug("Creating pool for name [{0}]", key);
+            return createPool(config, JCS_THREAD_POOL_MANAGER_PREFIX + key + 
"-");
+        });
     }
 
     /**
@@ -329,7 +459,7 @@ public static void dispose()
             log.debug( "Creating scheduler pool for name [{0}]", key );
             final PoolConfiguration config = loadConfig( 
PROP_NAME_SCHEDULER_ROOT + "." + key,
                     defaultSchedulerConfig );
-            return createSchedulerPool( config, "JCS-ThreadPoolManager-" + key 
+ "-", Thread.NORM_PRIORITY );
+            return createSchedulerPool( config, JCS_THREAD_POOL_MANAGER_PREFIX 
+ key + "-");
        });
     }
 }
diff --git 
a/commons-jcs4-core/src/test/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueueUnitTest.java
 
b/commons-jcs4-core/src/test/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueueUnitTest.java
new file mode 100644
index 00000000..96ca2d40
--- /dev/null
+++ 
b/commons-jcs4-core/src/test/java/org/apache/commons/jcs4/engine/control/event/ElementEventQueueUnitTest.java
@@ -0,0 +1,87 @@
+package org.apache.commons.jcs4.engine.control.event;
+
+/*
+ * 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
+ *
+ *   https://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.
+ */
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.jcs4.utils.threadpool.ThreadPoolManager;
+import org.junit.jupiter.api.Test;
+
+/** Tests the lifecycle of the element-event worker owned by the queue. */
+class ElementEventQueueUnitTest
+{
+    private static final String THREAD_PREFIX = 
ThreadPoolManager.JCS_THREAD_POOL_MANAGER_PREFIX + ElementEventQueue.POOL_NAME;
+
+    @Test
+    void testDisposeStopsOwnedWorkerThread()
+        throws InterruptedException
+    {
+        final Set<Long> threadsBefore = eventQueueThreadIds();
+        final ElementEventQueue queue = new ElementEventQueue();
+        final Thread worker = waitForNewWorker( threadsBefore );
+
+        assertNotNull( worker, "The element-event queue did not start its 
worker" );
+
+        queue.dispose();
+        worker.join( 2000 );
+
+        assertFalse( worker.isAlive(), "The element-event worker is still 
alive after dispose" );
+
+        // Disposal is a lifecycle operation and must be safe when invoked 
more than once.
+        queue.dispose();
+    }
+
+    private static Set<Long> eventQueueThreadIds()
+    {
+        final Set<Long> result = new HashSet<>();
+        for ( final Thread thread : Thread.getAllStackTraces().keySet() )
+        {
+            if (thread.getName().startsWith(THREAD_PREFIX))
+            {
+                result.add( thread.getId() );
+            }
+        }
+        return result;
+    }
+
+    private static Thread waitForNewWorker( final Set<Long> threadsBefore )
+        throws InterruptedException
+    {
+        final long deadline = System.currentTimeMillis() + 2000;
+        do
+        {
+            for ( final Thread thread : Thread.getAllStackTraces().keySet() )
+            {
+                if (thread.getName().startsWith(THREAD_PREFIX) && 
!threadsBefore.contains(thread.getId()))
+                {
+                    return thread;
+                }
+            }
+            Thread.sleep( 10 );
+        }
+        while (System.currentTimeMillis() < deadline);
+
+        return null;
+    }
+}
\ No newline at end of file

Reply via email to