[8.0.0-M6] @Transactional AST transform ignores @NotTransactional method opt-outs on class-annotated services
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
### Expected Behavior
A method annotated `grails.gorm.transactions.NotTransactional` inside a class annotated `grails.gorm.transactions.Transactional` runs without a transaction. This is the documented contract (i.e the 8.0.0-M6 API doc for `@NotTransactional` still reads "Marks a specific method to be skipped by the Transactional annotation added at class level"), and it is the behaviour on Grails 7.2.3: `javap` on the compiled service shows no `$tt__` wrapper for the opted-out method.
### Actual Behaviour
On 8.0.0-M6 the transform wraps the opted-out method anyway. `javap -p` shows a `$tt__optedOut(...)` wrapper, and `TransactionSynchronizationManager.isActualTransactionActive()` returns true inside the method at runtime.
Real-world consequence (production app, 265 domains, 741-test suite tracking each 8.0 milestone): our CSV import engine's entry point is `@NotTransactional` because the engine manages per-row `withNewTransaction` boundaries itself. Under M6 it silently gains an outer transaction; a `Propagation.REQUIRED` service call inside it (vocabulary get-or-create) joins that transaction instead of committing, and the per-row transactions - reading through new sessions - never see the row. A previously-green integration spec fails deterministically on every row. This codebase has **115 `@NotTransactional` methods**, all silently transactional under M6, including a message-queue publisher kept non-transactional precisely so publishes do not ride a transaction. Data-correctness-grade behaviour change.
Why we believe this is a regression, not a redesign: the M6 API doc states the opt-out contract unchanged; the Grails 8 upgrade guide documents intentional semantic breaks in detail (e.g. nullable-by-default) and says nothing about transaction semantics; and no changelog/PR announces a change, though the transform machinery churned in the M6 window (#16066 GormRegistry/SessionResolver landed in M6; #16063 on transaction-attribute state is open; #16212 removed an "unintended @ReadOnly" in the same period). The opt-out dates to GRAILS-10761 / #3228. The deliberate accessor-shape skip (#14539) is separate, documented behaviour.
Secondary observation: generated wrapper naming is inconsistent at M6 across classes in one compilation. Some services get `$tt__(...)`, others `$tt___(...)` — possibly hinting at two transform paths, one of which drops the `@NotTransactional` check.
### Steps To Reproduce
1. In a Grails 8.0.0-M6 app:
```groovy
import grails.gorm.transactions.Transactional
import grails.gorm.transactions.NotTransactional
@Transactional
class DemoService {
def wrapped() { }
@NotTransactional
def optedOut() { }
}
```
2. `./gradlew compileGroovy`
3. `javap -p build/classes/groovy/main/DemoService.class`
4. Observe `$tt__optedOut(...)` exists. Repeat on Grails 7.2.3: it does not.
### Environment Information
Linux, JDK 21 (Temurin 21.0.12), Gradle 8.14. Grails 8.0.0-M6 on the grails-hibernate5 BOM (Hibernate 5.6.15), Groovy 5.1.0. Verified correct on Grails 7.2.3 with a freshly compiled class
### Example Application
https://github.com/davidbairdala/grails8-nottransactional-repro
### Version
8.0.0-M6
Contributor guide
Research direction
Start with the transaction AST transform involved in `./gradlew compileGroovy`, using the linked reproduction and the minimal `DemoService` example. Compare `javap -p` output with Grails 7.2.3 and verify that `@NotTransactional` produces no `$tt__optedOut` wrapper and no active transaction at runtime; add regression coverage for that behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100