Skip to content

fix(flux): tune source-watcher concurrency + CPU to unblock platform install under load - #3163

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/source-watcher-concurrency
Jul 1, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/source-watcher-concurrency

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

source-watcher is the Flux controller that turns each PackageSource into an ExternalArtifact (one ArtifactGenerator per source, ~100 builds on the management cluster during install). The cozystack-platform HelmRelease blocks until all of those artifacts are Ready, so source-watcher's throughput sits on the install's critical path.

It was the only Flux controller left without concurrency/CPU tuning. In the bootstrap all-in-one pod it ran on the binary default (--concurrent=10) with the shared 100m CPU request; in the production FluxInstance it was absent from the reconciler patch that source-, kustomize- and helm-controller already receive. Under load its per-artifact builds finish at staggered times, leaving a random tail of not-yet-Ready artifacts that times out the platform install.

A failed run's diagnostics show the shape of this: the cozystack-platform HelmRelease timed out with timeout waiting for: PackageSource/… status: 'InProgress' (a run-to-run varying tail — e.g. bucket-application, linstor-gui, capi-provider-core), while its ~200 ExternalArtifacts reached Ready in a staggered trickle spread over ~60 seconds rather than together. That staggered completion of independent per-artifact builds is the signature of a throughput-limited builder, not a single all-or-nothing pull.

This raises source-watcher to --concurrent=20 in both install paths:

  • Bootstrap all-in-one pod (internal/fluxinstall/manifests/fluxcd.yaml, regenerated from flux-aio.cue + the flux-aio Makefile): --concurrent=20 plus a 500m CPU request. The AIO pod sets no CPU limits, so the request is what drives source-watcher's proportional CPU share when the node is saturated.
  • Production FluxInstance (packages/system/fluxcd/values.yaml): source-watcher is added to the shared reconciler patch, so it now gets the same --concurrent=20, --requeue-dependency=5s and 2000m/2048Mi limits as the other tuned controllers.

The flux-aio module exposes a per-controller resources override for the watcher (set via CUE) but no args override, so the bootstrap --concurrent bump is a guarded post-render patch in the Makefile; the embedded manifest is the product of that pipeline. source-watcher's support for both --concurrent and --requeue-dependency was confirmed against its cmd/main.go, and the production op: replace on resources/limits is safe because the distribution ships source-watcher with a resources.limits block (same shape as source-controller).

Optional follow-up, intentionally left out to keep this focused on the root cause: if the artifact-build tail still brushes the 10-minute platform HelmRelease timeout under load, the installer's HelmRelease install/upgrade timeout values give a safety margin.

Release note

fix(flux): tune source-watcher concurrency and CPU so the platform install does not time out under load

Summary by CodeRabbit

  • New Features

    • Increased the source watcher’s concurrency to improve throughput and better handle larger workloads.
    • Added higher CPU and memory settings for the watcher to support more stable operation under load.
  • Bug Fixes

    • Updated deployment patching so the watcher receives the same configuration adjustments as other controllers.

source-watcher reconciles one ArtifactGenerator per PackageSource and
builds the resulting ExternalArtifacts. On the management cluster that is
~100 artifact builds during install, and the platform HelmRelease blocks
until all of them are Ready.

It was the only Flux controller left untuned. In the bootstrap all-in-one
pod it ran without an explicit --concurrent (binary default 10) and with
the shared 100m CPU request; in the production FluxInstance it was missing
from the --concurrent / CPU patch that source-, kustomize- and
helm-controller already receive. Under load its per-artifact builds finish
at staggered times, leaving a random tail of not-yet-Ready artifacts that
times out the platform install.

Raise source-watcher to --concurrent=20 in both deployments. In the
bootstrap all-in-one pod also give it a 500m CPU request; the pod has no
CPU limits, so the request is what drives its CPU share under contention.
In the production FluxInstance add it to the shared reconciler patch, so it
now receives the same --requeue-dependency=5s and 2000m/2048Mi limits the
other tuned controllers already have.

The flux-aio module exposes a resources override for the watcher (set via
CUE) but no args override, so the bootstrap --concurrent bump is a
post-render patch in the Makefile.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 408a3da6-3dcb-4c25-8cc6-52ed0b4c566c

📥 Commits

