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 9cb44cd42f49929de2c58ae3de5282fa17cd528b Author: opencode <[email protected]> AuthorDate: Fri Oct 16 14:21:04 2026 +0200 Avoid busy spin in TaskQueue.take() when the keep-alive time is zero A worker whose thread is due to be stopped waits with a timed poll so that the check for whether the thread should stop runs periodically. The poll used the pool keep-alive time as the timeout. That time can be configured to zero, in which case the poll can not block and, for a core thread that is not subject to being culled, the wait degenerates into a loop that consumes a full CPU core while the queue is empty. When the configured timeout rounds down to zero, wait for the thread renewal delay instead. The interval at which a thread may stop itself is gated by that same delay, so the check still runs at a bounded interval without the spin, arriving tasks are still picked up immediately and the wait remains interruptible. Timed waits for non-core workers, which rely on a zero keep-alive time to be culled immediately, are not affected. Add TestTaskQueue with a regression test that counts the poll iterations performed by idle, renewal-due workers. Without the bounded timeout the count reaches millions within the test window. --- java/org/apache/tomcat/util/threads/TaskQueue.java | 11 ++- .../apache/tomcat/util/threads/TestTaskQueue.java | 92 ++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/java/org/apache/tomcat/util/threads/TaskQueue.java b/java/org/apache/tomcat/util/threads/TaskQueue.java index 3f69271aa0..d906d7a789 100644 --- a/java/org/apache/tomcat/util/threads/TaskQueue.java +++ b/java/org/apache/tomcat/util/threads/TaskQueue.java @@ -138,7 +138,16 @@ public class TaskQueue extends LinkedBlockingQueue<Runnable> implements Retryabl @Override public Runnable take() throws InterruptedException { if (parent != null && parent.currentThreadShouldBeStopped()) { - return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + long timeout = parent.getKeepAliveTime(TimeUnit.MILLISECONDS); + if (timeout <= 0) { + /* + * A timed wait with a zero timeout can not block. Waiting for the thread renewal delay instead keeps + * the check for whether the thread should stop at a bounded interval without consuming CPU while the + * queue is empty. + */ + timeout = Math.max(1, parent.getThreadRenewalDelay()); + } + return poll(timeout, TimeUnit.MILLISECONDS); // yes, this may return null (in case of timeout) which normally // does not occur with take() // but the ThreadPoolExecutor implementation allows this diff --git a/test/org/apache/tomcat/util/threads/TestTaskQueue.java b/test/org/apache/tomcat/util/threads/TestTaskQueue.java new file mode 100644 index 0000000000..6d117b9cce --- /dev/null +++ b/test/org/apache/tomcat/util/threads/TestTaskQueue.java @@ -0,0 +1,92 @@ +/* + * 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.tomcat.util.threads; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; + +public class TestTaskQueue { + + private static class CountingTaskQueue extends TaskQueue { + + private final AtomicInteger pollCount = new AtomicInteger(); + + @Override + public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { + pollCount.incrementAndGet(); + return super.poll(timeout, unit); + } + } + + + /* + * A thread that is due to be stopped waits with poll() rather than take() so the stop check runs periodically. If + * the keep-alive time is zero, a poll() with a zero timeout can not block and the worker spins while the queue is + * empty. The wait must be bounded to a non-zero interval instead. + */ + @Test + public void testTakeWithZeroKeepAliveDoesNotSpin() throws Exception { + CountingTaskQueue queue = new CountingTaskQueue(); + /* + * Two workers so that one stops itself (the first to claim the renewal slot) while the other remains due for + * renewal and waiting. A long renewal delay keeps the waiting thread renewing rather than stopping during the + * observation window below. + */ + ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0, TimeUnit.MILLISECONDS, queue, + new TaskThreadFactory("test-take-zero", false, Thread.NORM_PRIORITY)); + queue.setParent(executor); + executor.setThreadRenewalDelay(10000); + + try { + CountDownLatch executed = new CountDownLatch(4); + executor.execute(executed::countDown); + executor.execute(executed::countDown); + // Wait for both workers to be created and to enter take() + Thread.sleep(200); + + // Mark the (older) worker threads as due to be stopped + executor.contextStopping(); + executor.execute(executed::countDown); + executor.execute(executed::countDown); + // The workers complete these tasks and then re-enter take() which, since the threads are due to be + // stopped, switches to poll() with a bounded timeout + Assert.assertTrue(executed.await(5, TimeUnit.SECONDS)); + + int pollsAfterTask = queue.pollCount.get(); + Thread.sleep(500); + int spinIterations = queue.pollCount.get() - pollsAfterTask; + + // Without the bounded timeout this is orders of magnitude larger (millions in this window) + Assert.assertTrue("Unexpected poll count while idle: " + spinIterations, spinIterations < 100); + + // Both workers must have entered the renewal wait at least once + Assert.assertTrue("Renewal wait was not exercised", queue.pollCount.get() >= 2); + + // The workers must still be able to process tasks + CountDownLatch processed = new CountDownLatch(1); + executor.execute(processed::countDown); + Assert.assertTrue(processed.await(5, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
