Skip to content

fix: EXPOSED-1028 Return defensive copy from ExposedSpringTransactionAttributeSource#2812

Open
Yuya Urano (yurano) wants to merge 2 commits into
JetBrains:mainfrom
yurano:fix/exposed-1028-spring-attribute-source-thread-safety
Open

fix: EXPOSED-1028 Return defensive copy from ExposedSpringTransactionAttributeSource#2812
Yuya Urano (yurano) wants to merge 2 commits into
JetBrains:mainfrom
yurano:fix/exposed-1028-spring-attribute-source-thread-safety

Conversation

@yurano

Copy link
Copy Markdown

Description

Summary of the change: ExposedSpringTransactionAttributeSource now returns a defensive copy of the delegate's RuleBasedTransactionAttribute instead of mutating the shared, cached instance, fixing a thread-safety bug that caused NullPointerException under concurrent load.

Detailed description:

  • Why: The default delegate, AnnotationTransactionAttributeSource, caches the RuleBasedTransactionAttribute it returns per (method, targetClass). The previous implementation read attr.rollbackRules.toMutableList(), mutated the list, and assigned it back to attr.rollbackRules — a read-modify-write on shared state across concurrent callers. In production we observed NullPointerException thrown from ArrayList.toArray inside kotlin.collections.toMutableList: a reader thread observed the reference to the freshly-published ArrayList while its elementData field was still unset (classic unsafe publication through the non-volatile rollbackRules field). The stack trace and full analysis are in YouTrack EXPOSED-1028.
  • What: getTransactionAttribute now copies the delegate's attribute via the RuleBasedTransactionAttribute(other) copy constructor and only mutates the copy. The delegate's cached instance is no longer touched. The change is applied identically to spring-transaction (Spring 6) and spring7-transaction (Spring 7).
  • How: A copy via RuleBasedTransactionAttribute(other) produces an independent attribute with its own rollbackRules list. The configurable rollbackExceptions constructor parameter is preserved, and the returned attribute remains semantically equivalent to the previous implementation (same propagation, isolation, timeout, read-only flag, and full set of rollback rules including the configured exceptions). Spring's TransactionAttributeSource contract does not promise instance identity, and AbstractFallbackTransactionAttributeSource itself returns copies in similar scenarios, so this is a non-breaking change.

Commits are split for clarity: the first adds failing regression tests (deterministic invariant + stress), the second applies the fix and turns them green.


Type of Change

Please mark the relevant options with an "X":

  • Bug fix
  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • MariaDB
  • Mysql5
  • Mysql8
  • Oracle
  • Postgres
  • SqlServer
  • H2
  • SQLite

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)
  • All public methods affected by my PR has up to date API docs
  • Documentation for my change is up to date

Related Issues

…nsactionAttributeSource

The default delegate (AnnotationTransactionAttributeSource) caches the
RuleBasedTransactionAttribute it returns per (method, targetClass).
ExposedSpringTransactionAttributeSource currently mutates that shared
instance, which is a data race across concurrent callers and was observed
in production as a NullPointerException in ArrayList.toArray (unsafe
publication of the new ArrayList through the non-volatile rollbackRules
field).

These tests document the regression. The deterministic invariant test
('delegate's cached attribute is not mutated by source') fails against the
current implementation; the stress test ('concurrent getTransactionAttribute
calls are thread-safe') guards against future races.
…AttributeSource

The delegate (typically AnnotationTransactionAttributeSource) caches the
RuleBasedTransactionAttribute it returns. Mutating its rollbackRules in
place was a data race across concurrent callers and caused
NullPointerException in ArrayList.toArray under load (unsafe publication
of the new ArrayList through the non-volatile rollbackRules field).

Use the RuleBasedTransactionAttribute(other) copy constructor and only
modify the copy. The delegate's cached instance is no longer touched.

Applied identically to spring-transaction (Spring 6) and
spring7-transaction (Spring 7).
@yurano

Copy link
Copy Markdown
Author

Chantal Loncle (@bog-walk)
Friendly ping 🙂
Could you please take a look when you have time?

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.

1 participant