Reviewing files that changed from the base of the PR and between 9c18260 and 41ff94e.

📒 Files selected for processing (4)
  • internal/fluxinstall/manifests/fluxcd.yaml
  • packages/core/flux-aio/Makefile
  • packages/core/flux-aio/flux-aio.cue
  • packages/system/fluxcd/values.yaml

📝 Walkthrough

Walkthrough

This PR increases the concurrency setting (--concurrent=20) and resource allocations (CPU/memory requests and limits) for the source-watcher container across the generated Flux manifest, the Makefile that regenerates it, the flux-aio CUE configuration, and the values.yaml patch target selector.

Changes

source-watcher concurrency and resource tuning

Layer / File(s) Summary
Generated manifest updates
internal/fluxinstall/manifests/fluxcd.yaml
The source-watcher container args add --concurrent=20 and its CPU request is raised from 100m to 500m.
Build tooling and resource config
packages/core/flux-aio/Makefile, packages/core/flux-aio/flux-aio.cue
The Makefile's update target injects --concurrent=20 into the source-watcher args (guarded by a presence check), and the CUE bundle config adds watcher resource requests (cpu: 500m, memory: 64Mi) and a memory limit (1Gi).
Patch target selector
packages/system/fluxcd/values.yaml
The JSONPatch target name selector regex is expanded to also match source-watcher.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: `size/M`

Suggested reviewers: `androndo`, `IvanHunters`

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: tuning Flux source-watcher concurrency and CPU to improve install behavior under load.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/source-watcher-concurrency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size/S This PR changes 10-29 lines, ignoring generated files area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/bug Categorizes issue or PR as related to a bug labels Jul 1, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review July 1, 2026 06:43
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses performance bottlenecks in the Flux source-watcher controller that were causing platform installation timeouts. By increasing concurrency and CPU resource requests, the controller can now handle the high volume of concurrent artifact builds more efficiently, preventing the staggered completion times that previously triggered installation failures.

Highlights

  • Concurrency Tuning: Increased the concurrency of the source-watcher controller from 10 to 20 to improve throughput during platform installation.
  • Resource Allocation: Increased the CPU request for the source-watcher to 500m to provide necessary headroom and prevent starvation under load.
  • Configuration Updates: Updated the production FluxInstance reconciler patch to include source-watcher and added a post-render Makefile step to inject configuration into the bootstrap manifests.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request increases the concurrency and CPU resources for the source-watcher component in the flux-aio package. Specifically, it sets --concurrent=20 and increases the CPU request to 500m for the watcher controller, updating the manifests, Makefile, Cue configuration, and Helm values accordingly. The review feedback points out that the commit scope flux used in the PR title and release note is invalid according to the repository guidelines, and suggests using fluxcd or flux-aio instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

- target:
kind: Deployment
name: "(kustomize-controller|helm-controller|source-controller)"
name: "(kustomize-controller|helm-controller|source-controller|source-watcher)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The commit scope flux used in the PR title and release note is not a valid scope according to the repository guidelines. Please use fluxcd (under System scopes) or flux-aio (as a package-specific scope) to adhere to the Conventional Commits specification for this repository.

References
  1. Each commit must follow Conventional Commits format: type(scope): brief description. Valid scopes include System scopes like 'fluxcd' or package-specific scopes matching a directory under packages/. (link)

@IvanHunters

Copy link
Copy Markdown
Collaborator

Verdict

LGTM with non-blocking notes

The load-bearing risk (the production op: replace on resources/limits failing on a missing path) is disproven at source level: flux-operator's embedded source-watcher manifest for 2.8.x ships a resources.limits block, so the patch applies cleanly. Both --concurrent and --requeue-dependency are real source-watcher flags. Remaining items are consistency/framing notes, not blockers.

Claim mismatches

[PARTIAL] "It was the only Flux controller left without concurrency/CPU tuning ... now gets the same --concurrent=20" — accurate for the production FluxInstance path, but loose for the bootstrap AIO path. In internal/fluxinstall/manifests/fluxcd.yaml the three sibling controllers run --concurrent=5 (lines 7938, 8011, 8076) and requests.cpu: 100m, while this PR sets source-watcher to --concurrent=20 and requests.cpu: 500m. So in the AIO pod source-watcher is not brought in line with its siblings — it is set 4x their concurrency and 5x their CPU request. This is defensible (source-watcher is the one on the critical path), but the "same tuning / only one left untuned" framing does not match the AIO numbers.

