Class TransactionSpanningComputeRuleTest

java.lang.Object
ubic.gemma.core.architecture.TransactionSpanningComputeRuleTest

public class TransactionSpanningComputeRuleTest extends Object
No transaction may be open while a long computation runs.

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 Details

    • transactional_methods_must_not_span_a_long_computation

      public static final com.tngtech.archunit.lang.ArchRule transactional_methods_must_not_span_a_long_computation
      allowEmptyShould stays false: if the that() clause ever stops matching any method, that is a broken rule, not a clean codebase.

      The scope is every class, not only ..service.. and core.analysis.. as it first was. ExpressionExperimentPlatformSwitchService is in core.loader.expression and called createProcessedDataVectors from inside its own transaction after that method became NEVER; the narrower rule never looked at it, and ExpressionExperimentPlatformSwitchTest failed 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.