8000 refactor(reconciler): modernize controllers to modern controller-runtime by VLukiantsev · Pull Request #807 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

refactor(reconciler): modernize controllers to modern controller-runtime - #807

Closed
VLukiantsev wants to merge 3 commits into
mainfrom
refactor/modernize-reconciler-logic
Closed

VLukiantsev wants to merge 3 commits into
mainfrom
refactor/modernize-reconciler-logic

Conversation

@VLukiantsev
@VLukiantsev VLukiantsev commented Sep 7, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

Modernize the XMC reconciler (cmd/reconciler/) to modern controller-runtime paradigms. This replaces the bespoke CQCache with native Informer cache lookups, migrates controller registration to the declarative ctrl.NewControllerManagedBy(mgr) builder pattern, integrates background worker loops into manager.Runnable with graceful flush on shutdown and LIFO cleanup, fixes JobSet Pod label querying, hardens concurrency semaphores, aligns Helm RBAC and Pod Security Standards, and strips cargo-culted logging.Strip(ctx) workarounds.

Motivation & Context

The XMC reconciler was originally built against an older version of controller-runtime (v0.5.0) and retained custom synchronization workarounds (such as a separate clusterqueue.NameCache reconciler, manual controller.New + c.Watch wiring, unmanaged background goroutines, and private context stripping). Modernizing these components eliminates redundant caching layers (-550 lines), ensures deterministic goroutine lifecycles bounded by the Kubernetes manager, prevents dropped updates during pod termination, and tightens security and RBAC invariants.

Key Changes

  • Informer Cache for ClusterQueues: Deleted the custom cmd/reconciler/clusterqueue package and NameCache. Registered &v1beta1.ClusterQueue{}: {} under cache.SelectorsByObject in manager.go and switched kueue_sync.go to list ClusterQueues directly from the informer cache via r.Client.List(ctx, &cqList).
  • Controller-Runtime Builder Pattern: Migrated jobset and workload controller setup from manual controller.New + c.Watch to ctrl.NewControllerManagedBy(mgr).For(...).WithEventFilter(...).WithOptions(...).Complete(r).
  • Lifecycle Management via manager.Runnable & Graceful Batch Flush: Registered UpdateBatcher and kueue_sync.Reconciler as manager.Runnable instances with mgr.Add(...). Added graceful shutdown flushing in BatchProcessor using a 10s budget with context.WithoutCancel(ctx) in an isolated closure. Added batcher.Flush to the Manager.Close() LIFO cleanup chain so any late updates buffered while controllers drained during shutdown are committed before the Spanner client terminates. Eliminated the detached go m.kueueSync.RunPositionInQueueSync(ctx) goroutine.
  • Concurrency Hardening & Semaphore Boundaries: Sized workloadSem using r.MaxConcurrentKueueSyncReconciles globally across all queues. Added defensive boundary checks in NewReconciler ensuring concurrency limits default to $\ge 1$ and KueueSyncTickDuration defaults to 60s when non-positive (preventing time.NewTicker(0) panics). Added select on ctx.Done() when acquiring queueSem and workloadSem during shutdown to terminate queue worker loops immediately without leaking goroutines.
  • JobSet Indexing & Pod Query Fix: Removed the unused metadata.name indexer on v1alpha2.JobSet. Fixed listJobSetPods to query Pods by client.MatchingLabels{v1alpha2.JobSetNameKey: jobSet.Name} rather than MatchingFields. Cleaned up redundant WithIndex Pod indexers in jobset/reconciler_test.go.
  • Cleanup of Obsolete Test Doubles & Compatibility Rules: Removed the bespoke fakeClient wrapper struct in kueue_sync_test.go in favor of standard fake.Client now that fetchJobSetByName uses Client.Get. Removed dead c.Watch string replacement rules from compat.bzl.
  • Helm RBAC & Pod Security Baseline: Added nodes (get, list, watch) RBAC permissions to reconciler-role for TPU topology queries. Added runAsNonRoot: true to both pod and container securityContext in deploy/helm/templates/reconciler/deployment.yaml.
  • Telemetry Context Integrity: Removed cargo-culted logging.Strip(ctx) workarounds across jobset, workload, and kueue_sync predicates and API calls.

Assumptions

  • Leader Election Kept Disabled: Leader election is not enabled because RBAC in deploy/helm/templates/reconciler/clusterrole.yaml lacks leases permissions, the reconciler is deployed as a single replica (replicas: 1) with VPA, and ungraceful leader failover risks dropping in-flight in-memory batches.
  • Handcrafted Status Equality Preserved: Handcrafted condition comparison was retained instead of generic deep-equality because metav1.Condition.LastTransitionTime changes during upstream controller heartbeats; generic DeepEqual would cause continuous reconcile loops and Spanner write storms.
  • JobSet Pod Labeling: Assumed v1alpha2.JobSetNameKey (jobset.sigs.k8s.io/jobset-name) is injected as a label by the JobSet controller on all member Pods; verified via unit tests and Kubernetes API specs.

