From 5cfa9e634c76cbd5d3cfdf77d2db84e16b81e143 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:09:02 +0000 Subject: [PATCH 1/3] Initial plan From a1c12ef387a2572f466562e33bac236eda85639c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:14:16 +0000 Subject: [PATCH 2/3] fix(pacing): use direct API reader to prevent stale cache race on global cap The pacing engine was using the informer-cache-backed client to list active pull Pods. This caused a TOCTOU race: when two CachedImage reconciles ran back-to-back, the second reconcile could read a stale cache that didn't yet reflect the pod just created by the first reconcile, causing both to launch pods and exceed the maxConcurrentPulls global cap. Fix by switching the Engine's reader to client.Reader (backed by mgr.GetAPIReader()), which reads directly from the API server and always returns an up-to-date view of active pods. --- cmd/main.go | 2 +- internal/pacing/engine.go | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0b85d6d..ec02941 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -222,7 +222,7 @@ func main() { if err = (&controller.CachedImageReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - PacingEngine: pacing.NewEngine(mgr.GetClient(), podNamespace), + PacingEngine: pacing.NewEngine(mgr.GetAPIReader(), podNamespace), Recorder: mgr.GetEventRecorder("cachedimage-controller"), PodNamespace: podNamespace, }).SetupWithManager(mgr); err != nil { diff --git a/internal/pacing/engine.go b/internal/pacing/engine.go index 219cc93..598e777 100644 --- a/internal/pacing/engine.go +++ b/internal/pacing/engine.go @@ -21,13 +21,17 @@ type Decision struct { // Engine evaluates pacing constraints before creating new drop Pods. type Engine struct { - Client client.Client + // Reader is used to list active pull Pods. It must bypass the informer cache + // (e.g. mgr.GetAPIReader()) so that pods created in the current reconcile + // cycle are visible to subsequent reconciles before the cache is updated. + Reader client.Reader PodNamespace string } -// NewEngine creates a new pacing engine. -func NewEngine(c client.Client, podNamespace string) *Engine { - return &Engine{Client: c, PodNamespace: podNamespace} +// NewEngine creates a new pacing engine. reader should be a direct API-server +// reader (mgr.GetAPIReader()) to avoid stale cache reads when counting active pods. +func NewEngine(reader client.Reader, podNamespace string) *Engine { + return &Engine{Reader: reader, PodNamespace: podNamespace} } const ( @@ -74,7 +78,7 @@ func (e *Engine) PullSlots(ctx context.Context, policy *v1alpha1.PullPolicy, cac client.InNamespace(ns), client.MatchingLabels{podbuilder.LabelManagedBy: podbuilder.LabelManagedByValue}, } - if err := e.Client.List(ctx, podList, listOpts...); err != nil { + if err := e.Reader.List(ctx, podList, listOpts...); err != nil { return Decision{}, err } From 3b104e49b008fc6d315dac9e8c390937e9e4483a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:16:52 +0000 Subject: [PATCH 3/3] fix(pacing): use direct API reader to prevent stale cache race on global cap --- docs/go.mod | 2 -- docs/go.sum | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/go.mod b/docs/go.mod index cc0eced..a8b9b26 100644 --- a/docs/go.mod +++ b/docs/go.mod @@ -1,5 +1,3 @@ module github.com/corewire/drop/docs go 1.26.0 - -require github.com/imfing/hextra v0.12.3 // indirect diff --git a/docs/go.sum b/docs/go.sum index afa8680..e69de29 100644 --- a/docs/go.sum +++ b/docs/go.sum @@ -1,2 +0,0 @@ -github.com/imfing/hextra v0.12.3 h1:DZHY2rUWYteyzjlHi9r4n7Bb5e2Q+6LXe4C1Dqn0ZjM= -github.com/imfing/hextra v0.12.3/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo=