From 2fdff9ab229839f2a1916f45069324962d985f7e Mon Sep 17 00:00:00 2001 From: mk Date: Wed, 30 Sep 2020 12:53:57 -0300 Subject: [PATCH 1/2] [ETCM-48] Add json rpc http healthcheck --- .../ethereum/healthcheck/Healthcheck.scala | 37 +++++++++++ .../healthcheck/HealthcheckResponse.scala | 5 ++ .../healthcheck/HealthcheckResult.scala | 18 ++++++ .../healthcheck/HealthcheckStatus.scala | 6 ++ .../jsonrpc/JsonRpcControllerMetrics.scala | 2 + .../jsonrpc/JsonRpcHealthChecker.scala | 21 ++++++ .../ethereum/jsonrpc/JsonRpcHealthcheck.scala | 9 +++ .../jsonrpc/NodeJsonRpcHealthChecker.scala | 44 +++++++++++++ .../server/http/BasicJsonRpcHttpServer.scala | 10 ++- .../server/http/JsonRpcHttpServer.scala | 62 ++++++++++++++---- .../server/http/JsonRpcHttpsServer.scala | 64 ++++++++++++------- .../ethereum/nodebuilder/NodeBuilder.scala | 26 +++++--- .../server/http/JsonRpcHttpServerSpec.scala | 48 +++++++++----- 13 files changed, 286 insertions(+), 66 deletions(-) create mode 100644 src/main/scala/io/iohk/ethereum/healthcheck/Healthcheck.scala create mode 100644 src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResponse.scala create mode 100644 src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResult.scala create mode 100644 src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckStatus.scala create mode 100644 src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthChecker.scala create mode 100644 src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthcheck.scala create mode 100644 src/main/scala/io/iohk/ethereum/jsonrpc/NodeJsonRpcHealthChecker.scala diff --git a/src/main/scala/io/iohk/ethereum/healthcheck/Healthcheck.scala b/src/main/scala/io/iohk/ethereum/healthcheck/Healthcheck.scala new file mode 100644 index 0000000000..c9839477a2 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/healthcheck/Healthcheck.scala @@ -0,0 +1,37 @@ +package io.iohk.ethereum.healthcheck + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} + +/** + * Represents a health check, runs it and interprets the outcome. + * The outcome can be either a normal result, an application error, or + * an (unexpected) exception. + * + * @param description An one-word description of the health check. + * @param f The function that runs the health check. + * @param mapResultToError A function that interprets the result. + * @param mapErrorToError A function that interprets the application error. + * @param mapExceptionToError A function that interprets the (unexpected) exception. + * @tparam Error The type of the application error. + * @tparam Result The type of the actual value expected by normal termination of `f`. + */ +case class Healthcheck[Error, Result]( + description: String, + f: () ⇒ Future[Either[Error, Result]], + mapResultToError: Result ⇒ Option[String] = (_: Result) ⇒ None, + mapErrorToError: Error ⇒ Option[String] = (error: Error) ⇒ Some(String.valueOf(error)), + mapExceptionToError: Throwable ⇒ Option[String] = (t: Throwable) ⇒ Some(String.valueOf(t)) +) { + + def apply()(implicit ec: ExecutionContext): Future[HealthcheckResult] = { + f().transform { + case Success(Left(error)) ⇒ + Success(HealthcheckResult(description, mapErrorToError(error))) + case Success(Right(result)) ⇒ + Success(HealthcheckResult(description, mapResultToError(result))) + case Failure(t) ⇒ + Success(HealthcheckResult(description, mapExceptionToError(t))) + } + } +} diff --git a/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResponse.scala b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResponse.scala new file mode 100644 index 0000000000..42cbd0bbdc --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResponse.scala @@ -0,0 +1,5 @@ +package io.iohk.ethereum.healthcheck + +final case class HealthcheckResponse(checks: List[HealthcheckResult]) { + lazy val isOK: Boolean = checks.forall(_.isOK) +} diff --git a/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResult.scala b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResult.scala new file mode 100644 index 0000000000..480c28caef --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckResult.scala @@ -0,0 +1,18 @@ +package io.iohk.ethereum.healthcheck + +final case class HealthcheckResult private (description: String, status: String, error: Option[String]) { + assert( + status == HealthcheckStatus.OK && error.isEmpty || status == HealthcheckStatus.ERROR && error.isDefined + ) + + def isOK: Boolean = status == HealthcheckStatus.OK +} + +object HealthcheckResult { + def apply(description: String, error: Option[String]): HealthcheckResult = + new HealthcheckResult( + description = description, + status = error.fold(HealthcheckStatus.OK)(_ ⇒ HealthcheckStatus.ERROR), + error = error + ) +} diff --git a/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckStatus.scala b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckStatus.scala new file mode 100644 index 0000000000..067ee53f3d --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/healthcheck/HealthcheckStatus.scala @@ -0,0 +1,6 @@ +package io.iohk.ethereum.healthcheck + +object HealthcheckStatus { + final val OK = "OK" + final val ERROR = "ERROR" +} diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcControllerMetrics.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcControllerMetrics.scala index 1e8c889b3b..88cac37d1a 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcControllerMetrics.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcControllerMetrics.scala @@ -13,4 +13,6 @@ case object JsonRpcControllerMetrics extends MetricsContainer { final val MethodsSuccessCounter = metrics.counter("json.rpc.methods.success.counter") final val MethodsExceptionCounter = metrics.counter("json.rpc.methods.exception.counter") final val MethodsErrorCounter = metrics.counter("json.rpc.methods.error.counter") + + final val HealhcheckErrorCounter = metrics.counter("json.rpc.healthcheck.error.counter") } diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthChecker.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthChecker.scala new file mode 100644 index 0000000000..f66369cd45 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthChecker.scala @@ -0,0 +1,21 @@ +package io.iohk.ethereum.jsonrpc + +import io.iohk.ethereum.healthcheck.HealthcheckResponse + +import scala.concurrent.Future +import scala.util.{Failure, Success} +import scala.concurrent.ExecutionContext.Implicits.global + +trait JsonRpcHealthChecker { + def healthCheck(): Future[HealthcheckResponse] + + def handleResponse(responseF: Future[HealthcheckResponse]): Future[HealthcheckResponse] = { + responseF.andThen { + case Success(response) if (!response.isOK) => + JsonRpcControllerMetrics.HealhcheckErrorCounter.increment() + case Failure(t) => + JsonRpcControllerMetrics.HealhcheckErrorCounter.increment() + } + } + +} diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthcheck.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthcheck.scala new file mode 100644 index 0000000000..aaa5ba62a9 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/JsonRpcHealthcheck.scala @@ -0,0 +1,9 @@ +package io.iohk.ethereum.jsonrpc + +import io.iohk.ethereum.healthcheck.Healthcheck + +object JsonRpcHealthcheck { + type T[R] = Healthcheck[JsonRpcError, R] + + def apply[R](description: String, f: () ⇒ ServiceResponse[R]): T[R] = Healthcheck(description, f) +} diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/NodeJsonRpcHealthChecker.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/NodeJsonRpcHealthChecker.scala new file mode 100644 index 0000000000..b04771cd4d --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/NodeJsonRpcHealthChecker.scala @@ -0,0 +1,44 @@ +package io.iohk.ethereum.jsonrpc + +import io.iohk.ethereum.healthcheck.HealthcheckResponse +import io.iohk.ethereum.jsonrpc.EthService._ +import io.iohk.ethereum.jsonrpc.NetService._ + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class NodeJsonRpcHealthChecker( + netService: NetService, + ethService: EthService +) extends JsonRpcHealthChecker { + + protected def mainService: String = "node health" + + final val listeningHC = JsonRpcHealthcheck("listening", () ⇒ netService.listening(NetService.ListeningRequest())) + final val peerCountHC = JsonRpcHealthcheck("peerCount", () ⇒ netService.peerCount(PeerCountRequest())) + final val earliestBlockHC = JsonRpcHealthcheck( + "earliestBlock", + () ⇒ ethService.getBlockByNumber(BlockByNumberRequest(BlockParam.Earliest, true)) + ) + final val latestBlockHC = JsonRpcHealthcheck( + "latestBlock", + () ⇒ ethService.getBlockByNumber(BlockByNumberRequest(BlockParam.Latest, true)) + ) + final val pendingBlockHC = JsonRpcHealthcheck( + "pendingBlock", + () ⇒ ethService.getBlockByNumber(BlockByNumberRequest(BlockParam.Pending, true)) + ) + + override def healthCheck(): Future[HealthcheckResponse] = { + val listeningF = listeningHC() + val peerCountF = peerCountHC() + val earliestBlockF = earliestBlockHC() + val latestBlockF = latestBlockHC() + val pendingBlockF = pendingBlockHC() + + val allChecksF = List(listeningF, peerCountF, earliestBlockF, latestBlockF, pendingBlockF) + val responseF = Future.sequence(allChecksF).map(HealthcheckResponse) + + handleResponse(responseF) + } +} diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/BasicJsonRpcHttpServer.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/BasicJsonRpcHttpServer.scala index b6a120bdae..7110e04771 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/BasicJsonRpcHttpServer.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/BasicJsonRpcHttpServer.scala @@ -9,9 +9,13 @@ import io.iohk.ethereum.utils.Logger import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Failure, Success} -class BasicJsonRpcHttpServer(val jsonRpcController: JsonRpcController, config: JsonRpcHttpServerConfig) - (implicit val actorSystem: ActorSystem) - extends JsonRpcHttpServer with Logger { +class BasicJsonRpcHttpServer( + val jsonRpcController: JsonRpcController, + val jsonRpcHealthChecker: JsonRpcHealthChecker, + config: JsonRpcHttpServerConfig +)(implicit val actorSystem: ActorSystem) + extends JsonRpcHttpServer + with Logger { def run(): Unit = { val bindingResultF = Http(actorSystem).newServerAt(config.interface, config.port).bind(route) diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala index fcc3a5fa3c..0b3087e8a5 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala @@ -1,15 +1,15 @@ package io.iohk.ethereum.jsonrpc.server.http import akka.actor.ActorSystem -import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{MalformedRequestContentRejection, RejectionHandler, Route} +import akka.http.scaladsl.server._ import ch.megard.akka.http.cors.javadsl.CorsRejection import ch.megard.akka.http.cors.scaladsl.CorsDirectives._ import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcErrors, JsonRpcRequest, JsonRpcResponse} +import io.iohk.ethereum.jsonrpc._ import io.iohk.ethereum.utils.{ConfigUtils, Logger} import java.security.SecureRandom import org.json4s.JsonAST.JInt @@ -20,6 +20,7 @@ import scala.util.Try trait JsonRpcHttpServer extends Json4sSupport { val jsonRpcController: JsonRpcController + val jsonRpcHealthChecker: JsonRpcHealthChecker implicit val serialization = native.Serialization @@ -32,7 +33,8 @@ trait JsonRpcHttpServer extends Json4sSupport { .withAllowedOrigins(corsAllowedOrigins) implicit def myRejectionHandler: RejectionHandler = - RejectionHandler.newBuilder() + RejectionHandler + .newBuilder() .handle { case _: MalformedRequestContentRejection => complete((StatusCodes.BadRequest, JsonRpcResponse("2.0", None, Some(JsonRpcErrors.ParseError), JInt(0)))) @@ -42,7 +44,9 @@ trait JsonRpcHttpServer extends Json4sSupport { .result() val route: Route = cors(corsSettings) { - (pathEndOrSingleSlash & post) { + (path("healthcheck") & pathEndOrSingleSlash & get) { + handleHealthcheck() + } ~ (pathEndOrSingleSlash & post) { entity(as[JsonRpcRequest]) { request => handleRequest(request) } ~ entity(as[Seq[JsonRpcRequest]]) { request => @@ -56,6 +60,24 @@ trait JsonRpcHttpServer extends Json4sSupport { */ def run(): Unit + private def handleHealthcheck(): StandardRoute = { + val responseF = jsonRpcHealthChecker.healthCheck() + val httpResponseF = + responseF.map { + case response if response.isOK ⇒ + HttpResponse( + status = StatusCodes.OK, + entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response)) + ) + case response ⇒ + HttpResponse( + status = StatusCodes.InternalServerError, + entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response)) + ) + } + complete(httpResponseF) + } + private def handleRequest(request: JsonRpcRequest) = { complete(jsonRpcController.handleRequest(request)) } @@ -67,12 +89,18 @@ trait JsonRpcHttpServer extends Json4sSupport { object JsonRpcHttpServer extends Logger { - def apply(jsonRpcController: JsonRpcController, config: JsonRpcHttpServerConfig, secureRandom: SecureRandom) - (implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] = config.mode match { - case "http" => Right(new BasicJsonRpcHttpServer(jsonRpcController, config)(actorSystem)) - case "https" => Right(new JsonRpcHttpsServer(jsonRpcController, config, secureRandom)(actorSystem)) - case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected") - } + def apply( + jsonRpcController: JsonRpcController, + jsonRpcHealthchecker: JsonRpcHealthChecker, + config: JsonRpcHttpServerConfig, + secureRandom: SecureRandom + )(implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] = + config.mode match { + case "http" => Right(new BasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthchecker, config)(actorSystem)) + case "https" => + Right(new JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom)(actorSystem)) + case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected") + } trait JsonRpcHttpServerConfig { val mode: String @@ -99,9 +127,15 @@ object JsonRpcHttpServer extends Logger { override val corsAllowedOrigins = ConfigUtils.parseCorsAllowedOrigins(rpcHttpConfig, "cors-allowed-origins") - override val certificateKeyStorePath: Option[String] = Try(rpcHttpConfig.getString("certificate-keystore-path")).toOption - override val certificateKeyStoreType: Option[String] = Try(rpcHttpConfig.getString("certificate-keystore-type")).toOption - override val certificatePasswordFile: Option[String] = Try(rpcHttpConfig.getString("certificate-password-file")).toOption + override val certificateKeyStorePath: Option[String] = Try( + rpcHttpConfig.getString("certificate-keystore-path") + ).toOption + override val certificateKeyStoreType: Option[String] = Try( + rpcHttpConfig.getString("certificate-keystore-type") + ).toOption + override val certificatePasswordFile: Option[String] = Try( + rpcHttpConfig.getString("certificate-password-file") + ).toOption } } } diff --git a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala index af4f455806..b3b00fcf14 100644 --- a/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala +++ b/src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpsServer.scala @@ -3,7 +3,7 @@ package io.iohk.ethereum.jsonrpc.server.http import akka.actor.ActorSystem import akka.http.scaladsl.{ConnectionContext, Http} import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher -import io.iohk.ethereum.jsonrpc.JsonRpcController +import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcHealthChecker} import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpsServer.HttpsSetupResult import io.iohk.ethereum.utils.Logger @@ -14,20 +14,28 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.io.Source import scala.util.{Failure, Success, Try} -class JsonRpcHttpsServer(val jsonRpcController: JsonRpcController, config: JsonRpcHttpServerConfig, - secureRandom: SecureRandom)(implicit val actorSystem: ActorSystem) - extends JsonRpcHttpServer with Logger { +class JsonRpcHttpsServer( + val jsonRpcController: JsonRpcController, + val jsonRpcHealthChecker: JsonRpcHealthChecker, + config: JsonRpcHttpServerConfig, + secureRandom: SecureRandom +)(implicit val actorSystem: ActorSystem) + extends JsonRpcHttpServer + with Logger { def run(): Unit = { - val maybeSslContext = validateCertificateFiles(config.certificateKeyStorePath, config.certificateKeyStoreType, config.certificatePasswordFile).flatMap{ - case (keystorePath, keystoreType, passwordFile) => - val passwordReader = Source.fromFile(passwordFile) - try { - val password = passwordReader.getLines().mkString - obtainSSLContext(keystorePath, keystoreType, password) - } finally { - passwordReader.close() - } + val maybeSslContext = validateCertificateFiles( + config.certificateKeyStorePath, + config.certificateKeyStoreType, + config.certificatePasswordFile + ).flatMap { case (keystorePath, keystoreType, passwordFile) => + val passwordReader = Source.fromFile(passwordFile) + try { + val password = passwordReader.getLines().mkString + obtainSSLContext(keystorePath, keystoreType, password) + } finally { + passwordReader.close() + } } val maybeHttpsContext = maybeSslContext.map(sslContext => ConnectionContext.httpsServer(sslContext)) @@ -51,12 +59,16 @@ class JsonRpcHttpsServer(val jsonRpcController: JsonRpcController, config: JsonR * @param password for accessing the keystore with the certificate * @return the SSL context with the obtained certificate or an error if any happened */ - private def obtainSSLContext(certificateKeyStorePath: String, certificateKeyStoreType: String, password: String): HttpsSetupResult[SSLContext] = { + private def obtainSSLContext( + certificateKeyStorePath: String, + certificateKeyStoreType: String, + password: String + ): HttpsSetupResult[SSLContext] = { val passwordCharArray: Array[Char] = password.toCharArray - val maybeKeyStore: HttpsSetupResult[KeyStore] = Try(KeyStore.getInstance(certificateKeyStoreType)) - .toOption.toRight(s"Certificate keystore invalid type set: $certificateKeyStoreType") - val keyStoreInitResult: HttpsSetupResult[KeyStore] = maybeKeyStore.flatMap{ keyStore => + val maybeKeyStore: HttpsSetupResult[KeyStore] = Try(KeyStore.getInstance(certificateKeyStoreType)).toOption + .toRight(s"Certificate keystore invalid type set: $certificateKeyStoreType") + val keyStoreInitResult: HttpsSetupResult[KeyStore] = maybeKeyStore.flatMap { keyStore => val keyStoreFileCreationResult = Option(new FileInputStream(certificateKeyStorePath)) .toRight("Certificate keystore file creation failed") keyStoreFileCreationResult.flatMap { keyStoreFile => @@ -88,23 +100,27 @@ class JsonRpcHttpsServer(val jsonRpcController: JsonRpcController, config: JsonR * @param maybePasswordFile, with the path to the password file if it was configured * @return the certificate path and password file or the error detected */ - private def validateCertificateFiles(maybeKeystorePath: Option[String], - maybeKeystoreType: Option[String], - maybePasswordFile: Option[String]): HttpsSetupResult[(String, String, String)] = + private def validateCertificateFiles( + maybeKeystorePath: Option[String], + maybeKeystoreType: Option[String], + maybePasswordFile: Option[String] + ): HttpsSetupResult[(String, String, String)] = (maybeKeystorePath, maybeKeystoreType, maybePasswordFile) match { case (Some(keystorePath), Some(keystoreType), Some(passwordFile)) => val keystoreDirMissing = !new File(keystorePath).isFile val passwordFileMissing = !new File(passwordFile).isFile - if(keystoreDirMissing && passwordFileMissing) + if (keystoreDirMissing && passwordFileMissing) Left("Certificate keystore path and password file configured but files are missing") - else if(keystoreDirMissing) + else if (keystoreDirMissing) Left("Certificate keystore path configured but file is missing") - else if(passwordFileMissing) + else if (passwordFileMissing) Left("Certificate password file configured but file is missing") else Right((keystorePath, keystoreType, passwordFile)) case _ => - Left("HTTPS requires: certificate-keystore-path, certificate-keystore-type and certificate-password-file to be configured") + Left( + "HTTPS requires: certificate-keystore-path, certificate-keystore-type and certificate-password-file to be configured" + ) } override def corsAllowedOrigins: HttpOriginMatcher = config.corsAllowedOrigins diff --git a/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala b/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala index d899d11127..4b8118b8ef 100644 --- a/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala +++ b/src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala @@ -398,14 +398,21 @@ trait JSONRpcControllerBuilder { ) } +trait JSONRpcHealthcheckerBuilder { + this: NetServiceBuilder with EthServiceBuilder => + lazy val jsonRpcHealthChecker: JsonRpcHealthChecker = new NodeJsonRpcHealthChecker(netService, ethService) +} + trait JSONRpcHttpServerBuilder { self: ActorSystemBuilder with BlockchainBuilder with JSONRpcControllerBuilder + with JSONRpcHealthcheckerBuilder with SecureRandomBuilder with JSONRpcConfigBuilder => - lazy val maybeJsonRpcHttpServer = JsonRpcHttpServer(jsonRpcController, jsonRpcConfig.httpServerConfig, secureRandom) + lazy val maybeJsonRpcHttpServer = + JsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, jsonRpcConfig.httpServerConfig, secureRandom) } trait JSONRpcIpcServerBuilder { @@ -537,13 +544,15 @@ trait GenesisDataLoaderBuilder { trait SecureRandomBuilder extends Logger { lazy val secureRandom: SecureRandom = Config.secureRandomAlgo - .flatMap(name => Try(SecureRandom.getInstance(name)) match { - case Failure(exception) => - log.warn(s"Couldn't create SecureRandom instance using algorithm ${name}. Falling-back to default one") - None - case Success(value) => - Some(value) - }) + .flatMap(name => + Try(SecureRandom.getInstance(name)) match { + case Failure(exception) => + log.warn(s"Couldn't create SecureRandom instance using algorithm ${name}. Falling-back to default one") + None + case Success(value) => + Some(value) + } + ) .getOrElse(new SecureRandom()) } @@ -572,6 +581,7 @@ trait Node with QaServiceBuilder with KeyStoreBuilder with JSONRpcConfigBuilder + with JSONRpcHealthcheckerBuilder with JSONRpcControllerBuilder with JSONRpcHttpServerBuilder with JSONRpcIpcServerBuilder diff --git a/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala b/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala index 6008c799db..3a572ea62a 100644 --- a/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala +++ b/src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala @@ -7,7 +7,7 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.util.ByteString import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig -import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcResponse} +import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcResponse, JsonRpcHealthChecker} import org.json4s.JsonAST.{JInt, JString} import org.scalamock.scalatest.MockFactory import scala.concurrent.Future @@ -17,26 +17,32 @@ import org.scalatest.matchers.should.Matchers class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRouteTest { it should "pass valid json request to controller" in new TestSetup { - (mockJsonRpcController.handleRequest _).expects(*).returning(Future.successful(JsonRpcResponse("2.0", Some(JString("this is a response")), None, JInt(1)))) + (mockJsonRpcController.handleRequest _) + .expects(*) + .returning(Future.successful(JsonRpcResponse("2.0", Some(JString("this is a response")), None, JInt(1)))) val jsonRequest = ByteString("""{"jsonrpc":"2.0", "method": "asd", "id": "1"}""") - val postRequest = HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) + val postRequest = + HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) - postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { + postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { status shouldEqual StatusCodes.OK responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}""" } } it should "pass valid batch json request to controller" in new TestSetup { - (mockJsonRpcController.handleRequest _).expects(*) + (mockJsonRpcController.handleRequest _) + .expects(*) .twice() .returning(Future.successful(JsonRpcResponse("2.0", Some(JString("this is a response")), None, JInt(1)))) - val jsonRequest = ByteString("""[{"jsonrpc":"2.0", "method": "asd", "id": "1"}, {"jsonrpc":"2.0", "method": "asd", "id": "2"}]""") - val postRequest = HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) + val jsonRequest = + ByteString("""[{"jsonrpc":"2.0", "method": "asd", "id": "1"}, {"jsonrpc":"2.0", "method": "asd", "id": "2"}]""") + val postRequest = + HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) - postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { + postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { status === StatusCodes.OK responseAs[String] shouldEqual """[{"jsonrpc":"2.0","result":"this is a response","id":1},{"jsonrpc":"2.0","result":"this is a response","id":1}]""" } @@ -44,9 +50,10 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout it should "return BadRequest when malformed request is received" in new TestSetup { val jsonRequest = ByteString("""{"jsonrpc":"2.0", "method": "this is not a valid json""") - val postRequest = HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) + val postRequest = + HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) - postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { + postRequest ~> Route.seal(mockJsonRpcHttpServer.route) ~> check { status shouldEqual StatusCodes.BadRequest } } @@ -57,26 +64,30 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout HttpMethods.POST, uri = "/", headers = Origin(HttpOrigin("http://non_accepted_origin.com")) :: Nil, - entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) + entity = HttpEntity(MediaTypes.`application/json`, jsonRequest) + ) import mockJsonRpcHttpServerWithCors.myRejectionHandler - postRequest ~> Route.seal(mockJsonRpcHttpServerWithCors.route) ~> check { + postRequest ~> Route.seal(mockJsonRpcHttpServerWithCors.route) ~> check { status shouldEqual StatusCodes.Forbidden } } it should "accept CORS Requests" in new TestSetup { - (mockJsonRpcController.handleRequest _).expects(*).returning(Future.successful(JsonRpcResponse("2.0", Some(JString("this is a response")), None, JInt(1)))) + (mockJsonRpcController.handleRequest _) + .expects(*) + .returning(Future.successful(JsonRpcResponse("2.0", Some(JString("this is a response")), None, JInt(1)))) val jsonRequest = ByteString("""{"jsonrpc":"2.0", "method": "eth_blockNumber", "id": "1"}""") val postRequest = HttpRequest( HttpMethods.POST, uri = "/", headers = Origin(corsAllowedOrigin) :: Nil, - entity = HttpEntity(MediaTypes.`application/json`, jsonRequest)) + entity = HttpEntity(MediaTypes.`application/json`, jsonRequest) + ) - postRequest ~> Route.seal(mockJsonRpcHttpServerWithCors.route) ~> check { + postRequest ~> Route.seal(mockJsonRpcHttpServerWithCors.route) ~> check { status shouldEqual StatusCodes.OK } } @@ -94,8 +105,10 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout } val mockJsonRpcController = mock[JsonRpcController] + val mockJsonRpcHealthChecker = mock[JsonRpcHealthChecker] val mockJsonRpcHttpServer = new JsonRpcHttpServer { - val jsonRpcController = mockJsonRpcController + override val jsonRpcController = mockJsonRpcController + override val jsonRpcHealthChecker = mockJsonRpcHealthChecker def run(): Unit = () @@ -105,7 +118,8 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout val corsAllowedOrigin = HttpOrigin("http://localhost:3333") val mockJsonRpcHttpServerWithCors = new JsonRpcHttpServer { - val jsonRpcController = mockJsonRpcController + override val jsonRpcController = mockJsonRpcController + override val jsonRpcHealthChecker = mockJsonRpcHealthChecker def run(): Unit = () From 575f8f9d899aa2891ec8ba2c9df66c63714a9741 Mon Sep 17 00:00:00 2001 From: mk Date: Wed, 30 Sep 2020 14:39:21 -0300 Subject: [PATCH 2/2] [ETCM-48] Add /healthcheck in the insomnia workspace --- insomnia_workspace.json | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/insomnia_workspace.json b/insomnia_workspace.json index 5d2ffd7240..a8bbcdc28d 100644 --- a/insomnia_workspace.json +++ b/insomnia_workspace.json @@ -1,7 +1,7 @@ { "_type": "export", "__export_format": 4, - "__export_date": "2020-09-28T17:05:07.127Z", + "__export_date": "2020-09-30T20:01:40.792Z", "__export_source": "insomnia.desktop.app:v2020.4.1", "resources": [ { @@ -272,6 +272,35 @@ "metaSortKey": -1553869483792, "_type": "request_group" }, + { + "_id": "req_8c4ccaa3552544a4b61bc33cc9fa547c", + "parentId": "fld_f75b249a780c4b5e97a0a2980ad1d4b8", + "modified": 1580405661758, + "created": 1580405461883, + "url": "{{ node_url }}/healthcheck", + "name": "healthcheck node", + "description": "", + "method": "GET", + "body": {}, + "parameters": [], + "headers": [ + { + "id": "pair_088edc31f5e04f20a16b465a673871bb", + "name": "Cache-Control", + "value": "no-cache" + } + ], + "authentication": {}, + "metaSortKey": -1552939150156.3438, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, { "_id": "req_b60c1a4f9d604d868910f967c6a070d7", "parentId": "fld_a06eb77e183c4727800eb7dc43ceabe1",