apache / apache/grails-core

[8.0.0-M6] @Transactional AST transform ignores @NotTransactional method opt-outs on class-annotated services

Open
#16,334 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.