[PARTIAL] "source-watcher's support for both --concurrent and --requeue-dependency was confirmed against its cmd/main.go" — confirmed correct: --concurrent (default 10) and --requeue-dependency (default 5s) both exist. The PR body's "binary default --concurrent=10" is right.

Caveats

  • Path asymmetry between the two install paths: the production FluxInstance patch (packages/system/fluxcd/values.yaml:20-34) adds BOTH --concurrent=20 AND --requeue-dependency=5s to source-watcher, but the bootstrap AIO (internal/fluxinstall/manifests/fluxcd.yaml:8212) adds only --concurrent=20. The source-watcher default for --requeue-dependency is already 5s, so this is a no-op difference in practice — worth a one-line note that the AIO omission is intentional/harmless.
  • op: replace verified safe: the target path /spec/template/spec/containers/0/resources/limits exists in the source-watcher Deployment (fluxcd/source-watcher config/manager/deployment.yaml ships limits: {cpu: 1000m, memory: 1Gi}, requests: {cpu: 50m, memory: 64Mi}; flux-operator's embedded 2.8.x manifests carry the same block). containers/0 is the manager container, resulting request (50m) < limit (2000m). The patch does not fail on either fresh install or N-1 upgrade. Verified independently against both the upstream source-watcher repo and the flux-operator 2.8.0/2.8.8 embedded manifests.
  • Production upgrade behaviour: on an existing management cluster this change re-renders the source-watcher Deployment (new args + limits), triggering a rolling restart of the source-watcher pod. PackageSource→ExternalArtifact reconciliation pauses briefly during the restart — a benign, self-healing event, not an outage. The limits bump to 2000m/2048Mi does not change scheduling (requests unchanged at 50m), it only raises the CPU burst ceiling.
  • Generation-drift verified clean for the --concurrent injection: the Makefile yq guard (packages/core/flux-aio/Makefile:22) is idempotent — re-running it against the committed internal/fluxinstall/manifests/fluxcd.yaml produces zero diff and keeps a single --concurrent=20. The CUE-driven resources change (500m request) in the committed manifest was not reproduced locally (timoni not available); the CUE edit (flux-aio.cue:19-24) is well-formed and its controllers: watcher: resources maps to the committed source-watcher block. If CI does not run make -C packages/core/flux-aio update and diff, a reviewer should confirm the 500m value is what the module emits.
  • Fresh-install schedulability: the AIO pod's summed CPU request rises by 400m (source-watcher 100m→500m). The AIO sets no CPU limit, so this only affects proportional share under saturation, not scheduling hard-fail unless the node is already near capacity.

Recommended follow-ups

  • Reword the PR body's "only controller left untuned / same tuning" to acknowledge the AIO siblings run --concurrent=5 / 100m, so the AIO source-watcher is intentionally tuned higher rather than brought to parity.
  • Latent fragility on the shared op: replace (not introduced here, but now extended to source-watcher): distribution.version is the floating range 2.8.x, while op: replace hard-requires resources/limits to already exist in whatever patch version flux-operator resolves. If a future 2.8.z ships any of these controllers without a limits block, the op: replace fails the entire FluxInstance kustomize build and stalls flux cluster-wide. op: add on the same path (which succeeds whether or not the member pre-exists) or pinning the distribution to an exact version would remove this class of risk for all four controllers at once.
  • Consider pinning the flux-aio module (flux-aio.cue:8 uses version: "latest") in a separate change — pre-existing, not introduced here, but it makes the AIO regeneration non-reproducible.

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

Verdict

LGTM with non-blocking notes

The load-bearing risk (the production op: replace on resources/limits failing on a missing path) is disproven at source level: flux-operator's embedded source-watcher manifest for 2.8.x ships a resources.limits block, so the patch applies cleanly. Both --concurrent and --requeue-dependency are real source-watcher flags. Remaining items are consistency/framing notes, not blockers.

Claim mismatches

[PARTIAL] "It was the only Flux controller left without concurrency/CPU tuning ... now gets the same --concurrent=20" — accurate for the production FluxInstance path, but loose for the bootstrap AIO path. In internal/fluxinstall/manifests/fluxcd.yaml the three sibling controllers run --concurrent=5 (lines 7938, 8011, 8076) and requests.cpu: 100m, while this PR sets source-watcher to --concurrent=20 and requests.cpu: 500m. So in the AIO pod source-watcher is not brought in line with its siblings — it is set 4x their concurrency and 5x their CPU request. This is defensible (source-watcher is the one on the critical path), but the "same tuning / only one left untuned" framing does not match the AIO numbers.

[PARTIAL] "source-watcher's support for both --concurrent and --requeue-dependency was confirmed against its cmd/main.go" — confirmed correct: --concurrent (default 10) and --requeue-dependency (default 5s) both exist. The PR body's "binary default --concurrent=10" is right.

Caveats

  • Path asymmetry between the two install paths: the production FluxInstance patch (packages/system/fluxcd/values.yaml:20-34) adds BOTH --concurrent=20 AND --requeue-dependency=5s to source-watcher, but the bootstrap AIO (internal/fluxinstall/manifests/fluxcd.yaml:8212) adds only --concurrent=20. The source-watcher default for --requeue-dependency is already 5s, so this is a no-op difference in practice — worth a one-line note that the AIO omission is intentional/harmless.
  • op: replace verified safe: the target path /spec/template/spec/containers/0/resources/limits exists in the source-watcher Deployment (fluxcd/source-watcher config/manager/deployment.yaml ships limits: {cpu: 1000m, memory: 1Gi}, requests: {cpu: 50m, memory: 64Mi}; flux-operator's embedded 2.8.x manifests carry the same block). containers/0 is the manager container, resulting request (50m) < limit (2000m). The patch does not fail on either fresh install or N-1 upgrade. Verified independently against both the upstream source-watcher repo and the flux-operator 2.8.0/2.8.8 embedded manifests.
  • Production upgrade behaviour: on an existing management cluster this change re-renders the source-watcher Deployment (new args + limits), triggering a rolling restart of the source-watcher pod. PackageSource→ExternalArtifact reconciliation pauses briefly during the restart — a benign, self-healing event, not an outage. The limits bump to 2000m/2048Mi does not change scheduling (requests unchanged at 50m), it only raises the CPU burst ceiling.
  • Generation-drift verified clean for the --concurrent injection: the Makefile yq guard (packages/core/flux-aio/Makefile:22) is idempotent — re-running it against the committed internal/fluxinstall/manifests/fluxcd.yaml produces zero diff and keeps a single --concurrent=20. The CUE-driven resources change (500m request) in the committed manifest was not reproduced locally (timoni not available); the CUE edit (flux-aio.cue:19-24) is well-formed and its controllers: watcher: resources maps to the committed source-watcher block. If CI does not run make -C packages/core/flux-aio update and diff, a reviewer should confirm the 500m value is what the module emits.
  • Fresh-install schedulability: the AIO pod's summed CPU request rises by 400m (source-watcher 100m→500m). The AIO sets no CPU limit, so this only affects proportional share under saturation, not scheduling hard-fail unless the node is already near capacity.

Recommended follow-ups

  • Reword the PR body's "only controller left untuned / same tuning" to acknowledge the AIO siblings run --concurrent=5 / 100m, so the AIO source-watcher is intentionally tuned higher rather than brought to parity.
  • Latent fragility on the shared op: replace (not introduced here, but now extended to source-watcher): distribution.version is the floating range 2.8.x, while op: replace hard-requires resources/limits to already exist in whatever patch version flux-operator resolves. If a future 2.8.z ships any of these controllers without a limits block, the op: replace fails the entire FluxInstance kustomize build and stalls flux cluster-wide. op: add on the same path (which succeeds whether or not the member pre-exists) or pinning the distribution to an exact version would remove this class of risk for all four controllers at once.
  • Consider pinning the flux-aio module (flux-aio.cue:8 uses version: "latest") in a separate change — pre-existing, not introduced here, but it makes the AIO regeneration non-reproducible.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 03435dc into main Jul 1, 2026
42 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/source-watcher-concurrency branch July 1, 2026 10:20
IvanHunters added a commit that referenced this pull request Jul 4, 2026
…tus-patch stalls

Symptom
  cozystack-platform HelmRelease times out at 15m on fresh install
  with a single PackageSource stuck in InProgress. Snapshot from
  #3178 CI run 28587656430:

    PackageSource cozystack.vm-disk-application
      Ready: Unknown, reason: Progressing
      lastTransitionTime: 2026-07-02T13:39:26Z
      (snapshot generated 30+ minutes later — no state change)

    ArtifactGenerator cozystack.vm-disk-application
      status.inventory:              2 items with sha256 digests
      status.observedSourcesDigest:  populated
      status.conditions[Ready]:      Unknown, reason: Progressing
      status.conditions[Reconciling]: True, reason: Progressing

  ExternalArtifacts are produced. Downstream Flux consumes them and
  its HelmReleases install fine. But PackageSourceReconciler
  faithfully copies the Unknown Ready condition through, so
  cozystack-platform (which waits on PackageSource.Ready=True) never
  converges.

Root cause
  Traced in fluxcd/source-watcher:v2.1.0 log for the affected
  ArtifactGenerator:

    13:39:28.475  ExternalArtifact vm-disk       reconciled ✅
    13:39:28.641  ExternalArtifact vm-disk-rd    reconciled ✅
    13:39:45.719  ❌ failed to update status: etcdserver: request timed out
    13:39:45.720  ❌ Reconciler error (etcd timeout)
    13:39:46.485  ✅ No drift detected, 2 artifact(s) up to date
    ——— nothing else for this ArtifactGenerator ———

  Under first-install etcd pressure the patchStatus call times out.
  The reconciler returns the etcd error; controller-runtime prints
  the "returned both a result and non-nil error, RequeueAfter
  ignored" warning and requeues. The next reconcile hits the "No
  drift detected" short-circuit (artifacts are already up to date)
  and early-exits before touching status. Result: the Ready
  condition is stuck Unknown until either the process restarts or
  something forces a state change on the ArtifactGenerator. Neither
  happens in a real install run.

  This is an upstream bug in source-watcher's reconcile loop. It has
  been papered over once already at the runtime-tuning level in
  #3163 (source-watcher concurrency + CPU), but a bad etcd second
  during install still lands us here. Cozystack cannot afford to
  wait for an upstream release cycle to unblock every install.

Fix
  PackageSourceReconciler.updateStatus now checks two invariants
  before falling back to copying the (potentially stuck) upstream
  Ready condition:

    1. ArtifactGenerator.status.Inventory is non-empty
    2. ArtifactGenerator.status.ObservedSourcesDigest is set

  Both fields are only written by source-watcher after the artifacts
  have actually been produced — status.Inventory carries the
  ExternalArtifact digests downstream Flux is already consuming. If
  BOTH are present AND the upstream Ready condition is still Unknown
  (or absent), the PackageSource Ready condition is synthesised as
  True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus`
  and a message that names the upstream bug and the observable state
  it relied on. If the upstream Ready ever catches up (True or False)
  the Owns() watch re-fires the reconciler and the real condition
  overwrites the synthetic one — True stays True, False surfaces the
  real failure. Failures are NEVER masked: the synthesis path only
  triggers when Ready is Unknown or missing.

Safety
  * Never synthesises True over a real False → upstream failure is
    surfaced unchanged.
  * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set
    → a fresh ArtifactGenerator that has literally not started work
    cannot be prematurely marked Ready.
  * ObservedGeneration is stamped so a spec change re-invalidates
    the synthesis until the next observable-artifacts pass.
  * Reason string is greppable so operators can audit which
    PackageSources came up via the workaround.

Tests
  Added TestArtifactGeneratorObservablyReady with 7 cases covering
  every branch of the predicate:
    - fresh generator: false
    - inventory only:  false
    - digest only:     false
    - both set + Unknown Ready:   true  (the actual bug)
    - both set + no Ready cond:   true  (same window pre-stamp)
    - both set + Ready=True:      false (pass real condition)
    - both set + Ready=False:     false (never mask failure)
  All pass; existing operator unit tests are unaffected.

Signed-off-by: Ivan Okhotnikov <[email protected]>
IvanHunters added a commit that referenced this pull request Jul 4, 2026
…tus-patch stalls

Symptom
  cozystack-platform HelmRelease times out at 15m on fresh install
  with a single PackageSource stuck in InProgress. Snapshot from
  #3178 CI run 28587656430:

    PackageSource cozystack.vm-disk-application
      Ready: Unknown, reason: Progressing
      lastTransitionTime: 2026-07-02T13:39:26Z
      (snapshot generated 30+ minutes later — no state change)

    ArtifactGenerator cozystack.vm-disk-application
      status.inventory:              2 items with sha256 digests
      status.observedSourcesDigest:  populated
      status.conditions[Ready]:      Unknown, reason: Progressing
      status.conditions[Reconciling]: True, reason: Progressing

  ExternalArtifacts are produced. Downstream Flux consumes them and
  its HelmReleases install fine. But PackageSourceReconciler
  faithfully copies the Unknown Ready condition through, so
  cozystack-platform (which waits on PackageSource.Ready=True) never
  converges.

Root cause
  Traced in fluxcd/source-watcher:v2.1.0 log for the affected
  ArtifactGenerator:

    13:39:28.475  ExternalArtifact vm-disk       reconciled ✅
    13:39:28.641  ExternalArtifact vm-disk-rd    reconciled ✅
    13:39:45.719  ❌ failed to update status: etcdserver: request timed out
    13:39:45.720  ❌ Reconciler error (etcd timeout)
    13:39:46.485  ✅ No drift detected, 2 artifact(s) up to date
    ——— nothing else for this ArtifactGenerator ———

  Under first-install etcd pressure the patchStatus call times out.
  The reconciler returns the etcd error; controller-runtime prints
  the "returned both a result and non-nil error, RequeueAfter
  ignored" warning and requeues. The next reconcile hits the "No
  drift detected" short-circuit (artifacts are already up to date)
  and early-exits before touching status. Result: the Ready
  condition is stuck Unknown until either the process restarts or
  something forces a state change on the ArtifactGenerator. Neither
  happens in a real install run.

  This is an upstream bug in source-watcher's reconcile loop. It has
  been papered over once already at the runtime-tuning level in
  #3163 (source-watcher concurrency + CPU), but a bad etcd second
  during install still lands us here. Cozystack cannot afford to
  wait for an upstream release cycle to unblock every install.

Fix
  PackageSourceReconciler.updateStatus now checks two invariants
  before falling back to copying the (potentially stuck) upstream
  Ready condition:

    1. ArtifactGenerator.status.Inventory is non-empty
    2. ArtifactGenerator.status.ObservedSourcesDigest is set

  Both fields are only written by source-watcher after the artifacts
  have actually been produced — status.Inventory carries the
  ExternalArtifact digests downstream Flux is already consuming. If
  BOTH are present AND the upstream Ready condition is still Unknown
  (or absent), the PackageSource Ready condition is synthesised as
  True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus`
  and a message that names the upstream bug and the observable state
  it relied on. If the upstream Ready ever catches up (True or False)
  the Owns() watch re-fires the reconciler and the real condition
  overwrites the synthetic one — True stays True, False surfaces the
  real failure. Failures are NEVER masked: the synthesis path only
  triggers when Ready is Unknown or missing.

Safety
  * Never synthesises True over a real False → upstream failure is
    surfaced unchanged.
  * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set
    → a fresh ArtifactGenerator that has literally not started work
    cannot be prematurely marked Ready.
  * ObservedGeneration is stamped so a spec change re-invalidates
    the synthesis until the next observable-artifacts pass.
  * Reason string is greppable so operators can audit which
    PackageSources came up via the workaround.

Tests
  Added TestArtifactGeneratorObservablyReady with 7 cases covering
  every branch of the predicate:
    - fresh generator: false
    - inventory only:  false
    - digest only:     false
    - both set + Unknown Ready:   true  (the actual bug)
    - both set + no Ready cond:   true  (same window pre-stamp)
    - both set + Ready=True:      false (pass real condition)
    - both set + Ready=False:     false (never mask failure)
  All pass; existing operator unit tests are unaffected.

Signed-off-by: Ivan Okhotnikov <[email protected]>
IvanHunters added a commit that referenced this pull request Jul 5, 2026
…tus-patch stalls

Symptom
  cozystack-platform HelmRelease times out at 15m on fresh install
  with a single PackageSource stuck in InProgress. Snapshot from
  #3178 CI run 28587656430:

    PackageSource cozystack.vm-disk-application
      Ready: Unknown, reason: Progressing
      lastTransitionTime: 2026-07-02T13:39:26Z
      (snapshot generated 30+ minutes later — no state change)

    ArtifactGenerator cozystack.vm-disk-application
      status.inventory:              2 items with sha256 digests
      status.observedSourcesDigest:  populated
      status.conditions[Ready]:      Unknown, reason: Progressing
      status.conditions[Reconciling]: True, reason: Progressing

  ExternalArtifacts are produced. Downstream Flux consumes them and
  its HelmReleases install fine. But PackageSourceReconciler
  faithfully copies the Unknown Ready condition through, so
  cozystack-platform (which waits on PackageSource.Ready=True) never
  converges.

Root cause
  Traced in fluxcd/source-watcher:v2.1.0 log for the affected
  ArtifactGenerator:

    13:39:28.475  ExternalArtifact vm-disk       reconciled ✅
    13:39:28.641  ExternalArtifact vm-disk-rd    reconciled ✅
    13:39:45.719  ❌ failed to update status: etcdserver: request timed out
    13:39:45.720  ❌ Reconciler error (etcd timeout)
    13:39:46.485  ✅ No drift detected, 2 artifact(s) up to date
    ——— nothing else for this ArtifactGenerator ———

  Under first-install etcd pressure the patchStatus call times out.
  The reconciler returns the etcd error; controller-runtime prints
  the "returned both a result and non-nil error, RequeueAfter
  ignored" warning and requeues. The next reconcile hits the "No
  drift detected" short-circuit (artifacts are already up to date)
  and early-exits before touching status. Result: the Ready
  condition is stuck Unknown until either the process restarts or
  something forces a state change on the ArtifactGenerator. Neither
  happens in a real install run.

  This is an upstream bug in source-watcher's reconcile loop. It has
  been papered over once already at the runtime-tuning level in
  #3163 (source-watcher concurrency + CPU), but a bad etcd second
  during install still lands us here. Cozystack cannot afford to
  wait for an upstream release cycle to unblock every install.

Fix
  PackageSourceReconciler.updateStatus now checks two invariants
  before falling back to copying the (potentially stuck) upstream
  Ready condition:

    1. ArtifactGenerator.status.Inventory is non-empty
    2. ArtifactGenerator.status.ObservedSourcesDigest is set

  Both fields are only written by source-watcher after the artifacts
  have actually been produced — status.Inventory carries the
  ExternalArtifact digests downstream Flux is already consuming. If
  BOTH are present AND the upstream Ready condition is still Unknown
  (or absent), the PackageSource Ready condition is synthesised as
  True with a distinctive reason `ArtifactsGeneratedAwaitingUpstreamStatus`
  and a message that names the upstream bug and the observable state
  it relied on. If the upstream Ready ever catches up (True or False)
  the Owns() watch re-fires the reconciler and the real condition
  overwrites the synthetic one — True stays True, False surfaces the
  real failure. Failures are NEVER masked: the synthesis path only
  triggers when Ready is Unknown or missing.

Safety
  * Never synthesises True over a real False → upstream failure is
    surfaced unchanged.
  * Requires BOTH Inventory non-empty AND ObservedSourcesDigest set
    → a fresh ArtifactGenerator that has literally not started work
    cannot be prematurely marked Ready.
  * ObservedGeneration is stamped so a spec change re-invalidates
    the synthesis until the next observable-artifacts pass.
  * Reason string is greppable so operators can audit which
    PackageSources came up via the workaround.

Tests
  Added TestArtifactGeneratorObservablyReady with 7 cases covering
  every branch of the predicate:
    - fresh generator: false
    - inventory only:  false
    - digest only:     false
    - both set + Unknown Ready:   true  (the actual bug)
    - both set + no Ready cond:   true  (same window pre-stamp)
    - both set + Ready=True:      false (pass real condition)
    - both set + Ready=False:     false (never mask failure)
  All pass; existing operator unit tests are unaffected.

Signed-off-by: Ivan Okhotnikov <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/bug Categorizes issue or PR as related to a bug size/S This PR changes 10-29 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants