Skip to content

Executors/Threads: Enforce reasonable rules - #4675

Open
dsmiley wants to merge 1 commit into
apache:mainfrom
dsmiley:ExecutorChecks
Open

Executors/Threads: Enforce reasonable rules#4675
dsmiley wants to merge 1 commit into
apache:mainfrom
dsmiley:ExecutorChecks

Conversation

@dsmiley

@dsmiley dsmiley commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

There are foot-guns in creating Executors. This PR adds checks in ExecutorUtil designed to catch them. Read the javadocs I'm adding there.

Additionally, one ExecutorUtil.newMDCAwareCachedThreadPool method is misleading as-named since it creates a pool consistent with a fixed pool -- min & max size equal and a real/sized queue (not SynchronousQueue). Yes allowCoreThreadTimeOut is set but IMO that doesn't change the fundamental characterization of the pool. This even confused Claude Opus, as I worked on something and it called factory this method and then later realized it's name mislead it, as it should have chosen another one.

One ExecutorUtil.newMDCAwareCachedThreadPool method is misleading as-named, and should be newMDCAwareFixedThreadPool.  Renamed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

many changes in this PR are just for the rename of one newMDCAwareCachedThreadPool to newMDCAwareFixedThreadPool

ExecutorService coreCloseExecutor =
ExecutorUtil.newMDCAwareFixedThreadPool(
Integer.MAX_VALUE, new SolrNamedThreadFactory("coreCloseExecutor"));
64, new SolrNamedThreadFactory("coreCloseExecutor"));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new enforcement caught this problem

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checks caught this

}

private ExecutorService newThreadPerUpdatePool(List<?> updates) {
return ExecutorUtil.newMDCAwareFixedThreadPool(updates.size() + 1, 1, getTestName());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 queue size... we don't expect to use the queue at all in this test; this is a glorified thread-per-task generator

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much deserves to be a separate PR, see #4668

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much deserves a separate PR, see #4655 or could be combined with the Jetty one.

*
* <p>This is good for fixed-size workloads, like for heavy/intensive work.
*/
@Deprecated(since = "10.1") // prefer the overloaded one, thus explicit about queue capacity

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanting to see this deprecated because I think the queue capacity should be more visible at the call-site, surfacing a trade-off/choice. Also, this method doesn't do the 60 second core pool size reclamation, which seems to me a universally good thing. I'm looking for feedback here. If there are some cases that don't want idle reclaimation, they can easily toggle that aspect after pool creation.

// the ThreadPoolExecutor ignores the configured max value and only considers core pool size.
// Since we allow core threads to die when idle for too long, this ends in having a pool with
// lazily-initialized and cached threads.
public static ExecutorService newMDCAwareFixedThreadPool(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed. And using a String pool name since every caller is using SolrNamedThreadFactory. Many years ago, there was a thread factory split that doesn't exist today.

* capacity silently caps the pool at {@code corePoolSize} (or at one thread, which is always
* created to rescue a queued task when {@code corePoolSize} is 0).
*/
private static void checkPoolConfig(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the rules being added

// lazily-initialized and cached threads.
public static ExecutorService newMDCAwareFixedThreadPool(
int nThreads, int queueCapacity, String poolName) {
if (nThreads > 1024) { // likely wrong choice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a check

@psalagnac psalagnac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clarifies things.

* simultaneous tasks starts a thread apiece. Only use this where something upstream already
* limits how many tasks can be in flight.
*/
public static ExecutorService newMDCAwareCachedThreadPool(ThreadFactory threadFactory) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method still takes a ThreadFactory.
Should it be deprecated in favor of the one taking a string?

* Create a new pool of threads. Threads are created for new work if there is room to do so up to
* {@code maxThreads}. Beyond that, the queue is used up to {@code queueCapacity}. Beyond that,
* work is rejected with an exception. Unused threads will be closed after 60 seconds.
* Create a new pool of at most {@code nThreads} threads. Each submitted task starts a thread

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each submitted task starts a thread until nThreads exist — even when an idle thread could have taken it

Wow; I was really surprised by that so I wrote my own test to confirm this statement...

Maybe we should revisit this. This does not make sense to me to create more threads as long we have some available.
Why don't we always set the core pool size to 0 ?

}

/**
* Rejects a pool that can never reach {@code maximumPoolSize}. {@link ThreadPoolExecutor} only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this statement is correct.

A queue with unlimited capacity caps the pool at corePoolSize, but even a queue with a fixed capacity, like 1000 tasks, will allow the pool to grow wil the queue is full.
That's a pattern that we may want to disallow, but that's not an invalid pool config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants