diff --git a/server/app/com/xsn/explorer/models/SynchronizationProgress.scala b/server/app/com/xsn/explorer/models/SynchronizationProgress.scala new file mode 100644 index 00000000..7f1821ea --- /dev/null +++ b/server/app/com/xsn/explorer/models/SynchronizationProgress.scala @@ -0,0 +1,5 @@ +package com.xsn.explorer.models + +case class SynchronizationProgress(total: Int, synced: Int) { + def missing: Int = Math.max(0, total - synced) +} diff --git a/server/app/com/xsn/explorer/services/StatisticsService.scala b/server/app/com/xsn/explorer/services/StatisticsService.scala index cd97f570..06187f92 100644 --- a/server/app/com/xsn/explorer/services/StatisticsService.scala +++ b/server/app/com/xsn/explorer/services/StatisticsService.scala @@ -3,7 +3,7 @@ package com.xsn.explorer.services import com.alexitc.playsonify.core.FutureApplicationResult import com.alexitc.playsonify.core.FutureOr.Implicits.FutureOps import com.xsn.explorer.data.async.StatisticsFutureDataHandler -import com.xsn.explorer.models.StatisticsDetails +import com.xsn.explorer.models.{StatisticsDetails, SynchronizationProgress} import javax.inject.Inject import org.scalactic.{Bad, Good} @@ -27,6 +27,18 @@ class StatisticsService @Inject()(xsnService: XSNService, statisticsFutureDataHa result.toFuture } + def getSynchronizationProgress: FutureApplicationResult[SynchronizationProgress] = { + val dbStats = statisticsFutureDataHandler.getStatistics() + val rpcBlock = xsnService.getLatestBlock() + + val result = for { + stats <- dbStats.toFutureOr + latestBlock <- rpcBlock.toFutureOr + } yield SynchronizationProgress(total = latestBlock.height.int, synced = stats.blocks) + + result.toFuture + } + private def discardErrors[T](value: FutureApplicationResult[T]): FutureApplicationResult[Option[T]] = { value .map { diff --git a/server/app/controllers/HealthController.scala b/server/app/controllers/HealthController.scala index 9cb73705..5700e127 100644 --- a/server/app/controllers/HealthController.scala +++ b/server/app/controllers/HealthController.scala @@ -1,12 +1,22 @@ package controllers -import javax.inject.Inject - +import com.xsn.explorer.services.StatisticsService import controllers.common.{MyJsonController, MyJsonControllerComponents} +import javax.inject.Inject +import org.scalactic.Good -class HealthController @Inject()(cc: MyJsonControllerComponents) extends MyJsonController(cc) { +class HealthController @Inject()(cc: MyJsonControllerComponents, statsService: StatisticsService) + extends MyJsonController(cc) { - def check() = Action { - Ok + def check() = Action.async { _ => + statsService.getSynchronizationProgress + .map { + case Good(progress) if progress.missing < 10 => Ok + case Good(progress) => InternalServerError(s"There are ${progress.missing} missing blocks") + case _ => InternalServerError("Failed to check sync progress") + } + .recover { + case _: Throwable => InternalServerError("Failed to check sync progress") + } } } diff --git a/server/test/controllers/HealthControllerSpec.scala b/server/test/controllers/HealthControllerSpec.scala index e30d9a55..bf605c0d 100644 --- a/server/test/controllers/HealthControllerSpec.scala +++ b/server/test/controllers/HealthControllerSpec.scala @@ -1,18 +1,48 @@ package controllers +import com.xsn.explorer.data.StatisticsBlockingDataHandler +import com.xsn.explorer.helpers.DataGenerator +import com.xsn.explorer.models._ +import com.xsn.explorer.models.values._ +import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec +import org.mockito.MockitoSugar._ +import org.scalactic.Good import play.api.Application +import play.api.inject.bind import play.api.test.Helpers._ +import scala.concurrent.Future + class HealthControllerSpec extends MyAPISpec { - val application: Application = guiceApplicationBuilder.build() + private val xsnServiceMock = mock[XSNService] + private val statisticsDataHandlerMock = mock[StatisticsBlockingDataHandler] + + val application: Application = guiceApplicationBuilder + .overrides(bind[XSNService].to(xsnServiceMock)) + .overrides(bind[StatisticsBlockingDataHandler].to(statisticsDataHandlerMock)) + .build() "GET /health" should { "return OK" in { + val latestXSNBlock = DataGenerator.randomBlock().copy(height = Height(19)) + val stats = Statistics(10, 0, None, None) + when(xsnServiceMock.getLatestBlock()).thenReturn(Future.successful(Good(latestXSNBlock))) + when(statisticsDataHandlerMock.getStatistics()).thenReturn(Good(stats)) val response = GET("/health") status(response) mustEqual OK } + + "return Internal Server Error if there are 10 missing blocks" in { + val latestXSNBlock = DataGenerator.randomBlock().copy(height = Height(20)) + val stats = Statistics(10, 0, None, None) + when(xsnServiceMock.getLatestBlock()).thenReturn(Future.successful(Good(latestXSNBlock))) + when(statisticsDataHandlerMock.getStatistics()).thenReturn(Good(stats)) + val response = GET("/health") + + status(response) mustEqual INTERNAL_SERVER_ERROR + } } }