Skip to content

Find CompletableFuture async calls without an Executor - #1313

Merged
steve-aom-elliott merged 7 commits into
mainfrom
find-completable-future-default-executor
Oct 20, 2026
Merged

steve-aom-elliott merged 7 commits into
mainfrom
find-completable-future-default-executor

Conversation

@steve-aom-elliott

@steve-aom-elliott steve-aom-elliott commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What's changed

Adds FindCompletableFutureDefaultExecutor, a search recipe. It marks:

  • CompletableFuture / CompletionStage *Async(..) calls without a trailing Executor argument (supplyAsync, runAsync, thenApplyAsync, thenComposeAsync, thenCombineAsync, whenCompleteAsync, completeAsync, …).
  • CompletableFuture.delayedExecutor(long, TimeUnit) without a base executor.
  • Any of the above that pass ForkJoinPool.commonPool() explicitly. Before JDK 25, CompletableFuture substituted an explicit commonPool() with the thread-per-task fallback too, and JDK 25 removed that substitution.

What's your motivation?

JDK 25 changed CompletableFuture's default async executor:

JDK Default async executor
8–24 commonPool(), or thread-per-task if parallelism ≤ 1
25 ForkJoinPool.asyncCommonPool(), no fallback

On a host with ≤ 2 CPUs, common-pool parallelism is 1, and asyncCommonPool() only adjusts it when it is 0. Blocking work (JDBC, HTTP) submitted without an Executor therefore used to get a fresh thread per task, but on JDK 25 it queues behind a single worker. Small containers are common, so this can cause large latency regressions after an upgrade with no code change.

Anything in particular you'd like reviewers to focus on?

  • The recipe is standalone. It isn't added to UpgradeToJava25, because no Java upgrade composite currently includes a pure search recipe. Happy to add it there if preferred.
  • The recipe doesn't attempt an automatic fix, because the right executor depends on the workload.
  • Known false positive: subclasses of CompletableFuture that override defaultExecutor().
  • In a call chain, the marker prints at the start of the chain. That's why the message names the flagged method.

From JDK 25, CompletableFuture's default async executor is always
ForkJoinPool.asyncCommonPool(); the thread-per-task fallback used when
common-pool parallelism is <= 1 is gone. On hosts with <= 2 CPUs, blocking
work submitted without an Executor now queues behind a single worker.
Before JDK 25, CompletableFuture swapped an explicit commonPool() argument
for the thread-per-task fallback when common-pool parallelism was 1 or less.
JDK 25 dropped that substitution, so these calls regress the same way as
Executor-less calls.
@steve-aom-elliott
steve-aom-elliott marked this pull request as ready for review October 19, 2026 19:43
@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Oct 19, 2026

@timtebeek timtebeek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! Few small changes & added it to the larger composite, as the search result marker won't end up on committed changes (for CLI) anyway. Plugin users who see this will want to consciously choose an executor.

timtebeek and others added 3 commits October 20, 2026 11:52
@steve-aom-elliott
steve-aom-elliott merged commit c0fd0c4 into main Oct 20, 2026
2 checks passed
@steve-aom-elliott
steve-aom-elliott deleted the find-completable-future-default-executor branch October 20, 2026 21:53
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants