Skip to content

Commit 7113b73

Browse files
authored
Controller endpoint for checking the controller readiness (#4858)
* Provide readiness endpoint for controller * Fix test fmt * Controller route tests * Separate unit and integration tests * Scala fmt * Adding PR suggested changes
1 parent 1445b6e commit 7113b73

File tree

4 files changed

+132
-10
lines changed

4 files changed

+132
-10
lines changed

core/controller/src/main/resources/reference.conf

+1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ whisk {
3333
controller {
3434
protocol: http
3535
interface: "0.0.0.0"
36+
readiness-fraction: 100%
3637
}
3738
}

core/controller/src/main/scala/org/apache/openwhisk/core/controller/Controller.scala

+21-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import akka.Done
2121
import akka.actor.{ActorSystem, CoordinatedShutdown}
2222
import akka.event.Logging.InfoLevel
2323
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
24+
import akka.http.scaladsl.model.StatusCodes._
2425
import akka.http.scaladsl.model.Uri
2526
import akka.http.scaladsl.server.Route
2627
import akka.stream.ActorMaterializer
@@ -131,12 +132,14 @@ class Controller(val instance: ControllerInstanceId,
131132
private val swagger = new SwaggerDocs(Uri.Path.Empty, "infoswagger.json")
132133

133134
/**
134-
* Handles GET /invokers
135-
* /invokers/healthy/count
135+
* Handles GET /invokers - list of invokers
136+
* /invokers/healthy/count - nr of healthy invokers
137+
* /invokers/ready - 200 in case # of healthy invokers are above the expected value
138+
* - 500 in case # of healthy invokers are bellow the expected value
136139
*
137140
* @return JSON with details of invoker health or count of healthy invokers respectively.
138141
*/
139-
private val internalInvokerHealth = {
142+
protected[controller] val internalInvokerHealth = {
140143
implicit val executionContext = actorSystem.dispatcher
141144
(pathPrefix("invokers") & get) {
142145
pathEndOrSingleSlash {
@@ -151,6 +154,16 @@ class Controller(val instance: ControllerInstanceId,
151154
.invokerHealth()
152155
.map(_.count(_.status == InvokerState.Healthy).toJson)
153156
}
157+
} ~ path("ready") {
158+
onSuccess(loadBalancer.invokerHealth()) { invokersHealth =>
159+
val all = invokersHealth.size
160+
val healthy = invokersHealth.count(_.status == InvokerState.Healthy)
161+
val ready = Controller.readyState(all, healthy, Controller.readinessThreshold.getOrElse(1))
162+
if (ready)
163+
complete(JsObject("healthy" -> s"$healthy/$all".toJson))
164+
else
165+
complete(InternalServerError -> JsObject("unhealthy" -> s"${all - healthy}/$all".toJson))
166+
}
154167
}
155168
}
156169
}
@@ -172,6 +185,7 @@ object Controller {
172185

173186
protected val protocol = loadConfigOrThrow[String]("whisk.controller.protocol")
174187
protected val interface = loadConfigOrThrow[String]("whisk.controller.interface")
188+
protected val readinessThreshold = loadConfig[Double]("whisk.controller.readiness-fraction")
175189

176190
// requiredProperties is a Map whose keys define properties that must be bound to
177191
// a value, and whose values are default values. A null value in the Map means there is
@@ -207,6 +221,10 @@ object Controller {
207221
"max_action_logs" -> logLimit.max.toBytes.toJson),
208222
"runtimes" -> runtimes.toJson)
209223

224+
def readyState(allInvokers: Int, healthyInvokers: Int, readinessThreshold: Double): Boolean = {
225+
if (allInvokers > 0) (healthyInvokers / allInvokers) >= readinessThreshold else false
226+
}
227+
210228
def main(args: Array[String]): Unit = {
211229
implicit val actorSystem = ActorSystem("controller-actor-system")
212230
implicit val logger = new AkkaLogging(akka.event.Logging.getLogger(actorSystem, this))

tests/src/test/scala/org/apache/openwhisk/core/controller/test/ControllerApiTests.scala

+29-7
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717

1818
package org.apache.openwhisk.core.controller.test
1919

20-
import org.junit.runner.RunWith
21-
import org.scalatest.FlatSpec
22-
import org.scalatest.Matchers
23-
import org.scalatest.junit.JUnitRunner
24-
import io.restassured.RestAssured
2520
import common.StreamLogging
26-
import spray.json._
27-
import spray.json.DefaultJsonProtocol._
21+
import io.restassured.RestAssured
2822
import org.apache.openwhisk.core.WhiskConfig
2923
import org.apache.openwhisk.core.entity.{ExecManifest, LogLimit, MemoryLimit, TimeLimit}
24+
import org.junit.runner.RunWith
25+
import org.scalatest.{FlatSpec, Matchers}
26+
import org.scalatest.junit.JUnitRunner
27+
import spray.json.DefaultJsonProtocol._
28+
import spray.json._
3029
import system.rest.RestUtil
3130

3231
/**
@@ -68,4 +67,27 @@ class ControllerApiTests extends FlatSpec with RestUtil with Matchers with Strea
6867
response.body.asString.parseJson shouldBe (expectedJson)
6968
}
7069

70+
behavior of "Controller"
71+
72+
it should "return list of invokers" in {
73+
val response = RestAssured.given.config(sslconfig).get(s"$getServiceURL/invokers")
74+
75+
response.statusCode shouldBe 200
76+
response.body.asString shouldBe "{\"invoker0/0\":\"up\",\"invoker1/1\":\"up\"}"
77+
}
78+
79+
it should "return healthy invokers status" in {
80+
val response = RestAssured.given.config(sslconfig).get(s"$getServiceURL/invokers/healthy/count")
81+
82+
response.statusCode shouldBe 200
83+
response.body.asString shouldBe "2"
84+
}
85+
86+
it should "return healthy invokers" in {
87+
val response = RestAssured.given.config(sslconfig).get(s"$getServiceURL/invokers/ready")
88+
89+
response.statusCode shouldBe 200
90+
response.body.asString shouldBe "{\"healthy\":\"2/2\"}"
91+
}
92+
7193
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.openwhisk.core.controller.test
19+
20+
import akka.http.scaladsl.model.StatusCodes._
21+
import akka.http.scaladsl.server.Route
22+
import org.apache.openwhisk.common.AkkaLogging
23+
import org.apache.openwhisk.core.controller.Controller
24+
import org.apache.openwhisk.core.entity.ExecManifest.Runtimes
25+
import org.junit.runner.RunWith
26+
import org.scalatest.BeforeAndAfterEach
27+
import org.scalatest.junit.JUnitRunner
28+
import system.rest.RestUtil
29+
import spray.json._
30+
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
31+
import spray.json.DefaultJsonProtocol._
32+
33+
/**
34+
* Tests controller readiness.
35+
*
36+
* These tests will validate the endpoints that provide information on how many healthy invokers are available
37+
*/
38+
39+
@RunWith(classOf[JUnitRunner])
40+
class ControllerRoutesTests extends ControllerTestCommon with BeforeAndAfterEach with RestUtil {
41+
42+
implicit val logger = new AkkaLogging(akka.event.Logging.getLogger(actorSystem, this))
43+
44+
behavior of "Controller"
45+
46+
it should "return unhealthy invokers status" in {
47+
48+
configureBuildInfo()
49+
50+
val controller =
51+
new Controller(instance, Runtimes(Set.empty, Set.empty, None), whiskConfig, system, materializer, logger)
52+
Get("/invokers/ready") ~> Route.seal(controller.internalInvokerHealth) ~> check {
53+
status shouldBe InternalServerError
54+
responseAs[JsObject].fields("unhealthy") shouldBe JsString("0/0")
55+
}
56+
}
57+
58+
it should "return ready state true when healthy == total invokers" in {
59+
60+
val res = Controller.readyState(5, 5, 1.0)
61+
res shouldBe true
62+
}
63+
64+
it should "return ready state false when 0 invokers" in {
65+
66+
val res = Controller.readyState(0, 0, 0.5)
67+
res shouldBe false
68+
}
69+
70+
it should "return ready state false when threshold < (healthy / total)" in {
71+
72+
val res = Controller.readyState(7, 3, 0.5)
73+
res shouldBe false
74+
}
75+
76+
private def configureBuildInfo(): Unit = {
77+
System.setProperty("whisk.info.build-no", "")
78+
System.setProperty("whisk.info.date", "")
79+
}
80+
81+
}

0 commit comments

Comments
 (0)