Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClassCastException while using DB.beginTransaction in BeanPersistAdapter #3320

Closed
AntoineDuComptoirDesPharmacies opened this issue Feb 1, 2024 · 3 comments

Comments

@AntoineDuComptoirDesPharmacies

We searched in the Ebean documentation of BeanPersistAdapter, issues, discussions, and google forum but cannot find out if the usage of DB.beginTransaction is authorized in BeanPersistAdapter. Sorry, if we missed the information somewhere.

We tried to write some code in Adapter that insert/update DB rows during postUpdate and we used DB.beginTransaction the same way we would have done in main workflow.
However, we notice that this is working only when the initial DB.save have been encapsulated in a DB.beginTransaction block.
If we don't and call immediately DB.save, it create/use a JdbcTransaction that end up in a ClassCasException.

Additional question :
Considering DB.save is wrapped using DB.beginTransaction. Is calling DB.beginTransaction in the Adapter will ensure us that the rows will be inserted/updated in the same transaction or is it better to get transaction through request.transaction(); and passing it while calling DB.update(bean, transaction)?

Expected behavior

Using DB.beginTransaction in a BeanPersistAdapter may reuse the current transaction used when calling DB.save.

Actual behavior

A ClassCastException occurs when we call DB.beginTransaction in an Adapter and no transaction opened around DB.save.

Steps to reproduce

Ad adDao= Ad.find.byId(32L);

adDao.setName("test")

DB.save(adDao);
@Singleton
public class AdSyncQueueAdapter extends BeanPersistAdapter {
   [...]
   @Override
    public void postUpdate(BeanPersistRequest<?> request) {
        try (Transaction transaction = DB.beginTransaction()) {
            Log newLog= new Log();
            newLog.setText("Test");
            newLog.save();

            transaction.commit();
        }
    }
application-akka.actor.default-dispatcher-58 - ERROR - models.ebeanAdapters.algolia.AdSyncQueueAdapter - Error in AdSyncQueueAdapter.
java.lang.ClassCastException: class io.ebeaninternal.server.transaction.JdbcTransaction cannot be cast to class io.ebeaninternal.api.ScopedTransaction (io.ebeaninternal.server.transaction.JdbcTransaction and io.ebeaninternal.api.ScopedTransaction are in unnamed module of loader play.runsupport.NamedURLClassLoader @27492fb5)
        at io.ebeaninternal.server.transaction.TransactionManager.activeScoped(TransactionManager.java:183)
        at io.ebeaninternal.server.transaction.TransactionManager.beginScopedTransaction(TransactionManager.java:502)
        at io.ebeaninternal.server.core.DefaultServer.beginTransaction(DefaultServer.java:732)
        at io.ebeaninternal.server.core.DefaultServer.beginTransaction(DefaultServer.java:727)
        at io.ebean.DB.beginTransaction(DB.java:173)
        at domain.adSyncQueue.AdSyncQueueService.lambda$createAdSyncElementsInAlgoliaQueue$1(AdSyncQueueService.java:93)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
        at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:722)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
        at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:754)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
        at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:667)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
        at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:765)
        at domain.adSyncQueue.AdSyncQueueService.createAdSyncElementsInAlgoliaQueue(AdSyncQueueService.java:92)
        at domain.adSyncQueue.AdSyncQueueService.onUpdateAd(AdSyncQueueService.java:74)
        at models.ebeanAdapters.algolia.AdSyncQueueAdapter.onCrudAction(AdSyncQueueAdapter.java:162)
        at models.ebeanAdapters.algolia.AdSyncQueueAdapter.postUpdate(AdSyncQueueAdapter.java:86)
        at io.ebeaninternal.server.deploy.ChainedBeanPersistController.postUpdate(ChainedBeanPersistController.java:117)
        at io.ebeaninternal.server.core.PersistRequestBean.controllerPost(PersistRequestBean.java:917)
        at io.ebeaninternal.server.core.PersistRequestBean.postExecute(PersistRequestBean.java:864)
        at io.ebeaninternal.server.persist.dml.DmlHandler.checkRowCount(DmlHandler.java:100)
        at io.ebeaninternal.server.persist.dml.UpdateHandler.execute(UpdateHandler.java:70)
        at io.ebeaninternal.server.persist.dml.DmlHandler.executeNoBatch(DmlHandler.java:88)
        at io.ebeaninternal.server.persist.dml.DmlBeanPersister.execute(DmlBeanPersister.java:69)
        at io.ebeaninternal.server.persist.dml.DmlBeanPersister.update(DmlBeanPersister.java:54)
        at io.ebeaninternal.server.core.PersistRequestBean.executeUpdate(PersistRequestBean.java:1213)
        at io.ebeaninternal.server.core.PersistRequestBean.executeNow(PersistRequestBean.java:729)
        at io.ebeaninternal.server.core.PersistRequestBean.executeNoBatch(PersistRequestBean.java:770)
        at io.ebeaninternal.server.core.PersistRequestBean.executeOrQueue(PersistRequestBean.java:761)
        at io.ebeaninternal.server.persist.DefaultPersister.update(DefaultPersister.java:507)
        at io.ebeaninternal.server.persist.DefaultPersister.update(DefaultPersister.java:393)
        at io.ebeaninternal.server.persist.DefaultPersister.save(DefaultPersister.java:412)
        at io.ebeaninternal.server.core.DefaultServer.save(DefaultServer.java:1611)
        at io.ebeaninternal.server.core.DefaultServer.save(DefaultServer.java:1603)
        at io.ebean.DB.save(DB.java:351)
        at domain.ads.AdDatabaseRepository.update(AdDatabaseRepository.java:209)
        at domain.ads.AdDatabaseRepository.update(AdDatabaseRepository.java:455)
        at service.AdService.updateSaleOffer(AdService.java:376)
        at provide.saleOffer.controllers.ManageSaleOfferApiControllerImp.updateSaleOffer(ManageSaleOfferApiControllerImp.java:213)
        at provide.saleOffer.controllers.ManageSaleOfferApiController.updateSaleOffer(ManageSaleOfferApiController.java:343)
        at saleOffer.Routes$$anonfun$routes$1.$anonfun$applyOrElse$21(Routes.scala:365)
        at play.core.routing.HandlerInvokerFactory$$anon$8.resultCall(HandlerInvoker.scala:160)
        at play.core.routing.HandlerInvokerFactory$JavaActionInvokerFactory$$anon$3$$anon$4$$anon$5.invocation(HandlerInvoker.scala:125)
        at play.core.j.JavaAction$$anon$1.$anonfun$call$1(JavaAction.scala:127)
        at play.api.mvc.BodyParser$.$anonfun$parseBody$4(Action.scala:241)
        at scala.Option.getOrElse(Option.scala:201)
        at play.api.mvc.BodyParser$.parseBody(Action.scala:239)
        at play.core.j.JavaAction$$anon$1.call(JavaAction.scala:128)
        at play.http.DefaultActionCreator$1.call(DefaultActionCreator.java:31)
        at actions.SentryOn5XXAction.call(SentryOn5XXAction.java:36)
        at be.objectify.deadbolt.java.actions.AbstractDeadboltAction.authorizeAndExecute(AbstractDeadboltAction.java:280)
        at be.objectify.deadbolt.java.ConstraintLogic.pass(ConstraintLogic.java:475)
        at be.objectify.deadbolt.java.ConstraintLogic.lambda$restrict$5(ConstraintLogic.java:141)
        at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
        at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341)
        at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
        at be.objectify.deadbolt.java.ConstraintLogic.restrict(ConstraintLogic.java:129)
        at be.objectify.deadbolt.java.actions.RestrictAction.applyRestriction(RestrictAction.java:73)
        at be.objectify.deadbolt.java.actions.AbstractRestrictiveAction.lambda$null$1(AbstractRestrictiveAction.java:59)
        at java.base/java.util.Optional.orElseGet(Optional.java:364)
        at be.objectify.deadbolt.java.actions.AbstractRestrictiveAction.lambda$execute$2(AbstractRestrictiveAction.java:59)
        at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
        at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341)
        at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
        at be.objectify.deadbolt.java.actions.AbstractRestrictiveAction.execute(AbstractRestrictiveAction.java:58)
        at be.objectify.deadbolt.java.actions.AbstractDeadboltAction.call(AbstractDeadboltAction.java:121)
        at play.core.j.JavaAction.$anonfun$apply$8(JavaAction.scala:184)
        at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:687)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:467)
        at play.core.j.ClassLoaderExecutionContext.$anonfun$execute$1(ClassLoaderExecutionContext.scala:64)
        at play.api.libs.streams.Execution$trampoline$.execute(Execution.scala:65)
        at play.core.j.ClassLoaderExecutionContext.execute(ClassLoaderExecutionContext.scala:59)
        at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:429)
        at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:338)
        at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:312)
        at scala.concurrent.impl.Promise$DefaultPromise.map(Promise.scala:182)
        at scala.concurrent.Future$.apply(Future.scala:687)
        at play.core.j.JavaAction.apply(JavaAction.scala:185)
        at play.api.mvc.Action.$anonfun$apply$6(Action.scala:83)
        at play.api.mvc.BodyParser$.$anonfun$runParserThenInvokeAction$1(Action.scala:260)
        at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:470)
        at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:63)
        at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:100)
        at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
        at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
        at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:100)
        at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:49)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.run(Thread.java:1583)

Thanks in advance for your help and tips.
Yours faithfully,
LCDP

@rbygrave
Copy link
Member

rbygrave commented Feb 2, 2024

DB.beginTransaction is authorized in BeanPersistAdapter

Well no it isn't.

BeanPersistAdapter are occurring DURING the execution of the transaction - so it's not ok to commit() that transaction which is effectively what is happening here.

We can access the current transaction via BeanPersistRequest.transaction() - this is what we are expected to do, so the code I'd expect to see would be more like:

@Singleton
public class AdSyncQueueAdapter extends BeanPersistAdapter {
   [...]
   @Override
    public void postUpdate(BeanPersistRequest<?> request) {

        // get the current transaction
        Transaction transaction = request.transaction();

        // get the current database
        Database database = request.database();

        Log newLog= new Log();
        newLog.setText("Test");
        database.save(newLog, transaction);
    }

If the code used DB.createTransaction() instead of DB.beginTransaction() and created a NEW transaction [that isn't put into the thread local etc] then that might be closer to the intention BUT ... I'll suggest this isn't a good idea in that this postUpdate() is being executed during a transaction and its generally the case that whatever is done here is done using that transaction and will succeed or fail with that current transaction.

That is, very unusual to create a new transaction in a BeanPersistAdapter.

The javadoc for BeanPersistAdapter is poor and doesn't say this.

Note that BeanPersistListener are non-transactional in that they are executed outside of a transaction after the successful commit of a transaction.

@rbygrave
Copy link
Member

rbygrave commented Feb 8, 2024

We can close this issue right?

@AntoineDuComptoirDesPharmacies
Copy link
Author

Hi @rbygrave,
It's a perfect answer, thank you for this clear view of the transaction usage through BeanAdapter ! 👍

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

No branches or pull requests

2 participants