Skip to content

Fix @ConcurrencyLimit leak for non-singleton beans - #37652

Open
Ridzzz0Alam wants to merge 1 commit into
spring-projects:mainfrom
Ridzzz0Alam:fix/concurrency-limit-leak
Open

Ridzzz0Alam wants to merge 1 commit into
spring-projects:mainfrom
Ridzzz0Alam:fix/concurrency-limit-leak

Conversation

@Ridzzz0Alam

Copy link
Copy Markdown

The issue

ConcurrencyLimitBeanPostProcessor retains every prototype, request or session scoped bean with a @ConcurrencyLimit method forever. After creating 1000 prototype instances and running GC all 1000 proxies were still reachable.

Why it happened

Since gh-35788 one shared interceptor keeps each proxy's concurrency state in a strong IdentityHashMap keyed by proxy instance and entries are never removed. Strong references were the right fix for that race but the missing piece is a lifecycle for the entries.

The fix

  • New protected getAdvisor(Advisor, Object, String) hook in AbstractAdvisingBeanPostProcessor returns the shared advisor by default.
  • ConcurrencyLimitBeanPostProcessor overrides it to apply a dedicated interceptor per proxy so the state lives on the proxy and is collected with it. The map is removed so throttling stays per proxy instance as before.

Tests

  • New ConcurrencyLimitTests#withPostProcessorForPrototypeBeans: fails before the fix, passes after.
  • ./gradlew :spring-aop:check :spring-context:check :spring-tx:check pass.

Not covered

  • The shared advisor's own interceptor is now only used for eligibility and type determination. I left it in place to keep the diff small.
  • No GC-based test, none exist in the codebase and they tend to be flaky. The leak was confirmed with a local GC check.

Prior to this commit, ConcurrencyLimitBeanPostProcessor applied a
single shared interceptor that kept the concurrency state of every
proxy in a strong IdentityHashMap keyed by proxy instance. Entries were
never removed, so each prototype, request or session scoped bean with
a `@ConcurrencyLimit` method was retained for the lifetime of the
post-processor, leaking memory in proportion to the number of created
instances.

This commit introduces a protected getAdvisor(Advisor, Object, String)
hook in AbstractAdvisingBeanPostProcessor, returning the shared advisor
by default, and overrides it in ConcurrencyLimitBeanPostProcessor to
apply a dedicated interceptor per proxy. The concurrency state is
therefore held by the proxy itself and released with it, while
throttling remains scoped per proxy instance as before.

Signed-off-by: Ridzzz0Alam <ridwan0gts@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 6, 2026
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Nov 7, 2026
@sbrannen
sbrannen requested a review from jhoeller November 7, 2026 11:18

This branch has not been deployed

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

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants