diff --git a/docs/manual/working/javaGuide/main/sql/JavaEbean.md b/docs/manual/working/javaGuide/main/sql/JavaEbean.md index 05c2b04a..65edf5c2 100644 --- a/docs/manual/working/javaGuide/main/sql/JavaEbean.md +++ b/docs/manual/working/javaGuide/main/sql/JavaEbean.md @@ -135,6 +135,41 @@ If your class is an action, you can annotate your action method with `@play.db.e @[annotation](code/javaguide/ebean/JavaEbeanTest.java) +The transaction gets committed as soon as the action returns its result, or rolled back if the action throws an exception. As Ebean binds the transaction to the current thread, it only covers the database operations that the action runs on its own thread, before it returns: + +- Database operations that run on another thread, e.g. in a `CompletableFuture.supplyAsync`, don't run in the transaction, even if they start before the action returns. Ebean then runs each of them in a transaction of its own, so they aren't atomic together anymore. +- The same applies to the action method itself, if another action that runs between `@Transactional` and the action method calls it asynchronously, e.g. only once an asynchronous check completed. +- If the action throws an exception, the transaction gets rolled back. If it returns a `CompletionStage` that fails instead, even one that failed already, the transaction gets committed. + +To run asynchronous database operations in a transaction, begin it on the thread that runs them, and run all database operations of the transaction synchronously in there, e.g. with `DB.executeCall` on an execution context meant for blocking database calls: + +@[async](code/javaguide/ebean/TaskService.java) + +Don't return a `CompletionStage` from the callable that runs in the transaction, as the transaction would get committed before that completes as well. The `DatabaseExecutionContext` is a [`CustomExecutionContext`](https://www.playframework.com/documentation/latest/JavaAsync#Using-CustomExecutionContext-and-ClassLoaderExecution) for a dispatcher that keeps the blocking database calls off Play's default thread pool: + +@[execution-context](code/javaguide/ebean/DatabaseExecutionContext.java) + +Configure the dispatcher in `application.conf`. As each of its threads blocks on a database connection, give it as many threads as the connection pool has connections: + +``` +# Number of database connections +fixedConnectionPool = 9 + +play.db.prototype.hikaricp { + minimumIdle = ${fixedConnectionPool} + maximumPoolSize = ${fixedConnectionPool} +} + +# Worker threads matched to the connection pool +database.dispatcher { + executor = "thread-pool-executor" + throughput = 1 + thread-pool-executor { + fixed-pool-size = ${fixedConnectionPool} + } +} +``` + Or if you want a more traditional approach you can begin, commit and rollback transactions explicitly: @[traditional](code/javaguide/ebean/JavaEbeanTest.java) diff --git a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/DatabaseExecutionContext.java b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/DatabaseExecutionContext.java new file mode 100644 index 00000000..bc95b980 --- /dev/null +++ b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/DatabaseExecutionContext.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +// #execution-context +// ###replace: package services; +package javaguide.ebean; + +import javax.inject.Inject; +import org.apache.pekko.actor.ActorSystem; +import play.libs.concurrent.CustomExecutionContext; + +public class DatabaseExecutionContext extends CustomExecutionContext { + + @Inject + public DatabaseExecutionContext(ActorSystem actorSystem) { + // The dispatcher configured as "database.dispatcher" in application.conf + super(actorSystem, "database.dispatcher"); + } +} +// #execution-context diff --git a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java index 01289eb1..2a7cea40 100644 --- a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java +++ b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.junit.*; import play.Application; import play.db.ebean.Transactional; @@ -27,6 +28,7 @@ protected Application provideApplication() { Map config = new HashMap<>(); config.putAll(inMemoryDatabase()); config.put("ebean.default", "javaguide.ebean.Task"); + config.put("database.dispatcher.executor", "thread-pool-executor"); return fakeApplication(config); } @@ -149,6 +151,21 @@ public void traditional() { assertThat(Task.find.byId(34L).isDone(), is(true)); } + @Test + public void asyncTransaction() throws Exception { + createTask(); + + Task task = + app.injector() + .instanceOf(TaskService.class) + .markDone(34L) + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + assertThat(task.isDone(), is(true)); + assertThat(Task.find.byId(34L).isDone(), is(true)); + } + @Test public void startupDataLoader() { // Evolutions get applied automatically in test mode diff --git a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/TaskService.java b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/TaskService.java new file mode 100644 index 00000000..d6d78f41 --- /dev/null +++ b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/TaskService.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +// #async +// ###replace: package services; +package javaguide.ebean; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import io.ebean.DB; +import java.util.concurrent.CompletionStage; +import javax.inject.Inject; + +// ###insert: import models.Task; + +public class TaskService { + + private final DatabaseExecutionContext executionContext; + + @Inject + public TaskService(DatabaseExecutionContext executionContext) { + this.executionContext = executionContext; + } + + public CompletionStage markDone(long id) { + return supplyAsync( + () -> + DB.executeCall( + () -> { + // All database operations of the transaction run in here, synchronously + Task task = Task.find.byId(id); + task.setDone(true); + + task.save(); + return task; + }), + executionContext); + } +} +// #async diff --git a/play-ebean/src/main/java/play/db/ebean/Transactional.java b/play-ebean/src/main/java/play/db/ebean/Transactional.java index 320b39c3..9f6627fc 100644 --- a/play-ebean/src/main/java/play/db/ebean/Transactional.java +++ b/play-ebean/src/main/java/play/db/ebean/Transactional.java @@ -10,7 +10,17 @@ import java.lang.annotation.Target; import play.mvc.With; -/** Wraps the annotated action in an Ebean transaction. */ +/** + * Wraps the annotated action in an Ebean transaction. + * + *

The transaction gets committed as soon as the action returns its result, or rolled back if the + * action throws an exception. As Ebean binds the transaction to the current thread, it only covers + * the database operations that the action runs on its own thread, before it returns. Database + * operations that run on another thread, e.g. in {@code CompletableFuture.supplyAsync}, don't run + * in this transaction, even if they start before the action returns, and a {@code CompletionStage} + * that fails doesn't roll it back. To run asynchronous database operations in a transaction, begin + * it on the thread that runs them, e.g. with {@code DB.executeCall}. + */ @With(TransactionalAction.class) @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)