Skip to content

fix: preserve full transaction attribute state in GrailsTransactionAttribute (GORM) copy constructors - #16064

Open
borinquenkid wants to merge 1 commit into
apache:8.0.xfrom
borinquenkid:fix/gorm-transaction-attribute-copy
Open

fix: preserve full transaction attribute state in GrailsTransactionAttribute (GORM) copy constructors#16064
borinquenkid wants to merge 1 commit into
apache:8.0.xfrom
borinquenkid:fix/gorm-transaction-attribute-copy

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

fix: preserve full transaction attribute state in GrailsTransactionAttribute copy constructors

Follow-up to #16063 (fix/customizable-rollback-tx-attribute-copy), which fixed the same bug in org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute. That PR's "known follow-up" section named this class as carrying the identical pattern; this PR is that fix.

Problem

grails.gorm.transactions.GrailsTransactionAttribute (the attribute type used by GrailsTransactionTemplate) had lossy copy constructors:

  • The (TransactionAttribute) overload copied only the five TransactionDefinition fields (propagation, isolation, timeout, readOnly, name) — rollback rules, qualifier, labels, descriptor, and timeoutString were silently dropped.
  • The (TransactionDefinition) overload had the same gap.
  • The (RuleBasedTransactionAttribute) overload called super(other) (so definition fields and rules were copied) but still dropped qualifier, labels, descriptor, and timeoutString.

In practice, a NoRollbackRuleAttribute configured on an attribute passed into GrailsTransactionTemplate was ignored: every exception rolled back regardless of the configured rule.

Fix

Same pattern as the CRTA fix (4042a87d54), adapted to this class's @CompileStatic Groovy source:

  • (TransactionAttribute) now delegates to (TransactionDefinition).
  • (TransactionDefinition) copies the five definition fields, then recovers the dynamic type via instanceof: copyAttributeState for TransactionAttribute sources, and a rollback-rules snapshot through a temporary new RuleBasedTransactionAttribute(other).getRollbackRules() — never the source's lazy getter, so the source is never mutated.
  • (RuleBasedTransactionAttribute) does super(other) + copyAttributeState + copyGrailsState.
  • New private helpers copyAttributeState (descriptor/timeoutString when source is DefaultTransactionAttribute, qualifier, defensive new ArrayList<String>(labels) copy) and copyGrailsState (inheritRollbackOnly, this class's only Grails-specific field).

No other production files touched — CustomizableRollbackTransactionAttribute and GrailsTransactionTemplate are unmodified.

Behavior change (release-note material)

Same shape as the CRTA change: an application that passes its own rule-bearing TransactionAttribute into GrailsTransactionTemplate will now have those rules honored — an exception matching a NoRollbackRuleAttribute commits instead of rolling back. Previously the rules were silently dropped and every exception rolled back. One-directional: no scenario turns a commit into a rollback.

Tests

GrailsTransactionAttributeSpec (13 tests, modeled on CustomizableRollbackTransactionAttributeSpec): rule-list and labels deep-copy independence in both directions with explicit non-mutation-of-source assertions (source.getRollbackRules().is(originalList)), qualifier/labels/inheritRollbackOnly preservation, all five definition fields, descriptor/timeoutString, a plain RuleBasedTransactionAttribute source, rollback-on behavior (no-rollback rule honored, deepest rule wins, rollback-everything default), and the statically-dispatched TransactionDefinition/TransactionAttribute entry points via @CompileStatic private static helpers.

No end-to-end GrailsTransactionTemplate rules test is included here: GrailsTransactionTemplate's internal conversion to CustomizableRollbackTransactionAttribute is fixed on the sibling CRTA branch, not this one, so an end-to-end test would depend on both PRs merging together. Verified separately with both fixes applied.

  • :grails-datamapping-core:test — 306 tests, 0 failures.
  • :grails-datamapping-core:codeStyle — clean.

🤖 Generated with Claude Code

…tribute copy constructors

The TransactionAttribute and TransactionDefinition overloads previously
copied only the five TransactionDefinition fields (propagation, isolation,
timeout, readOnly, name), silently dropping rollback rules, qualifier,
labels, descriptor and timeoutString. The RuleBasedTransactionAttribute
overload delegated to super(other), which carries the rules but still
loses the attribute-level state because Spring 7's
DefaultTransactionAttribute(TransactionAttribute) copy constructor only
copies the TransactionDefinition fields.

The TransactionAttribute overload now delegates to the
TransactionDefinition overload, which recovers the dynamic type via
instanceof and snapshots rollback rules through a temporary
RuleBasedTransactionAttribute copy - reading the source's rule field
without invoking its lazy getRollbackRules(), which would mutate the
source by assigning a new list into it. All paths now explicitly carry
descriptor, timeoutString, qualifier and labels (defensively copied,
since setLabels stores the given reference), plus inheritRollbackOnly
when the source is a GrailsTransactionAttribute.

Mirrors the CustomizableRollbackTransactionAttribute fix split out of
the GormRegistry consolidation per review on apache#15779.

Covered by GrailsTransactionAttributeSpec (copy independence and state
preservation for every constructor dispatch path, including the
statically dispatched TransactionDefinition/TransactionAttribute
entries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@testlens-app

testlens-app Bot commented Jul 29, 2026

Copy link
Copy Markdown

🚨 TestLens detected 12 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI - Groovy Joint Validation Build / build_grails > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

CI / Build Grails-Core (macos-latest, 21) > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

CI / Build Grails-Core (ubuntu-latest, 21) > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

CI / Build Grails-Core (ubuntu-latest, 25) > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

CI / Build Grails-Core (windows-latest, 25) > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-core:test

Test Runs Flakiness
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer 0% 🟢
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default 0% 🟢

🏷️ Commit: 42faa33
▶️ Tests: 41356 executed
⚪️ Checks: 59/59 completed

Test Failures (first 10 of 12)

GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@153af60c

	at org.apache.grails.core.GrailsBootstrapRegistryInitializerSpec.defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer(GrailsBootstrapRegistryInitializerSpec.groovy:257)
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@3c1a6d8c

	at grails.util.GrailsUtilStackFiltererSpec.installed DefaultStackTraceFilterer emits Full Stack Trace by default(GrailsUtilStackFiltererSpec.groovy:143)
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@4600f352

	at org.apache.grails.core.GrailsBootstrapRegistryInitializerSpec.defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer(GrailsBootstrapRegistryInitializerSpec.groovy:257)
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@51b8d7f1

	at grails.util.GrailsUtilStackFiltererSpec.installed DefaultStackTraceFilterer emits Full Stack Trace by default(GrailsUtilStackFiltererSpec.groovy:143)
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 25))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@29c29f5f

	at org.apache.grails.core.GrailsBootstrapRegistryInitializerSpec.defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer(GrailsBootstrapRegistryInitializerSpec.groovy:257)
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 25))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@20ba2011

	at grails.util.GrailsUtilStackFiltererSpec.installed DefaultStackTraceFilterer emits Full Stack Trace by default(GrailsUtilStackFiltererSpec.groovy:143)
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer (:grails-core:test in CI / Build Grails-Core (windows-latest, 25))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@585f319e

	at org.apache.grails.core.GrailsBootstrapRegistryInitializerSpec.defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer(GrailsBootstrapRegistryInitializerSpec.groovy:257)
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default (:grails-core:test in CI / Build Grails-Core (windows-latest, 25))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@443641bf

	at grails.util.GrailsUtilStackFiltererSpec.installed DefaultStackTraceFilterer emits Full Stack Trace by default(GrailsUtilStackFiltererSpec.groovy:143)
GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer (:grails-core:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@6c24d248

	at org.apache.grails.core.GrailsBootstrapRegistryInitializerSpec.defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer(GrailsBootstrapRegistryInitializerSpec.groovy:257)
GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default (:grails-core:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))
Condition not satisfied:

baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE)
|    |          |        |                  |
|    ""         false    |                  Full Stack Trace:
|                        interface org.grails.exceptions.reporting.StackTraceFilterer
java.io.ByteArrayOutputStream@1864eba6

	at grails.util.GrailsUtilStackFiltererSpec.installed DefaultStackTraceFilterer emits Full Stack Trace by default(GrailsUtilStackFiltererSpec.groovy:143)

Muted Tests

Select tests to mute in this pull request:

  • GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer
  • GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes lossy copy-constructor behavior in grails.gorm.transactions.GrailsTransactionAttribute so that rule/metadata state (rollback rules, qualifier, labels, descriptor, timeoutString, and Grails-specific inheritRollbackOnly) is preserved when copying from TransactionDefinition / TransactionAttribute / RuleBasedTransactionAttribute. This aligns Grails’ programmatic transaction attribute handling with the intended rollback semantics when attributes are passed into GrailsTransactionTemplate.

Changes:

  • Update GrailsTransactionAttribute copy constructors to preserve full attribute state (including defensive copies for rollback rules and labels) without mutating the source.
  • Add focused Spock coverage validating deep-copy behavior, preservation of all relevant fields, and rollbackOn behavior with rule precedence.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionAttribute.groovy Fixes copy constructors to preserve rollback rules and attribute metadata (qualifier/labels/descriptor/timeoutString) and Grails-specific state.
grails-datamapping-core/src/test/groovy/grails/gorm/transactions/GrailsTransactionAttributeSpec.groovy Adds comprehensive tests covering deep-copy independence, non-mutation of source state, and rollback rule behavior/precedence across constructor entry points.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@borinquenkid

Copy link
Copy Markdown
Member Author

The TestLens failures for GrailsUtilStackFiltererSpec > installed DefaultStackTraceFilterer emits Full Stack Trace by default and GrailsBootstrapRegistryInitializerSpec > defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer are a pre-existing bug on 8.0.x, unrelated to this PR's changes -- reproducible on plain 8.0.x today. DefaultStackTraceFilterer.STACK_LOG routes through a jcl-over-slf4j commons-logging binding, so the tests' System.setErr() capture never observes the emitted message.

Fix: #16067 (replaces the System.err capture with a Logback appender attached directly to the logger). Once that merges, rebasing onto 8.0.x should clear this failure here.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants