From 1743c7c5ef6476a2ee97cb1b95006a3217cb69e7 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 16 Apr 2019 17:06:18 +0200 Subject: [PATCH 01/17] Backup running channel database when needed Everytime our channel database needs to be persisted, we create a backup which is always safe to copy even when the system is busy. --- eclair-core/src/main/resources/reference.conf | 2 + .../main/scala/fr/acinq/eclair/Setup.scala | 14 ++- .../fr/acinq/eclair/db/BackupHandler.scala | 87 +++++++++++++++++++ .../scala/fr/acinq/eclair/db/Databases.scala | 9 +- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 9805aebb91..e3373f1719 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -18,6 +18,8 @@ eclair { watcher-type = "bitcoind" // other *experimental* values include "electrum" + backup-file = "eclair.sqlite.backup" + bitcoind { host = "localhost" rpcport = 18332 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index a27a5cab28..b7ce99bd12 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -43,7 +43,7 @@ import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} import fr.acinq.eclair.blockchain.{EclairWallet, _} import fr.acinq.eclair.channel.Register import fr.acinq.eclair.crypto.LocalKeyManager -import fr.acinq.eclair.db.Databases +import fr.acinq.eclair.db.{BackupHandler, Databases} import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ @@ -220,11 +220,11 @@ class Setup(datadir: File, routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out"))) _ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil) + chaindir = new File(datadir, chain) wallet = bitcoin match { case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient) case Electrum(electrumClient) => // TODO: DRY - val chaindir = new File(datadir, chain) val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "wallet.sqlite")}") val walletDb = new SqliteWalletDb(sqlite) val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet") @@ -234,7 +234,15 @@ class Setup(datadir: File, _ = wallet.getFinalAddress.map { case address => logger.info(s"initial wallet address=$address") } - + backupHandler = system.actorOf( + SimpleSupervisor.props(Props( + new BackupHandler( + nodeParams.db, + new File(chaindir, config.getString("backup-file").concat(".wip")), + new File(chaindir, config.getString("backup-file")) + ) + ), "backup", SupervisorStrategy.Resume) + ) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { case "local" => LocalPaymentHandler.props(nodeParams) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala new file mode 100644 index 0000000000..153ccbeb77 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -0,0 +1,87 @@ +package fr.acinq.eclair.db + +import java.io.File + +import akka.actor.{Actor, ActorLogging, Status} +import akka.pattern.pipe +import fr.acinq.eclair.channel.ChannelPersisted +import grizzled.slf4j.{Logger, Logging} + +import scala.concurrent.{ExecutionContext, Future} + +class BackupHandler(databases: Databases, wip: File, destination: File)(implicit ec: ExecutionContext) extends Actor with ActorLogging { + import BackupHandler._ + + // we listen to ChannelPersisted events, which will trigger a backup + override def preStart(): Unit = { + super.preStart() + context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) + } + + override def unhandled(message: Any): Unit = { + super.unhandled(message) + log.warning(s"unhandled message $message") + } + + def receive = idle + + // idle mode: start backup as soon as we receive an event + def idle: Receive = { + case persisted: ChannelPersisted => + doBackup(databases, wip, destination, persisted) pipeTo self + context become busy(None) + } + + // busy mode: there is already a backup in progress + // `again` tells us if we have to do another one when we're done with the current one + def busy(again: Option[ChannelPersisted]): Receive = { + case persisted: ChannelPersisted => + log.info(s"replacing pending backup with a newer one") + context become busy(Some(persisted)) + + case Done if again.isDefined => + doBackup(databases, wip, destination, again.get) pipeTo self + context become busy(None) + + case Done => + context become idle + + // we use the pipe pattern so errors are wrapped in akka.Status.Failure + case Status.Failure(cause) if again.isDefined => + log.error("database backup failed: {}", cause) + doBackup(databases, wip, destination, again.get) pipeTo self + context become busy(None) + + case Status.Failure(cause) => + log.error("database backup failed: {}", cause) + context become idle + } +} + +object BackupHandler extends Logging { + + case object Done + /** + * Database backup of our database to a destination file. + * Backup is done is 2 steps: first it it written to a wip file, then this file is renamed to the destination file. + * Renaming/moving a file * should * be atomic so the destination file is always consistent and usable but this is + * implementation dependent and it may not be the case with some file systems and/or operating systems + * + * @param databases database to backup + * @param wip work-in-progress file + * @param destination destination file + * @param persisted channel persistence event that triggered the backup + * @param ec execution context + * @return `Done` if backup was successful + */ + def doBackup(databases: Databases, wip: File, destination: File, persisted: ChannelPersisted)(implicit ec: ExecutionContext) : Future[BackupHandler.Done.type] = Future { + logger.info(s" database backup triggered by ${persisted.channelId} to $destination starts") + val start = System.currentTimeMillis() + databases.backup(wip) + val result = wip.renameTo(destination) + require(result, s"cannot rename $wip to $destination") + val end = System.currentTimeMillis() + logger.info(s" database backup triggered by ${persisted.channelId} to $destination done in ${end - start} ms") + Done + } +} 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 ff2badd0aa..451ab41e41 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 @@ -19,6 +19,7 @@ trait Databases { val pendingRelay: PendingRelayDb + def backup(file: File) : Unit } object Databases { @@ -45,6 +46,12 @@ object Databases { override val peers = new SqlitePeersDb(eclairJdbc) override val payments = new SqlitePaymentsDb(eclairJdbc) override val pendingRelay = new SqlitePendingRelayDb(eclairJdbc) + override def backup(file: File): Unit = { + SqliteUtils.using(eclairJdbc.createStatement()) { + statement => { + statement.executeUpdate(s"backup to ${file.getAbsolutePath}") + } + } + } } - } \ No newline at end of file From 2d24f676d94771897b211793d3e72abbbb7cf0d0 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2019 11:44:55 +0200 Subject: [PATCH 02/17] Upgrade sqlite-jdbc to 3.27.2.1 --- eclair-core/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index e2541f1a0d..91a5c4be96 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -203,7 +203,7 @@ org.xerial sqlite-jdbc - 3.21.0.1 + 3.27.2.1 From 604bf87d26793de85c8c6b697252d4f438b63841 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2019 20:30:52 +0200 Subject: [PATCH 03/17] Backup handler: use a custom mailbox This is an alternate design for handling multiple backup events: we use a custom mailbox with a max capacity of 1 message (ignore messages go directly to dead-letters) and use synchronous backups in the actual event handler. --- eclair-core/src/main/resources/reference.conf | 8 +- .../main/scala/fr/acinq/eclair/Setup.scala | 14 ++- .../fr/acinq/eclair/db/BackupHandler.scala | 103 ++++++------------ .../acinq/eclair/db/sqlite/SqliteUtils.scala | 2 +- 4 files changed, 47 insertions(+), 80 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index e3373f1719..8f67f77551 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -18,7 +18,13 @@ eclair { watcher-type = "bitcoind" // other *experimental* values include "electrum" - backup-file = "eclair.sqlite.backup" + // specific mail box for our backup handler, bounded to 1 message + // DO NOT overwrite these settings + backup-mailbox { + mailbox-type = "akka.dispatch.BoundedMailbox" + mailbox-capacity = 1 + mailbox-push-timeout-time = 0 + } bitcoind { host = "localhost" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index b7ce99bd12..8cbc7d1d6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -235,13 +235,12 @@ class Setup(datadir: File, case address => logger.info(s"initial wallet address=$address") } backupHandler = system.actorOf( - SimpleSupervisor.props(Props( - new BackupHandler( + SimpleSupervisor.props( + BackupHandler.props( nodeParams.db, - new File(chaindir, config.getString("backup-file").concat(".wip")), - new File(chaindir, config.getString("backup-file")) - ) - ), "backup", SupervisorStrategy.Resume) + new File(chaindir, "eclair.bak.wip"), + new File(chaindir, "eclair.bak") + ), "backup", SupervisorStrategy.Resume) ) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { @@ -353,8 +352,11 @@ class Setup(datadir: File, // @formatter:off sealed trait Bitcoin + case class Bitcoind(bitcoinClient: BasicBitcoinJsonRPCClient) extends Bitcoin + case class Electrum(electrumClient: ActorRef) extends Bitcoin + // @formatter:on case class Kit(nodeParams: NodeParams, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 153ccbeb77..645c6d1dc0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -2,86 +2,45 @@ package fr.acinq.eclair.db import java.io.File -import akka.actor.{Actor, ActorLogging, Status} -import akka.pattern.pipe +import akka.actor.{Actor, ActorLogging, Props} import fr.acinq.eclair.channel.ChannelPersisted -import grizzled.slf4j.{Logger, Logging} -import scala.concurrent.{ExecutionContext, Future} -class BackupHandler(databases: Databases, wip: File, destination: File)(implicit ec: ExecutionContext) extends Actor with ActorLogging { - import BackupHandler._ +/** + * README ! + * This actor will synchronously make a backup of the database it was initialized with whenever it receives + * a ChannelPersisted event. + * To avoid piling up messages and entering an endless backup loop, it is supposed to be used with a bounded mailbox + * with a single item: + * + * backup-mailbox { + * mailbox-type = "akka.dispatch.BoundedMailbox" + * mailbox-capacity = 1 + * mailbox-push-timeout-time = 0 + * } + * + * Messages that cannot be processed will be sent to dead letters + * + * @param databases database to backup + * @param wip work-in-progress file + * @param destination destination file + */ +class BackupHandler(databases: Databases, wip: File, destination: File) extends Actor with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup - override def preStart(): Unit = { - super.preStart() - context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) - } - - override def unhandled(message: Any): Unit = { - super.unhandled(message) - log.warning(s"unhandled message $message") - } - - def receive = idle + context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) - // idle mode: start backup as soon as we receive an event - def idle: Receive = { + def receive = { case persisted: ChannelPersisted => - doBackup(databases, wip, destination, persisted) pipeTo self - context become busy(None) - } - - // busy mode: there is already a backup in progress - // `again` tells us if we have to do another one when we're done with the current one - def busy(again: Option[ChannelPersisted]): Receive = { - case persisted: ChannelPersisted => - log.info(s"replacing pending backup with a newer one") - context become busy(Some(persisted)) - - case Done if again.isDefined => - doBackup(databases, wip, destination, again.get) pipeTo self - context become busy(None) - - case Done => - context become idle - - // we use the pipe pattern so errors are wrapped in akka.Status.Failure - case Status.Failure(cause) if again.isDefined => - log.error("database backup failed: {}", cause) - doBackup(databases, wip, destination, again.get) pipeTo self - context become busy(None) - - case Status.Failure(cause) => - log.error("database backup failed: {}", cause) - context become idle + val start = System.currentTimeMillis() + databases.backup(wip) + val result = wip.renameTo(destination) + require(result, s"cannot rename $wip to $destination") + val end = System.currentTimeMillis() + log.info(s" database backup triggered by ${persisted.channelId} to $destination done in ${end - start} ms") } } -object BackupHandler extends Logging { - - case object Done - /** - * Database backup of our database to a destination file. - * Backup is done is 2 steps: first it it written to a wip file, then this file is renamed to the destination file. - * Renaming/moving a file * should * be atomic so the destination file is always consistent and usable but this is - * implementation dependent and it may not be the case with some file systems and/or operating systems - * - * @param databases database to backup - * @param wip work-in-progress file - * @param destination destination file - * @param persisted channel persistence event that triggered the backup - * @param ec execution context - * @return `Done` if backup was successful - */ - def doBackup(databases: Databases, wip: File, destination: File, persisted: ChannelPersisted)(implicit ec: ExecutionContext) : Future[BackupHandler.Done.type] = Future { - logger.info(s" database backup triggered by ${persisted.channelId} to $destination starts") - val start = System.currentTimeMillis() - databases.backup(wip) - val result = wip.renameTo(destination) - require(result, s"cannot rename $wip to $destination") - val end = System.currentTimeMillis() - logger.info(s" database backup triggered by ${persisted.channelId} to $destination done in ${end - start} ms") - Done - } +object BackupHandler { + def props(databases: Databases, wip: File, destination: File) = Props(new BackupHandler(databases: Databases, wip: File, destination: File)).withMailbox("eclair.backup-mailbox") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 8d8e602e60..641e7107ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -82,7 +82,7 @@ object SqliteUtils { * Obtain an exclusive lock on a sqlite database. This is useful when we want to make sure that only one process * accesses the database file (see https://www.sqlite.org/pragma.html). * - * The lock will be kept until the database is closed, or if the locking mode is explicitely reset. + * The lock will be kept until the database is closed, or if the locking mode is explicitly reset. * * @param sqlite */ From 0d0879bdf76ec0ff2616b98e0d690795c2a77cb0 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 18 Apr 2019 11:52:03 +0200 Subject: [PATCH 04/17] Renamed backup handler parameters Co-Authored-By: sstone --- .../scala/fr/acinq/eclair/db/BackupHandler.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 645c6d1dc0..821df0af1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -22,10 +22,10 @@ import fr.acinq.eclair.channel.ChannelPersisted * Messages that cannot be processed will be sent to dead letters * * @param databases database to backup - * @param wip work-in-progress file - * @param destination destination file + * @param tmpFile temporary file + * @param backupFile final backup file */ -class BackupHandler(databases: Databases, wip: File, destination: File) extends Actor with ActorLogging { +class BackupHandler(databases: Databases, tmpFile: File, backupFile: File) extends Actor with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -33,14 +33,14 @@ class BackupHandler(databases: Databases, wip: File, destination: File) extends def receive = { case persisted: ChannelPersisted => val start = System.currentTimeMillis() - databases.backup(wip) - val result = wip.renameTo(destination) - require(result, s"cannot rename $wip to $destination") + databases.backup(tmpFile) + val result = tmpFile.renameTo(backupFile) + require(result, s"cannot rename $tmpFile to $backupFile") val end = System.currentTimeMillis() - log.info(s" database backup triggered by ${persisted.channelId} to $destination done in ${end - start} ms") + log.info(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms") } } object BackupHandler { - def props(databases: Databases, wip: File, destination: File) = Props(new BackupHandler(databases: Databases, wip: File, destination: File)).withMailbox("eclair.backup-mailbox") + def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases: Databases, wip: File, destination: File)).withMailbox("eclair.backup-mailbox") } From cc46e5c22b08a7bf46f18e0f802ab95309e5b65e Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 18 Apr 2019 15:01:19 +0200 Subject: [PATCH 05/17] BackupHandler: configure a specific mailbox In our configuration we specify a custom bounded mailbox for our backup handler, which is assigned by matching on its name. --- eclair-core/src/main/resources/reference.conf | 20 +++++++++++-------- .../main/scala/fr/acinq/eclair/Setup.scala | 10 ++++------ .../fr/acinq/eclair/db/BackupHandler.scala | 14 ++++++++++--- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8f67f77551..76ecb36a49 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -18,14 +18,6 @@ eclair { watcher-type = "bitcoind" // other *experimental* values include "electrum" - // specific mail box for our backup handler, bounded to 1 message - // DO NOT overwrite these settings - backup-mailbox { - mailbox-type = "akka.dispatch.BoundedMailbox" - mailbox-capacity = 1 - mailbox-push-timeout-time = 0 - } - bitcoind { host = "localhost" rpcport = 18332 @@ -143,3 +135,15 @@ eclair { private-key-file = "tor.dat" } } + +eclair.backup-mailbox { + mailbox-type = "akka.dispatch.BoundedMailbox" + mailbox-capacity = 1 + mailbox-push-timeout-time = 0 +} + +akka.actor.deployment { + "/*/backuphandler" { + mailbox = eclair.backup-mailbox + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 8cbc7d1d6c..73afd8555c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -88,11 +88,12 @@ class Setup(datadir: File, val config = NodeParams.loadConfiguration(datadir, overrideDefaults) val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) val chain = config.getString("chain") + val chaindir = new File(datadir, chain) val keyManager = new LocalKeyManager(seed, NodeParams.makeChainHash(chain)) val database = db match { case Some(d) => d - case None => Databases.sqliteJDBC(new File(datadir, chain)) + case None => Databases.sqliteJDBC(chaindir) } val nodeParams = NodeParams.makeNodeParams(config, keyManager, initTor(), database) @@ -220,7 +221,6 @@ class Setup(datadir: File, routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out"))) _ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil) - chaindir = new File(datadir, chain) wallet = bitcoin match { case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient) case Electrum(electrumClient) => @@ -234,13 +234,14 @@ class Setup(datadir: File, _ = wallet.getFinalAddress.map { case address => logger.info(s"initial wallet address=$address") } + // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox backupHandler = system.actorOf( SimpleSupervisor.props( BackupHandler.props( nodeParams.db, new File(chaindir, "eclair.bak.wip"), new File(chaindir, "eclair.bak") - ), "backup", SupervisorStrategy.Resume) + ), "backuphandler", SupervisorStrategy.Resume) ) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { @@ -352,11 +353,8 @@ class Setup(datadir: File, // @formatter:off sealed trait Bitcoin - case class Bitcoind(bitcoinClient: BasicBitcoinJsonRPCClient) extends Bitcoin - case class Electrum(electrumClient: ActorRef) extends Bitcoin - // @formatter:on case class Kit(nodeParams: NodeParams, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 821df0af1e..e231de6ac3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -1,10 +1,15 @@ package fr.acinq.eclair.db import java.io.File +import java.util.concurrent.TimeUnit -import akka.actor.{Actor, ActorLogging, Props} +import akka.actor.{Actor, ActorLogging, ActorSystem, Deploy, PoisonPill, Props} +import akka.dispatch.{BoundedMailbox, BoundedMessageQueueSemantics, NonBlockingBoundedMailbox, PriorityGenerator, RequiresMessageQueue, UnboundedPriorityMailbox} +import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.eclair.channel.ChannelPersisted +import scala.concurrent.duration.FiniteDuration + /** * README ! @@ -25,7 +30,7 @@ import fr.acinq.eclair.channel.ChannelPersisted * @param tmpFile temporary file * @param backupFile final backup file */ -class BackupHandler(databases: Databases, tmpFile: File, backupFile: File) extends Actor with ActorLogging { +class BackupHandler(databases: Databases, tmpFile: File, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -41,6 +46,9 @@ class BackupHandler(databases: Databases, tmpFile: File, backupFile: File) exten } } +trait BackupHandlerMailboxSemantics + object BackupHandler { - def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases: Databases, wip: File, destination: File)).withMailbox("eclair.backup-mailbox") + + def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases, tmpFile, backupFile)) } From cc3a711a49403732294a39bb7dd1f6b0fb8fd3cc Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 18 Apr 2019 15:44:41 +0200 Subject: [PATCH 06/17] Backup: make sure that our custom mailbox is always use BackupHandler is now private, users have to call BackupHandler.props() which always specifies our custom bounded maibox. --- eclair-core/src/main/resources/reference.conf | 7 +------ .../scala/fr/acinq/eclair/db/BackupHandler.scala | 14 ++++++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 76ecb36a49..ae7de67d94 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -136,14 +136,9 @@ eclair { } } +// do not edit or move this section eclair.backup-mailbox { mailbox-type = "akka.dispatch.BoundedMailbox" mailbox-capacity = 1 mailbox-push-timeout-time = 0 } - -akka.actor.deployment { - "/*/backuphandler" { - mailbox = eclair.backup-mailbox - } -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index e231de6ac3..2805d66912 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -1,15 +1,11 @@ package fr.acinq.eclair.db import java.io.File -import java.util.concurrent.TimeUnit -import akka.actor.{Actor, ActorLogging, ActorSystem, Deploy, PoisonPill, Props} -import akka.dispatch.{BoundedMailbox, BoundedMessageQueueSemantics, NonBlockingBoundedMailbox, PriorityGenerator, RequiresMessageQueue, UnboundedPriorityMailbox} -import com.typesafe.config.{Config, ConfigFactory} +import akka.actor.{Actor, ActorLogging, Props} +import akka.dispatch.{BoundedMessageQueueSemantics, RequiresMessageQueue} import fr.acinq.eclair.channel.ChannelPersisted -import scala.concurrent.duration.FiniteDuration - /** * README ! @@ -29,8 +25,10 @@ import scala.concurrent.duration.FiniteDuration * @param databases database to backup * @param tmpFile temporary file * @param backupFile final backup file + * + * Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox */ -class BackupHandler(databases: Databases, tmpFile: File, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { +class BackupHandler private(databases: Databases, tmpFile: File, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -50,5 +48,5 @@ trait BackupHandlerMailboxSemantics object BackupHandler { - def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases, tmpFile, backupFile)) + def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases, tmpFile, backupFile)).withMailbox("eclair.backup-mailbox") } From 77bfea81c57d3bc9eee990585bb499ca2d2e2c81 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 18 Apr 2019 16:20:49 +0200 Subject: [PATCH 07/17] Backup: add unit test --- .../acinq/eclair/db/BackupHandlerSpec.scala | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala new file mode 100644 index 0000000000..243fdc501d --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala @@ -0,0 +1,38 @@ +package fr.acinq.eclair.db + +import java.io.File +import java.sql.DriverManager +import java.util.UUID + +import akka.actor.{ActorSystem, Props} +import akka.testkit.TestKit +import fr.acinq.eclair.channel.ChannelPersisted +import fr.acinq.eclair.db.sqlite.SqliteChannelsDb +import fr.acinq.eclair.{TestConstants, TestUtils, randomBytes32} +import org.scalatest.FunSuiteLike + +import scala.concurrent.duration._ + +class BackupHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + + test("process backups") { + val db = TestConstants.inMemoryDb() + val wip = new File(TestUtils.BUILD_DIRECTORY, s"wip-${UUID.randomUUID()}") + val dest = new File(TestUtils.BUILD_DIRECTORY, s"backup-${UUID.randomUUID()}") + wip.deleteOnExit() + dest.deleteOnExit() + val channel = ChannelStateSpec.normal + db.channels.addOrUpdateChannel(channel) + assert(db.channels.listLocalChannels() == Seq(channel)) + + val handler = system.actorOf(BackupHandler.props(db, wip, dest)) + handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) + handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) + handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) + awaitCond(dest.exists(), 5 seconds) + + val db1 = new SqliteChannelsDb(DriverManager.getConnection(s"jdbc:sqlite:$dest")) + val check = db1.listLocalChannels() + assert(check == Seq(channel)) + } +} From d91913f89ec81cafdc32272e45ff326462b57e62 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 18 Apr 2019 16:56:33 +0200 Subject: [PATCH 08/17] BackupHandler: minor changes Remove tmp file paramater, we just add ".tmp" to the backup file --- .../src/main/scala/fr/acinq/eclair/Setup.scala | 10 +--------- .../scala/fr/acinq/eclair/db/BackupHandler.scala | 14 +++++--------- .../fr/acinq/eclair/db/BackupHandlerSpec.scala | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 73afd8555c..8c7b48c422 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -224,7 +224,6 @@ class Setup(datadir: File, wallet = bitcoin match { case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient) case Electrum(electrumClient) => - // TODO: DRY val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "wallet.sqlite")}") val walletDb = new SqliteWalletDb(sqlite) val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet") @@ -235,14 +234,7 @@ class Setup(datadir: File, case address => logger.info(s"initial wallet address=$address") } // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox - backupHandler = system.actorOf( - SimpleSupervisor.props( - BackupHandler.props( - nodeParams.db, - new File(chaindir, "eclair.bak.wip"), - new File(chaindir, "eclair.bak") - ), "backuphandler", SupervisorStrategy.Resume) - ) + backupHandler = system.actorOf(SimpleSupervisor.props(BackupHandler.props(nodeParams.db, new File(chaindir, "eclair.bak")), "backuphandler", SupervisorStrategy.Resume)) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { case "local" => LocalPaymentHandler.props(nodeParams) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 2805d66912..551da35806 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -8,7 +8,6 @@ import fr.acinq.eclair.channel.ChannelPersisted /** - * README ! * This actor will synchronously make a backup of the database it was initialized with whenever it receives * a ChannelPersisted event. * To avoid piling up messages and entering an endless backup loop, it is supposed to be used with a bounded mailbox @@ -22,13 +21,12 @@ import fr.acinq.eclair.channel.ChannelPersisted * * Messages that cannot be processed will be sent to dead letters * - * @param databases database to backup - * @param tmpFile temporary file - * @param backupFile final backup file + * @param databases database to backup + * @param backupFile backup file * * Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox */ -class BackupHandler private(databases: Databases, tmpFile: File, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { +class BackupHandler private(databases: Databases, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -36,6 +34,7 @@ class BackupHandler private(databases: Databases, tmpFile: File, backupFile: Fil def receive = { case persisted: ChannelPersisted => val start = System.currentTimeMillis() + val tmpFile = new File(backupFile.getAbsolutePath.concat(".tmp")) databases.backup(tmpFile) val result = tmpFile.renameTo(backupFile) require(result, s"cannot rename $tmpFile to $backupFile") @@ -44,9 +43,6 @@ class BackupHandler private(databases: Databases, tmpFile: File, backupFile: Fil } } -trait BackupHandlerMailboxSemantics - object BackupHandler { - - def props(databases: Databases, tmpFile: File, backupFile: File) = Props(new BackupHandler(databases, tmpFile, backupFile)).withMailbox("eclair.backup-mailbox") + def props(databases: Databases, backupFile: File) = Props(new BackupHandler(databases, backupFile)).withMailbox("eclair.backup-mailbox") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala index 243fdc501d..94fa798d61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala @@ -25,7 +25,7 @@ class BackupHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { db.channels.addOrUpdateChannel(channel) assert(db.channels.listLocalChannels() == Seq(channel)) - val handler = system.actorOf(BackupHandler.props(db, wip, dest)) + val handler = system.actorOf(BackupHandler.props(db, dest)) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) From dae2657eff1839ca76fac0900906beecbf194961 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 18 Apr 2019 17:19:35 +0200 Subject: [PATCH 09/17] Document backup process [ci skip] --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index f08e517e72..362e410d2f 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,21 @@ eclair.bitcoind.rpcuser= eclair.bitcoind.rpcpassword= ``` +### Backups + +The files that you need to backup are located in your data directory. You must backup: +- your seed (`seed.dat`) +- your channel database (`eclair.bak` in`mainnet`, `testnet` or `regtest` depending on which chain you're running on) + +Your seed never changes once it is created, but your channels do change whenever you receive our send payments. +`eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup +process in 2 steps: +- first, rename `eclair.bak` to a new local file +- then, backup the renamed file using whatever tool you like. + + + + ## Resources - [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja - [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell From b44e1094b79d6e38b98a03f03295ae9b5c07cab3 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 19 Apr 2019 10:28:56 +0200 Subject: [PATCH 10/17] Missing space README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 362e410d2f..dbc654349e 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ eclair.bitcoind.rpcpassword= The files that you need to backup are located in your data directory. You must backup: - your seed (`seed.dat`) -- your channel database (`eclair.bak` in`mainnet`, `testnet` or `regtest` depending on which chain you're running on) +- your channel database (`eclair.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) Your seed never changes once it is created, but your channels do change whenever you receive our send payments. `eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup From 4096078d92c42f61a8f50a1a31d56b74c5e4c899 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 19 Apr 2019 10:37:11 +0200 Subject: [PATCH 11/17] moved the backup part to the proper subsection --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index dbc654349e..d0ac942fce 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,18 @@ Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a different java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gui--.jar ``` +#### Backup + +The files that you need to backup are located in your data directory. You must backup: +- your seed (`seed.dat`) +- your channel database (`eclair.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) + +Your seed never changes once it is created, but your channels do change whenever you receive our send payments. +`eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup +process in 2 steps: +- first, rename `eclair.bak` to a new local file +- then, backup the renamed file using whatever tool you like. + ## Docker A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. @@ -202,18 +214,6 @@ eclair.bitcoind.rpcuser= eclair.bitcoind.rpcpassword= ``` -### Backups - -The files that you need to backup are located in your data directory. You must backup: -- your seed (`seed.dat`) -- your channel database (`eclair.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) - -Your seed never changes once it is created, but your channels do change whenever you receive our send payments. -`eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup -process in 2 steps: -- first, rename `eclair.bak` to a new local file -- then, backup the renamed file using whatever tool you like. - From 3b64171cb8785e24f1e6bf6156cda30bfc0534ca Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 19 Apr 2019 14:18:28 +0200 Subject: [PATCH 12/17] rename -> move (more explicit) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0ac942fce..c1880f2f14 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ The files that you need to backup are located in your data directory. You must b Your seed never changes once it is created, but your channels do change whenever you receive our send payments. `eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup process in 2 steps: -- first, rename `eclair.bak` to a new local file +- first, move `eclair.bak` to a new local file - then, backup the renamed file using whatever tool you like. ## Docker From 031dfe25132570d5c6ad0fee93e47142fd0cce51 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 19 Apr 2019 16:54:08 +0200 Subject: [PATCH 13/17] BackupHandler: use a specific threadpool with a single thread --- eclair-core/src/main/resources/reference.conf | 16 +++++++++++----- .../scala/fr/acinq/eclair/db/BackupHandler.scala | 4 +++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ae7de67d94..89561140bd 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -137,8 +137,14 @@ eclair { } // do not edit or move this section -eclair.backup-mailbox { - mailbox-type = "akka.dispatch.BoundedMailbox" - mailbox-capacity = 1 - mailbox-push-timeout-time = 0 -} +eclair { + backup-mailbox { + mailbox-type = "akka.dispatch.BoundedMailbox" + mailbox-capacity = 1 + mailbox-push-timeout-time = 0 + } + backup-dispatcher { + executor = "thread-pool-executor" + type = PinnedDispatcher + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 551da35806..618279ffec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -44,5 +44,7 @@ class BackupHandler private(databases: Databases, backupFile: File) extends Acto } object BackupHandler { - def props(databases: Databases, backupFile: File) = Props(new BackupHandler(databases, backupFile)).withMailbox("eclair.backup-mailbox") + // using this method is the only way to create a BackupHandler actor + // we make sure that it uses a custom bounded mailbox, and a custom pinned dispatcher (i.e our actor will have its own thread pool with 1 single thread) + def props(databases: Databases, backupFile: File) = Props(new BackupHandler(databases, backupFile)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher") } From d9f36e2010e250a6ae4e5f6941d1ee0e7863a85b Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 19 Apr 2019 17:05:07 +0200 Subject: [PATCH 14/17] Reword backup instructions [ci skip] --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c1880f2f14..4895e01e11 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,10 @@ The files that you need to backup are located in your data directory. You must b - your seed (`seed.dat`) - your channel database (`eclair.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) -Your seed never changes once it is created, but your channels do change whenever you receive our send payments. -`eclair.bak` is safe to backup even when your system is running. We recommend that you implement your backup -process in 2 steps: -- first, move `eclair.bak` to a new local file -- then, backup the renamed file using whatever tool you like. +Your seed never changes once it has been created, but your channels will change whenever you receive our send payments. Eclair will +create and maintain a snapshot of its database, named `eclair.bak`, in your data directory, and update it when needed. This file is +always consistent and safe to use even when Eclair is running, and this is what you should backup (for example, you could +move/rename it and upload the renamed file to a dedicated backup system, ...) ## Docker From 2aa06b9c76ecb14d8e557dd6caba4d20396b5eb2 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 19 Apr 2019 18:09:56 +0200 Subject: [PATCH 15/17] Add backup notification script Once a new backup has been created, call an optional user defined exe/script. --- README.md | 15 ++++++++++++--- eclair-core/src/main/resources/reference.conf | 3 +++ .../src/main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../scala/fr/acinq/eclair/db/BackupHandler.scala | 16 ++++++++++++++-- .../fr/acinq/eclair/db/BackupHandlerSpec.scala | 2 +- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4895e01e11..84e60b89a9 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,19 @@ The files that you need to backup are located in your data directory. You must b - your seed (`seed.dat`) - your channel database (`eclair.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) -Your seed never changes once it has been created, but your channels will change whenever you receive our send payments. Eclair will +Your seed never changes once it has been created, but your channels will change whenever you receive or send payments. Eclair will create and maintain a snapshot of its database, named `eclair.bak`, in your data directory, and update it when needed. This file is -always consistent and safe to use even when Eclair is running, and this is what you should backup (for example, you could -move/rename it and upload the renamed file to a dedicated backup system, ...) +always consistent and safe to use even when Eclair is running, and this is what you should backup regularly, e.g. with a `cron` task. + +Note that depending on your filesystem, we recommend first moving `eclair.bak` to some temporary file before copying that file to your final backup location. + +You can configure an optional script/exe to be called by eclair once a new database snapshot has been created, using the following option: + +``` +eclair.backup-notify-script = "absolute-path-to-your-script" +``` + +Make sure that your script is executable and uses an absolute path name for `eclair.bak` ## Docker diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 89561140bd..64a9a6bc8a 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -16,6 +16,9 @@ eclair { use-old-api = false } + // override this with a script/exe that will be called everytime a new database backup has been created + backup-notify-script = "" + watcher-type = "bitcoind" // other *experimental* values include "electrum" bitcoind { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 8c7b48c422..242cb46ca3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -234,7 +234,7 @@ class Setup(datadir: File, case address => logger.info(s"initial wallet address=$address") } // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox - backupHandler = system.actorOf(SimpleSupervisor.props(BackupHandler.props(nodeParams.db, new File(chaindir, "eclair.bak")), "backuphandler", SupervisorStrategy.Resume)) + backupHandler = system.actorOf(SimpleSupervisor.props(BackupHandler.props(nodeParams.db, new File(chaindir, "eclair.bak"), config.getString("backup-notify-script")), "backuphandler", SupervisorStrategy.Resume)) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { case "local" => LocalPaymentHandler.props(nodeParams) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 618279ffec..078bc0d5c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -6,6 +6,8 @@ import akka.actor.{Actor, ActorLogging, Props} import akka.dispatch.{BoundedMessageQueueSemantics, RequiresMessageQueue} import fr.acinq.eclair.channel.ChannelPersisted +import scala.util.{Failure, Success, Try} + /** * This actor will synchronously make a backup of the database it was initialized with whenever it receives @@ -26,7 +28,7 @@ import fr.acinq.eclair.channel.ChannelPersisted * * Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox */ -class BackupHandler private(databases: Databases, backupFile: File) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { +class BackupHandler private(databases: Databases, backupFile: File, backupScript: String) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -40,11 +42,21 @@ class BackupHandler private(databases: Databases, backupFile: File) extends Acto require(result, s"cannot rename $tmpFile to $backupFile") val end = System.currentTimeMillis() log.info(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms") + if (!backupScript.isEmpty) { + Try { + val processBuilder = new ProcessBuilder(backupScript) + // this process will run in the background + processBuilder.start() + } match { + case Success(_) => log.info(s"backup notify script $backupScript started successfully") + case Failure(cause) => log.warning(s"cannot start backup notify script $backupScript: $cause") + } + } } } object BackupHandler { // using this method is the only way to create a BackupHandler actor // we make sure that it uses a custom bounded mailbox, and a custom pinned dispatcher (i.e our actor will have its own thread pool with 1 single thread) - def props(databases: Databases, backupFile: File) = Props(new BackupHandler(databases, backupFile)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher") + def props(databases: Databases, backupFile: File, backupScript: String) = Props(new BackupHandler(databases, backupFile, backupScript)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala index 94fa798d61..d95f486179 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala @@ -25,7 +25,7 @@ class BackupHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { db.channels.addOrUpdateChannel(channel) assert(db.channels.listLocalChannels() == Seq(channel)) - val handler = system.actorOf(BackupHandler.props(db, dest)) + val handler = system.actorOf(BackupHandler.props(db, dest, "")) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) From 341600444afc7597c477022d7deffd2009e21d76 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 19 Apr 2019 19:04:19 +0200 Subject: [PATCH 16/17] Run backup notification script in the backup handler's thread We wait until it terminates and log its exit code. --- README.md | 11 +++++------ .../src/main/scala/fr/acinq/eclair/Setup.scala | 8 +++++++- .../scala/fr/acinq/eclair/db/BackupHandler.scala | 16 ++++++++-------- .../fr/acinq/eclair/db/BackupHandlerSpec.scala | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 84e60b89a9..ed6091af4a 100644 --- a/README.md +++ b/README.md @@ -146,9 +146,7 @@ Your seed never changes once it has been created, but your channels will change create and maintain a snapshot of its database, named `eclair.bak`, in your data directory, and update it when needed. This file is always consistent and safe to use even when Eclair is running, and this is what you should backup regularly, e.g. with a `cron` task. -Note that depending on your filesystem, we recommend first moving `eclair.bak` to some temporary file before copying that file to your final backup location. - -You can configure an optional script/exe to be called by eclair once a new database snapshot has been created, using the following option: +You can configure an optional notification script/exe to be called by eclair once a new database snapshot has been created, using the following option: ``` eclair.backup-notify-script = "absolute-path-to-your-script" @@ -156,6 +154,10 @@ eclair.backup-notify-script = "absolute-path-to-your-script" Make sure that your script is executable and uses an absolute path name for `eclair.bak` +Note that depending on your filesystem, in your backup process we recommend first moving `eclair.bak` to some temporary file +before copying that file to your final backup location. + + ## Docker A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. @@ -222,9 +224,6 @@ eclair.bitcoind.rpcuser= eclair.bitcoind.rpcpassword= ``` - - - ## Resources - [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja - [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 242cb46ca3..1af32c42b4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -234,7 +234,13 @@ class Setup(datadir: File, case address => logger.info(s"initial wallet address=$address") } // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox - backupHandler = system.actorOf(SimpleSupervisor.props(BackupHandler.props(nodeParams.db, new File(chaindir, "eclair.bak"), config.getString("backup-notify-script")), "backuphandler", SupervisorStrategy.Resume)) + + backupHandler = system.actorOf(SimpleSupervisor.props( + BackupHandler.props( + nodeParams.db, + new File(chaindir, "eclair.bak"), + if (config.hasPath("backup-notify-script")) Some(config.getString("backup-notify-script")) else None + ),"backuphandler", SupervisorStrategy.Resume)) audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { case "local" => LocalPaymentHandler.props(nodeParams) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 078bc0d5c5..c607c268a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -6,6 +6,7 @@ import akka.actor.{Actor, ActorLogging, Props} import akka.dispatch.{BoundedMessageQueueSemantics, RequiresMessageQueue} import fr.acinq.eclair.channel.ChannelPersisted +import scala.sys.process.Process import scala.util.{Failure, Success, Try} @@ -28,7 +29,7 @@ import scala.util.{Failure, Success, Try} * * Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox */ -class BackupHandler private(databases: Databases, backupFile: File, backupScript: String) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { +class BackupHandler private(databases: Databases, backupFile: File, backupScript_opt: Option[String]) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging { // we listen to ChannelPersisted events, which will trigger a backup context.system.eventStream.subscribe(self, classOf[ChannelPersisted]) @@ -42,21 +43,20 @@ class BackupHandler private(databases: Databases, backupFile: File, backupScript require(result, s"cannot rename $tmpFile to $backupFile") val end = System.currentTimeMillis() log.info(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms") - if (!backupScript.isEmpty) { + backupScript_opt.foreach(backupScript => { Try { - val processBuilder = new ProcessBuilder(backupScript) - // this process will run in the background - processBuilder.start() + // run the script in the current thread and wait until it terminates + Process(backupScript).! } match { - case Success(_) => log.info(s"backup notify script $backupScript started successfully") + case Success(exitCode) => log.info(s"backup notify script $backupScript returned $exitCode") case Failure(cause) => log.warning(s"cannot start backup notify script $backupScript: $cause") } - } + }) } } object BackupHandler { // using this method is the only way to create a BackupHandler actor // we make sure that it uses a custom bounded mailbox, and a custom pinned dispatcher (i.e our actor will have its own thread pool with 1 single thread) - def props(databases: Databases, backupFile: File, backupScript: String) = Props(new BackupHandler(databases, backupFile, backupScript)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher") + def props(databases: Databases, backupFile: File, backupScript_opt: Option[String]) = Props(new BackupHandler(databases, backupFile, backupScript_opt)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala index d95f486179..d57d9a072d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/BackupHandlerSpec.scala @@ -25,7 +25,7 @@ class BackupHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { db.channels.addOrUpdateChannel(channel) assert(db.channels.listLocalChannels() == Seq(channel)) - val handler = system.actorOf(BackupHandler.props(db, dest, "")) + val handler = system.actorOf(BackupHandler.props(db, dest, None)) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) handler ! ChannelPersisted(null, TestConstants.Alice.nodeParams.nodeId, randomBytes32, null) From 9a47d81570ba78cf79d3431cb2fc131dd4c888f2 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Fri, 19 Apr 2019 19:14:07 +0200 Subject: [PATCH 17/17] Update backup instructions --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ed6091af4a..cfbdd14b67 100644 --- a/README.md +++ b/README.md @@ -144,15 +144,13 @@ The files that you need to backup are located in your data directory. You must b Your seed never changes once it has been created, but your channels will change whenever you receive or send payments. Eclair will create and maintain a snapshot of its database, named `eclair.bak`, in your data directory, and update it when needed. This file is -always consistent and safe to use even when Eclair is running, and this is what you should backup regularly, e.g. with a `cron` task. - -You can configure an optional notification script/exe to be called by eclair once a new database snapshot has been created, using the following option: +always consistent and safe to use even when Eclair is running, and this is what you should backup regularly. +For example you could configure a `cron` task for your backup job. Or you could configure an optional notification script to be called by eclair once a new database snapshot has been created, using the following option: ``` eclair.backup-notify-script = "absolute-path-to-your-script" ``` - -Make sure that your script is executable and uses an absolute path name for `eclair.bak` +Make sure that your script is executable and uses an absolute path name for `eclair.bak`. Note that depending on your filesystem, in your backup process we recommend first moving `eclair.bak` to some temporary file before copying that file to your final backup location.