Skip to content

Conversation

@nilo85
Copy link

@nilo85 nilo85 commented Oct 30, 2025

The @Transactional annotation in SpringTransactionManager is not compatible with multiple transaction managers, as it tries to force the default one. (Which probably does not even exist and it will complain about expecting one but got several)

I have modified it so that it programatically runs transactions and made txManager and dataSource injectable.

Renamed the example test application to example.simple (it shows it still works with default txManager & dataSource)
I then also added a "copy" called example.multipledatasources showcasing how to use library with multi datasource setup.

Happy to address any concerns you see, kept the commits in some meaningful chunked unit, hopefully it helps reviewing, happy to rebase and squash if you prefer that =)

@nilo85
Copy link
Author

nilo85 commented Oct 31, 2025

@badgerwithagun somehting like this?
3c1606b

I have not used the throws E pattern before so I am a bit baffled about it =)

But I do have a semi guilty conscience about how I implemented it, was so happy to re-use the existing mechanism but dont know the macro perspective.

@nilo85
Copy link
Author

nilo85 commented Oct 31, 2025

Or even better, reuse the class you have provided
bde0927

@nilo85 nilo85 requested a review from badgerwithagun October 31, 2025 12:37
@nilo85
Copy link
Author

nilo85 commented Oct 31, 2025

Just ran mvn com.spotify.fmt:fmt-maven-plugin:format

Didnt notice it was in place until now.

Was only building inside the spring module as the oracle image fails to run for me on arm, it outputs warnings about being slow and then after quite some time it timeouts..

[INFO] Running com.gruelbox.transactionoutbox.acceptance.TestOracle18
14:11:13.621 [main] INFO  t.gvenzl/oracle-xe:18-slim-faststart - Creating container for image: gvenzl/oracle-xe:18-slim-faststart
14:11:13.641 [main] INFO  t.gvenzl/oracle-xe:18-slim-faststart - Container gvenzl/oracle-xe:18-slim-faststart is starting: 5edbc16051c98f6f5e55a97443237c1953c24bd52ddcdbe005e9a44ee8c66dea
14:11:13.955 [main] WARN  t.gvenzl/oracle-xe:18-slim-faststart - The architecture 'amd64' for image 'gvenzl/oracle-xe:18-slim-faststart' (ID sha256:b56ec741f421c5f4c475b4d20ec31236c8490b304d73ba46f06ddc39b25484a1) does not match the Docker server architecture 'arm64'. This will cause the container to execute much more slowly due to emulation and may lead to timeout failures.

@nilo85
Copy link
Author

nilo85 commented Nov 18, 2025

@badgerwithagun any chance moving this forward? Not super critical, have my own copy of this class as a workaround for now but would be nice to remove need for custom class =)

@badgerwithagun
Copy link
Member

Hey @nilo85 - I'm not certain about the casting either. Seems brittle to me!

I think we need to start with a suitable test. You need to add something to the existing full-stack test in the Spring module which does this sort of thing: schedule two pieces of work with the same unique id and confirm that the second throws AlreadyScheduledException.

// Schedule some work
outbox.with().uniqueRequestId("foo").schedule(getClass()).aMethod();

// Make sure we can't repeat the same work
Assertions.assertThrows(
  AlreadyScheduledException.class,
  () ->outbox .with().uniqueRequestId("foo") .schedule(getClass()).aMethod()));

I suggest testing this first with your fix reversed.

…ionReturnsThrows executes transaction as expected and preserves various exceptions that are thrown
@nilo85
Copy link
Author

nilo85 commented Nov 19, 2025

Thanks for your feedback!

I added a bit of coverage for the SpringTransactionManager regarding how it handles exceptions, maybe this is enough already..

Will try to think about where it makes sense to put such an integration test you mentioned and see if I can run the test on initial code vs new code somehow.

@nilo85
Copy link
Author

nilo85 commented Nov 19, 2025

@badgerwithagun
I have created a new branch with this commit applied before any of my changes
nilo85@9676663

I get the following exception both before and after my changes:

org.opentest4j.AssertionFailedError: Unexpected exception type thrown, 
Expected :class com.gruelbox.transactionoutbox.AlreadyScheduledException
Actual   :class java.lang.reflect.UndeclaredThrowableException
<Click to see difference>


	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:67)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3128)
	at com.gruelbox.transactionoutbox.spring.test.AlreadyScheduledTest.lambda$aa$2(AlreadyScheduledTest.java:31)
	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
	at com.gruelbox.transactionoutbox.spring.test.AlreadyScheduledTest.aa(AlreadyScheduledTest.java:29)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.reflect.UndeclaredThrowableException
	at com.gruelbox.transactionoutbox.spring.$Proxy138.executeUpdate(Unknown Source)
	at com.gruelbox.transactionoutbox.DefaultPersistor.save(DefaultPersistor.java:126)
	at com.gruelbox.transactionoutbox.TransactionOutboxImpl.lambda$schedule$18(TransactionOutboxImpl.java:265)
	at com.gruelbox.transactionoutbox.spi.Utils.uncheckedly(Utils.java:52)
	at com.gruelbox.transactionoutbox.TransactionOutboxImpl.lambda$schedule$19(TransactionOutboxImpl.java:250)
	at com.gruelbox.transactionoutbox.spi.ProxyFactory.lambda$constructProxy$2(ProxyFactory.java:87)
	at com.gruelbox.transactionoutbox.spring.test.MyRemoteService$ByteBuddy$Sby2GMHl.execute(Unknown Source)
	at com.gruelbox.transactionoutbox.spring.test.AlreadyScheduledTest.lambda$aa$1(AlreadyScheduledTest.java:33)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:53)
	... 8 more
Caused by: java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at com.gruelbox.transactionoutbox.spring.SpringTransactionManager$BatchCountingStatementHandler.invoke(SpringTransactionManager.java:148)
	... 17 more
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.CONSTRAINT_3C INDEX PUBLIC.CONSTRAINT_3C_INDEX_2 ON PUBLIC.TXNO_OUTBOX(UNIQUEREQUESTID NULLS FIRST) VALUES ( /* 1 */ 'my-unique-request' )"; SQL statement:
INSERT INTO TXNO_OUTBOX (id, uniqueRequestId, invocation, topic, seq, lastAttemptTime, nextAttemptTime, attempts, blocked, processed, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-240]
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:520)
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
	at org.h2.message.DbException.get(DbException.java:223)
	at org.h2.message.DbException.get(DbException.java:199)
	at org.h2.index.Index.getDuplicateKeyException(Index.java:535)
	at org.h2.mvstore.db.MVSecondaryIndex.checkUnique(MVSecondaryIndex.java:223)
	at org.h2.mvstore.db.MVSecondaryIndex.add(MVSecondaryIndex.java:184)
	at org.h2.mvstore.db.MVTable.addRow(MVTable.java:520)
	at org.h2.command.dml.Insert.insertRows(Insert.java:174)
	at org.h2.command.dml.Insert.update(Insert.java:135)
	at org.h2.command.dml.DataChangeStatement.update(DataChangeStatement.java:77)
	at org.h2.command.CommandContainer.update(CommandContainer.java:139)
	at org.h2.command.Command.executeUpdate(Command.java:306)
	at org.h2.command.Command.executeUpdate(Command.java:250)
	at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:213)
	at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:172)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	... 19 more

Not sure if I am doing something wrong, or expectation of getting AlreadyScheduledException is wrong?

@badgerwithagun
Copy link
Member

THe test in nilo85@9676663 looks good, and you appear to have uncovered an existing issue!

Just having a look now...

@badgerwithagun
Copy link
Member

I think the issue is in SpringTransactionManager$BatchCountingStatementHandler.invoke; it needs to unwrap InvocationTargetExceptions

@badgerwithagun
Copy link
Member

badgerwithagun commented Nov 19, 2025

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("getBatchCount".equals(method.getName())) {
        return count;
      }
      try {
        return method.invoke(delegate, args);
      } catch (InvocationTargetExcepion e) {
        throw e.getCause();
      } finally {
        if ("addBatch".equals(method.getName())) {
          ++count;
        }
      }
    }

I think

@nilo85
Copy link
Author

nilo85 commented Nov 19, 2025

@badgerwithagun I applied the fix in a separate pr #977

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants