health_check_service is built on one distinction, stated in its own
module docstring:
Required and optional are different too. Gemini or Twilio being
unconfigured is degraded: the assistant and SMS summaries stop
working and the core cycle-tracking product does not. Firestore being
mocked is down.
and enforced by HealthReport.ready:
@property
def ready(self) -> bool:
return not any(
component.required and component.status == STATUS_DOWN
for component in self.components
)
Every check declares its own required flag, and the two optional ones
declare required=False. But the flag is only carried on the paths where
the check returns. On the two paths where it does not, _run_one
invents one:
# backend/services/health_check_service.py — _run_one
except FutureTimeout:
...
return ComponentHealth(
name=name,
status=STATUS_DOWN,
required=True, # <-- not the check's own value
detail=f"Check did not respond within {timeout} seconds.",
...
)
except Exception as exc:
...
return ComponentHealth(
name=name,
status=STATUS_DOWN,
required=True, # <-- same
detail="Check failed. See server logs for details.",
...
)
So a check that returns degraded, required=False keeps this instance
in rotation, and the same check raising takes it out — for the same
underlying dependency, with required decided by which branch the
failure happened to take.
Reproduction
check_sms_config reads three environment variables. Give it something
that raises rather than returns — the simplest is a monkeypatched
os.getenv, but any future check that opens a socket to Twilio or Gemini
gets there without help:
import services.health_check_service as hc
def boom():
raise RuntimeError("probe blew up")
boom.__name__ = "check_sms"
report = hc.run_checks() # with boom in CHECKS
assert report.ready # FAILS — ready is False
GET /health/ready then answers 503, the orchestrator pulls the instance
out of the load balancer, and every user gets an error — because the
Twilio configuration check raised. Firestore is fine. Auth is fine. The
product works. Nobody can reach it.
The timeout path is the more likely one in practice. _run_one bounds
every check at HEALTH_CHECK_TIMEOUT (default 3s). The moment any
optional probe does real network work — and check_assistant_config's
docstring already anticipates growing into one — a slow third party
becomes a fleet-wide outage on a 3-second stopwatch.
Also wrong on the same two lines
name is recovered from check.__name__.replace("check_", ""), so a
timed-out or raised check is reported under a name derived from the
function rather than the name the check itself uses. check_auth_config
returns name="auth" when it succeeds and is reported as
"auth_config" when it times out, so an operator grepping their
dashboards for a component finds it under two different names depending
on how it failed.
Expected
Whether a dependency is required is a property of the dependency. It
should be declared once, alongside the check, and used identically on
the success, timeout and exception paths — so a probe that hangs
produces the same verdict about readiness that the same probe
returning down would.
test_a_hanging_check_becomes_a_result_not_a_hang in
backend/tests/test_health_checks.py already covers the hang itself; it
asserts on elapsed wall-clock and does not look at required.
health_check_serviceis built on one distinction, stated in its ownmodule docstring:
and enforced by
HealthReport.ready:Every check declares its own
requiredflag, and the two optional onesdeclare
required=False. But the flag is only carried on the paths wherethe check returns. On the two paths where it does not,
_run_oneinvents one:
So a check that returns
degraded, required=Falsekeeps this instancein rotation, and the same check raising takes it out — for the same
underlying dependency, with
requireddecided by which branch thefailure happened to take.
Reproduction
check_sms_configreads three environment variables. Give it somethingthat raises rather than returns — the simplest is a monkeypatched
os.getenv, but any future check that opens a socket to Twilio or Geminigets there without help:
GET /health/readythen answers 503, the orchestrator pulls the instanceout of the load balancer, and every user gets an error — because the
Twilio configuration check raised. Firestore is fine. Auth is fine. The
product works. Nobody can reach it.
The timeout path is the more likely one in practice.
_run_oneboundsevery check at
HEALTH_CHECK_TIMEOUT(default 3s). The moment anyoptional probe does real network work — and
check_assistant_config'sdocstring already anticipates growing into one — a slow third party
becomes a fleet-wide outage on a 3-second stopwatch.
Also wrong on the same two lines
nameis recovered fromcheck.__name__.replace("check_", ""), so atimed-out or raised check is reported under a name derived from the
function rather than the name the check itself uses.
check_auth_configreturns
name="auth"when it succeeds and is reported as"auth_config"when it times out, so an operator grepping theirdashboards for a component finds it under two different names depending
on how it failed.
Expected
Whether a dependency is required is a property of the dependency. It
should be declared once, alongside the check, and used identically on
the success, timeout and exception paths — so a probe that hangs
produces the same verdict about readiness that the same probe
returning
downwould.test_a_hanging_check_becomes_a_result_not_a_hanginbackend/tests/test_health_checks.pyalready covers the hang itself; itasserts on elapsed wall-clock and does not look at
required.