Class TransactionSpanningComputeRuleTest
Gemma sets hibernate.connection.handling_mode = DELAYED_ACQUISITION_AND_HOLD, so a transaction holds
its pooled connection from first statement to commit, including through a stretch that issues no statements
at all. gemma.db.hikari.maxLifetime recycles connections at 30 minutes. A computation that runs
inside a transaction for longer than that holds a connection the pool expects to have turned over, and holds
it for nothing — the computation never uses it.
Measured on production 2026-09-17: corrMat -force on GSE260875 spent 44 minutes in quantile
normalization inside one @Transactional(readOnly = true) and then failed on its next statement. Four
services had the same shape, and the reads and writes on every one of them were cleanly separable from the
arithmetic between them. Paul's ruling that day: the read and the write have to be independent of the
compute, in general.
The correct shape is @Transactional(propagation = Propagation.NEVER) on the orchestrator, with each
read and write step carrying its own annotation on a bean the orchestrator calls through its proxy. That
idiom is not new here — PreprocessorServiceImpl, DifferentialExpressionAnalyzerServiceImpl,
OutlierFlaggingServiceImpl and DataUpdaterImpl are among sixteen classes already using it.
Why this searches transitively
A direct-call rule would have caught none of the four.SampleCoexpressionAnalysisServiceImpl.prepare
is three hops from quantileNormalize, and two of those hops are interface calls. So the search
follows calls through ubic.gemma and resolves an interface call to the implementations of that
interface, which is where the body actually is.- See Also:
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final com.tngtech.archunit.lang.ArchRuleallowEmptyShouldstaysfalse: if thethat()clause ever stops matching any method, that is a broken rule, not a clean codebase. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic voidclasses_are_actually_imported(com.tngtech.archunit.core.domain.JavaClasses classes) 🛑 The guard against a guard that checks nothing.
-
Field Details
-
transactional_methods_must_not_span_a_long_computation
public static final com.tngtech.archunit.lang.ArchRule transactional_methods_must_not_span_a_long_computationallowEmptyShouldstaysfalse: if thethat()clause ever stops matching any method, that is a broken rule, not a clean codebase.The scope is every class, not only
..service..andcore.analysis..as it first was.ExpressionExperimentPlatformSwitchServiceis incore.loader.expressionand calledcreateProcessedDataVectorsfrom inside its own transaction after that method becameNEVER; the narrower rule never looked at it, andExpressionExperimentPlatformSwitchTestfailed on it.
-
-
Constructor Details
-
TransactionSpanningComputeRuleTest
public TransactionSpanningComputeRuleTest()
-
-
Method Details
-
classes_are_actually_imported
public static void classes_are_actually_imported(com.tngtech.archunit.core.domain.JavaClasses classes) 🛑 The guard against a guard that checks nothing.ArchUnit reads compiled bytecode through a bundled ASM, and when that ASM is older than the class file version we compile to it imports zero classes without complaining — every rule then passes for a reason that has nothing to do with the codebase. That is what archunit 1.3.0 did to Java 25 bytecode. 1.3.2 onwards reads it and the parent pom is on 1.4.1, so this is closed; the sentinel stays because a green run that checked nothing is indistinguishable from a green run that checked everything.
-