Skip to content

Startup unwraps DelegatingDataSource but the runtime validator does not: publish() always fails behind a TransactionAwareDataSourceProxy #91

Description

@endrju19

The startup PTM↔DataSource check and the runtime transaction check disagree about how to compare DataSources, so a TransactionAwareDataSourceProxy passes validation at startup and then makes every publish() fail.

The asymmetry

Startup validation unwraps DelegatingDataSource chains before comparing identity (OutboxAutoConfiguration.kt:361-362):

val unwrappedPtm = unwrapDataSource(ptmDataSource)
val unwrappedOutbox = unwrapDataSource(outboxDataSource)

The runtime validator compares against the raw bean reference (SpringTransactionContextValidator.kt:31):

TransactionSynchronizationManager.getResource(dataSource) != null

When the outbox DataSource bean is a proxy and the PlatformTransactionManager holds the raw target, the transaction is bound to the raw DataSource, so getResource(proxy) returns null.

Reproduction

Spring's own guidance is that a TransactionAwareDataSourceProxy must not be handed to a PlatformTransactionManager, so the natural wiring is: proxy as the DataSource bean, PTM on the raw target.

@Bean fun outboxDataSource(@Qualifier("rawOutboxDataSource") raw: DataSource): DataSource =
    TransactionAwareDataSourceProxy(raw)

@Bean fun outboxTxManager(@Qualifier("rawOutboxDataSource") raw: DataSource): PlatformTransactionManager =
    DataSourceTransactionManager(raw)

with okapi.datasource-qualifier=outboxDataSource and okapi.transaction-manager-qualifier=outboxTxManager.

Result — startup succeeds, then:

### CONTEXT STARTUP: OK
IllegalStateException: No active read-write transaction on the outbox DataSource. Ensure publish()
is called within a @Transactional method that uses the same DataSource as the outbox table.
In multi-datasource setups, verify okapi.datasource-qualifier is set correctly.

Verified on v1.0.0 (e199775), inside an active TransactionTemplate on the qualified transaction manager.

Impact

Fail-loud rather than silent, so no duplicate delivery or data loss. But the two checks contradict each other — startup reports the wiring as verified, and the runtime message then points at okapi.datasource-qualifier, which is in fact correct. That sends the user hunting in the wrong place.

Possible direction

Make the runtime lookup use the same unwrapping the startup check already performs — e.g. resolve TransactionSynchronizationManager.getResource() against the unwrapped target as well as the raw reference — so both checks agree on DataSource identity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions