From 1f6a7afd47e989b7c083ae9da075a9e1dd88e457 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 27 Jan 2022 16:29:46 +0100 Subject: [PATCH] Have sqlite also write to jdbc url file (#2153) Eclair will now refuse to start if the database config is changed from `sqlite` to `postgres` or the opposite. --- .../scala/fr/acinq/eclair/db/Databases.scala | 48 +++++++++++-------- .../scala/fr/acinq/eclair/db/pg/PgUtils.scala | 3 -- .../scala/fr/acinq/eclair/TestDatabases.scala | 6 ++- .../fr/acinq/eclair/db/PgUtilsSpec.scala | 5 +- .../fr/acinq/eclair/db/SqliteUtilsSpec.scala | 33 ++++++++++++- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 2e78f6d9d1..8f6f768f6d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -73,15 +73,18 @@ object Databases extends Logging { } object SqliteDatabases { - def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection): SqliteDatabases = SqliteDatabases( - network = new SqliteNetworkDb(networkJdbc), - audit = new SqliteAuditDb(auditJdbc), - channels = new SqliteChannelsDb(eclairJdbc), - peers = new SqlitePeersDb(eclairJdbc), - payments = new SqlitePaymentsDb(eclairJdbc), - pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), - backupConnection = eclairJdbc - ) + def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { + jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _)) + SqliteDatabases( + network = new SqliteNetworkDb(networkJdbc), + audit = new SqliteAuditDb(auditJdbc), + channels = new SqliteChannelsDb(eclairJdbc), + peers = new SqlitePeersDb(eclairJdbc), + payments = new SqlitePaymentsDb(eclairJdbc), + pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), + backupConnection = eclairJdbc + ) + } } case class PostgresDatabases private(network: PgNetworkDb, @@ -241,19 +244,22 @@ object Databases extends Logging { databases } + } - private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { - def readString(path: Path): String = Files.readAllLines(path).get(0) + /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ + case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`. If this was intended, make sure you have migrated your data, otherwise your channels will be force-closed and you may lose funds.") - def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { + def readString(path: Path): String = Files.readAllLines(path).get(0) - if (urlFile.exists()) { - val oldUrl = readString(urlFile.toPath) - if (oldUrl != url) - throw JdbcUrlChanged(oldUrl, url) - } else { - writeString(urlFile.toPath, url) - } + def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + + if (urlFile.exists()) { + val oldUrl = readString(urlFile.toPath) + if (oldUrl != url) + throw JdbcUrlChanged(oldUrl, url) + } else { + writeString(urlFile.toPath, url) } } @@ -278,10 +284,12 @@ object Databases extends Logging { */ def sqlite(dbdir: File): SqliteDatabases = { dbdir.mkdirs() + val jdbcUrlFile = new File(dbdir, "last_jdbcurl") SqliteDatabases( eclairJdbc = SqliteUtils.openSqliteFile(dbdir, "eclair.sqlite", exclusiveLock = true, journalMode = "wal", syncFlag = "full"), // there should only be one process writing to this file networkJdbc = SqliteUtils.openSqliteFile(dbdir, "network.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal"), // we don't need strong durability guarantees on the network db - auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full") + auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full"), + jdbcUrlFile_opt = Some(jdbcUrlFile) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala index 23d662c5d9..bea3a81171 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala @@ -32,9 +32,6 @@ import scala.concurrent.duration._ object PgUtils extends JdbcUtils { - /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ - case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`") - sealed trait PgLock { def obtainExclusiveLock(implicit ds: DataSource): Unit diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 868707f4d3..e626c06f7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -41,7 +41,7 @@ object TestDatabases { def inMemoryDb(): Databases = { val connection = sqliteInMemory() - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = None) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } @@ -89,7 +89,9 @@ object TestDatabases { // @formatter:off override val connection: SQLiteConnection = sqliteInMemory() override lazy val db: Databases = { - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val jdbcUrlFile: File = new File(TestUtils.BUILD_DIRECTORY, s"jdbcUrlFile_${UUID.randomUUID()}.tmp") + jdbcUrlFile.deleteOnExit() + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = Some(jdbcUrlFile)) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } override def close(): Unit = () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index ff90d0ea41..3a65c0d8f0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -1,11 +1,12 @@ package fr.acinq.eclair.db import com.opentable.db.postgres.embedded.EmbeddedPostgres -import com.typesafe.config.{Config, ConfigFactory, ConfigValue} +import com.typesafe.config.{Config, ConfigFactory} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.PgLock.{LeaseLock, LockFailure, LockFailureHandler} -import fr.acinq.eclair.db.pg.PgUtils.{JdbcUrlChanged, migrateTable, using} +import fr.acinq.eclair.db.pg.PgUtils.{migrateTable, using} import fr.acinq.eclair.payment.ChannelPaymentRelayed import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index 03082bb10b..feead2ce86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -16,11 +16,16 @@ package fr.acinq.eclair.db -import java.sql.SQLException -import fr.acinq.eclair.{TestConstants, TestDatabases} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.sqlite.SqliteUtils.using +import fr.acinq.eclair.{TestDatabases, TestUtils} import org.scalatest.funsuite.AnyFunSuite +import java.io.File +import java.nio.file.Files +import java.sql.SQLException +import java.util.UUID + class SqliteUtilsSpec extends AnyFunSuite { test("using with auto-commit disabled") { @@ -74,4 +79,28 @@ class SqliteUtilsSpec extends AnyFunSuite { } } + test("jdbc url check") { + val datadir = new File(TestUtils.BUILD_DIRECTORY, s"sqlite_test_${UUID.randomUUID()}") + val jdbcUrlPath = new File(datadir, "last_jdbcurl") + datadir.mkdirs() + + // first start : write to file + val db1 = Databases.sqlite(datadir) + db1.channels.close() + + assert(Files.readString(jdbcUrlPath.toPath).trim == "sqlite") + + // 2nd start : no-op + val db2 = Databases.sqlite(datadir) + db2.channels.close() + + // we modify the file + Files.writeString(jdbcUrlPath.toPath, "postgres") + + // boom + intercept[JdbcUrlChanged] { + Databases.sqlite(datadir) + } + } + }