Skip to content

Commit 79a21a7

Browse files
committed
Pass example builder instance to /version /health handlers, /health handler is customisable
1 parent 9066873 commit 79a21a7

File tree

3 files changed

+85
-57
lines changed

3 files changed

+85
-57
lines changed

binderhub/app.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from .builder import BuildHandler
4747
from .config import ConfigHandler
4848
from .events import EventLog
49-
from .health import HealthHandler
49+
from .health import HealthHandler, KubernetesHealthHandler
5050
from .launcher import Launcher
5151
from .log import log_request
5252
from .main import LegacyRedirectHandler, MainHandler, ParameterizedMainHandler
@@ -304,6 +304,18 @@ def _valid_badge_base_url(self, proposal):
304304
config=True,
305305
)
306306

307+
health_handler_class = Type(
308+
HealthHandler,
309+
help="The Tornado /health handler class",
310+
config=True,
311+
)
312+
313+
@default("health_handler_class")
314+
def _default_health_handler_class(self):
315+
if issubclass(self.build_class, KubernetesBuildExecutor):
316+
return KubernetesHealthHandler
317+
return HealthHandler
318+
307319
per_repo_quota = Integer(
308320
0,
309321
help="""
@@ -869,9 +881,9 @@ def initialize(self, *args, **kwargs):
869881

870882
launch_quota = self.launch_quota_class(parent=self, executor=self.executor)
871883

872-
# Construct a Builder so that we can extract the version string to pass to the
873-
# /version handler
874-
temporary_builder = self.build_class(parent=self)
884+
# Construct a Builder so that we can extract parameters such as the
885+
# configuration or the version string to pass to /version and /health handlers
886+
example_builder = self.build_class(parent=self)
875887
self.tornado_settings.update(
876888
{
877889
"log_function": log_request,
@@ -884,7 +896,7 @@ def initialize(self, *args, **kwargs):
884896
"build_token_check_origin": self.build_token_check_origin,
885897
"build_token_secret": self.build_token_secret,
886898
"build_token_expires_seconds": self.build_token_expires_seconds,
887-
"builder_identifier": temporary_builder.identifier,
899+
"example_builder": example_builder,
888900
"pod_quota": self.pod_quota,
889901
"per_repo_quota": self.per_repo_quota,
890902
"per_repo_quota_higher": self.per_repo_quota_higher,
@@ -969,7 +981,7 @@ def initialize(self, *args, **kwargs):
969981
{"path": os.path.join(self.tornado_settings["static_path"], "images")},
970982
),
971983
(r"/about", AboutHandler),
972-
(r"/health", HealthHandler, {"hub_url": self.hub_url_local}),
984+
(r"/health", self.health_handler_class, {"hub_url": self.hub_url_local}),
973985
(r"/_config", ConfigHandler),
974986
(r"/", MainHandler),
975987
(r".*", Custom404),

binderhub/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ async def get(self):
247247
self.write(
248248
json.dumps(
249249
{
250-
"builder": self.settings["builder_identifier"],
250+
"builder": self.settings["example_builder"].identifier,
251251
"binderhub": binder_version,
252252
}
253253
)

binderhub/health.py

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -104,34 +104,7 @@ class HealthHandler(BaseHandler):
104104

105105
def initialize(self, hub_url=None):
106106
self.hub_url = hub_url
107-
108-
@at_most_every
109-
async def _get_pods(self):
110-
"""Get information about build and user pods"""
111-
namespace = self.settings["build_namespace"]
112-
k8s = self.settings["kubernetes_client"]
113-
pool = self.settings["executor"]
114-
115-
app_log.info(f"Getting pod statistics for {namespace}")
116-
117-
label_selectors = [
118-
"app=jupyterhub,component=singleuser-server",
119-
"component=binderhub-build",
120-
]
121-
requests = [
122-
asyncio.wrap_future(
123-
pool.submit(
124-
k8s.list_namespaced_pod,
125-
namespace,
126-
label_selector=label_selector,
127-
_preload_content=False,
128-
_request_timeout=KUBE_REQUEST_TIMEOUT,
129-
)
130-
)
131-
for label_selector in label_selectors
132-
]
133-
responses = await asyncio.gather(*requests)
134-
return [json.loads(resp.read())["items"] for resp in responses]
107+
self.ignored_checks = set()
135108

136109
@false_if_raises
137110
@retry
@@ -155,23 +128,9 @@ async def check_docker_registry(self):
155128
)
156129
return True
157130

158-
async def check_pod_quota(self):
159-
"""Compare number of active pods to available quota"""
160-
user_pods, build_pods = await self._get_pods()
161-
162-
n_user_pods = len(user_pods)
163-
n_build_pods = len(build_pods)
164-
165-
quota = self.settings["pod_quota"]
166-
total_pods = n_user_pods + n_build_pods
167-
usage = {
168-
"total_pods": total_pods,
169-
"build_pods": n_build_pods,
170-
"user_pods": n_user_pods,
171-
"quota": quota,
172-
"ok": total_pods <= quota if quota is not None else True,
173-
}
174-
return usage
131+
async def check_quotas(self):
132+
"""Check whether any quotas are exceeded"""
133+
return {"ok": True}
175134

176135
async def check_all(self):
177136
"""Runs all health checks and returns a tuple (overall, checks).
@@ -189,19 +148,20 @@ async def check_all(self):
189148
check_futures.append(self.check_jupyterhub_api(self.hub_url))
190149
checks.append({"service": "JupyterHub API", "ok": False})
191150

192-
check_futures.append(self.check_pod_quota())
193-
checks.append({"service": "Pod quota", "ok": False})
151+
check_futures.append(self.check_quotas())
152+
checks.append({"service": "Quotas", "ok": False})
194153

195154
for result, check in zip(await asyncio.gather(*check_futures), checks):
196155
if isinstance(result, bool):
197156
check["ok"] = result
198157
else:
199158
check.update(result)
200159

201-
# The pod quota is treated as a soft quota this means being above
202-
# quota doesn't mean the service is unhealthy
160+
# Some checks are for information but do not count as a health failure
203161
overall = all(
204-
check["ok"] for check in checks if check["service"] != "Pod quota"
162+
check["ok"]
163+
for check in checks
164+
if check["service"] not in self.ignored_checks
205165
)
206166
if not overall:
207167
unhealthy = [check for check in checks if not check["ok"]]
@@ -218,3 +178,59 @@ async def head(self):
218178
overall, checks = await self.check_all()
219179
if not overall:
220180
self.set_status(503)
181+
182+
183+
class KubernetesHealthHandler(HealthHandler):
184+
"""Serve health status on Kubernetes"""
185+
186+
def initialize(self, **args):
187+
super().initialize(**args)
188+
# The pod quota is treated as a soft quota
189+
# Being above quota doesn't mean the service is unhealthy
190+
self.ignored_checks.add("Quotas")
191+
192+
@at_most_every
193+
async def _get_pods(self):
194+
"""Get information about build and user pods"""
195+
namespace = self.settings["example_builder"].namespace
196+
k8s = self.settings["example_builder"].api
197+
pool = self.settings["executor"]
198+
199+
app_log.info(f"Getting pod statistics for {namespace}")
200+
201+
label_selectors = [
202+
"app=jupyterhub,component=singleuser-server",
203+
"component=binderhub-build",
204+
]
205+
requests = [
206+
asyncio.wrap_future(
207+
pool.submit(
208+
k8s.list_namespaced_pod,
209+
namespace,
210+
label_selector=label_selector,
211+
_preload_content=False,
212+
_request_timeout=KUBE_REQUEST_TIMEOUT,
213+
)
214+
)
215+
for label_selector in label_selectors
216+
]
217+
responses = await asyncio.gather(*requests)
218+
return [json.loads(resp.read())["items"] for resp in responses]
219+
220+
async def check_quotas(self):
221+
"""Compare number of active pods to available quota"""
222+
user_pods, build_pods = await self._get_pods()
223+
224+
n_user_pods = len(user_pods)
225+
n_build_pods = len(build_pods)
226+
227+
quota = self.settings["pod_quota"]
228+
total_pods = n_user_pods + n_build_pods
229+
usage = {
230+
"total_pods": total_pods,
231+
"build_pods": n_build_pods,
232+
"user_pods": n_user_pods,
233+
"quota": quota,
234+
"ok": total_pods <= quota if quota is not None else True,
235+
}
236+
return usage

0 commit comments

Comments
 (0)