1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 09:54:02 +01:00

Improved handling of timeouts in API and GUI (#484)

Both the GUI and the API should handle AskTimeoutException failures as
specific cases: GUI should ignore them, API should print a pretty
response. Increased ask timeout to 60s.

Akka http server responses with the same format as other errors.
Fixes #414 where eclair-cli would fail to parse the server
timeout response.
This commit is contained in:
Dominique 2018-03-15 19:33:12 +01:00 committed by GitHub
parent dceb6b9b06
commit 3d48ec871e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 52 deletions

View File

@ -1,7 +1,8 @@
package fr.acinq.eclair.api
import akka.actor.ActorRef
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._
@ -15,12 +16,12 @@ import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment, _}
import fr.acinq.eclair.router.ChannelDesc
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JInt, JString}
import org.json4s.{JValue, jackson}
@ -51,7 +52,7 @@ trait Service extends Logging {
implicit val serialization = jackson.Serialization
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointKeySerializer + new ColorSerializer + new ShortChannelIdSerializer
implicit val timeout = Timeout(30 seconds)
implicit val timeout = Timeout(60 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
import Json4sSupport.{marshaller, unmarshaller}
@ -74,7 +75,7 @@ trait Service extends Logging {
val myExceptionHandler = ExceptionHandler {
case t: Throwable =>
extractRequest { _ =>
logger.info(s"API call failed with cause=${t.getMessage}")
logger.error(s"API call failed with cause=${t.getMessage}")
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1"))
}
}
@ -83,6 +84,7 @@ trait Service extends Logging {
case Success(s) => completeRpc(requestId, s)
case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage))
}
def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId))
val myRejectionHandler: RejectionHandler = RejectionHandler.newBuilder()
@ -106,6 +108,7 @@ trait Service extends Logging {
val route: Route =
respondWithDefaultHeaders(customHeaders) {
withRequestTimeoutResponse(r => HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """)) {
handleExceptions(myExceptionHandler) {
handleRejections(myRejectionHandler) {
authenticateBasic(realm = "Access restricted", userPassAuthenticator) { _ =>
@ -118,18 +121,18 @@ trait Service extends Logging {
req.method match {
// utility methods
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
case "help" => completeRpc(req.id, help)
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
case "help" => completeRpc(req.id, help)
// channel lifecycle methods
case "connect" => req.params match {
case "connect" => req.params match {
case JString(pubkey) :: JString(host) :: JInt(port) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String])
case JString(uri) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]"))
}
case "open" => req.params match {
case "open" => req.params match {
case JString(nodeId) :: JInt(fundingSatoshis) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshis.toLong), MilliSatoshi(0), fundingTxFeeratePerKw_opt = None, channelFlags = None)).mapTo[String])
case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: Nil =>
@ -140,7 +143,7 @@ trait Service extends Logging {
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = Some(flags.toByte))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshis], [nodeId, fundingSatoshis, pushMsat], [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte] or [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte, flag]"))
}
case "close" => req.params match {
case "close" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
@ -151,11 +154,11 @@ trait Service extends Logging {
}
// local network methods
case "peers" => completeRpcFuture(req.id, for {
case "peers" => completeRpcFuture(req.id, for {
peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
} yield peerinfos)
case "channels" => req.params match {
case "channels" => req.params match {
case Nil =>
val f = for {
channels_id <- (register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys)
@ -164,26 +167,26 @@ trait Service extends Logging {
} yield channels
completeRpcFuture(req.id, f)
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
case Success(pk) =>
val f = for {
channels_id <- (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
} yield channels
completeRpcFuture(req.id, f)
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
}
case Success(pk) =>
val f = for {
channels_id <- (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
} yield channels
completeRpcFuture(req.id, f)
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
}
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
}
case "channel" => req.params match {
case "channel" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
}
// global network methods
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]])
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))))
case "allupdates" => req.params match {
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]])
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))))
case "allupdates" => req.params match {
case JString(nodeId) :: Nil => Try(PublicKey(nodeId)) match {
case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values))
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'"))
@ -192,7 +195,7 @@ trait Service extends Logging {
}
// payment methods
case "receive" => req.params match {
case "receive" => req.params match {
// only the payment description is given: user may want to generate a donation payment request
case JString(description) :: Nil =>
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
@ -201,7 +204,7 @@ trait Service extends Logging {
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description]"))
}
case "send" => req.params match {
case "send" => req.params match {
// user manually sets the payment information
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
@ -254,6 +257,7 @@ trait Service extends Logging {
}
}
}
}
}
}

View File

@ -15,7 +15,7 @@ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor._
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.ElectrumEvent
import fr.acinq.eclair.channel.ChannelEvent
import fr.acinq.eclair.gui.controllers.{MainController, NotificationsController}
import fr.acinq.eclair.payment.PaymentEvent
import fr.acinq.eclair.payment.{PaymentEvent, PaymentResult}
import fr.acinq.eclair.router.NetworkEvent
import grizzled.slf4j.Logging
@ -80,6 +80,7 @@ class FxApp extends Application with Logging {
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentResult])
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
pKit.completeWith(setup.bootstrap)

View File

@ -17,7 +17,7 @@ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDiscon
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumConnected, ElectrumDisconnected}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{NORMAL => _, _}
import fr.acinq.eclair.wire.NodeAnnouncement
@ -184,6 +184,22 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
mainController.networkChannelsList.update(idx, c)
}
}
case p: PaymentSucceeded =>
val message = CoinUtils.formatAmountInUnit(MilliSatoshi(p.amountMsat), FxApp.getUnit, withUnit = true)
mainController.handlers.notification("Payment Sent", message, NOTIFICATION_SUCCESS)
case p: PaymentFailed =>
val distilledFailures = PaymentLifecycle.transformForUser(p.failures)
val message = s"${distilledFailures.size} attempts:\n${
distilledFailures.map {
case LocalFailure(t) => s"- (local) ${t.getMessage}"
case RemoteFailure(_, e) => s"- (remote) ${e.failureMessage.message}"
case _ => "- Unknown error"
}.mkString("\n")
}"
mainController.handlers.notification("Payment Failed", message, NOTIFICATION_ERROR)
case p: PaymentSent =>
log.debug(s"payment sent with h=${p.paymentHash}, amount=${p.amount}, fees=${p.feesPaid}")
runInGuiThread(() => mainController.paymentSentList.prepend(new PaymentSentRecord(p, LocalDateTime.now())))

View File

@ -2,7 +2,7 @@ package fr.acinq.eclair.gui
import java.io.{File, FileWriter}
import akka.pattern.ask
import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout
import fr.acinq.bitcoin.MilliSatoshi
import fr.acinq.eclair._
@ -20,7 +20,7 @@ import scala.util.{Failure, Success}
*/
class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends Logging {
implicit val timeout = Timeout(30 seconds)
implicit val timeout = Timeout(60 seconds)
private var notifsController: Option[NotificationsController] = None
@ -49,6 +49,8 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
case Success(s) =>
logger.info(s"successfully opened channel $s")
notification("Channel created", s.toString, NOTIFICATION_SUCCESS)
case Failure(_: AskTimeoutException) =>
logger.info("opening channel is taking a long time, notifications will not be shown")
case Failure(t) =>
logger.info("could not open channel ", t)
notification("Channel creation failed", t.getMessage, NOTIFICATION_ERROR)
@ -65,34 +67,22 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
val amountMsat = overrideAmountMsat_opt
.orElse(req.amount.map(_.amount))
.getOrElse(throw new RuntimeException("you need to manually specify an amount for this payment request"))
logger.info(s"sending $amountMsat to ${req.paymentHash} @ ${req.nodeId}")
val sendPayment = req.minFinalCltvExpiry match {
case None => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo)
case Some(minFinalCltvExpiry) => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo, finalCltvExpiry = minFinalCltvExpiry)
}
// completed payment will be handled from GUIUpdater by listening to PaymentSucceeded/PaymentFailed events
(for {
kit <- fKit
res <- (kit.paymentInitiator ? sendPayment).mapTo[PaymentResult]
} yield res)
.onComplete {
case Success(_: PaymentSucceeded) =>
val message = CoinUtils.formatAmountInUnit(MilliSatoshi(amountMsat), FxApp.getUnit, withUnit = true)
notification("Payment Sent", message, NOTIFICATION_SUCCESS)
case Success(PaymentFailed(_, failures)) =>
val distilledFailures = PaymentLifecycle.transformForUser(failures)
val message = s"${distilledFailures.size} attempts:\n${
distilledFailures.map {
case LocalFailure(t) => s"- (local) ${t.getMessage}"
case RemoteFailure(_, e) => s"- (remote) ${e.failureMessage.message}"
case _ => "- Unknown error"
}.mkString("\n")
}"
notification("Payment Failed", message, NOTIFICATION_ERROR)
case Failure(t) =>
val message = t.getMessage
notification("Payment Failed", message, NOTIFICATION_ERROR)
}
} yield res).recover {
case _: AskTimeoutException =>
logger.info("sending payment is taking a long time, notifications will not be shown")
case t =>
val message = t.getMessage
notification("Payment Failed", message, NOTIFICATION_ERROR)
}
}
def receive(amountMsat_opt: Option[MilliSatoshi], description: String): Future[String] = for {
@ -100,7 +90,6 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
res <- (kit.paymentHandler ? ReceivePayment(amountMsat_opt, description)).mapTo[PaymentRequest].map(PaymentRequest.write)
} yield res
def exportToDot(file: File) = for {
kit <- fKit
dot <- (kit.router ? 'dot).mapTo[String]

View File

@ -1,4 +1,5 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "INFO"

View File

@ -81,7 +81,7 @@
<appender-ref ref="CYAN"/>
</logger>
<logger name="fr.acinq.eclair.gui" level="WARN" additivity="false">
<logger name="fr.acinq.eclair.gui" level="INFO" additivity="false">
<appender-ref ref="MAGENTA"/>
</logger>