Conversation
Retain decorated and original tasks in a private wrapper instead of updating a shared weak-reference map on every decorated submission. This removes reference-map resizing and purging from the submission path while preserving cancellation of queued futures during shutdown. Add regression tests for single execution, queued future cancellation, future-producing decorators, and identity decorators. Signed-off-by: 문정환 <70272622+bazzi2548@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the shared weak-reference bookkeeping map in ThreadPoolTaskExecutor with a task-local wrapper that retains both the decorated task and its original task. This removes reference-map updates from the submission path while preserving cancellation of queued futures during shutdown.
Problem
When a TaskDecorator returns a different Runnable, every submission inserts a mapping into decoratedTaskMap before reaching ThreadPoolExecutor.execute(). Completed tasks do not explicitly remove those mappings, so cleanup depends on weak-reference processing and later map maintenance. ConcurrentReferenceHashMap resizing and purging can make submitting threads contend before work reaches the executor queue.
This was observed in a production deployment where weak-reference bookkeeping on the decorated task submission path accumulated and later contended during map maintenance. Request threads were observed waiting before work reached the executor, while executor workers were idle. Replacing the executor with an implementation that avoided this shared map resolved the production symptom. These observations motivate the change; the reproducible measurements below are a separate local experiment and are not production data.
Change
This removes reference-map resizing and purging from the submission path at the cost of one wrapper per decorated task.
Validation
Reproducible experiment
A standalone harness compares the original executor from the parent revision with the patched executor, with an identity-decorator control. It uses distinct metrics wrappers, independently paced producers, separate JVMs, JFR ThreadPark events, GC logs, stack sampling, and reflective map counters. It does not retain map entries, force hash collisions, or force GC in the normal experiment.
One local run used Temurin 25.0.3+9-LTS, Generational ZGC, a 2 GiB heap, 400 producers, 8 workers, 100,000 queue capacity, and 15,000 offered submissions/second for 90 seconds per variant:
All accepted tasks completed, with no rejections. The longest baseline JFR event included ReentrantLock.lock, ConcurrentReferenceHashMap$Segment.doTask, ConcurrentReferenceHashMap.doTask, ConcurrentReferenceHashMap.put, and ThreadPoolTaskExecutor$1.execute. The 612 events summed to 13.23 thread-seconds; this is thread-time, not service-outage time. The sampled idle-worker observations are not evidence of one continuous starvation interval, and the measurements do not establish that resizing caused every wait.
The baseline GC log contained three warmup major collections and one minor collection. Concurrent non-strong processing in the major collections was 1.358 ms, 2.848 ms, and 2.025 ms. No explicit GC was used. This run demonstrates the submission-path map lock under the stated load.
Compatibility note
When decoration changes the task, the underlying executor now receives a private wrapper. Custom rejection handlers, execution hooks, and callers inspecting the native executor queue will observe that wrapper rather than the decorator's returned object.