Skip to content

feat(deploy): health-checked rolling restarts — container-local /readyz, aligned shutdown budget, pgbus-health probe - #388

Merged
mhenrixon merged 3 commits into
mainfrom
issue-386-rolling-restart-readiness
Aug 3, 2026
Merged

feat(deploy): health-checked rolling restarts — container-local /readyz, aligned shutdown budget, pgbus-health probe#388
mhenrixon merged 3 commits into
mainfrom
issue-386-rolling-restart-readiness

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements #386 — makes the job container a correct participant in health-gated rolling restarts (Kamal dash-style: start new → poll healthcheck → stop old).

  • Container-local /readyz on the standalone health server (lib/pgbus/web/health_app.rb, lib/pgbus/process/supervisor.rb, new lib/pgbus/process/readiness_snapshot.rb). ⚠️ Behavior change: the health_port server no longer serves the cluster-wide verdict — a new container can't pass the deploy gate on the old container's workers. States: BOOTING / OK / DEGRADED / DRAINING; the supervisor swaps an immutable snapshot per monitor pass, so the accept thread never touches @forks. The Rails-mounted HealthApp keeps the cluster verdict.
  • Alignable shutdown budget (lib/pgbus/configuration.rb, supervisor.rb, worker.rb, consumer.rb). New config.shutdown_timeout, default drain_timeout + 5, replaces the supervisor's hardcoded 30s SIGKILL deadline; Consumer's drain wait follows drain_timeout; Worker's post-drain residual wait drops 30s → 5s. Documented rule: orchestrator stop_timeout > shutdown_timeout > drain_timeout.
  • pgbus-health probe (exe/pgbus-health, lib/pgbus/health_probe.rb): stdlib-only, loads neither Bundler nor the gem (spec-enforced), exit 0/1/2 — cheap enough for 1–5s docker HEALTHCHECK intervals, no curl needed.
  • Docs: README "Rolling restarts (Kamal, docker)" guide + probe-table split; docs-site "Rolling restarts" Operations page; CHANGELOG (Added + Changed, behavior change flagged).

Closes #386

Test plan

  • bundle exec rake rubocop — 546 files, no offenses (root task; docs app rake lint also clean)
  • Touched specs green together: 747 examples, 0 failures
  • Full suite vs main, same seed (--seed 20260803, excluding the locally-unloadable spec/rubocop cop spec): identical 2 pre-existing i18n failures on both, zero regressions; branch adds 26 examples
  • Docs app: 81 specs green (config-reference drift spec covers the new shutdown_timeout)
  • End-to-end smoke: real exe/pgbus-health process against a real HealthServer — exit 0 on a ready snapshot, exit 1 on a draining one

Deviations & judgment calls

  • expected children = fork-table size after boot_processes, stamped once. A child whose fork itself failed at boot (EAGAIN/ENOMEM) is not counted, so readiness could report OK without it — accepted because deriving the expectation from config is wrong in legitimate cases (scheduler role declines to boot when no recurring tasks exist). Rare failure, conservative direction (over-ready only for a child that also crash-looped at fork time).
  • Readiness refresh is called directly from graceful_shutdown/immediate_shutdown, not only from the monitor loop, so DRAINING is visible to the very next probe instead of up to 1s later.
  • Probe grew --timeout and --path flags beyond the issue's minimum (--port/env); parsing is hand-rolled (~15 lines) rather than optparse to keep the load profile at exactly socket.
  • HealthApp's local-mode error path mirrors the cluster path (503 {"status":"ERROR"} + logged) — the issue specified only the happy-path body shape.
  • Suite baseline discovery: the memory'd "~97–100 pre-existing failures" is stale — current baseline on main is 2 i18n failures plus one load error (spec/rubocop/cop/pgbus/no_ruby_timeout_spec.rb can't require "rubocop/pgbus" locally; pre-existing, excluded from the comparison run).
  • Issue's manual smoke (probe against a live pgbus start) replaced with an equivalent that needs no DB: the probe exe against a real HealthServer + local snapshot, both readiness polarities. The DB-dependent boot path is covered by the existing supervisor #run specs.

Summary by CodeRabbit

  • New Features

    • Added container-local readiness reporting with booting, healthy, degraded, and draining states.
    • Added the lightweight pgbus-health probe for deployment health checks.
    • Added configurable shutdown timing for graceful worker and supervisor termination.
  • Documentation

    • Added guidance for rolling restarts, readiness endpoints, shutdown budgets, overlap safety, and forced termination impacts.
    • Clarified the difference between cluster-wide and container-local health checks.
  • Bug Fixes

    • Replaced fixed shutdown waits with configurable drain and shutdown timeouts.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ae31ccca-2ecc-4fe0-a06a-221892958166

📥 Commits

Reviewing files that changed from the base of the PR and between 1e2041d and 28950b6.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • README.md
  • docs/app/models/config_reference.rb
  • docs/app/models/doc.rb
  • docs/app/views/docs/pages/observability.rb
  • docs/app/views/docs/pages/rolling_restarts.rb
  • exe/pgbus-health
  • lib/pgbus/configuration.rb
  • lib/pgbus/health_probe.rb
  • lib/pgbus/process/consumer.rb
  • lib/pgbus/process/readiness_snapshot.rb
  • lib/pgbus/process/supervisor.rb
  • lib/pgbus/process/worker.rb
  • lib/pgbus/web/health_app.rb
  • spec/pgbus/configuration_spec.rb
  • spec/pgbus/health_probe_spec.rb
  • spec/pgbus/process/consumer_spec.rb
  • spec/pgbus/process/readiness_snapshot_spec.rb
  • spec/pgbus/process/supervisor_spec.rb
  • spec/pgbus/process/worker_spec.rb
  • spec/pgbus/web/health_app_spec.rb

📝 Walkthrough

Walkthrough

The change adds container-local supervisor readiness, the pgbus-health probe, configurable shutdown budgets, bounded worker and consumer termination waits, rolling-restart documentation, and comprehensive tests.

Changes

Health and shutdown lifecycle

Layer / File(s) Summary
Supervisor readiness flow
lib/pgbus/process/readiness_snapshot.rb, lib/pgbus/process/supervisor.rb, lib/pgbus/web/health_app.rb, spec/pgbus/process/*, spec/pgbus/web/health_app_spec.rb
Supervisors publish boot, child-count, and shutdown state. Local /readyz returns readiness data without cluster analysis.
Configurable shutdown budget
lib/pgbus/configuration.rb, lib/pgbus/process/{consumer,supervisor,worker}.rb, spec/pgbus/configuration_spec.rb, spec/pgbus/process/*_spec.rb
shutdown_timeout supports derived defaults, validation, warnings, and supervisor escalation. Consumers use drain_timeout, and workers use a five-second residual wait.
Standalone health probe
lib/pgbus/health_probe.rb, exe/pgbus-health, spec/pgbus/health_probe_spec.rb
The dependency-free probe checks localhost HTTP readiness and returns separate healthy, unhealthy, and usage exit codes.
Rolling restart documentation
README.md, CHANGELOG.md, docs/app/models/config_reference.rb, docs/app/models/doc.rb, docs/app/views/docs/pages/observability.rb, docs/app/views/docs/pages/rolling_restarts.rb
Documentation describes readiness states, probe configuration, shutdown timing, overlap behavior, and hard-kill effects.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Supervisor
  participant HealthApp
  participant HealthProbe
  Supervisor->>HealthApp: provide local readiness callback
  HealthProbe->>HealthApp: request /readyz
  HealthApp->>Supervisor: read readiness snapshot
  Supervisor-->>HealthApp: return status and child counts
  HealthApp-->>HealthProbe: return 200 or 503
Loading

Possibly related PRs

Suggested labels: enhancement, documentation

Poem

A rabbit checks readiness at dawn,
Workers drain before they’re gone.
The probe hops through localhost air,
Snapshots show the children there.
Timed shutdowns keep queues bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes container-local readiness, aligned shutdown timing, and the pgbus-health probe.
Linked Issues check ✅ Passed The changes implement the coding objectives in direct issue [#386], including readiness, shutdown budgets, probe support, documentation, and tests.
Out of Scope Changes check ✅ Passed The changes stay within issue [#386] and cover implementation, tests, documentation, configuration, and changelog updates.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-386-rolling-restart-readiness

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.45.0)
spec/pgbus/configuration_spec.rb

ast-grep timed out on this file


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/pgbus/process/supervisor.rb (1)

919-929: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate shutdown_timeout before Supervisor#run enters its ensure path. eager_validation = false allows invalid values to reach shutdown; Time.now + config.shutdown_timeout can then raise TypeError and mask the boot error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pgbus/process/supervisor.rb` around lines 919 - 929, Validate
config.shutdown_timeout before Supervisor#run can enter its ensure-driven
shutdown path, rejecting invalid values during startup rather than allowing
shutdown to perform Time.now arithmetic on them. Update the existing Supervisor
startup/config validation flow and preserve the shutdown deadline behavior for
valid timeout values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/app/models/doc.rb`:
- Line 24: In docs/app/models/doc.rb, keep only the docs declaration for
“Rolling restarts” at line 45 under the Operations group and remove the
duplicate declarations at lines 24, 37, 49, 56, and 61, so the navigation
registers that page once.

In `@lib/pgbus/configuration.rb`:
- Around line 785-790: Update validate_shutdown_timeout! to accept nil only or a
finite, positive real numeric value, rejecting Float::INFINITY and non-real
Numeric implementations before shutdown calculations; add coverage for infinite
and non-real Numeric inputs while preserving the existing ConfigurationError and
message behavior.

In `@lib/pgbus/health_probe.rb`:
- Around line 45-52: Update the validation flow in HealthProbe#run to reject
ports outside 1–65535 and return usage_failure before calling probe. Also
validate the raw timeout input before conversion so non-numeric --timeout values
such as "abc" return the usage exit code 2 instead of becoming 0.0; preserve
valid timeout handling.

In `@lib/pgbus/process/supervisor.rb`:
- Around line 131-139: Update the boot lifecycle around mark_booted,
boot_processes, and fork_worker/fork_consumer to track the configured child-fork
intent separately from `@forks.size`, incrementing only for roles that should
actually boot and excluding legitimately disabled or empty roles. Set
`@expected_children` from this intent count at boot completion, then compare it
with the fork table so failed or rescued forks keep readiness unhealthy rather
than lowering the baseline.

In `@README.md`:
- Line 1202: Update the fenced code block in README.md to specify the text
language identifier, using ```text while preserving the existing timeline
content and formatting.

---

Outside diff comments:
In `@lib/pgbus/process/supervisor.rb`:
- Around line 919-929: Validate config.shutdown_timeout before Supervisor#run
can enter its ensure-driven shutdown path, rejecting invalid values during
startup rather than allowing shutdown to perform Time.now arithmetic on them.
Update the existing Supervisor startup/config validation flow and preserve the
shutdown deadline behavior for valid timeout values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1269691f-8c85-4b10-be8c-56d10b9736d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab300f and 1e2041d.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • README.md
  • docs/app/models/config_reference.rb
  • docs/app/models/doc.rb
  • docs/app/views/docs/pages/observability.rb
  • docs/app/views/docs/pages/rolling_restarts.rb
  • exe/pgbus-health
  • lib/pgbus/configuration.rb
  • lib/pgbus/health_probe.rb
  • lib/pgbus/process/consumer.rb
  • lib/pgbus/process/readiness_snapshot.rb
  • lib/pgbus/process/supervisor.rb
  • lib/pgbus/process/worker.rb
  • lib/pgbus/web/health_app.rb
  • spec/pgbus/configuration_spec.rb
  • spec/pgbus/health_probe_spec.rb
  • spec/pgbus/process/consumer_spec.rb
  • spec/pgbus/process/readiness_snapshot_spec.rb
  • spec/pgbus/process/supervisor_spec.rb
  • spec/pgbus/process/worker_spec.rb
  • spec/pgbus/web/health_app_spec.rb

Comment thread docs/app/models/doc.rb Outdated
Comment thread lib/pgbus/configuration.rb
Comment thread lib/pgbus/health_probe.rb
Comment thread lib/pgbus/process/supervisor.rb
Comment thread README.md Outdated
…yz, aligned shutdown budget, pgbus-health probe

The supervisor's standalone /readyz previously answered with the
cluster-wide HealthAnalyzer verdict, so during a rolling deploy a
freshly-booted container passed the orchestrator's health gate on the
strength of the OLD container's still-heartbeating workers. It now
answers container-local: 200 only once THIS supervisor verified its
connection, bootstrapped queues, and every child it forked is alive —
503 BOOTING / DEGRADED (child in crash backoff) / DRAINING otherwise.
The supervisor publishes an immutable ReadinessSnapshot per monitor
pass (refreshed after reap-and-restart, so clean recycles never flap);
the Rails-mounted HealthApp keeps the cluster verdict.

Shutdown budgets become alignable: new config.shutdown_timeout (default
drain_timeout + 5) replaces the supervisor's hardcoded 30s SIGKILL
deadline, Consumer's drain wait follows drain_timeout instead of a
hardcoded 30s, and Worker's post-drain residual wait drops to 5s.

New pgbus-health executable: stdlib-only probe (never loads Bundler or
the gem) for docker HEALTHCHECK blocks — exit 0/1/2.

## Test Coverage
- configuration_spec: shutdown_timeout derivation, validation, warning
- readiness_snapshot_spec: ready?/status truth table
- supervisor_spec: boot/degrade/drain transitions, /readyz wiring,
  configurable SIGKILL deadline
- worker_spec / consumer_spec: pool-wait bounds
- health_app_spec: local-readiness verdicts + error path
- health_probe_spec: real-socket probe against HealthServer, exit
  codes, no-gem-load guard

## Verification
- [x] bundle exec rake rubocop — 546 files, no offenses
- [x] touched specs green (747 examples)
- [x] full suite vs main, same seed: identical 2 pre-existing i18n
      failures, zero regressions

Refs #386
New Operations page covering the health-gated rolling restart flow:
the container-local readiness gate, pgbus-health HEALTHCHECK wiring,
the stop_timeout > shutdown_timeout > drain_timeout alignment rule,
overlap-window duplicate-supervisor safety, and the read_ct-vs-deploy-
kill DLQ caveat. Observability page's probe table now distinguishes
mounted (cluster verdict) from standalone (container-local) /readyz,
and the config reference documents shutdown_timeout (drift spec green).

Refs #386
@mhenrixon
mhenrixon force-pushed the issue-386-rolling-restart-readiness branch from 1e2041d to c0de308 Compare August 3, 2026 07:25
…shutdown_timeout, probe input validation, docs registry dedupe

CodeRabbit review on #388:
- Readiness baseline now counts INTENDED boot-time forks (max of attempt
  count and fork table), so a failed fork at boot leaves the gate
  DEGRADED instead of silently lowering expected — a rolling deploy can
  no longer promote a container missing workers. Restart re-forks after
  boot never inflate the baseline.
- validate! rejects non-finite / non-real shutdown_timeout (INFINITY
  previously blew up Supervisor#shutdown before child cleanup).
- pgbus-health exits 2 on out-of-range ports (was a SocketError
  backtrace) and on non-numeric/non-positive --timeout (was a silent
  0.0 deadline that failed every probe); SocketError added to the
  probe rescue.
- docs page generator had injected the Rolling restarts registry line
  after every group — deduped to one entry under Operations.
- README: language identifier on the budget-alignment fence (MD040).

Refs #386
@mhenrixon
mhenrixon merged commit bcb87fe into main Aug 3, 2026
14 checks passed
@mhenrixon
mhenrixon deleted the issue-386-rolling-restart-readiness branch August 3, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(deploy): health-checked rolling restarts — container-local /readyz, configurable shutdown budget, pgbus-health probe

1 participant