perf(healthcheck): probe on a start cadence instead of the full interval

The emitted block carried test, interval, timeout, retries and start_period,
but never start_interval. Docker therefore fires the probe only every interval
for the whole start_period, so a service that answers after three seconds is
still reported healthy at the next tick: 30s for the base flavor, a full minute
for the curl ones (probes.py:119,131,215). The swarm converge wait sits on that
signal, so every stack pays the quantisation of its slowest service.

Emit start_interval, defaulting to 5s and overridable per service like every
other timing. This changes when the same probe runs, never what it tests:
test, timeout, retries and start_period are untouched, so a service that never
becomes healthy still fails after exactly the same grace and the same attempts.

Supported by the engine that runs it: the failing job reports Docker 28.0.4 and
Compose v2.38.2 (runtime.txt), and start_interval has existed since Docker 25.0.

The saving is reasoned, not measured. Expected padding is about half an
interval per service, so roughly 500-1000s across 17 stacks and two passes,
but only for services that are genuinely ready well before their interval. How
many those are is what a run has to answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-09 13:49:51 +02:00
1 parent c66781ab19
commit 42ebd9ea71
1 file changed
+8 -1
+8 -1
View File
@@ -57,6 +57,7 @@ class Probe:
timeout: ClassVar[str] = "5s"
retries: ClassVar[int] = 3
start_period: ClassVar[str] = "30s"
start_interval: ClassVar[str] = "5s"
def __init__(self, **context: Any) -> None:
self.port = context.get("port", "")
@@ -86,11 +87,17 @@ class Probe:
def block(self, overrides: dict[str, Any]) -> dict[str, Any]:
"""Assemble the healthcheck mapping.
`start_interval` is the cadence while `start_period` is still running.
Without it a probe only fires every `interval`, so a service ready after
three seconds is reported healthy at the next tick -- 30s, or a minute
for the curl flavors -- and a stack converges no faster than its slowest
quantisation. It changes when the same probe runs, never what it tests.
Args:
overrides: service level values that win over the flavor defaults.
"""
block: dict[str, Any] = {"test": self.test()}
for key in ("interval", "timeout", "retries", "start_period"):
for key in ("interval", "timeout", "retries", "start_period", "start_interval"):
block[key] = overrides.get(key, getattr(self, key))
return block