This is an automated email from the ASF dual-hosted git repository.

fanningpj pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-management.git


The following commit(s) were added to refs/heads/main by this push:
     new 2caae136 Add startup checks (#293)
2caae136 is described below

commit 2caae136191c6c82ce1bbd555bbf8730bb7648be
Author: Philippus Baalman <[email protected]>
AuthorDate: Wed Sep 25 11:15:44 2024 +0200

    Add startup checks (#293)
---
 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 ++
 .../pekko/management/HealthCheckSettings.scala     | 61 ++++++++++++++++++++++
 .../pekko/management/ManagementLogMarker.scala     |  6 +++
 .../management/internal/HealthChecksImpl.scala     | 33 +++++++++++-
 .../pekko/management/javadsl/HealthChecks.scala    | 29 ++++++++++
 .../pekko/management/scaladsl/HealthChecks.scala   | 27 ++++++++++
 .../apache/pekko/management/HealthCheckTest.java   | 53 +++++++++++++++++--
 .../pekko/management/HealthCheckRoutesSpec.scala   |  4 ++
 .../pekko/management/HealthCheckSettingsSpec.scala | 53 ++++++++++++++++++-
 .../apache/pekko/management/HealthChecksSpec.scala | 56 +++++++++++++++-----
 15 files changed, 350 insertions(+), 23 deletions(-)

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 = 
<fully qualified class
 Health checks can be hosted via the Pekko management HTTP server. The 
`pekko.management.HealthCheckRoutes` is enabled
 by default as a Pekko management route provider.
 
-By default all readiness checks are hosted on `/ready` and liveness checks are 
hosted on `/alive`. If all of the checks
+By default all startup checks are hosted on `/startup`, readiness checks are 
hosted on `/ready` and liveness checks are hosted on `/alive`. If all of the 
checks
 for an endpoint succeed a `200` is returned, if any fail or return `false` a 
`500` is returned. The paths are
-configurable via `pekko.management.health-checks.readiness-path` and 
`pekko.management.health-checks.liveness-path` e.g.
+configurable via `pekko.management.health-checks.startup-path`, 
`pekko.management.health-checks.readiness-path` and 
`pekko.management.health-checks.liveness-path` e.g.
 
 @@snip 
[application.conf](/integration-test/local/src/main/resources/application.conf) 
 { #health }
 
 The `org.apache.pekko.management.HealthCheckRoutes` can be disabled with the 
following configuration but that also
-means that the configured `readiness-checks` and `liveness-checks` will not be 
used.
+means that the configured `startup-checks`, `readiness-checks` and 
`liveness-checks` will not be used.
 
 ```
 pekko.management.http.routes {
diff --git a/integration-test/local/src/main/resources/application.conf 
b/integration-test/local/src/main/resources/application.conf
index 5d040acd..fbcba00c 100644
--- a/integration-test/local/src/main/resources/application.conf
+++ b/integration-test/local/src/main/resources/application.conf
@@ -48,6 +48,7 @@ pekko.discovery {
 
 #health
 pekko.management.health-checks {
+  startup-path = "health/startup"
   readiness-path = "health/ready"
   liveness-path = "health/alive"
 }
diff --git 
a/integration-test/local/src/test/scala/org/apache/pekko/management/LocalBootstrapTest.scala
 
b/integration-test/local/src/test/scala/org/apache/pekko/management/LocalBootstrapTest.scala
index 08fb3e7a..069d01d7 100644
--- 
a/integration-test/local/src/test/scala/org/apache/pekko/management/LocalBootstrapTest.scala
+++ 
b/integration-test/local/src/test/scala/org/apache/pekko/management/LocalBootstrapTest.scala
@@ -95,6 +95,8 @@ class LocalBootstrapTest extends AnyWordSpec with 
ScalaFutures with Matchers wit
     super.afterAll()
   }
 
+  def startupStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
+    healthCheckStatus(port, "health/startup")
   def readyStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
     healthCheckStatus(port, "health/ready")
   def aliveStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
@@ -112,6 +114,14 @@ class LocalBootstrapTest extends AnyWordSpec with 
ScalaFutures with Matchers wit
     // for http client
     implicit val system: ActorSystem = systems(0)
 
+    "not started up initially" in {
+      eventually {
+        managementPorts.foreach { port =>
+          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.actor.setup.ActorSystemSetup;
 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 class HealthCheckTest extends JUnitSuite {
         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<NamedHealthCheck> 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<NamedHealthCheck> healthChecks =
         Collections.singletonList(
             new NamedHealthCheck("Ok", 
"org.apache.pekko.management.HealthCheckTest$Ok"));
@@ -95,8 +128,10 @@ public class HealthCheckTest extends JUnitSuite {
             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 class HealthCheckTest extends JUnitSuite {
         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 class HealthCheckTest extends JUnitSuite {
 
   @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 class HealthCheckTest extends JUnitSuite {
     // 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 class HealthCheckTest extends JUnitSuite {
               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 {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to