At commit 8e3846991d30a9a577dae9664996c036895f04cc in the Spring Framework, the NoTransactionInContextException was made a static instance. While this change can reduce overhead in non-reactive contexts, it introduces a potential issue in reactive pipelines: it can cause OutOfMemoryError (OOM) due to suppressed exceptions being accumulated.

In the Reactor Core implementation, as seen in the file FluxOnAssembly.java, the problem is appears at line 612:

Here, the NoTransactionInContextException is reused statically, and when it is thrown inside a reactive pipeline, it retains references to OnAssemblyException instance. This can lead to suppressed exceptions being stored indefinitely, creating a memory leak.

The behavior can be reproduced with the following Kotlin code:

/**
 * Stackless variant of [NoTransactionException] for reactive flows.
 */
private class NoTransactionInContextException : NoTransactionException("No transaction in context") {

    @Synchronized
    override fun fillInStackTrace(): Throwable {
        // Stackless exception
        return this
    }
}

private val NO_TRANSACTION_IN_CONTEXT_EXCEPTION = NoTransactionInContextException()

fun main() {
    repeat(1000) {
        runCatching {
            Flux.fromIterable((1..2))
                .flatMap<Unit> { Mono.error(NO_TRANSACTION_IN_CONTEXT_EXCEPTION) }
                .blockLast()
        }
    }

    System.gc()
}

Explanation of the Issue:

  • Each time the static NO_TRANSACTION_IN_CONTEXT_EXCEPTION is thrown within a reactive flow, the OnAssemblyException created by Reactor is added to the suppressed exceptions of NO_TRANSACTION_IN_CONTEXT_EXCEPTION.

  • Since the exception is static, it retains all these references, causing memory consumption to grow unbounded.

  • This pattern can quickly lead to an OutOfMemo`ryError, especially in high-throughput reactive applications.

Comment From: sdeleuze

Reverted after double-checking with @chemicL that the deferred exception accumulation mechanism is the founding technology for checkpoint() for instance, or used in block(), so static exception should be avoided with Reactor, as recently documented.