Skip to content

Commit 95fffe3

Browse files
authored
Reduce pg transaction isolation (#1860)
I was able to reproduce #1856 by replaying the "concurrent channel updates" test with hardcoded additional delays in the database code. It was due to a conflict between `addOrUpdateChannel` and `updateChannelMetaTimestampColumn`. The two calls run in parallel and the latter completed before the former, causing it to fail. Reducing the isolation level makes the problem disappear. We reduce the transaction isolation level from `SERIALIZABLE` to `READ_COMMITTED`. Note that [*]: > Read Committed is the default isolation level in PostgreSQL. I'm not sure why we were using a stricter isolation level than the default one, since we only use very basic queries. Doc does say that: > This behavior makes Read Committed mode unsuitable for commands that involve complex search conditions; however, it is just right for simpler cases To make sure this didn't cause regression withe the locking mechanism, I wrote an additional test specifically on the `withLock` method. Here is what the doc says on the `INSERT ON CONFLICT DO UPDATE` statement, which we use for `addOrUpdateChannel`: > INSERT with an ON CONFLICT DO UPDATE clause behaves similarly. In Read Committed mode, each row proposed for insertion will either insert or update. Unless there are unrelated errors, one of those two outcomes is guaranteed. If a conflict originates in another transaction whose effects are not yet visible to the INSERT, the UPDATE clause will affect that row, even though possibly no version of that row is conventionally visible to the command. In the scenario described above, the `addOrUpdate` will update the row which timestamp was updated in parallel by the `updateChannelMetaTimestampColumn`, and it's exactly what we want. Fixes #1856. [*] https://www.postgresql.org/docs/13/transaction-iso.html
1 parent cea3fc0 commit 95fffe3

File tree

2 files changed

+38
-6
lines changed

2 files changed

+38
-6
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ object PgUtils extends JdbcUtils {
268268

269269
def inTransaction[T](f: Connection => T)(implicit dataSource: DataSource): T = {
270270
withConnection { connection =>
271-
inTransactionInternal(IsolationLevel.TRANSACTION_SERIALIZABLE)(connection)(f)
271+
inTransactionInternal(IsolationLevel.TRANSACTION_READ_COMMITTED)(connection)(f)
272272
}
273273
}
274274

eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala

+37-5
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ package fr.acinq.eclair.db
22

33
import com.opentable.db.postgres.embedded.EmbeddedPostgres
44
import com.typesafe.config.{Config, ConfigFactory}
5-
import fr.acinq.eclair.db.pg.PgUtils.{JdbcUrlChanged, migrateTable, using}
5+
import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._
66
import fr.acinq.eclair.db.pg.PgUtils.PgLock.{LockFailure, LockFailureHandler}
7+
import fr.acinq.eclair.db.pg.PgUtils.{JdbcUrlChanged, migrateTable, using}
78
import fr.acinq.eclair.{TestKitBaseClass, TestUtils}
8-
import grizzled.slf4j.Logging
9-
import org.postgresql.PGConnection
10-
import org.postgresql.jdbc.PgConnection
119
import grizzled.slf4j.{Logger, Logging}
10+
import org.postgresql.jdbc.PgConnection
11+
import org.postgresql.util.PGInterval
1212
import org.scalatest.concurrent.Eventually
1313
import org.scalatest.funsuite.AnyFunSuiteLike
14-
import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._
1514

1615
import java.io.File
1716
import java.util.UUID
17+
import javax.sql.DataSource
1818

1919
class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually {
2020

@@ -67,6 +67,38 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually
6767
pg.close()
6868
}
6969

70+
test("withLock utility method") {
71+
val pg = EmbeddedPostgres.start()
72+
val config = PgUtilsSpec.testConfig(pg.getPort)
73+
val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}")
74+
datadir.mkdirs()
75+
val instanceId1 = UUID.randomUUID()
76+
// this will lock the database for this instance id
77+
val db = Databases.postgres(config, instanceId1, datadir, LockFailureHandler.logAndThrow)
78+
implicit val ds: DataSource = db.dataSource
79+
80+
// dummy query works
81+
db.lock.withLock { conn =>
82+
conn.createStatement().executeQuery("SELECT 1")
83+
}
84+
85+
intercept[LockFailureHandler.LockException] {
86+
db.lock.withLock { conn =>
87+
// we start with a dummy query
88+
conn.createStatement().executeQuery("SELECT 1")
89+
// but before we complete the query, a separate connection takes the lock
90+
using(pg.getPostgresDatabase.getConnection.prepareStatement(s"UPDATE lease SET expires_at = now() + ?, instance = ? WHERE id = 1")) {
91+
statement =>
92+
statement.setObject(1, new PGInterval("60 seconds"))
93+
statement.setString(2, UUID.randomUUID().toString)
94+
statement.executeUpdate()
95+
}
96+
}
97+
}
98+
99+
pg.close()
100+
}
101+
70102
test("jdbc url check") {
71103
val pg = EmbeddedPostgres.start()
72104
val config = PgUtilsSpec.testConfig(pg.getPort)

0 commit comments

Comments
 (0)