Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions persistence/play-jdbc/src/main/scala/play/api/db/Databases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.util.control.NonFatal
import com.typesafe.config.Config
import play.api.Configuration
import play.api.Environment
import play.api.Logger
import play.utils.ProxyDriver
import play.utils.Reflect

Expand Down Expand Up @@ -112,6 +113,8 @@ object Databases {
* Provides driver registration and connection methods.
*/
abstract class DefaultDatabase(val name: String, configuration: Config, environment: Environment) extends Database {
import DefaultDatabase._

private val config = Configuration(configuration)
val databaseConfig: DatabaseConfig = DatabaseConfig.fromConfig(config, environment)

Expand Down Expand Up @@ -195,7 +198,7 @@ abstract class DefaultDatabase(val name: String, configuration: Config, environm
connection.commit()
throw e
case e: Throwable =>
connection.rollback()
rollbackQuietly(connection)
throw e
}
}
Expand All @@ -214,14 +217,38 @@ abstract class DefaultDatabase(val name: String, configuration: Config, environm
connection.commit()
throw e
case e: Throwable =>
connection.rollback()
rollbackQuietly(connection)
throw e
} finally {
connection.setTransactionIsolation(oldIsolationLevel)
restoreIsolationLevelQuietly(connection, oldIsolationLevel)
}
}
}

private def rollbackQuietly(connection: Connection): Unit = {
Comment thread
bursauxa marked this conversation as resolved.
try {
Comment thread
bursauxa marked this conversation as resolved.
// attempt to do things in a clean way, with explicit rollback
connection.rollback()
} catch {
// we failed to rollback: the connection handle is dead anyways
// it will be, or has already been, rollbacked server-side
// swallow the exception so we can throw the original one from the block statement
case NonFatal(ex) =>
logger.warn(s"Could not rollback transaction on database [$name], its connection is likely already dead", ex)
}
}

private def restoreIsolationLevelQuietly(connection: Connection, isolationLevel: Int): Unit = {
try {
connection.setTransactionIsolation(isolationLevel)
Comment thread
bursauxa marked this conversation as resolved.
Outdated
} catch {
// the connection is already dead, its isolation level no longer matters
// swallow the exception so we can throw the original one from the block statement
case NonFatal(ex) =>
logger.warn(s"Could not restore transaction isolation level on database [$name]", ex)
}
}

// shutdown

def shutdown(): Unit = {
Expand All @@ -234,6 +261,10 @@ abstract class DefaultDatabase(val name: String, configuration: Config, environm
}
}

object DefaultDatabase {
private val logger = Logger(classOf[DefaultDatabase])
}

/**
* Default implementation of the database API using a connection pool.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
package play.api.db

import java.sql.SQLException
Comment thread
bursauxa marked this conversation as resolved.

import java.sql.SQLNonTransientConnectionException
import java.sql.SQLSyntaxErrorException

import acolyte.jdbc.ConnectionHandler
import acolyte.jdbc.QueryResult
import acolyte.jdbc.ResourceHandler
import acolyte.jdbc.StatementHandler
import acolyte.jdbc.UpdateResult
import org.jdbcdslog.ConnectionPoolDataSourceProxy
import org.specs2.mutable.After
import org.specs2.mutable.Specification
Expand Down Expand Up @@ -124,6 +131,14 @@ class DatabasesSpec extends Specification {
}
}

"resurface the original error when the rollback fails" in {
withDeadConnectionDatabase("test-withTransaction-deadConnection") { db =>
db.withTransaction { c =>
c.createStatement.execute("insert into test (id, name) values (1, 'alice')")
} must throwA[SQLSyntaxErrorException](message = "Invalid SQL")
}
}

"manual setup transaction isolation level" in new WithDatabase {
val db = Databases.inMemory(name = "test-manualSetupTrasactionIsolationLevel")

Expand All @@ -133,6 +148,14 @@ class DatabasesSpec extends Specification {
}
}

"resurface the original error when the rollback fails, with isolation level" in {
withDeadConnectionDatabase("test-withTransactionIsolationLevel-deadConnection") { db =>
db.withTransaction(TransactionIsolationLevel.Serializable) { c =>
c.createStatement.execute("insert into test (id, name) values (1, 'alice')")
} must throwA[SQLSyntaxErrorException](message = "Invalid SQL")
}
}

"not supply connections after shutdown" in {
val db = Databases.inMemory(name = "test-shutdown")
db.getConnection().close()
Expand All @@ -152,6 +175,53 @@ class DatabasesSpec extends Specification {
}
}

// statement-level error, as reported on invalid SQL
def invalidSql(): SQLException = new SQLSyntaxErrorException("Invalid SQL", "42000")
Comment thread
bursauxa marked this conversation as resolved.
Outdated

// connection-level error, as reported on lost socket
def connectionLost(): SQLException = new SQLNonTransientConnectionException("Socket error", "08S01")

/**
* A database that rejects every statement as invalid SQL, and whose connections turn out to be
* gone once the transaction is cleaned up, so that the rollback fails. The two errors are
* deliberately distinct, so that a test can tell which one the caller ends up with.
*/
private def deadConnectionDatabase(name: String): Database = {
acolyte.jdbc.Driver.register(
"DatabasesSpec-deadConnection",
new ConnectionHandler.Default(
new StatementHandler {
def isQuery(sql: String): Boolean = false

def whenSQLQuery(sql: String, parameters: java.util.List[StatementHandler.Parameter]): QueryResult =
throw invalidSql()

def whenSQLUpdate(sql: String, parameters: java.util.List[StatementHandler.Parameter]): UpdateResult =
throw invalidSql()
},
new ResourceHandler {
// only the rollback matters here, the transaction is never committed
def whenCommitTransaction(connection: acolyte.jdbc.Connection): Unit = ()
def whenRollbackTransaction(connection: acolyte.jdbc.Connection): Unit = throw connectionLost()
}
)
)

Databases(
driver = "acolyte.jdbc.Driver",
url = "jdbc:acolyte:DatabasesSpec?handler=DatabasesSpec-deadConnection",
name = name
)
}

// Runs the given block against such a database, then shuts it down.
// Provides isolations for Acolyte testing.
def withDeadConnectionDatabase[T](name: String)(block: Database => T): T = {
val db = deadConnectionDatabase(name)
try block(db)
finally db.shutdown()
}

trait WithDatabase extends After {
def db: Database
def after: Unit = () // db.shutdown()
Expand Down
Loading