Skip to content

a readiness gate on a user-sized operation holds one of the shard's five helm-controller slots for its whole run #4145

Open

Description

A HelmRelease whose readiness gate waits on a long operation holds one of the shard's five concurrency slots for the whole wait.

The mechanics. packages/system/flux-shard-operator/values.yaml:19 sets concurrent: 5, which internal/fluxshardoperator/provisioner.go:292 turns into --concurrent=5 for each shard's helm-controller. Application HelmReleases carry sharding.fluxcd.io/key, and helm-controller runs the Helm action synchronously inside Reconcile, so a release that waits is a slot that is occupied rather than a goroutine that is parked.

The exposure is not new. http-cache, tcp-balancer, vpn and postgres render plain Deployments, set no disable-wait, and already hold a slot for their whole Helm wait on the 10m default. What varies between kinds is what the wait is bounded by: for those it is deployment readiness, which the platform sizes, and for a kind gated on an image import it is the size of the data the user asked for.

kubernetes-rd and tenant-rd also wait, and their windows are longer still, but they arrive once with tenant setup rather than with every object a tenant creates. So the axis that matters is not whether a kind waits, it is the product of how long the wait can run and how many of that kind a tenant makes.

Two consequences worth pricing together. A gate that never resolves, as in #2908 where a cross-StorageClass clone rests in an in-flight phase indefinitely, produces a permanent duty cycle rather than one long occupancy: take a slot, hold it for the window, time out, release for the retry interval, take it again. At a 30m window and a 30s retry interval that is roughly 98% occupancy for as long as the object exists. And shrinking the window does not help, since occupancy runs at window/(window+retry) and a shorter window only raises the retry frequency.

So the lever is the shard's concurrent, or keeping kinds whose gate can wait on user-sized work off the shared shard. What would settle it is a number nobody here has: how many such objects a busy tenant creates at once, and how often one of them is permanently stuck.

Surfaced while adding a DataVolume readiness gate to vm-disk-rd, which widens that kind's window from the default to 30m.

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. lexfrei commented on Sep 7, 2026

    @lexfrei
    ContributorAuthor

    Sharper than the body, from the case that prompted this.

    A disk whose clone never completes does not reach a terminal phase. The cross-StorageClass failure in #2908 leaves the DataVolume resting in an in-flight phase indefinitely, so the gate never resolves either way. What that produces is not one long occupancy but a permanent duty cycle: the release takes a slot, holds it for the whole install window, times out, frees the slot for the retry interval, and takes it again. With a 30m window and a 30s retry interval that is roughly 98% occupancy, for as long as the disk exists and nobody deletes it.

    So the shape is worse than "a big import is slow". A handful of permanently broken disks, which nobody is watching precisely because the failure used to be silent, can hold the shard's slots more or less continuously and starve every other release on it. The disks do not have to be created simultaneously; they only have to exist.

    That also means shrinking the install window does not help, and would make it slightly worse: occupancy runs at window/(window+retry), so a shorter window raises the retry frequency while barely moving the fraction. The lever really is the shard's concurrent, or keeping kinds whose gate can wait on user-sized work off the shared shard.

    Nothing here changes the decision this issue is for; it changes the number that decision should be made against.

  3. lexfrei commented on Sep 22, 2026

    @lexfrei
    ContributorAuthor

    I traced the wait in source, and I'd move readiness for kinds that tenants create per workload out of the Helm wait and into WorkloadMonitor. Cozystack paths below are at origin/main a759190, Flux paths at the tag named in each paragraph.

    The wait holds a reconcile worker for its whole length. Cozystack pins helm-controller v1.5.0 (internal/fluxinstall/manifests/fluxcd.yaml:8097). At that tag the Helm action runs from AtomicRelease.Reconcile (internal/controller/helmrelease_controller.go:418). The waiter then calls WaitForSetWithContext (internal/action/wait.go:191). In fluxcd/pkg ssa/v0.67.1 that function blocks on <-done (ssa/manager_wait.go:152) until everything is Current, something is Failed, or the timeout fires. The number of workers is --concurrent (main.go:292 at helm-controller v1.5.0).

    A CEL error does not end the wait early. In fluxcd/pkg runtime/v0.100.2 the evaluator returns the error (runtime/cel/status_evaluator.go:120-122), and the poller records it as Unknown. The terminal check and FailFast only look at Failed (ssa/manager_wait.go:133-144 at ssa/v0.67.1). So a DataVolume without a status key waits out the full window. The same goes for one where no expression is true, which reads as InProgress (status_evaluator.go:129).

    Flux has no non-blocking mode up to v1.6.4. disableWait switches to the hook-only strategy (internal/action/wait.go:51 at v1.5.0). In that mode CEL is not evaluated at all (api/v2/helmrelease_types.go:193-199). disableWaitForJobs only drops the wait on Jobs, and both waitStrategy values go through the same waiter. CancelHealthCheckOnNewRevision interrupts a wait only when a new request for the same HelmRelease arrives, so it frees nothing for other releases. internal/action/wait.go at v1.6.4 is byte-identical to v1.5.0, so a Flux upgrade does not change this.

    The duty cycle in the issue body holds in source. A failed attempt requeues after RetryInterval (internal/controller/helmrelease_controller.go:436 at v1.5.0). Cozystack sets it to 30s for every generated release (pkg/cmd/server/start.go:90). There is no per-kind override for it.

    Neither lever in the issue body fixes this. Raising concurrent (packages/system/flux-shard-operator/values.yaml:19) only changes how many stuck releases it takes to fill a shard. Keeping a kind off the shared shard is not possible today, because the admission webhook overwrites any shard key with the tenant's shard (internal/fluxshardoperator/webhook.go:92-101). Making placement per kind would give disks a shard shared across tenants, where one tenant's stuck disks block the others.

    WorkloadMonitor can already carry this signal. The Application's WorkloadsReady condition is computed from WorkloadMonitor.Status.Operational (pkg/registry/apps/application/rest.go:1440-1495). vm-disk already renders a monitor whose selector matches its DataVolume. Right now Operational only counts pods (internal/controller/workloadmonitor_controller.go:727-729), and vm-disk sets minReplicas: 0, so it is always true. If the monitor controller reads DataVolume phases, the signal is continuous, holds no Helm slot, and also covers disks created before the change. vm-instance already sets minReplicas: 1 and virt-launcher carries the instance label, so its WorkloadsReady should already be False for a VM that is not running.

    A Helm gate is fine on the system controller. vm-default-images is installed by a PackageSource into cozy-system, gets no shard key, and is reconciled by the flux-aio helm-controller (internal/fluxinstall/manifests/fluxcd.yaml:8076). It is a single release, so the worst case is one slot there.

    A small correction to #4146: postgres-rd already sets healthCheckExprs (packages/system/postgres-rd/cozyrds/postgres.yaml:57), so vm-disk-rd would not be the first.

    Open questions that block the choice:

    1. Does the dashboard show WorkloadsReady? And should Application Ready ever include workload state? Today it deliberately does not (rest.go:1497-1506).
    2. How should a VM stopped on purpose (runStrategy: Halted) count in WorkloadsReady? It has no virt-launcher pod, so it would read as not operational.
  4. 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
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