@@ -44,7 +44,11 @@ import (
4444
4545var (
4646 errPoolNotSynced = errors .New ("InferencePool is not initialized in data store" )
47- AllPodsPredicate = func (_ fwkdl.Endpoint ) bool { return true }
47+ // errRegistrationDropped reports an endpoint that could not be tracked: its collector is
48+ // still registered from an earlier registration (an upsert overlapping an in-flight delete)
49+ // or failed to start. Callers match it with errors.Is to decide whether to retry.
50+ errRegistrationDropped = errors .New ("endpoint registration dropped: collector already registered or failed to start" )
51+ AllPodsPredicate = func (_ fwkdl.Endpoint ) bool { return true }
4852)
4953
5054const (
@@ -88,10 +92,15 @@ type Datastore interface {
8892
8993 // PodList lists pods matching the given predicate.
9094 PodList (predicate func (fwkdl.Endpoint ) bool ) []fwkdl.Endpoint
91- PodUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod ) bool
95+ // PodUpdateOrAddIfNotExist stores or updates the endpoints for the given pod. It returns an
96+ // error when an endpoint registration was dropped (see upsertEndpoint); the pod is then not
97+ // tracked by the datastore and the caller must retry (e.g. by requeuing the reconcile).
98+ PodUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod ) error
9299 PodDelete (podName string )
93100
94101 // EndpointUpsert adds or updates an endpoint from a non-Kubernetes discovery source.
102+ // A dropped registration is logged; the endpoint stays untracked until the discovery
103+ // source re-emits it.
95104 EndpointUpsert (ctx context.Context , meta * fwkdl.EndpointMetadata )
96105 // EndpointDelete removes the endpoint with the given namespaced name.
97106 EndpointDelete (id types.NamespacedName )
@@ -130,6 +139,11 @@ type datastore struct {
130139 // key: types.NamespacedName, value: fwkdl.Endpoint
131140 pods * sync.Map
132141 epf datalayer.EndpointFactory
142+ // needsResync forces the next PoolSet to run podResyncAll even when the pool is unchanged.
143+ // PoolSet stores the pool before resyncing, so without this flag a PoolSet retried after a
144+ // resync failure would compare the incoming pool against the already-stored identical pool
145+ // and skip the resync. Guarded by mu.
146+ needsResync bool
133147}
134148
135149func (ds * datastore ) WithEndpointPool (pool * datalayer.EndpointPool ) Datastore {
@@ -167,7 +181,7 @@ func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, endpoint
167181 selectorChanged := oldEndpointPool == nil || ! selectorEqual (oldEndpointPool .Selector , endpointPool .Selector )
168182 targetPortsChanged := oldEndpointPool != nil && ! slices .Equal (oldEndpointPool .TargetPorts , endpointPool .TargetPorts )
169183
170- if selectorChanged || targetPortsChanged {
184+ if selectorChanged || targetPortsChanged || ds . needsResync {
171185 logger .V (logutil .DEFAULT ).Info ("Updating endpoints" , "selector" , endpointPool .Selector , "targetPortsChanged" , targetPortsChanged )
172186 // A full resync is required to address the following cases:
173187 // 1) At startup, the pod events may get processed before the pool is synced with the datastore,
@@ -178,8 +192,10 @@ func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, endpoint
178192 // 3) If the targetPorts changed, we need to resync to remove orphaned rank endpoints that no longer
179193 // exist in the new targetPorts configuration.
180194 if err := ds .podResyncAll (ctx , reader ); err != nil {
195+ ds .needsResync = true
181196 return fmt .Errorf ("failed to update pods according to the pool selector - %w" , err )
182197 }
198+ ds .needsResync = false
183199 }
184200
185201 return nil
@@ -282,23 +298,30 @@ func (ds *datastore) PodList(predicate func(fwkdl.Endpoint) bool) []fwkdl.Endpoi
282298 return res
283299}
284300
285- func (ds * datastore ) PodUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod ) bool {
301+ func (ds * datastore ) PodUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod ) error {
286302 // Take a reference to pool under read lock to avoid racing with PoolSet().
287303 // This is safe because PoolSet() replaces the entire pool struct rather than
288304 // updating it in-place.
289305 ds .mu .RLock ()
290306 pool := ds .pool
291307 ds .mu .RUnlock ()
292308
309+ if pool == nil {
310+ // Without the pool's target ports the pod cannot be mapped to endpoints; the resync
311+ // triggered when the pool syncs picks the pod up.
312+ log .FromContext (ctx ).V (logutil .DEBUG ).Info ("Skipping pod upsert, InferencePool not synced" , "name" , pod .Name )
313+ return nil
314+ }
293315 return ds .podUpdateOrAddIfNotExist (ctx , pod , pool )
294316}
295317
296318// podUpdateOrAddIfNotExist is the lock-free inner implementation.
297- // Callers must ensure pool is a consistent snapshot (either read under lock
319+ // Callers must ensure pool is a non-nil consistent snapshot (either read under lock
298320// or already held, as in podResyncAll which runs under ds.mu.Lock via PoolSet).
299- func (ds * datastore ) podUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod , pool * datalayer.EndpointPool ) bool {
321+ // It returns a joined error covering every endpoint of the pod whose registration was dropped.
322+ func (ds * datastore ) podUpdateOrAddIfNotExist (ctx context.Context , pod * corev1.Pod , pool * datalayer.EndpointPool ) error {
300323 if pool == nil {
301- return true
324+ return nil
302325 }
303326
304327 labels := make (map [string ]string , len (pod .GetLabels ()))
@@ -329,12 +352,26 @@ func (ds *datastore) podUpdateOrAddIfNotExist(ctx context.Context, pod *corev1.P
329352 "pod" , pod .Name , "namespace" , pod .Namespace , "targetPorts" , pool .TargetPorts )
330353 }
331354
332- result := true
355+ added := false
356+ var errs []error
333357 existingEpSet := sets.Set [types.NamespacedName ]{}
334358 for _ , endpointMetadata := range pods {
335359 existingEpSet .Insert (endpointMetadata .NamespacedName )
336- if ds .upsertEndpoint (ctx , endpointMetadata ) {
337- result = false
360+ created , err := ds .upsertEndpoint (ctx , endpointMetadata )
361+ if err != nil {
362+ errs = append (errs , err )
363+ continue
364+ }
365+ if created {
366+ added = true
367+ }
368+ }
369+ logger := log .FromContext (ctx )
370+ if len (errs ) == 0 {
371+ if added {
372+ logger .V (logutil .DEFAULT ).Info ("Pod added" , "name" , pod .Name )
373+ } else {
374+ logger .V (logutil .DEFAULT ).Info ("Pod already exists" , "name" , pod .Name )
338375 }
339376 }
340377
@@ -351,7 +388,7 @@ func (ds *datastore) podUpdateOrAddIfNotExist(ctx context.Context, pod *corev1.P
351388 }
352389 }
353390
354- return result
391+ return errors . Join ( errs ... )
355392}
356393
357394func (ds * datastore ) PodDelete (podName string ) {
@@ -366,7 +403,9 @@ func (ds *datastore) PodDelete(podName string) {
366403}
367404
368405func (ds * datastore ) EndpointUpsert (ctx context.Context , meta * fwkdl.EndpointMetadata ) {
369- ds .upsertEndpoint (ctx , meta )
406+ if _ , err := ds .upsertEndpoint (ctx , meta ); err != nil {
407+ log .FromContext (ctx ).Error (err , "failed to register endpoint" , "endpoint" , meta .NamespacedName )
408+ }
370409}
371410
372411func (ds * datastore ) EndpointDelete (id types.NamespacedName ) {
@@ -376,29 +415,38 @@ func (ds *datastore) EndpointDelete(id types.NamespacedName) {
376415}
377416
378417// upsertEndpoint stores or updates a single endpoint in the pods map.
379- // Returns true if the endpoint was newly created, false if it already existed
380- // or if NewEndpoint returned nil (duplicate-start race).
418+ // Returns true if the endpoint was newly created, false if it already existed.
381419// Shared by EndpointUpsert and podUpdateOrAddIfNotExist.
382- func (ds * datastore ) upsertEndpoint (ctx context.Context , meta * fwkdl.EndpointMetadata ) bool {
383- existing , ok := ds .pods .Load (meta .NamespacedName )
384- if ! ok {
420+ //
421+ // It returns an error wrapping errRegistrationDropped when the endpoint cannot be tracked:
422+ // NewEndpoint returns nil when a collector is still registered for this endpoint or the
423+ // collector failed to start. When a concurrent upsert has stored an entry for the key, this
424+ // call's metadata is applied through the update path; when no entry exists (the upsert
425+ // overlapped an in-flight delete that removed the entry before deregistering the collector,
426+ // or the collector failed to start), the endpoint is untracked and the caller must retry.
427+ func (ds * datastore ) upsertEndpoint (ctx context.Context , meta * fwkdl.EndpointMetadata ) (bool , error ) {
428+ for {
429+ if existing , ok := ds .pods .Load (meta .NamespacedName ); ok {
430+ ep := existing .(fwkdl.Endpoint )
431+ if ep .GetMetadata ().Equal (meta ) {
432+ return false , nil
433+ }
434+ ep .UpdateMetadata (meta )
435+ ds .epf .UpdateEndpoint (ctx , ep )
436+ return false , nil
437+ }
385438 ep := ds .epf .NewEndpoint (ds .parentCtx , meta )
386439 if ep == nil {
387- // NewEndpoint returns nil when a collector is already running for this
388- // endpoint (duplicate reconcile race). The existing entry in ds.pods
389- // is still valid; skip re-registering it.
390- return false
440+ if _ , ok := ds .pods .Load (meta .NamespacedName ); ok {
441+ // A concurrent upsert won the registration; apply this call's metadata through
442+ // the update path above.
443+ continue
444+ }
445+ return false , fmt .Errorf ("endpoint %s: %w" , meta .NamespacedName , errRegistrationDropped )
391446 }
392447 ds .pods .Store (meta .NamespacedName , ep )
393- return true
448+ return true , nil
394449 }
395- ep := existing .(fwkdl.Endpoint )
396- if ep .GetMetadata ().Equal (meta ) {
397- return false
398- }
399- ep .UpdateMetadata (meta )
400- ds .epf .UpdateEndpoint (ctx , ep )
401- return false
402450}
403451
404452func (ds * datastore ) podResyncAll (ctx context.Context , reader client.Reader ) error {
@@ -414,19 +462,18 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err
414462 // Track active endpoints by their full name (including rank suffix).
415463 // This ensures orphaned rank endpoints are removed when targetPorts shrinks.
416464 activeEndpoints := sets .New [types.NamespacedName ]()
465+ var errs []error
417466 for _ , pod := range podList .Items {
418467 if ! podutil .IsPodReady (& pod ) {
419468 continue
420469 }
421- namespacedName := types.NamespacedName {Name : pod .Name , Namespace : pod .Namespace }
422470 // Calculate expected endpoint names based on current targetPorts.
423471 for idx := range ds .pool .TargetPorts {
424472 activeEndpoints .Insert (createEndpointNamespacedName (& pod , idx ))
425473 }
426- if ! ds .podUpdateOrAddIfNotExist (ctx , & pod , ds .pool ) {
427- logger .V (logutil .DEFAULT ).Info ("Pod added" , "name" , namespacedName )
428- } else {
429- logger .V (logutil .DEFAULT ).Info ("Pod already exists" , "name" , namespacedName )
474+ if err := ds .podUpdateOrAddIfNotExist (ctx , & pod , ds .pool ); err != nil {
475+ // Propagate so PoolSet fails; needsResync makes the retried PoolSet resync again.
476+ errs = append (errs , err )
430477 }
431478 }
432479
@@ -442,7 +489,7 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err
442489 return true
443490 })
444491
445- return nil
492+ return errors . Join ( errs ... )
446493}
447494
448495// extractActivePorts extracts the active ports from a pod's annotations.
0 commit comments