Verification & Testing

  • Bazel Test Suite: bazel test --nocache_test_results //cmd/reconciler/... (6/6 targets passed):
    • //cmd/reconciler:reconciler_test: PASSED
    • //cmd/reconciler/hypercomputeclient:hypercomputeclient_test: PASSED
    • //cmd/reconciler/jobset:jobset_test: PASSED
    • //cmd/reconciler/kueue_sync:kueue_sync_test: PASSED (including new TestNewReconciler_Defaults)
    • //cmd/reconciler/shared:shared_test: PASSED (including TestBatchProcessorFlushesOnShutdown)
    • //cmd/reconciler/workload:workload_test: PASSED
  • Artifact Build: //cmd/reconciler:reconciler_image_tarball built successfully.
  • Helm Lint: helm lint deploy/helm passed cleanly (1 chart linted, 0 failed).
  • Pre-Commit Checks: ./.githooks/pre-commit passed cleanly (Buildifier, gofmt, golangci-lint, go test determinism, addlicense, Helm lint, and Trivy security scanner).
  • Diff Sizing Check: Total diff across 21 files is +224 / -550 lines (-326 net lines; production code changes well below the 400-line threshold).

Related Issues / Bugs

N/A

TAG=agy
CONV=e4cc9b60-fdd6-4d8e-961b-43be76e282da

- Migrate JobSet and Workload controller registration to ctrl.NewControllerManagedBy(mgr) builder pattern
- Register UpdateBatcher and KueueSync reconciler as manager.Runnable instances, eliminating unmanaged background goroutines
- Add graceful shutdown flush with 10s budget and context.WithoutCancel(ctx) in UpdateBatcher
- Remove bespoke clusterqueue.NameCache and reconciler; use informer cache (r.Client.List) with pre-warmed SelectorsByObject
- Fix Pod query in listJobSetPods to use client.MatchingLabels with v1alpha2.JobSetNameKey
- Remove redundant metadata.name indexer on JobSet
- Remove cargo-culted logging.Strip(ctx) workarounds across reconcilers and predicates

TAG=agy
CONV=e4cc9b60-fdd6-4d8e-961b-43be76e282da
@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the reconciler architecture by removing the dedicated clusterqueue reconciler and its cache, opting instead to list ClusterQueues directly from the client cache in kueue_sync. It registers background tasks like UpdateBatcher and KueueSync as runnables with the controller manager, simplifies controller setup using ctrl.NewControllerManagedBy, and cleans up unnecessary logging.Strip(ctx) calls. Additionally, UpdateBatcher now flushes pending updates on shutdown. Feedback is provided regarding a potential deadlock or panic risk in kueue_sync.go if concurrency limits are misconfigured, along with a bottleneck that restricts global concurrency.

Comment thread cmd/reconciler/kueue_sync/kueue_sync.go Resolved
…e_sync

Ensure global workload concurrency is bounded by MaxConcurrentKueueSyncReconciles
instead of being constricted to 1/10th across all queues. Validate that the limit
is at least 1 to prevent unbuffered channel deadlocks or negative size panics.

Comment link: #807 (comment)

TAG=agy
CONV=e4cc9b60-fdd6-4d8e-961b-43be76e282da
- shared/batcher: ensure shutdown flush context cancellation is deferred in closure and expose Flush()
- manager: register post-drain batcher flush in Close() cleanup LIFO chain
- controllers: simplify registerControllers signature by removing unused reconciler return
- kueue_sync: default KueueSyncTickDuration to 60s when non-positive to prevent NewTicker panic
- kueue_sync: select on ctx.Done() when acquiring queueSem and workloadSem during shutdown
- kueue_sync_test: remove obsolete fakeClient wrapper in favor of standard fake.Client
- jobset_test: remove redundant Pod WithIndex indexers since MatchingLabels is used
- compat.bzl: remove dead c.Watch compatibility replacement rules
- helm: add nodes RBAC to clusterrole and runAsNonRoot: true to reconciler deployment

TAG=agy
CONV=e4cc9b60-fdd6-4d8e-961b-43be76e282da
@VLukiantsev
VLukiantsev marked this pull request as ready for review September 7, 2026 16:31
@janzak-dev janzak-dev 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.

Please run the e2e workflow for this PR - you're touching a bunch of files that have interactions with each other that might not be caught by unit tests.

// This function CANNOT BE CALLED WITHOUT A STRIPPED CONTEXT. If it gets it's hand on an slog,
// it blocks the entire logging system for some reason.
if err := r.Get(logging.Strip(ctx), req.NamespacedName, &workload); err != nil {
if err := r.Get(ctx, req.NamespacedName, &workload); err != nil {
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.

Previously we've had a stripped context here with a comment:

	// This function CANNOT BE CALLED WITHOUT A STRIPPED CONTEXT. If it gets it's hand on an slog,
	// it blocks the entire logging system for some reason.

how have you validated that this is not the case anymore?

Comment thread cmd/reconciler/manager.go
return nil, fmt.Errorf("unable to start manager: %w", err)
}

if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha2.JobSet{}, "metadata.name", func(rawObj runtimeclient.Object) []string {
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.

JobSet Indexing & Pod Query Fix: Removed the unused metadata.name indexer on v1alpha2.JobSet. Fixed listJobSetPods to query Pods by client.MatchingLabels{v1alpha2.JobSetNameKey: jobSet.Name} rather than MatchingFields. Cleaned up redundant WithIndex Pod indexers in jobset/reconciler_test.go.

From what I see we used to have this call inside fetchJobsetByName - why are removing it? I don't understand the rationale here.

- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
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.

Why do we need this permission now?

8875 @VLukiantsev VLukiantsev closed this Sep 29, 2026
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.

2 participants

0