Skip to content

fix: Restart Backstage when startup fails to reach ready - #82

Merged
ecv merged 2 commits into
mainfrom
fix/exit-on-startup-failure
Sep 18, 2026
Merged

ecv merged 2 commits into
mainfrom
fix/exit-on-startup-failure

Conversation

@ecv

@ecv ecv commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Backstage was down for nearly three hours because a brief database outage at startup left it running while serving nothing.

The plugins that could not reach the database gave up and asked for a shutdown, yet the process stayed alive, readiness kept answering 503, and liveness kept answering 200, so Kubernetes had no reason to restart the pod.

Startup now ends the process when a plugin fails, liveness fails once the backend has never reached ready within a bounded deadline, and a plugin waits out a short database absence, closing well inside the liveness deadline, before it gives up.

This reaches production only once the deployment repository bumps to an image that carries it.

Test plan

  • Build, typecheck, lint and unit tests pass
  • A backend started against an unreachable database retries, then exits non-zero
  • A backend started against a healthy database reports ready and stays live
  • Liveness fails once a backend has never reached ready within the deadline

Fixes #81

@ecv
ecv force-pushed the fix/exit-on-startup-failure branch from e002be7 to ba33385 Compare September 16, 2026 01:10
@ecv ecv mentioned this pull request Sep 17, 2026
4 tasks
@ecv
ecv force-pushed the fix/exit-on-startup-failure branch from ba33385 to 61bdd1f Compare September 17, 2026 23:44
@ecv
ecv marked this pull request as ready for review September 17, 2026 23:49
@ecv
ecv marked this pull request as draft September 18, 2026 00:00
ecv added 2 commits September 17, 2026 20:01
A plugin that failed during startup left the process alive and serving
nothing: the startup error surfaced as an unhandled rejection that the
backend only logged, readiness stayed at 503, and liveness kept passing,
so the pod was never restarted. Production sat in that state for nearly
three hours after a momentary database outage.

Key changes:
- Await the backend start and exit non-zero when it fails, so a failed
  startup becomes a container restart rather than a pod that serves
  nothing
- Replace the root health service so liveness fails once the backend has
  never reached ready within a bounded deadline, leaving liveness
  untouched for a backend that has started
- Wrap the database service so a plugin retries an unreachable database
  for a bounded window before giving up, which absorbs a short blip
  without costing a restart

The retry window closes well before the liveness deadline, so a database
that stays away still ends the process rather than holding it open.
The retry wrapper called the same plugin database service on every
attempt, and that service memoizes its first connection promise for the
lifetime of its manager. A rejected promise stays in that cache, so
every later attempt resolved the original failure instantly and no
second connection was ever opened. A database that returned after a
second still failed the startup it was meant to survive.

Key changes:
- Build a database manager per attempt, so each retry carries its own
  cache and opens a real connection
- Cover the case with a delegate that memoizes its first failure, which
  is the behaviour the upstream manager has and the previous mock did
  not
@ecv
ecv force-pushed the fix/exit-on-startup-failure branch from 61bdd1f to a5de69e Compare September 18, 2026 00:02
@ecv
ecv marked this pull request as ready for review September 18, 2026 00:12
@ecv
ecv enabled auto-merge September 18, 2026 00:16

@kevwilliams kevwilliams 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.

Fixes a real incident-causing gap: a failed plugin startup used to leave the process alive serving nothing (readiness 503, liveness 200 forever). Now backend.start() failure exits the process, a new bounded-deadline root health service fails liveness only if the backend never reaches ready within 5 minutes (so it won't flap on a legitimately slow start), and the DB retry wrapper rebuilds its delegate per attempt to avoid replaying a memoized first failure. Retry window (30s) closes well inside the liveness deadline (300s), and tests cover both the healthy and failed-startup paths with real assertions, not just no-throw checks.

@ecv
ecv merged commit eb2f50c into main Sep 18, 2026
7 checks passed
@ecv
ecv deleted the fix/exit-on-startup-failure branch September 18, 2026 00:24
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.

Backstage stays running but never serves after a database blip at startup

2 participants