From dc05700a76231aa8fbef1f3c3617a8e4c6e755ef Mon Sep 17 00:00:00 2001 From: philippus Date: Sat, 24 Aug 2024 12:10:01 -0500 Subject: [PATCH] Add startup checks --- docs/src/main/paradox/healthchecks.md | 6 +- .../local/src/main/resources/application.conf | 1 + .../pekko/management/LocalBootstrapTest.scala | 10 +++ .../startup-checks.backwards.excludes | 20 ++++++ management/src/main/resources/reference.conf | 9 ++- .../pekko/management/HealthCheckRoutes.scala | 5 ++ .../management/HealthCheckSettings.scala | 61 +++++++++++++++++++ .../management/ManagementLogMarker.scala | 6 ++ .../internal/HealthChecksImpl.scala | 33 +++++++++- .../management/javadsl/HealthChecks.scala | 29 +++++++++ .../management/scaladsl/HealthChecks.scala | 27 ++++++++ .../pekko/management/HealthCheckTest.java | 53 +++++++++++++++- .../management/HealthCheckRoutesSpec.scala | 4 ++ .../management/HealthCheckSettingsSpec.scala | 53 +++++++++++++++- .../pekko/management/HealthChecksSpec.scala | 56 +++++++++++++---- 15 files changed, 350 insertions(+), 23 deletions(-) create mode 100644 management/src/main/mima-filters/1.1.x.backwards.excludes/startup-checks.backwards.excludes diff --git a/docs/src/main/paradox/healthchecks.md b/docs/src/main/paradox/healthchecks.md index 64010bdf..c4930f1d 100644 --- a/docs/src/main/paradox/healthchecks.md +++ b/docs/src/main/paradox/healthchecks.md @@ -58,14 +58,14 @@ Application specific health checks can be added a `name = + startupStatusCode(port) shouldEqual StatusCodes.InternalServerError + } + } + } + "not be ready initially" in { eventually { managementPorts.foreach { port => diff --git a/management/src/main/mima-filters/1.1.x.backwards.excludes/startup-checks.backwards.excludes b/management/src/main/mima-filters/1.1.x.backwards.excludes/startup-checks.backwards.excludes new file mode 100644 index 00000000..9bbf27b4 --- /dev/null +++ b/management/src/main/mima-filters/1.1.x.backwards.excludes/startup-checks.backwards.excludes @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Add startup checks +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.management.scaladsl.HealthChecks.startup") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.management.scaladsl.HealthChecks.startupResult") diff --git a/management/src/main/resources/reference.conf b/management/src/main/resources/reference.conf index 3b8ea102..c5bd579f 100644 --- a/management/src/main/resources/reference.conf +++ b/management/src/main/resources/reference.conf @@ -55,7 +55,7 @@ pekko.management { # The FQCN is the fully qualified class name of the `ManagementRoutesProvider`. # # By default the `org.apache.pekko.management.HealthCheckRoutes` is enabled, see `health-checks` section of how - # configure specific readiness and liveness checks. + # configure specific startup, readiness and liveness checks. # # Route providers included by a library (from reference.conf) can be excluded by an application # by using "" or null as the FQCN of the named entry, for example: @@ -72,8 +72,10 @@ pekko.management { route-providers-read-only = true } - # Health checks for readiness and liveness + # Health checks for startup, readiness and liveness health-checks { + # When exposing health checks via Pekko Management, the path to expose startup checks on + startup-path = "startup" # When exposing health checks via Pekko Management, the path to expose readiness checks on readiness-path = "ready" # When exposing health checks via Pekko Management, the path to expose readiness checks on @@ -90,6 +92,9 @@ pekko.management { # # Libraries and frameworks that contribute checks are expected to add their own checks to their reference.conf. # Applications can add their own checks to application.conf. + startup-checks { + + } readiness-checks { } diff --git a/management/src/main/scala/org/apache/pekko/management/HealthCheckRoutes.scala b/management/src/main/scala/org/apache/pekko/management/HealthCheckRoutes.scala index 79735d59..61ec3a70 100644 --- a/management/src/main/scala/org/apache/pekko/management/HealthCheckRoutes.scala +++ b/management/src/main/scala/org/apache/pekko/management/HealthCheckRoutes.scala @@ -49,6 +49,11 @@ private[pekko] class HealthCheckRoutes(system: ExtendedActorSystem) extends Mana override def routes(mrps: ManagementRouteProviderSettings): Route = { concat( + path(PathMatchers.separateOnSlashes(settings.startupPath)) { + get { + onComplete(healthChecks.startupResult())(healthCheckResponse) + } + }, path(PathMatchers.separateOnSlashes(settings.readinessPath)) { get { onComplete(healthChecks.readyResult())(healthCheckResponse) diff --git a/management/src/main/scala/org/apache/pekko/management/HealthCheckSettings.scala b/management/src/main/scala/org/apache/pekko/management/HealthCheckSettings.scala index d5952c5d..55e06bff 100644 --- a/management/src/main/scala/org/apache/pekko/management/HealthCheckSettings.scala +++ b/management/src/main/scala/org/apache/pekko/management/HealthCheckSettings.scala @@ -32,6 +32,15 @@ object HealthCheckSettings { } new HealthCheckSettings( + config + .getConfig("startup-checks") + .root + .unwrapped + .asScala + .collect { + case (name, value) if validFQCN(value) => NamedHealthCheck(name, value.toString) + } + .toList, config .getConfig("readiness-checks") .root @@ -50,6 +59,7 @@ object HealthCheckSettings { case (name, value) if validFQCN(value) => NamedHealthCheck(name, value.toString) } .toList, + config.getString("startup-path"), config.getString("readiness-path"), config.getString("liveness-path"), config.getDuration("check-timeout").asScala) @@ -63,6 +73,27 @@ object HealthCheckSettings { /** * Java API */ + def create( + startupChecks: java.util.List[NamedHealthCheck], + readinessChecks: java.util.List[NamedHealthCheck], + livenessChecks: java.util.List[NamedHealthCheck], + startupPath: String, + readinessPath: String, + livenessPath: String, + checkDuration: java.time.Duration) = + new HealthCheckSettings( + startupChecks.asScala.toList, + readinessChecks.asScala.toList, + livenessChecks.asScala.toList, + startupPath, + readinessPath, + livenessPath, + checkDuration.asScala) + + /** + * Java API + */ + @deprecated("Use create that takes `startupChecks` and `startupPath` parameters instead", "1.1.0") def create( readinessChecks: java.util.List[NamedHealthCheck], livenessChecks: java.util.List[NamedHealthCheck], @@ -70,27 +101,57 @@ object HealthCheckSettings { livenessPath: String, checkDuration: java.time.Duration) = new HealthCheckSettings( + Nil, readinessChecks.asScala.toList, livenessChecks.asScala.toList, + "", readinessPath, livenessPath, checkDuration.asScala) } /** + * @param startupChecks List of FQCN of startup checks * @param readinessChecks List of FQCN of readiness checks * @param livenessChecks List of FQCN of liveness checks + * @param startupPath The path to serve startup on * @param readinessPath The path to serve readiness on * @param livenessPath The path to serve liveness on * @param checkTimeout how long to wait for all health checks to complete */ final class HealthCheckSettings( + val startupChecks: immutable.Seq[NamedHealthCheck], val readinessChecks: immutable.Seq[NamedHealthCheck], val livenessChecks: immutable.Seq[NamedHealthCheck], + val startupPath: String, val readinessPath: String, val livenessPath: String, val checkTimeout: FiniteDuration) { + @deprecated("Use constructor that takes `startupChecks` and `startupPath` parameters instead", "1.1.0") + def this( + readinessChecks: immutable.Seq[NamedHealthCheck], + livenessChecks: immutable.Seq[NamedHealthCheck], + readinessPath: String, + livenessPath: String, + checkTimeout: FiniteDuration + ) = { + this( + Nil, + readinessChecks, + livenessChecks, + "", + readinessPath, + livenessPath, + checkTimeout + ) + } + + /** + * Java API + */ + def getStartupChecks(): java.util.List[NamedHealthCheck] = startupChecks.asJava + /** * Java API */ diff --git a/management/src/main/scala/org/apache/pekko/management/ManagementLogMarker.scala b/management/src/main/scala/org/apache/pekko/management/ManagementLogMarker.scala index 69f318ff..04786c9c 100644 --- a/management/src/main/scala/org/apache/pekko/management/ManagementLogMarker.scala +++ b/management/src/main/scala/org/apache/pekko/management/ManagementLogMarker.scala @@ -40,6 +40,12 @@ object ManagementLogMarker { def boundHttp(boundAddress: String): LogMarker = LogMarker("pekkoManagementBound", Map(Properties.HttpAddress -> boundAddress)) + /** + * Marker "pekkoStartupCheckFailed" of log event when a startup check fails. + */ + val startupCheckFailed: LogMarker = + LogMarker("pekkoStartupCheckFailed") + /** * Marker "pekkoReadinessCheckFailed" of log event when a readiness check fails. */ diff --git a/management/src/main/scala/org/apache/pekko/management/internal/HealthChecksImpl.scala b/management/src/main/scala/org/apache/pekko/management/internal/HealthChecksImpl.scala index 43b9480c..c46a8c62 100644 --- a/management/src/main/scala/org/apache/pekko/management/internal/HealthChecksImpl.scala +++ b/management/src/main/scala/org/apache/pekko/management/internal/HealthChecksImpl.scala @@ -24,7 +24,8 @@ import pekko.event.Logging import pekko.management.{ HealthCheckSettings, InvalidHealthCheckException, ManagementLogMarker, NamedHealthCheck } import pekko.management.javadsl.{ LivenessCheckSetup => JLivenessCheckSetup } import pekko.management.javadsl.{ ReadinessCheckSetup => JReadinessCheckSetup } -import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup } +import pekko.management.javadsl.{ StartupCheckSetup => JStartupCheckSetup } +import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup, StartupCheckSetup } import pekko.util.FutureConverters._ import pekko.util.ccompat.JavaConverters._ @@ -47,6 +48,9 @@ final private[pekko] class HealthChecksImpl(system: ExtendedActorSystem, setting private val log = Logging.withMarker(system, classOf[HealthChecksImpl]) + log.info( + "Loading startup checks [{}]", + settings.startupChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", ")) log.info( "Loading readiness checks [{}]", settings.readinessChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", ")) @@ -54,6 +58,19 @@ final private[pekko] class HealthChecksImpl(system: ExtendedActorSystem, setting "Loading liveness checks [{}]", settings.livenessChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", ")) + private val startupChecks: immutable.Seq[HealthCheck] = { + val fromScaladslSetup = system.settings.setup.get[StartupCheckSetup] match { + case None => Nil + case Some(setup) => setup.createHealthChecks(system) + } + val fromJavadslSetup = system.settings.setup.get[JStartupCheckSetup] match { + case None => Nil + case Some(setup) => convertSuppliersToScala(setup.createHealthChecks(system)) + } + val fromConfig = load(settings.startupChecks) + fromConfig ++ fromScaladslSetup ++ fromJavadslSetup + } + private val readiness: immutable.Seq[HealthCheck] = { val fromScaladslSetup = system.settings.setup.get[ReadinessCheckSetup] match { case None => Nil @@ -138,6 +155,20 @@ final private[pekko] class HealthChecksImpl(system: ExtendedActorSystem, setting } } + def startupResult(): Future[Either[String, Unit]] = { + val result = check(startupChecks) + result.onComplete { + case Success(Right(())) => + case Success(Left(reason)) => + log.info(ManagementLogMarker.startupCheckFailed, reason) + case Failure(e) => + log.warning(ManagementLogMarker.startupCheckFailed, e.getMessage) + } + result + } + + def startup(): Future[Boolean] = startupResult().map(_.isRight) + def readyResult(): Future[Either[String, Unit]] = { val result = check(readiness) result.onComplete { diff --git a/management/src/main/scala/org/apache/pekko/management/javadsl/HealthChecks.scala b/management/src/main/scala/org/apache/pekko/management/javadsl/HealthChecks.scala index d3901301..f4c71fe8 100644 --- a/management/src/main/scala/org/apache/pekko/management/javadsl/HealthChecks.scala +++ b/management/src/main/scala/org/apache/pekko/management/javadsl/HealthChecks.scala @@ -37,6 +37,18 @@ final class HealthChecks(system: ExtendedActorSystem, settings: HealthCheckSetti private val delegate = new HealthChecksImpl(system, settings) + /** + * Returns CompletionStage(result), containing the system's startup result + */ + def startupResult(): CompletionStage[CheckResult] = + delegate.startupResult().map(new CheckResult(_))(system.dispatcher).asJava + + /** + * Returns CompletionStage(true) if the system has started + */ + def startup(): CompletionStage[java.lang.Boolean] = + startupResult().thenApply(((r: CheckResult) => r.isSuccess).asJava) + /** * Returns CompletionStage(result), containing the system's readiness result */ @@ -64,6 +76,23 @@ final class HealthChecks(system: ExtendedActorSystem, settings: HealthCheckSetti aliveResult().thenApply(((r: CheckResult) => r.isSuccess).asJava) } +object StartupCheckSetup { + + /** + * Programmatic definition of startup checks + */ + def create(createHealthChecks: JFunction[ActorSystem, JList[Supplier[CompletionStage[java.lang.Boolean]]]]) + : StartupCheckSetup = { + new StartupCheckSetup(createHealthChecks) + } +} + +/** + * Setup for startup checks, constructor is *Internal API*, use factories in [[StartupCheckSetup]] + */ +final class StartupCheckSetup private ( + val createHealthChecks: JFunction[ActorSystem, JList[Supplier[CompletionStage[java.lang.Boolean]]]]) extends Setup + object ReadinessCheckSetup { /** diff --git a/management/src/main/scala/org/apache/pekko/management/scaladsl/HealthChecks.scala b/management/src/main/scala/org/apache/pekko/management/scaladsl/HealthChecks.scala index aad4f34d..c92753e2 100644 --- a/management/src/main/scala/org/apache/pekko/management/scaladsl/HealthChecks.scala +++ b/management/src/main/scala/org/apache/pekko/management/scaladsl/HealthChecks.scala @@ -39,6 +39,16 @@ object HealthChecks { @DoNotInherit abstract class HealthChecks { + /** + * Returns Future(true) if the system has started + */ + def startup(): Future[Boolean] + + /** + * Returns Future(result) containing the system's startup result + */ + def startupResult(): Future[Either[String, Unit]] + /** * Returns Future(true) if the system is ready to receive user traffic */ @@ -62,6 +72,23 @@ abstract class HealthChecks { def aliveResult(): Future[Either[String, Unit]] } +object StartupCheckSetup { + + /** + * Programmatic definition of startup checks + */ + def apply(createHealthChecks: ActorSystem => immutable.Seq[HealthChecks.HealthCheck]): StartupCheckSetup = { + new StartupCheckSetup(createHealthChecks) + } + +} + +/** + * Setup for startup checks, constructor is *Internal API*, use factories in [[StartupCheckSetup]] + */ +final class StartupCheckSetup private ( + val createHealthChecks: ActorSystem => immutable.Seq[HealthChecks.HealthCheck]) extends Setup + object ReadinessCheckSetup { /** diff --git a/management/src/test/java/org/apache/pekko/management/HealthCheckTest.java b/management/src/test/java/org/apache/pekko/management/HealthCheckTest.java index ad05ff34..ca951848 100644 --- a/management/src/test/java/org/apache/pekko/management/HealthCheckTest.java +++ b/management/src/test/java/org/apache/pekko/management/HealthCheckTest.java @@ -30,6 +30,7 @@ import org.apache.pekko.management.javadsl.HealthChecks; import org.apache.pekko.management.javadsl.LivenessCheckSetup; import org.apache.pekko.management.javadsl.ReadinessCheckSetup; +import org.apache.pekko.management.javadsl.StartupCheckSetup; import org.apache.pekko.testkit.javadsl.TestKit; import org.junit.AfterClass; import org.junit.Assert; @@ -78,15 +79,47 @@ public void okReturnsTrue() throws Exception { new HealthChecks( system, HealthCheckSettings.create( - healthChecks, healthChecks, "ready", "alive", java.time.Duration.ofSeconds(1))); + healthChecks, + healthChecks, + healthChecks, + "startup", + "ready", + "alive", + java.time.Duration.ofSeconds(1))); + assertEquals(true, checks.startupResult().toCompletableFuture().get().isSuccess()); assertEquals(true, checks.aliveResult().toCompletableFuture().get().isSuccess()); assertEquals(true, checks.readyResult().toCompletableFuture().get().isSuccess()); + assertEquals(true, checks.startup().toCompletableFuture().get()); assertEquals(true, checks.alive().toCompletableFuture().get()); assertEquals(true, checks.ready().toCompletableFuture().get()); } @Test public void notOkayReturnsFalse() throws Exception { + List healthChecks = + Collections.singletonList( + new NamedHealthCheck("Ok", "org.apache.pekko.management.HealthCheckTest$Ok")); + HealthChecks checks = + new HealthChecks( + system, + HealthCheckSettings.create( + healthChecks, + healthChecks, + healthChecks, + "startup", + "ready", + "alive", + java.time.Duration.ofSeconds(1))); + assertEquals(true, checks.startupResult().toCompletableFuture().get().isSuccess()); + assertEquals(true, checks.aliveResult().toCompletableFuture().get().isSuccess()); + assertEquals(true, checks.readyResult().toCompletableFuture().get().isSuccess()); + assertEquals(true, checks.startup().toCompletableFuture().get()); + assertEquals(true, checks.alive().toCompletableFuture().get()); + assertEquals(true, checks.ready().toCompletableFuture().get()); + } + + @Test + public void creatableThroughLegacyConstructor() throws Exception { List healthChecks = Collections.singletonList( new NamedHealthCheck("Ok", "org.apache.pekko.management.HealthCheckTest$Ok")); @@ -95,8 +128,10 @@ public void notOkayReturnsFalse() throws Exception { system, HealthCheckSettings.create( healthChecks, healthChecks, "ready", "alive", java.time.Duration.ofSeconds(1))); + assertEquals(true, checks.startupResult().toCompletableFuture().get().isSuccess()); assertEquals(true, checks.aliveResult().toCompletableFuture().get().isSuccess()); assertEquals(true, checks.readyResult().toCompletableFuture().get().isSuccess()); + assertEquals(true, checks.startup().toCompletableFuture().get()); assertEquals(true, checks.alive().toCompletableFuture().get()); assertEquals(true, checks.ready().toCompletableFuture().get()); } @@ -110,7 +145,13 @@ public void throwsReturnsFailed() throws Exception { new HealthChecks( system, HealthCheckSettings.create( - healthChecks, healthChecks, "ready", "alive", java.time.Duration.ofSeconds(1))); + healthChecks, + healthChecks, + healthChecks, + "startup", + "ready", + "alive", + java.time.Duration.ofSeconds(1))); try { checks.alive().toCompletableFuture().get(); Assert.fail("Expected exception"); @@ -121,6 +162,8 @@ public void throwsReturnsFailed() throws Exception { @Test public void defineViaActorSystemSetup() throws Exception { + StartupCheckSetup startupCheckSetup = + StartupCheckSetup.create(system -> Collections.singletonList(new NotOk(system))); ReadinessCheckSetup readinessSetup = ReadinessCheckSetup.create(system -> Arrays.asList(new Ok(), new NotOk(system))); LivenessCheckSetup livenessSetup = @@ -128,7 +171,7 @@ public void defineViaActorSystemSetup() throws Exception { // bootstrapSetup is needed for config (otherwise default config) BootstrapSetup bootstrapSetup = BootstrapSetup.create(ConfigFactory.parseString("some=thing")); ActorSystemSetup actorSystemSetup = - ActorSystemSetup.create(bootstrapSetup, readinessSetup, livenessSetup); + ActorSystemSetup.create(bootstrapSetup, startupCheckSetup, readinessSetup, livenessSetup); ExtendedActorSystem sys2 = (ExtendedActorSystem) ActorSystem.create("HealthCheckTest2", actorSystemSetup); try { @@ -138,11 +181,15 @@ public void defineViaActorSystemSetup() throws Exception { HealthCheckSettings.create( Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), + "startup", "ready", "alive", java.time.Duration.ofSeconds(1))); + assertEquals(false, checks.startupResult().toCompletableFuture().get().isSuccess()); assertEquals(false, checks.aliveResult().toCompletableFuture().get().isSuccess()); assertEquals(false, checks.readyResult().toCompletableFuture().get().isSuccess()); + assertEquals(false, checks.startup().toCompletableFuture().get()); assertEquals(false, checks.alive().toCompletableFuture().get()); assertEquals(false, checks.ready().toCompletableFuture().get()); } finally { diff --git a/management/src/test/scala/org/apache/pekko/management/HealthCheckRoutesSpec.scala b/management/src/test/scala/org/apache/pekko/management/HealthCheckRoutesSpec.scala index 942b4406..de08ce17 100644 --- a/management/src/test/scala/org/apache/pekko/management/HealthCheckRoutesSpec.scala +++ b/management/src/test/scala/org/apache/pekko/management/HealthCheckRoutesSpec.scala @@ -29,10 +29,13 @@ class HealthCheckRoutesSpec extends AnyWordSpec with Matchers with ScalatestRout private val eas = system.asInstanceOf[ExtendedActorSystem] private def testRoute( + startupResultValue: Future[Either[String, Unit]] = Future.successful(Right(())), readyResultValue: Future[Either[String, Unit]] = Future.successful(Right(())), aliveResultValue: Future[Either[String, Unit]] = Future.successful(Right(()))): Route = { new HealthCheckRoutes(eas) { override protected val healthChecks: HealthChecks = new HealthChecks { + override def startupResult(): Future[Either[String, Unit]] = startupResultValue + override def startup(): Future[Boolean] = startupResultValue.map(_.isRight) override def readyResult(): Future[Either[String, Unit]] = readyResultValue override def ready(): Future[Boolean] = readyResultValue.map(_.isRight) override def aliveResult(): Future[Either[String, Unit]] = aliveResultValue @@ -41,6 +44,7 @@ class HealthCheckRoutesSpec extends AnyWordSpec with Matchers with ScalatestRout }.routes(ManagementRouteProviderSettings(Uri("http://whocares"), readOnly = false)) } + tests("/startup", result => testRoute(startupResultValue = result)) tests("/ready", result => testRoute(readyResultValue = result)) tests("/alive", result => testRoute(aliveResultValue = result)) diff --git a/management/src/test/scala/org/apache/pekko/management/HealthCheckSettingsSpec.scala b/management/src/test/scala/org/apache/pekko/management/HealthCheckSettingsSpec.scala index 894c8898..27851153 100644 --- a/management/src/test/scala/org/apache/pekko/management/HealthCheckSettingsSpec.scala +++ b/management/src/test/scala/org/apache/pekko/management/HealthCheckSettingsSpec.scala @@ -13,32 +13,83 @@ package org.apache.pekko.management +import scala.annotation.nowarn +import scala.concurrent.duration.DurationInt + import com.typesafe.config.ConfigFactory import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +@nowarn("msg=deprecated") class HealthCheckSettingsSpec extends AnyWordSpec with Matchers { "Health Check Settings" should { "filter out blank fqcn" in { HealthCheckSettings(ConfigFactory.parseString(""" + startup-checks { + cluster-membership = "" + } + readiness-checks {} + liveness-checks {} + startup-path = "" + readiness-path = "" + liveness-path = "" + check-timeout = 1s + """)).startupChecks shouldEqual Nil + HealthCheckSettings(ConfigFactory.parseString(""" + startup-checks {} readiness-checks { cluster-membership = "" } liveness-checks {} + startup-path = "" readiness-path = "" liveness-path = "" check-timeout = 1s """)).readinessChecks shouldEqual Nil HealthCheckSettings(ConfigFactory.parseString(""" + startup-checks {} liveness-checks { cluster-membership = "" } readiness-checks {} + startup-path = "" readiness-path = "" liveness-path = "" check-timeout = 1s - """)).readinessChecks shouldEqual Nil + """)).livenessChecks shouldEqual Nil + } + + "be creatable with primary constructor" in { + HealthCheckSettings.create( + startupChecks = java.util.Collections.emptyList(), + readinessChecks = java.util.Collections.emptyList(), + livenessChecks = java.util.Collections.emptyList(), + startupPath = "", + readinessPath = "", + livenessPath = "", + checkDuration = java.time.Duration.ofSeconds(1L)) + .startupChecks shouldEqual Nil + } + + "be creatable with legacy create method" in { + HealthCheckSettings.create( + readinessChecks = java.util.Collections.emptyList(), + livenessChecks = java.util.Collections.emptyList(), + readinessPath = "", + livenessPath = "", + checkDuration = java.time.Duration.ofSeconds(1L)) + .startupChecks shouldEqual Nil + } + + "be creatable with legacy constructor" in { + val healthCheckSettings = new HealthCheckSettings( + readinessChecks = Nil, + livenessChecks = Nil, + readinessPath = "", + livenessPath = "", + checkTimeout = 1.seconds) + healthCheckSettings.livenessChecks shouldEqual Nil } } diff --git a/management/src/test/scala/org/apache/pekko/management/HealthChecksSpec.scala b/management/src/test/scala/org/apache/pekko/management/HealthChecksSpec.scala index b901d40f..40e0d410 100644 --- a/management/src/test/scala/org/apache/pekko/management/HealthChecksSpec.scala +++ b/management/src/test/scala/org/apache/pekko/management/HealthChecksSpec.scala @@ -19,7 +19,7 @@ import pekko.actor.setup.ActorSystemSetup import pekko.actor.{ ActorSystem, BootstrapSetup, ExtendedActorSystem } import pekko.management.HealthChecksSpec.{ ctxException, failedCause } import pekko.management.internal.{ CheckFailedException, CheckTimeoutException } -import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup } +import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup, StartupCheckSetup } import pekko.testkit.TestKit import org.scalatest.BeforeAndAfterAll import org.scalatest.concurrent.ScalaFutures @@ -114,14 +114,17 @@ class HealthChecksSpec val DoesNotExist = NamedHealthCheck("DoesNotExist", "org.apache.pekko.management.DoesNotExist") val CtrExceptionCheck = NamedHealthCheck("CtrExceptionCheck", "org.apache.pekko.management.CtrException") - def settings(readiness: im.Seq[NamedHealthCheck], liveness: im.Seq[NamedHealthCheck]) = - new HealthCheckSettings(readiness, liveness, "ready", "alive", 500.millis) + def settings(startup: im.Seq[NamedHealthCheck], readiness: im.Seq[NamedHealthCheck], + liveness: im.Seq[NamedHealthCheck]) = + new HealthCheckSettings(startup, readiness, liveness, "startup", "ready", "alive", 500.millis) "HealthCheck" should { "succeed by default" in { - val checks = HealthChecks(eas, settings(Nil, Nil)) + val checks = HealthChecks(eas, settings(Nil, Nil, Nil)) + checks.startupResult().futureValue shouldEqual Right(()) checks.aliveResult().futureValue shouldEqual Right(()) checks.readyResult().futureValue shouldEqual Right(()) + checks.startup().futureValue shouldEqual true checks.alive().futureValue shouldEqual true checks.ready().futureValue shouldEqual true } @@ -129,10 +132,13 @@ class HealthChecksSpec val checks = HealthChecks( eas, settings( + im.Seq(OkCheck), im.Seq(OkCheck), im.Seq(OkCheck))) + checks.startupResult().futureValue shouldEqual Right(()) checks.aliveResult().futureValue shouldEqual Right(()) checks.readyResult().futureValue shouldEqual Right(()) + checks.startup().futureValue shouldEqual true checks.alive().futureValue shouldEqual true checks.ready().futureValue shouldEqual true } @@ -140,10 +146,13 @@ class HealthChecksSpec val checks = HealthChecks( eas, settings( + im.Seq(NoArgsCtrCheck), im.Seq(NoArgsCtrCheck), im.Seq(NoArgsCtrCheck))) + checks.startupResult().futureValue shouldEqual Right(()) checks.aliveResult().futureValue shouldEqual Right(()) checks.readyResult().futureValue shouldEqual Right(()) + checks.startup().futureValue shouldEqual true checks.alive().futureValue shouldEqual true checks.ready().futureValue shouldEqual true } @@ -151,10 +160,13 @@ class HealthChecksSpec val checks = HealthChecks( eas, settings( + im.Seq(FalseCheck), im.Seq(FalseCheck), im.Seq(FalseCheck))) + checks.startupResult().futureValue.isRight shouldEqual false checks.readyResult().futureValue.isRight shouldEqual false checks.aliveResult().futureValue.isRight shouldEqual false + checks.startup().futureValue shouldEqual false checks.ready().futureValue shouldEqual false checks.alive().futureValue shouldEqual false } @@ -162,12 +174,17 @@ class HealthChecksSpec val checks = HealthChecks( eas, settings( + im.Seq(ThrowsCheck), im.Seq(ThrowsCheck), im.Seq(ThrowsCheck))) + checks.startupResult().failed.futureValue shouldEqual + CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) checks.readyResult().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) checks.aliveResult().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) + checks.startup().failed.futureValue shouldEqual + CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) checks.ready().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) checks.alive().failed.futureValue shouldEqual @@ -178,11 +195,15 @@ class HealthChecksSpec OkCheck, ThrowsCheck, FalseCheck) - val hc = HealthChecks(eas, settings(checks, checks)) + val hc = HealthChecks(eas, settings(checks, checks, checks)) + hc.startupResult().failed.futureValue shouldEqual + CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) hc.readyResult().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) hc.aliveResult().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) + hc.startup().failed.futureValue shouldEqual + CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) hc.ready().failed.futureValue shouldEqual CheckFailedException("Check [org.apache.pekko.management.Throws] failed: null", failedCause) hc.alive().failed.futureValue shouldEqual @@ -191,9 +212,11 @@ class HealthChecksSpec "return failure if check throws" in { val checks = im.Seq( NaughtyCheck) - val hc = HealthChecks(eas, settings(checks, checks)) + val hc = HealthChecks(eas, settings(checks, checks, checks)) + hc.startupResult().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" hc.readyResult().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" hc.aliveResult().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" + hc.startup().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" hc.ready().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" hc.alive().failed.futureValue.getMessage shouldEqual "Check [org.apache.pekko.management.Naughty] failed: bad" } @@ -201,11 +224,15 @@ class HealthChecksSpec val checks = im.Seq( SlowCheck, OkCheck) - val hc = HealthChecks(eas, settings(checks, checks)) + val hc = HealthChecks(eas, settings(checks, checks, checks)) + Await.result(hc.startupResult().failed, 1.second) shouldEqual CheckTimeoutException( + "Check [org.apache.pekko.management.Slow] timed out after 500 milliseconds") Await.result(hc.readyResult().failed, 1.second) shouldEqual CheckTimeoutException( "Check [org.apache.pekko.management.Slow] timed out after 500 milliseconds") Await.result(hc.aliveResult().failed, 1.second) shouldEqual CheckTimeoutException( "Check [org.apache.pekko.management.Slow] timed out after 500 milliseconds") + Await.result(hc.startup().failed, 1.second) shouldEqual CheckTimeoutException( + "Check [org.apache.pekko.management.Slow] timed out after 500 milliseconds") Await.result(hc.ready().failed, 1.second) shouldEqual CheckTimeoutException( "Check [org.apache.pekko.management.Slow] timed out after 500 milliseconds") Await.result(hc.alive().failed, 1.second) shouldEqual CheckTimeoutException( @@ -214,43 +241,46 @@ class HealthChecksSpec "provide useful error if user's ctr is invalid" in { intercept[InvalidHealthCheckException] { val checks = im.Seq(InvalidCtrCheck) - HealthChecks(eas, settings(checks, checks)) + HealthChecks(eas, settings(checks, checks, checks)) }.getMessage shouldEqual "Health checks: [NamedHealthCheck(InvalidCtr,org.apache.pekko.management.InvalidCtr)] must have a no args constructor or a single argument constructor that takes an ActorSystem" } "provide useful error if invalid type" in { intercept[InvalidHealthCheckException] { val checks = im.Seq(WrongTypeCheck) - HealthChecks(eas, settings(checks, checks)) + HealthChecks(eas, settings(checks, checks, checks)) }.getMessage shouldEqual "Health checks: [NamedHealthCheck(WrongType,org.apache.pekko.management.WrongType)] must have type: () => Future[Boolean]" } "provide useful error if class not found" in { intercept[InvalidHealthCheckException] { val checks = im.Seq(DoesNotExist, OkCheck) - HealthChecks(eas, settings(checks, checks)) + HealthChecks(eas, settings(checks, checks, checks)) }.getMessage shouldEqual "Health check: [org.apache.pekko.management.DoesNotExist] not found" } "provide useful error if class ctr throws" in { intercept[InvalidHealthCheckException] { val checks = im.Seq(OkCheck, CtrExceptionCheck) - HealthChecks(eas, settings(checks, checks)) + HealthChecks(eas, settings(checks, checks, checks)) }.getCause shouldEqual ctxException } "be possible to define via ActorSystem Setup" in { + val startupSetup = StartupCheckSetup(system => List(new Ok(system), new False(system))) val readinessSetup = ReadinessCheckSetup(system => List(new Ok(system), new False(system))) val livenessSetup = LivenessCheckSetup(system => List(new False(system))) // bootstrapSetup is needed for config (otherwise default config) val bootstrapSetup = BootstrapSetup(ConfigFactory.parseString("some=thing")) - val actorSystemSetup = ActorSystemSetup(bootstrapSetup, readinessSetup, livenessSetup) + val actorSystemSetup = ActorSystemSetup(bootstrapSetup, startupSetup, readinessSetup, livenessSetup) val sys2 = ActorSystem("HealthCheckSpec2", actorSystemSetup).asInstanceOf[ExtendedActorSystem] try { val checks = HealthChecks( sys2, - settings(Nil, Nil) // no checks from config + settings(Nil, Nil, Nil) // no checks from config ) + checks.startupResult().futureValue shouldEqual Left("Check [org.apache.pekko.management.False] not ok") checks.aliveResult().futureValue shouldEqual Left("Check [org.apache.pekko.management.False] not ok") checks.readyResult().futureValue shouldEqual Left("Check [org.apache.pekko.management.False] not ok") + checks.startup().futureValue shouldEqual false checks.alive().futureValue shouldEqual false checks.ready().futureValue shouldEqual false } finally {