Skip to content

fix(deploy): give Compose its own wait deadline before the SIGKILL - #94

Closed
owine wants to merge 1 commit into
fix/retry-transient-registry-throttlesfrom
fix/compose-wait-timeout
Closed

owine wants to merge 1 commit into
fix/retry-transient-registry-throttlesfrom
fix/compose-wait-timeout

Conversation

@owine

@owine owine commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #93. Base is fix/retry-transient-registry-throttles, because both PRs touch the same up invocations. GitHub retargets this to main automatically once #93 merges. Review #93 first.

The problem

Every up --wait in this workflow is bounded only by the external timeout command. That means the way a slow stack fails is a SIGKILL delivered wherever Compose happens to be — including between the rename and the removal of a recreate, which strands a <shortid>_<service> container still holding the service's fixed container_name.

That isn't hypothetical; the workflow already documents it, in the comment on the step that exists to mop it up:

A compose up --wait that gets SIGKILLed by the per-stack timeout mid-recreate can strand a renamed <shortid>_<service> container that still owns the service's fixed container_name. Left in place it blocks both the next recreate and the rollback job's own compose up with "container name already in use" — a hard failure, not a health one.

So today the cleanup step is load-bearing for damage caused by how the timeout fires.

The change

--wait-timeout at 90% of service-startup-timeout on all five up --wait sites — three in deploy, two in rollback. Compose hits its own deadline first and exits on its own terms, naming the services that never became healthy rather than dying silently mid-operation. The external timeout is demoted to a backstop for a genuinely wedged Compose.

90% rather than a fixed subtrahend so the margin scales with whatever callers configure (300s → 270s) without a second input to keep in sync. Computed in shell because GitHub expressions have no arithmetic operators.

What this does not fix

The cleanup step stays, and should. --wait-timeout bounds the final --wait phase, not the depends_on: service_healthy waits Compose performs mid-sequence. A stack that stalls on a dependency can still burn the whole budget and take the SIGKILL with later services half-recreated. This makes the stranding rare; it does not make it impossible. The code comment says so explicitly so nobody deletes the cleanup step on the strength of this PR.

Verification

  • yamllint --strict clean
  • actionlint clean apart from the pre-existing job.workflow_sha finding (present on main too)
  • docker compose up --dry-run -d --quiet-pull --wait --wait-timeout 270 --remove-orphans confirmed rc=0 against Compose v5.5.1 on the piwine runner — non-mutating, and its plan happens to show the exact a774a6ece886_network-optimizer rename this guards against

Summary by Sourcery

Give every deployment and rollback Compose wait a scaled internal deadline so failures occur cleanly before the external timeout intervenes.

Bug Fixes:

  • Prevent Compose operations from being abruptly SIGKILLed during the normal wait period, reducing the risk of stranded containers and subsequent fixed-name conflicts.

Enhancements:

  • Apply a Compose-managed wait deadline equal to 90% of the configured service startup timeout across deployment and rollback workflows while retaining the external timeout as a backstop.
  • Document the workflow's two-level timeout behavior and its operational limitations.

Documentation:

  • Document the Compose wait-timeout behavior in the project workflow guidance.

Every `up --wait` is bounded only by the external `timeout` command, so
the way a slow stack fails is a SIGKILL delivered wherever Compose
happens to be. Killed between the rename and the removal of a recreate,
that strands a `<shortid>_<service>` container still holding the
service's fixed `container_name` — which is why "Cleanup failed existing
stacks on failure" exists at all.

Add `--wait-timeout` at 90% of `service-startup-timeout` to all five
`up --wait` sites (three deploy, two rollback). Compose now hits its own
deadline first and exits on its own terms, naming the services that never
became healthy instead of dying silently, and the external `timeout`
becomes a backstop for a genuinely wedged Compose.

This is a mitigation, not a cure, and the cleanup step stays:
`--wait-timeout` bounds the final `--wait` phase, not the `depends_on:
service_healthy` waits Compose performs mid-sequence, so a stack that
stalls on a dependency can still burn the whole budget and take the
SIGKILL with later services half-recreated.

90% rather than a fixed subtrahend so the margin scales with whatever
callers configure (300s -> 270s) without a second input to keep in sync.
Computed in shell because GitHub expressions have no arithmetic
operators.

Verified: yamllint --strict clean; actionlint clean apart from the
pre-existing job.workflow_sha finding; `up --dry-run -d --quiet-pull
--wait --wait-timeout 270 --remove-orphans` confirmed rc=0 against
Compose v5.5.1 on the piwine runner, whose plan shows the very
`<shortid>_<service>` rename this guards.
@sourcery-ai

sourcery-ai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The workflow now gives Compose its own wait deadline at 90% of service-startup-timeout across all deployment and rollback up --wait calls, allowing Compose to report unhealthy services and exit cleanly before the external timeout becomes a SIGKILL backstop; existing cleanup remains because dependency waits can still exceed the Compose wait phase.

Sequence diagram for Compose wait deadline and timeout backstop

sequenceDiagram
    participant Workflow
    participant Compose
    participant Cleanup

    Workflow->>Workflow: Compute Compose wait budget
    Workflow->>Compose: docker compose up --wait --wait-timeout COMPOSE_WAIT_TIMEOUT
    alt services become healthy
        Compose-->>Workflow: Success
    else Compose wait deadline reached
        Compose-->>Workflow: Failure naming unhealthy services
        Workflow->>Cleanup: Run existing failed-stack cleanup
    else Compose remains wedged
        Workflow--xCompose: External timeout sends SIGKILL
        Workflow->>Cleanup: Run existing failed-stack cleanup
    end
Loading

File-Level Changes

Change Details Files
Add a Compose-level wait deadline before the external timeout can SIGKILL it during deployment recreates.
  • Compute a 90%-of-configured startup timeout budget in shell and export it through GITHUB_ENV.
  • Pass --wait-timeout to all three deployment compose up invocations while retaining the external timeout backstop.
  • Document why the deadline reduces, but does not eliminate, stranded-container risk and why cleanup remains required.
.github/workflows/deploy.yml
Apply the same bounded Compose wait behavior to rollback operations.
  • Pass the shared Compose wait budget to both rollback compose up invocations.
  • Update rollback comments to describe Compose's deadline followed by the external timeout and preserve manual-recovery warnings.
.github/workflows/deploy.yml
Document the new timeout behavior in workflow maintenance guidance.
  • Record that every up --wait uses a wait-timeout set to 90% of service-startup-timeout.
CLAUDE.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. This changes production deployment timing and can cause Compose to stop during a recreate before all services are healthy, leaving a partially updated stack or triggering rollback/cleanup paths. Reverting prevents future occurrences, but any interrupted deployment or resulting outage must be repaired separately.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@owine

owine commented Sep 20, 2026

Copy link
Copy Markdown
Owner Author

Note on CI: Lint Workflows is scoped to pull_request: branches: [main], so it does not run while this PR is based on fix/retry-transient-registry-throttles. It will run automatically once #93 merges and GitHub retargets this to main.

Ran the same three checks locally against this branch in the meantime:

  • yamllint --strict .github/workflows/ — clean
  • actionlint — clean apart from the pre-existing job.workflow_sha finding
  • shellcheck -x over all 16 scripts under scripts/ — clean

@owine
owine deleted the branch fix/retry-transient-registry-throttles September 20, 2026 18:55
@owine owine closed this Sep 20, 2026
@owine

owine commented Sep 20, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #95 — same commit rebased onto main. This PR was auto-closed when #93 merged with --delete-branch, removing this PR's base branch, and a closed PR's base cannot be retargeted.

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.

1 participant