Skip to content

generated HelmReleases retry a failed install forever, with no cap, backoff, or notion of a terminal failure #4155

Open

Description

Every HelmRelease the platform generates retries a failed install or upgrade forever, and nothing in the policy can tell a release that is slow from one that will never succeed.

Both generators, pkg/registry/apps/application/rest.go and internal/operator/package_reconciler.go, set Install.Strategy and Upgrade.Strategy to RetryOnFailure with an explicit RetryInterval, defaulting to 30s from pkg/cmd/server/start.go:90. Neither sets Remediation, so there is no retry count and no exhaustion. A release whose failure is permanent re-enters its wait 30 seconds after each timeout, for as long as the object exists.

For a transient cause that is exactly right, and it is why the setting exists. The comment at rest.go:1580 records the reasoning: RetryOnFailure with an explicit interval was chosen instead of Remediation{Retries: -1} precisely to decouple retry timing from Spec.Interval, because the previous coupling made failed installs retry only every five minutes and blew past E2E budgets. Any fix here has to keep that property, so reaching back for a remediation-based cap would undo it.

What is missing is the other half. A release that cannot succeed, for instance one whose readiness gate waits on an object the storage layer will never be able to create, is indistinguishable from one that is merely slow, and the policy treats both the same forever. The cost is not only the retry itself: each attempt re-enters a wait that occupies one of the shard's helm-controller slots for its whole window, which is what turns a permanent failure into near-continuous occupancy rather than a one-off. That side is #4145, and this is the other lever on the same symptom; either one alone leaves the other standing.

Worth deciding together rather than separately, because the options trade against each other. A retry cap bounds the damage but makes the platform stop converging on a cause that later clears itself. Backoff keeps convergence and reduces occupancy without a cliff. Detecting terminal failure is the most precise and the most work, and helm-controller already has the notion, since it stalls a release with a terminal error for an uncompilable CEL expression; the distinction exists in the controller and simply is not reachable from a wait timeout.

Nothing here measures how often a permanently failing release actually occurs in a running cluster, and that number is what should decide how much of this is worth building.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)
    on Sep 7, 2026
  2. added
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to complete
    and removed
    triage/needs-triageIndicates an issue needs triage by a maintainer
    on Sep 23, 2026
  3. scooby87 commented on Oct 26, 2026

    @scooby87
    Contributor

    Opened #5109 with one concrete, opt-in option from this issue (a per-kind single-attempt policy with a terminal Stalled status, requested for the 1.6 line).

  4. kvaps commented on Oct 26, 2026

    @kvaps
    Member

    We discussed this internally. I'd keep retries unbounded, endless reconciliation is how Kubernetes is supposed to work. The problem is the fixed 30s loop, so I'd replace it with exponential backoff and move the retry decision from the HelmRelease spec to flux-plunger:

    1. Generated HelmReleases go back to the Flux default policy: no Strategy, no Remediation. This is RemediateOnFailure with retries: 0 and remediateLastFailure: false, so one attempt, no uninstall, no rollback (atomic_release.go:540). The release stalls with Stalled=True/RetriesExceeded.
    2. flux-plunger retries stalled releases using the native force request: reconcile.fluxcd.io/requestedAt + reconcile.fluxcd.io/forceAt with the same token. Flux handles it even when retries are exhausted, for install, upgrade and failed releases (atomic_release.go:424, :444, :533).
    3. Delay is min(30s * 2^(n-1), cap), where n is Flux's own installFailures / upgradeFailures. Annotations don't change the generation, so Flux doesn't reset these counters and plunger needs no state of its own.
    4. A spec change resets the counters in Flux itself (reset.go), so a new desired state gets a fast first attempt. Suspended releases are not touched.

    Plunger never writes the spec, so it doesn't fight with the generators, and the first retry is still 30s, so E2E is not affected. It also reduces slot occupancy for #4145, and makes #5109 simple: an Application annotation that disables retries in plunger leaves the release in terminal Stalled state.

    Needs to be checked on a stand: retrying a failed first install goes through "forcing upgrade for failed release" and may hit has no deployed releases. Plunger already handles that, but the retry may take two steps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/bugCategorizes issue or PR as related to a bugpriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions