Skip to content

feat: make manifest hydration queue concurrency configurable (#27926) - #27948

Merged
crenshaw-dev merged 4 commits into
argoproj:masterfrom
GuruduGanesh:feat/hydration-queue-concurrency-27926
Jun 10, 2026
Merged

feat: make manifest hydration queue concurrency configurable (#27926)#27948
crenshaw-dev merged 4 commits into
argoproj:masterfrom
GuruduGanesh:feat/hydration-queue-concurrency-27926

Conversation

@GuruduGanesh

Copy link
Copy Markdown
Contributor

What / why

Closes #27926.

The manifest hydration queue used by the Source Hydrator is currently drained by a single goroutine, so source repos are hydrated one at a time. In hub-spoke topologies where a single Argo CD instance hydrates many repos, this serial processing becomes the bottleneck for how quickly changes become deployable.

This PR makes the hydration queue concurrency tunable, mirroring the existing --status-processors / --operation-processors pattern:

  • Adds --hydration-processors (env ARGOCD_APPLICATION_CONTROLLER_HYDRATION_PROCESSORS), default 5, minimum 1.
  • The hydrationQueue is drained by N workers; appHydrateQueue stays single-worker. The queue is keyed by {SourceRepoURL, SourceTargetRevision, DestinationBranch} and is rate-limiting, so the same key is never processed by two workers at once — additional workers only parallelize across distinct keys.
  • Threaded through Run(...), the application-controller manifests, argocd-cmd-params-cm, and the HA/operator docs.

Additional correctness fix (required to enable concurrency safely): running multiple workers makes a pre-existing race likely — a hydration item can be processed before all apps for a key are marked Hydrating (nil CurrentOperation), which previously panicked and could overwrite status. To handle this safely the hydrator now always commits the complete app set for a key in a single pass (the commit server records a git note per dry SHA and short-circuits later commits, so a partial commit would leave some apps marked hydrated without their manifests), and only the per-app status writes are guarded (apps not yet Hydrating are skipped and finalize on a later refresh/resync pass). The worker count is clamped to a minimum of 1 so a 0/negative flag value cannot silently stall hydration.

Checklist:

  • Either (a) I've created an enhancement proposal and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
  • The title of the PR states what changed and the related issues number (used for the release note).
  • The title of the PR conforms to the Title of the PR
  • I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
  • I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
  • Does this PR require documentation updates?
  • I've updated documentation as required by this PR.
  • I have signed off all my commits as required by DCO
  • I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
  • My build is green (troubleshooting builds).
  • My new feature complies with the feature status guidelines.
  • I have added a brief description of why this PR is necessary and/or what this PR solves.
  • Optional. My organization is added to USERS.md.
  • Optional. For bug fixes, I've indicated what older releases this fix should be cherry-picked into (this may or may not happen depending on risk/complexity).

@GuruduGanesh
GuruduGanesh requested review from a team as code owners May 20, 2026 21:46
@bunnyshell

bunnyshell Bot commented May 20, 2026

Copy link
Copy Markdown

❌ Preview Environment deleted from Bunnyshell

Available commands (reply to this comment):

  • 🚀 /bns:deploy to deploy the environment

@GuruduGanesh
GuruduGanesh force-pushed the feat/hydration-queue-concurrency-27926 branch from fa9e8f8 to 6a829c5 Compare May 20, 2026 21:57
@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.70%. Comparing base (6bb2027) to head (447b20a).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #27948      +/-   ##
==========================================
+ Coverage   64.67%   64.70%   +0.02%     
==========================================
  Files         423      423              
  Lines       58428    58437       +9     
==========================================
+ Hits        37790    37811      +21     
+ Misses      17116    17111       -5     
+ Partials     3522     3515       -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@crenshaw-dev

Copy link
Copy Markdown
Member

@GuruduGanesh picking this up this week. :-)

Comment thread controller/appcontroller.go
GuruduGanesh added a commit to GuruduGanesh/argo-cd that referenced this pull request Jun 9, 2026
…worker

Per @crenshaw-dev's review on argoproj#27948, the per-app Hydrating status update
that used to live on the single-threaded app hydrate queue now happens
inside the hydration queue worker, where the workqueue's per-key dedup
guarantees exclusive ownership of the whole app group. This removes the
race condition described in argoproj#27926 by construction (rather than guarding
each status write against a nil/stale CurrentOperation), and the
ProcessAppHydrateQueueItem worker becomes a pure enqueuer for the
hydration key.

The new helper Hydrator.markAppsHydrating runs at the start of
ProcessHydrationQueueItem and stamps every app in the group as Hydrating
before validation or hydration runs, so the success and failure paths
can drop their CurrentOperation nil guards. The complete-path-set
commit behavior is preserved.

Tests updated:
- Replace the race-window tests
  (TestProcessHydrationQueueItem_RaceConditionAppNotHydrating,
   _MixedPhases_OnlyHydratingAppsPersisted,
   _SkippedAppFinalizesOnLaterPass,
   TestSetAppHydratorError_NilCurrentOperation)
  with tests that lock the new contract:
  - _MarksAllAppsHydratingThenHydrated (mixed initial phases)
  - _MarksHydratingBeforeValidation (ordering guarantee)
  - _LargeGroupAllAppsPersisted (20-app scale check)
- _CommitsCompletePathSet kept as the partial-hydration regression
  guard.
- Updated PAHQI tests
  (_HydrationNeeded_NoCurrentOperation, _RevisionChanges) to reflect
  that PAHQI no longer mutates CurrentOperation.

Refs: argoproj#27926

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Ganesh Gurudu <ganesh.gurudu@gmail.com>
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

…j#27926)

Signed-off-by: Ganesh Gurudu <ganesh.gurudu@gmail.com>
…rebase

Signed-off-by: GuruduGanesh <ganesh.gurudu@gmail.com>
@GuruduGanesh
GuruduGanesh force-pushed the feat/hydration-queue-concurrency-27926 branch from 510787c to 9a01966 Compare June 9, 2026 20:56
…on/DeleteWithInvalidClusterName

This is an extra fix outside the scope of argoproj#27926 (hydration queue concurrency).
It addresses a pre-existing data race in TestFinalizeAppDeletion that the
"Run unit tests with -race" CI stage caught on this PR. I am including it
here because the race is currently the only thing keeping the PR from a
deterministic green CI run.

Race details (Go race detector report on CI and reproduced locally):
- Write: fakeAppCs.ReactionChain = nil
    controller/appcontroller_test.go:1165 (TestFinalizeAppDeletion.func4.1)
- Read:  k8s.io/client-go/testing.(*Fake).Invokes
    via setAppCondition → Patch, fired from the application informer's
    namespace indexer at controller/appcontroller.go:2562

The DeleteWithInvalidClusterName/app3 case sets both Destination.Name and
Destination.Server, which makes argo.GetDestinationCluster return
"destination can't have both name and server defined". The namespace
indexer in newApplicationInformerAndLister handles that error by calling
ctrl.setAppCondition, which patches the app via the fake clientset on the
informer goroutine. Meanwhile the test goroutine wipes the reactor chain
in place. The fake client's RWMutex protects against this, but the direct
field write ReactionChain = nil bypasses the mutex.

Confirmed pre-existing in upstream: reproduced 2/20 on plain upstream/master
without any of this PR's changes applied, in a disposable git worktree. The
same risky pattern (ReactionChain = nil) appears at 21 sites in this file,
but only this one trips because only DeleteWithInvalidClusterName constructs
a destination that drives the informer into setAppCondition. The other 20
sites are left untouched here to keep the fix minimal and tightly scoped.

Fix: wrap the chain read+swap in fakeAppCs.Fake.Lock/Unlock, which serializes
with the RLock that Fake.Invokes already takes. AddReactor below acquires the
lock internally.

Local verification on this branch:
- go test -race -count=40 -run '^TestFinalizeAppDeletion$' ./controller: 0 races, PASS
- go test -race -count=1 ./controller (full suite): 0 races, PASS

Refs: argoproj#27926
Signed-off-by: Ganesh Gurudu <ganesh.gurudu@gmail.com>
Signed-off-by: GuruduGanesh <ganesh.gurudu@gmail.com>
@GuruduGanesh

Copy link
Copy Markdown
Contributor Author

Thanks @crenshaw-dev, addressed.

I moved the per-app Hydrating status update into ProcessHydrationQueueItem, so the hydration queue worker now gathers the full app set for the hydration key, marks the group Hydrating, then validates/hydrates/finalizes status from there. The app hydrate queue is now only the per-app detector/enqueuer.

This is safe under parallel hydration workers because the workqueue dedups by key: ProcessHydrationQueueItem always holds exclusive ownership of the whole app group when it runs.

I also fixed a pre-existing data race that the latest CI run surfaced. That failure was in TestFinalizeAppDeletion/DeleteWithInvalidClusterName: the test was mutating the fake client ReactionChain while an informer-driven Patch could read it through Fake.Invokes. I reproduced it on plain upstream/master with -race to confirm it was not introduced by this PR. I wrapped that test setup mutation with the fake client lock, then fixed the staticcheck lint finding by using the embedded lock directly.

Local validation:

  • go test -race ./controller -run "TestFinalizeAppDeletion/DeleteWithInvalidClusterName" -count=50
  • go test -race ./controller -run "TestFinalizeAppDeletion" -count=20
  • go test -race ./controller -count=1
  • go test -race ./controller/hydrator/... -count=3

@crenshaw-dev crenshaw-dev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you, @GuruduGanesh!

@crenshaw-dev
crenshaw-dev merged commit 86936e2 into argoproj:master Jun 10, 2026
29 checks passed
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.

hydrator: Concurrency for the manifest hydration queue

2 participants