Skip to content

FINERACT-2449: Replace unbounded SimpleAsyncTaskExecutor with ThreadPoolTaskExecutor#5432

Merged
adamsaghy merged 1 commit intoapache:developfrom
DeathGun44:FINERACT-2449/replace-unbounded-executor
Feb 20, 2026
Merged

FINERACT-2449: Replace unbounded SimpleAsyncTaskExecutor with ThreadPoolTaskExecutor#5432
adamsaghy merged 1 commit intoapache:developfrom
DeathGun44:FINERACT-2449/replace-unbounded-executor

Conversation

@DeathGun44
Copy link
Contributor

@DeathGun44 DeathGun44 commented Feb 1, 2026

Description

This PR addresses FINERACT-2449.

Background

The current SimpleAsyncTaskExecutor creates a new thread for every asynchronous event. While effective for light loads, this unbounded behavior poses a risk of thread exhaustion (OutOfMemoryError: unable to create native thread) under high-concurrency scenarios.

Motivation

Relying on an unbounded executor is contrary to Spring Boot best practices for production-grade financial systems. This change proactively addresses the risk before it manifests in production.

Solution

As discussed with @vidakovic, a properties-only approach was insufficient due to security context requirements and synchronous defaults. This solution uses the standard Spring Boot Builder to honor spring.task.execution properties while enforcing Fineract-specific safety.

Component Change
SpringConfig.java Replaced SimpleAsyncTaskExecutor with ThreadPoolTaskExecutor via ThreadPoolTaskExecutorBuilder. Implemented "Smart Defaults" (CPU-aware) if properties are unset. Added CallerRunsPolicy for zero data loss.
SpringConfigTest.java Added 14 unit tests using ApplicationContextRunner and concurrency latches to prove bounded behavior, backpressure, and mode compatibility.
application.properties Removed custom fineract.task-executor properties in favor of standard spring.task.execution.pool.*.

Testing

The updated SpringConfigTest.java suite provides the following proofs:

  • Thread Explosion Prevention: Submitting 500 concurrent tasks now results in ≤10 threads (capped) instead of 500 threads (unbounded).
  • Zero Data Loss: Verifies CallerRunsPolicy forces the main thread to execute tasks when the pool is saturated.
  • Mode Compatibility: Uses ApplicationContextRunner to verify the Event Executor bean is correctly configured in READ, WRITE, BATCH_WORKER, and BATCH_MANAGER modes.
  • Configuration Safety: Proves that maxPoolSize is automatically adjusted if user configuration is invalid (e.g., Core > Max).

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • Write the commit message as per our guidelines
  • Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
  • Create/update unit or integration tests for verifying the changes made.
  • Follow our coding conventions.
  • Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes
  • This PR must not be a "code dump". Large changes can be made in a branch, with assistance. Ask for help on the developer mailing list.

@vidakovic
Copy link
Contributor

I think there is a need to address this... but (having best practices in mind): let's first have a look here https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html ... there are already (auto) configuration properties available, so why invent our own and create more Java configuration when they are not really necessary. I only cross-read that documentation quickly, but I think everything that is done here can be achieved with adding the proper "spring" prefixed properties to application.properties (with environment variable placeholders, sure); I think no extra/custom Java configuration would be needed.

@DeathGun44
Copy link
Contributor Author

Hi @vidakovic, thanks for the review.

I investigated using spring.task.execution.* as suggested. While those properties
correctly auto-configure a TaskExecutor for @Async and related infrastructure,
they don't fully address the requirement here for two reasons:

1. Synchronous by default
From the Spring Boot docs:

"The auto-configured AsyncTaskExecutor is used for: Execution of asynchronous tasks
using @EnableAsync, Spring MVC, Spring WebFlux, Spring WebSocket, JPA bootstrap..."

SimpleApplicationEventMulticaster is not in that list. Per the
Spring Framework JavaDoc:

"Default is equivalent to SyncTaskExecutor, executing all listeners synchronously
in the calling thread."

Unless setTaskExecutor() is explicitly called, event listeners run synchronously.

2. Security context propagation
Spring Boot's auto-configured executor is not wrapped with DelegatingSecurityContextAsyncTaskExecutor,
nor is it associated with the event multicaster—both of which are required for correct
async event handling in Fineract.

Because of this, a properties-only solution isn't sufficient. That said, I'm happy to
refactor the configuration to use Spring Boot's ThreadPoolTaskExecutorBuilder, so we
can still honor the standard spring.task.execution.* pool sizing properties while
explicitly wrapping the executor for security and wiring it into the multicaster.

Does that approach work for you?

@adamsaghy
Copy link
Contributor

Hi @vidakovic, thanks for the review.

I investigated using spring.task.execution.* as suggested. While those properties correctly auto-configure a TaskExecutor for @Async and related infrastructure, they don't fully address the requirement here for two reasons:

1. Synchronous by default From the Spring Boot docs:

"The auto-configured AsyncTaskExecutor is used for: Execution of asynchronous tasks
using @EnableAsync, Spring MVC, Spring WebFlux, Spring WebSocket, JPA bootstrap..."

SimpleApplicationEventMulticaster is not in that list. Per the Spring Framework JavaDoc:

"Default is equivalent to SyncTaskExecutor, executing all listeners synchronously
in the calling thread."

Unless setTaskExecutor() is explicitly called, event listeners run synchronously.

2. Security context propagation Spring Boot's auto-configured executor is not wrapped with DelegatingSecurityContextAsyncTaskExecutor, nor is it associated with the event multicaster—both of which are required for correct async event handling in Fineract.

Because of this, a properties-only solution isn't sufficient. That said, I'm happy to refactor the configuration to use Spring Boot's ThreadPoolTaskExecutorBuilder, so we can still honor the standard spring.task.execution.* pool sizing properties while explicitly wrapping the executor for security and wiring it into the multicaster.

Does that approach work for you?

Sounds right to me.

@vidakovic
Copy link
Contributor

I learned again something... Thanks for digging this up. Suggestion sound good, let's then move on

@DeathGun44 DeathGun44 force-pushed the FINERACT-2449/replace-unbounded-executor branch from 773a82c to 4741fbc Compare February 11, 2026 13:42
@DeathGun44
Copy link
Contributor Author

@vidakovic @adamsaghy Thank you for the feedbacks!
I've updated the PR with the discussed hybrid approach (Builder + Smart Defaults) and added tests proving the fix for thread exhaustion. Description is updated with full details ready for re-review when you have a moment!

@DeathGun44 DeathGun44 force-pushed the FINERACT-2449/replace-unbounded-executor branch from 4741fbc to edb6bea Compare February 18, 2026 16:51
@DeathGun44
Copy link
Contributor Author

@adamsaghy This test failure seems unrelated to my changes

@adamsaghy adamsaghy merged commit 045e3fe into apache:develop Feb 20, 2026
36 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants