Skip to content

fix: bound restart attempts across all strategies (#318) - #320

Open
FedericoPonzi wants to merge 1 commit into
masterfrom
fponzi/318
Open

fix: bound restart attempts across all strategies (#318)#320
FedericoPonzi wants to merge 1 commit into
masterfrom
fponzi/318

Conversation

@FedericoPonzi

Copy link
Copy Markdown
Owner

Bound repeated post-start crashes so  restart.attempts  is honored by all restart strategies, reset the budget only once a service is genuinely stable, and add a configurable  healthiness.healthy-after  stable interval.

Motivation and Context

Fixes #318.
restart.attempts was documented as a bound on rapid restart loops, but the implementation diverged from that intent in three ways:

  1. The budget was ignored by  on-failure  and  always .  handle_restart_strategy  only consulted  restart_attempts_are_over()  for the  never  strategy, so a crashing  on-failure / always  service restarted forever regardless of  attempts .
  2. The counter reset too early.  restart_attempts  was reset on the  Started  transition. For a service with no healthcheck,  Started → Running  happens immediately, so a tight crash loop reset its budget on every respawn and never exhausted it.
  3. Docs were out of sync (claimed a default of  10  and that  on-failure  honored attempts; the real default is  0  and it did not).

Description

handle_restart_strategy now applies the budget check to all three strategies, so an exhausted service goes to FinishedFailed instead of restarting forever; attempts = 0~ still means unbounded, so the default behavior is unchanged. The restart_attemptsreset moves from theStartedto theRunningtransition, so only a service that actually becomes stable clears its budget. A new  healthiness.healthy-after  duration (default  0s ) makes "stable" configurable for services without an explicit healthcheck by delayingHealthyuntil that much uptime has elapsed, with stale health cleared on relaunch. Docs are corrected accordingly, and unit and integration tests cover the budget across strategies, the reset semantics,healthy-after` parsing, and crash loops terminating after exhausting their attempts.

How Has This Been Tested?

Unit and integ tests.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Performance enhancement (non-breaking change which improves efficiency)
  • Code cleanup (non-breaking change which makes code smaller or more readable)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (a change to man pages or other documentation)

Checklist:

  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.

@FedericoPonzi
FedericoPonzi force-pushed the fponzi/318 branch 2 times, most recently from 22fb11c to 86bd3ff Compare July 24, 2026 19:34
@FedericoPonzi
FedericoPonzi requested a review from kemingy July 28, 2026 19:22

@kemingy kemingy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One question: will the fork() failure considered as retry-able?

Comment thread docs/README.md Outdated
Comment thread horust/src/horust/supervisor/service_handler.rs Outdated
FedericoPonzi added a commit that referenced this pull request Aug 3, 2026
Address review feedback on #320.

A service that fails to spawn never produces a ServiceExited event, which
is the only place restart_attempts was incremented. Spawn failures were
therefore restarted forever without consuming the attempts budget, and
since the retry delay is backoff * attempts_made, the delay stayed at zero
and the service was respawned on every supervisor iteration.

This is not limited to fork() failing: exec_args/find_program run in the
parent before forking, so a command that isn't found on PATH takes the
same path.

Increment restart_attempts when handling SpawnFailed. A process that never
came to exist is by definition an early failure, so it counts as a rapid
failure like any other.

Also clarify in the docs what counts as failing "too quickly", pointing at
healthiness.healthy-after, and note that spawn failures count too.
@FedericoPonzi
FedericoPonzi requested a review from kemingy August 5, 2026 22:23
restart.attempts previously failed to bound repeated post-start
crashes: the on-failure and always strategies ignored the budget and
restarted forever, and the attempts counter was reset on the Started
transition, so a service that briefly reached Started before crashing
never exhausted its budget.

Make all three restart strategies honor the attempts budget:
- attempts = 0 keeps the existing unbounded behavior (default), so
  on-failure/always still restart forever unless a budget is set, while
  never still stops immediately on failure.
- attempts > 0 bounds every strategy: once the budget is exhausted the
  service becomes FinishedFailed instead of restarting.

Reset the attempts counter when the service reaches Running (genuinely
stable/green) rather than on Started, so crash loops that momentarily
reach Started still exhaust their budget. Keep never distinct from
on-failure by requiring an actual rapid failure to have been counted
before it restarts: failures after a service becomes stable never consume
the budget, so restarting them would loop forever.

Count spawn failures against the budget too. A command that cannot be
executed at all never produces a process exit, so it previously bypassed
the only site that incremented the counter and was retried forever.

Cap the backoff multiplier at the configured budget, or at 10 when it is
unbounded. Since the counter now survives until a service is stable, the
respawn delay (backoff * attempts_made) would otherwise grow without
bound and push restarts infinitely far apart.

Add healthiness.healthy-after (duration, default 0s): for services with
no explicit health check, delay the Healthy signal until the process has
survived this long, keeping a crash-prone service in the not-yet-green
state so its budget deterministically exhausts. Stale health-check
results are cleared on each (re)start.

Update docs and add unit and integration tests.
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.

restart.attempts does not bound repeated post-start crashes

2 participants