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.
The startup PTM↔DataSource check and the runtime transaction check disagree about how to compare DataSources, so a
TransactionAwareDataSourceProxypasses validation at startup and then makes everypublish()fail.The asymmetry
Startup validation unwraps
DelegatingDataSourcechains before comparing identity (OutboxAutoConfiguration.kt:361-362):The runtime validator compares against the raw bean reference (
SpringTransactionContextValidator.kt:31):When the outbox
DataSourcebean is a proxy and thePlatformTransactionManagerholds the raw target, the transaction is bound to the raw DataSource, sogetResource(proxy)returnsnull.Reproduction
Spring's own guidance is that a
TransactionAwareDataSourceProxymust not be handed to aPlatformTransactionManager, so the natural wiring is: proxy as theDataSourcebean, PTM on the raw target.with
okapi.datasource-qualifier=outboxDataSourceandokapi.transaction-manager-qualifier=outboxTxManager.Result — startup succeeds, then:
Verified on v1.0.0 (
e199775), inside an activeTransactionTemplateon 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.