Commit Graph
7 Commits
Author SHA1 Message Date
Alejandro Roman 4b9a834546 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-21 03:25:20 +04:00
kevinveenbirkenbachandClaude Opus 5 96408f23a0 perf(healthcheck): let the start window outlast a real boot
start_interval is the cadence while start_period runs, and only while it runs.
Once the window closes the probe falls back to interval, so a service that
becomes ready just after it pays a whole tick anyway and the cadence never
reached it. Six of the seven flavors were shaped that way, Curl worst: a thirty
second window against a one minute interval.

Measured across 151 converge waits in four job logs, 63 sit exactly on a tick
boundary, 36 in the thirty second step and 27 in the sixty second one.
Attributed by application, web-app-seaweedfs is the largest at 1501s over
sixteen waits with a median of 57.7s: it probes with curl and fell straight into
the gap. web-app-zammad probes with connect, whose window is fifteen minutes,
and its 154s median is real boot time with nothing to recover.

So the window moves to ten minutes and the steady state interval to two, and the
flavor overrides that sat below them go: Curl, Wget and Connect lose interval
1m, Http its twenty second window, Nc its ten. Connect keeps fifteen minutes.

Neither number costs anything while a container works. start_period delays
nothing, it only stops failures counting, and interval no longer touches
convergence at all since that hangs on the window alone; it is steady state
load, which drops from 120 probes an hour per service to 30.

Forty six role files follow. Twenty seven declarations go that repeat the new
default or undercut it, an undercut window being the defect itself. Eight
windows longer than ten minutes keep their value and now say why. The literal
blocks and the three Dockerfiles this repository builds spell every timing out,
because outside the lookup an omission takes docker's value and not the
platform's.

A fourth lint rule keeps the shape: start_period must outlast one interval,
checked against the effective values, so raising interval without the window
cannot quietly bring the cliff back.

Two faults in the lint surfaced while building it and are fixed here. It
compared timings as strings, so 600s and 10m read as different values, and it
matched the healthcheck key exactly, so a line carrying a trailing nocheck was
skipped whole, which is how the two swarm-init partials went unexamined.

One regression, bounded. A container that starts and never turns healthy is
neither rejected nor errored, so stack_ready.sh only calls it fatal after three
killed tasks, and each cycle now spans the longer window. That reaches twenty
minutes rather than six, capped there by swarm_stack_converge_retries at 600
with a two second delay, and it lands only on runs that were failing anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:54:26 +02:00
kevinveenbirkenbachandClaude Opus 5 bcc467a435 refactor(healthcheck): name the timing defaults instead of inlining them
Probe carried the five platform timings as literals on its class body. They are
the answer to a question asked outside the class as well, by anything that has
to decide whether a declared value adds something or merely repeats what the
service would have inherited.

Name them, and let Probe read the names. TIMING_DEFAULTS collects the five so a
caller can walk them without restating the list and drifting from it.

No behaviour changes: every value is the one that stood there, and the class
attributes still resolve to the same strings and the same integer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:13:09 +02:00
kevinveenbirkenbachandClaude Opus 5 42ebd9ea71 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>
2026-09-09 13:49:51 +02:00
Alejandro Roman e6596d51f2 fix(stalwart): add the fix PR into main PR 2026-09-09 00:21:58 +04:00
Alejandro Roman 42f267407a fix(stalwart): fix fediwall, mailu and nexcloud issues in CI, all green now 2026-09-04 23:02:01 +04:00
kevinveenbirkenbachandClaude Opus 5 0c2ce12c7a feat(healthcheck): compose a probe from prefixes, and gate Moodle's mail on it
Moodle renders the same msmtprc as WordPress and mounts it into two services,
but nothing ever proved a message left the container: its healthcheck opens the
FPM port and nothing else. Moodle 0 of run 31658241728 passed green while mail
could not have worked -- the defect 2da285067 fixed for WordPress was invisible
here, and would have stayed invisible until a user reset a password.

WordPress proves delivery with the msmtp_curl flavor: the probe sends one mail
and writes a marker, and a task after convergence waits for that marker. Moodle
cannot use it, because curl cannot judge a port that answers FastCGI. Rather
than add a second msmtp_* class per liveness shape, the declaration now composes:

    healthcheck:
      flavor:
        - msmtp
        - connect
      port_key: fpm

msmtp stops being a flavor and becomes a prefix -- shell that runs on the way
past and never decides liveness. connect joins the probes as the one that only
asks whether the port accepts a TCP connection. A declaration names exactly one
probe and any number of prefixes, as a string or a list; msmtp_curl stays as an
alias for [msmtp, curl], and the lint that validates declarations now shares
resolve_flavors with the lookup instead of keeping its own idea of a flavor.

The module became five responsibilities in one file, so it is a package:
probes.py answers which probes exist, prefixes.py what runs before them,
compose.py how a declaration becomes a rendered block. The curl helpers sit with
the probes and mail_branch with the prefix -- each next to its only caller,
rather than together in a module header.

Two defects in this work were found by measuring, not reading:

  - the timing merge took the maximum across prefixes only, which cut Moodle's
    start period from 15m to 120s and would have marked a heavy PHP app dead
    while it was still installing. A prefix may now raise a timing and never
    lower one, pinned by test.
  - composing moved msmtp_curl's interval from 30s to 1m, because the composed
    class derives from Curl where MsmtpCurl derived from Probe. That is the
    interval every other curl probe uses, but it was not intended; it now has
    its own test rather than riding along unseen.

Regression measured across the whole repo rather than argued: every declared
healthcheck was rendered with the pre-refactor code and with this one. 58 render
byte-identically, 1 is Moodle's intentionally changed declaration, and 2 differ
-- friendica and wordpress, in that interval alone, with the probe command
itself unchanged.

Coverage was checked in both directions. No test was removed: the four deleted
lines are an import, the registry literal, one class name in a url test, and the
helper, which now goes through compose() instead of reaching into PROBES -- a
tightening. New code had two gaps, both closed: connect was the only probe whose
emitted command no test pinned, and compose() raised KeyError for an unknown
name unobserved.

Not verified: no deploy has run any of these probes. The composed class is built
dynamically, so what it emits is proven for all 59 services while docker
accepting it is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 02:02:31 +02:00