e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
962 lines
34 KiB
Go
962 lines
34 KiB
Go
// Copyright 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
gerrors "errors"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/fields"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/apimachinery/pkg/util/json"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/record"
|
|
"k8s.io/client-go/util/retry"
|
|
"k8s.io/client-go/util/workqueue"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/eviction"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/recycle"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils"
|
|
controllerutils "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/controller"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/expectations"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/fieldindex"
|
|
)
|
|
|
|
const (
|
|
defaultRetryTime = 5 * time.Second
|
|
)
|
|
|
|
const (
|
|
LabelPoolName = "sandbox.opensandbox.io/pool-name"
|
|
LabelPoolRevision = "sandbox.opensandbox.io/pool-revision"
|
|
)
|
|
|
|
const (
|
|
defaultSyncSandboxAllocConcurrency = 256
|
|
envSyncSandboxAllocConcurrency = "SYNC_SANDBOX_ALLOC_CONCURRENCY"
|
|
|
|
defaultRecyclePodConcurrency = 64
|
|
envRecyclePodConcurrency = "RECYCLE_POD_CONCURRENCY"
|
|
)
|
|
|
|
var (
|
|
PoolScaleExpectations = expectations.NewScaleExpectations()
|
|
syncSandboxAllocConcurrency int
|
|
recyclePodConcurrency int
|
|
)
|
|
|
|
func init() {
|
|
syncSandboxAllocConcurrency = defaultSyncSandboxAllocConcurrency
|
|
if val := os.Getenv(envSyncSandboxAllocConcurrency); val != "" {
|
|
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
|
syncSandboxAllocConcurrency = n
|
|
}
|
|
}
|
|
recyclePodConcurrency = defaultRecyclePodConcurrency
|
|
if val := os.Getenv(envRecyclePodConcurrency); val != "" {
|
|
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
|
recyclePodConcurrency = n
|
|
}
|
|
}
|
|
}
|
|
|
|
// PoolReconciler reconciles a Pool object
|
|
type PoolReconciler struct {
|
|
client.Client
|
|
Scheme *runtime.Scheme
|
|
Recorder record.EventRecorder
|
|
Allocator Allocator
|
|
RestConfig *rest.Config
|
|
}
|
|
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=pools,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=pools/status,verbs=get;update;patch
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=pools/finalizers,verbs=update
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes,verbs=get;list;watch;patch
|
|
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create
|
|
// +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch
|
|
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
|
|
|
|
func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
|
|
log := logf.FromContext(ctx)
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Info("Reconcile finished", "duration", time.Since(start).String(), "requeueAfter", result.RequeueAfter.String(), "error", retErr)
|
|
}()
|
|
// Fetch the Pool instance
|
|
pool := &sandboxv1alpha1.Pool{}
|
|
if err := r.Get(ctx, req.NamespacedName, pool); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
// Pool resource not found, could have been deleted
|
|
controllerKey := req.NamespacedName.String()
|
|
PoolScaleExpectations.DeleteExpectations(controllerKey)
|
|
r.Allocator.ClearPoolAllocation(ctx, req.Namespace, req.Name)
|
|
log.Info("Pool resource not found, cleaned up scale expectations", "pool", controllerKey)
|
|
return ctrl.Result{}, nil
|
|
}
|
|
// Error reading the object - requeue the request
|
|
log.Error(err, "Failed to get Pool")
|
|
return ctrl.Result{}, err
|
|
}
|
|
if !pool.DeletionTimestamp.IsZero() {
|
|
controllerKey := controllerutils.GetControllerKey(pool)
|
|
PoolScaleExpectations.DeleteExpectations(controllerKey)
|
|
r.Allocator.ClearPoolAllocation(ctx, req.Namespace, req.Name)
|
|
log.Info("Pool resource is being deleted, cleaned up scale expectations", "pool", controllerKey)
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
// List all pods of the pool
|
|
podList := &corev1.PodList{}
|
|
if err := r.List(ctx, podList, &client.ListOptions{
|
|
Namespace: pool.Namespace,
|
|
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForOwnerRefUID: string(pool.UID)}),
|
|
}); err != nil {
|
|
log.Error(err, "Failed to list pods")
|
|
return reconcile.Result{}, err
|
|
}
|
|
pods := make([]*corev1.Pod, 0, len(podList.Items))
|
|
for i := range podList.Items {
|
|
pod := podList.Items[i]
|
|
PoolScaleExpectations.ObserveScale(controllerutils.GetControllerKey(pool), expectations.Create, pod.Name)
|
|
if pod.DeletionTimestamp.IsZero() {
|
|
pods = append(pods, &pod)
|
|
}
|
|
}
|
|
|
|
// List all batch sandboxes ref to the pool
|
|
batchSandboxList := &sandboxv1alpha1.BatchSandboxList{}
|
|
if err := r.List(ctx, batchSandboxList, &client.ListOptions{
|
|
Namespace: pool.Namespace,
|
|
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForPoolRef: pool.Name}),
|
|
}); err != nil {
|
|
log.Error(err, "Failed to list batch sandboxes")
|
|
return reconcile.Result{}, err
|
|
}
|
|
batchSandboxes := make([]*sandboxv1alpha1.BatchSandbox, 0, len(batchSandboxList.Items))
|
|
for i := range batchSandboxList.Items {
|
|
batchSandbox := batchSandboxList.Items[i]
|
|
if batchSandbox.Spec.Template != nil {
|
|
continue
|
|
}
|
|
batchSandboxes = append(batchSandboxes, &batchSandbox)
|
|
}
|
|
log.Info("Pool reconcile", "pool", pool.Name, "pods", len(pods), "batchSandboxes", len(batchSandboxes))
|
|
return r.reconcilePool(ctx, pool, batchSandboxes, pods)
|
|
}
|
|
|
|
// reconcilePool contains the main reconciliation logic
|
|
func (r *PoolReconciler) reconcilePool(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (ctrl.Result, error) {
|
|
var result ctrl.Result
|
|
|
|
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
|
// 1. Get latest Pool CR
|
|
latestPool := &sandboxv1alpha1.Pool{}
|
|
if err := r.Get(ctx, client.ObjectKeyFromObject(pool), latestPool); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Handle pod eviction
|
|
schedulePods, evictionErr := r.handleEviction(ctx, latestPool, pods)
|
|
if schedulePods == nil {
|
|
return evictionErr
|
|
}
|
|
|
|
// 3. Schedule sandbox (compute + persist + sync)
|
|
schedResult, err := r.scheduleSandbox(ctx, latestPool, batchSandboxes, schedulePods)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Requeue if there are pending sandboxes waiting for scheduling
|
|
if schedResult.SupplyCnt > 0 {
|
|
result = ctrl.Result{RequeueAfter: defaultRetryTime}
|
|
}
|
|
|
|
// 4. Handle pool upgrade
|
|
updateResult, err := r.updatePool(ctx, latestPool, schedulePods, schedResult.IdlePods)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 5. Handle pool scale
|
|
toDeletePods := append(updateResult.ToDeletePods, schedResult.ToDelete...)
|
|
args := &scaleArgs{
|
|
updateRevision: updateResult.UpdateRevision,
|
|
pods: schedulePods,
|
|
totalPodCnt: int32(len(pods)),
|
|
allocatedCnt: int32(len(schedResult.LatestAllocation)),
|
|
idlePods: updateResult.IdlePods,
|
|
toDeletePods: toDeletePods,
|
|
supplyCnt: schedResult.SupplyCnt + updateResult.SupplyUpdateRevision,
|
|
}
|
|
|
|
if err := r.scalePool(ctx, latestPool, args); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 6. Update pool status
|
|
if err := r.updatePoolStatus(ctx, updateResult.UpdateRevision, latestPool, pods, schedulePods, schedResult.LatestAllocation); err != nil {
|
|
return err
|
|
}
|
|
|
|
if evictionErr != nil {
|
|
return evictionErr
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return result, err
|
|
}
|
|
|
|
func (r *PoolReconciler) calculateRevision(pool *sandboxv1alpha1.Pool) (string, error) {
|
|
template, err := json.Marshal(pool.Spec.Template)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
revision := sha256.Sum256(template)
|
|
return hex.EncodeToString(revision[:8]), nil
|
|
}
|
|
|
|
// SetupWithManager sets up the controller with the Manager.
|
|
// Todo pod deletion expectations
|
|
func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int) error {
|
|
filterBatchSandbox := predicate.Funcs{
|
|
CreateFunc: func(e event.CreateEvent) bool {
|
|
bsb, ok := e.Object.(*sandboxv1alpha1.BatchSandbox)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return bsb.Spec.PoolRef != ""
|
|
},
|
|
UpdateFunc: func(e event.UpdateEvent) bool {
|
|
oldObj, okOld := e.ObjectOld.(*sandboxv1alpha1.BatchSandbox)
|
|
newObj, okNew := e.ObjectNew.(*sandboxv1alpha1.BatchSandbox)
|
|
if !okOld || !okNew {
|
|
return false
|
|
}
|
|
if newObj.Spec.PoolRef == "" {
|
|
return false
|
|
}
|
|
oldVal := oldObj.Annotations[AnnoAllocReleaseKey]
|
|
newVal := newObj.Annotations[AnnoAllocReleaseKey]
|
|
if oldVal != newVal {
|
|
return true
|
|
}
|
|
if oldObj.Spec.Replicas != newObj.Spec.Replicas {
|
|
return true
|
|
}
|
|
// Trigger reconcile when sandbox enters terminating state (DeletionTimestamp is set).
|
|
if oldObj.DeletionTimestamp.IsZero() && !newObj.DeletionTimestamp.IsZero() {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
DeleteFunc: func(e event.DeleteEvent) bool {
|
|
bsb, ok := e.Object.(*sandboxv1alpha1.BatchSandbox)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return bsb.Spec.PoolRef != ""
|
|
},
|
|
GenericFunc: func(e event.GenericEvent) bool {
|
|
bsb, ok := e.Object.(*sandboxv1alpha1.BatchSandbox)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return bsb.Spec.PoolRef != ""
|
|
},
|
|
}
|
|
|
|
findPoolForBatchSandbox := func(ctx context.Context, obj client.Object) []reconcile.Request {
|
|
log := logf.FromContext(ctx)
|
|
batchSandbox, ok := obj.(*sandboxv1alpha1.BatchSandbox)
|
|
if !ok {
|
|
log.Error(nil, "Invalid object type, expected BatchSandbox")
|
|
return nil
|
|
}
|
|
return []reconcile.Request{
|
|
{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: batchSandbox.Namespace,
|
|
Name: batchSandbox.Spec.PoolRef,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
filterBatchSandboxDetached := predicate.Funcs{
|
|
UpdateFunc: func(e event.UpdateEvent) bool {
|
|
oldObj, okOld := e.ObjectOld.(*sandboxv1alpha1.BatchSandbox)
|
|
newObj, okNew := e.ObjectNew.(*sandboxv1alpha1.BatchSandbox)
|
|
if !okOld || !okNew {
|
|
return false
|
|
}
|
|
return oldObj.Spec.PoolRef != "" && newObj.Spec.PoolRef == ""
|
|
},
|
|
}
|
|
|
|
enqueueOldPoolForDetachedBatchSandbox := handler.Funcs{
|
|
UpdateFunc: func(_ context.Context, e event.UpdateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
|
oldObj, ok := e.ObjectOld.(*sandboxv1alpha1.BatchSandbox)
|
|
if !ok || oldObj.Spec.PoolRef == "" {
|
|
return
|
|
}
|
|
q.Add(reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: oldObj.Namespace,
|
|
Name: oldObj.Spec.PoolRef,
|
|
},
|
|
})
|
|
},
|
|
}
|
|
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
For(&sandboxv1alpha1.Pool{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
|
|
Owns(&corev1.Pod{}).
|
|
Watches(
|
|
&sandboxv1alpha1.BatchSandbox{},
|
|
handler.EnqueueRequestsFromMapFunc(findPoolForBatchSandbox),
|
|
builder.WithPredicates(filterBatchSandbox),
|
|
).
|
|
Watches(
|
|
&sandboxv1alpha1.BatchSandbox{},
|
|
enqueueOldPoolForDetachedBatchSandbox,
|
|
builder.WithPredicates(filterBatchSandboxDetached),
|
|
).
|
|
Named("pool").
|
|
WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}).
|
|
Complete(r)
|
|
}
|
|
|
|
func (r *PoolReconciler) doAllocate(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod, toAllocate map[string][]string) error {
|
|
// 1. Compute latest allocated pods per sandbox (merge current + newly allocated).
|
|
toSyncMap := r.getLatestAllocated(ctx, pool, batchSandboxes, toAllocate)
|
|
|
|
// 2. Concurrently sync each sandbox's Allocated annotation (AddFinalizer is called inside SyncSandboxAllocation).
|
|
return r.syncSandboxConcurrently(ctx, batchSandboxes, toSyncMap, r.Allocator.SyncSandboxAllocation, "allocated")
|
|
}
|
|
|
|
// getLatestAllocated computes the latest allocated pods for each sandbox by merging current allocation with new pods to allocate.
|
|
func (r *PoolReconciler) getLatestAllocated(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, toAllocate map[string][]string) map[string][]string {
|
|
log := logf.FromContext(ctx)
|
|
sandboxByName := make(map[string]*sandboxv1alpha1.BatchSandbox, len(batchSandboxes))
|
|
for _, bs := range batchSandboxes {
|
|
sandboxByName[bs.Name] = bs
|
|
}
|
|
|
|
toSyncMap := make(map[string][]string, len(toAllocate))
|
|
for sandboxName, allocPods := range toAllocate {
|
|
if len(allocPods) == 0 {
|
|
continue
|
|
}
|
|
sandbox, ok := sandboxByName[sandboxName]
|
|
if !ok {
|
|
log.Error(nil, "Sandbox not found for allocate", "sandbox", sandboxName)
|
|
continue
|
|
}
|
|
currentAllocated, err := r.Allocator.GetSandboxAllocation(ctx, sandbox)
|
|
if err != nil {
|
|
log.Error(err, "Failed to get sandbox allocated", "sandbox", sandboxName)
|
|
continue
|
|
}
|
|
toSyncMap[sandboxName] = append(currentAllocated, allocPods...)
|
|
}
|
|
return toSyncMap
|
|
}
|
|
|
|
// syncSandboxConcurrently syncs allocation or released state for each sandbox concurrently.
|
|
// Each sandbox is an independent resource, so concurrent writes are safe.
|
|
func (r *PoolReconciler) syncSandboxConcurrently(ctx context.Context, batchSandboxes []*sandboxv1alpha1.BatchSandbox, toSyncMap map[string][]string, syncFn func(context.Context, *sandboxv1alpha1.BatchSandbox, []string) error, label string) error {
|
|
log := logf.FromContext(ctx)
|
|
|
|
sandboxByName := make(map[string]*sandboxv1alpha1.BatchSandbox, len(batchSandboxes))
|
|
for _, bs := range batchSandboxes {
|
|
sandboxByName[bs.Name] = bs
|
|
}
|
|
|
|
errCh := make(chan error, len(toSyncMap))
|
|
sem := make(chan struct{}, syncSandboxAllocConcurrency)
|
|
var wg sync.WaitGroup
|
|
for sandboxName, pods := range toSyncMap {
|
|
sandbox, ok := sandboxByName[sandboxName]
|
|
if !ok {
|
|
log.Error(nil, "Sandbox not found for sync "+label, "sandbox", sandboxName)
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
if err := syncFn(ctx, sandbox, pods); err != nil {
|
|
log.Error(err, "Failed to sync sandbox "+label, "sandbox", sandbox.Name)
|
|
errCh <- err
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errCh)
|
|
var errs []error
|
|
for err := range errCh {
|
|
errs = append(errs, err)
|
|
}
|
|
return gerrors.Join(errs...)
|
|
}
|
|
|
|
func (r *PoolReconciler) doRecycle(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod, toRecycle map[string][]string) (map[string][]string, []string, error) {
|
|
if len(toRecycle) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
handler, err := recycle.NewHandler(r.Client, r.RestConfig, pool)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get recycle handler for pool %s: %w", pool.Name, err)
|
|
}
|
|
|
|
results := r.runRecycleTasks(ctx, pool, pods, toRecycle, handler)
|
|
return collectRecycleResults(ctx, results)
|
|
}
|
|
|
|
type recycleResult struct {
|
|
sandboxName string
|
|
podName string
|
|
status *recycle.Status
|
|
err error
|
|
}
|
|
|
|
// runRecycleTasks executes TryRecycle concurrently for each (sandbox, pod) pair and
|
|
// returns one result per task.
|
|
func (r *PoolReconciler) runRecycleTasks(ctx context.Context, pool *sandboxv1alpha1.Pool, pods []*corev1.Pod, toRecycle map[string][]string, handler recycle.Handler) []recycleResult {
|
|
podByName := make(map[string]*corev1.Pod, len(pods))
|
|
for _, p := range pods {
|
|
podByName[p.Name] = p
|
|
}
|
|
|
|
// Flatten the map into an ordered slice so goroutines can write by index.
|
|
type task struct {
|
|
sandboxName string
|
|
podName string
|
|
}
|
|
var tasks []task
|
|
for sandboxName, podNames := range toRecycle {
|
|
for _, podName := range podNames {
|
|
tasks = append(tasks, task{sandboxName: sandboxName, podName: podName})
|
|
}
|
|
}
|
|
|
|
// Results are written by index so each goroutine writes to a unique slot without synchronization.
|
|
results := make([]recycleResult, len(tasks))
|
|
sem := make(chan struct{}, recyclePodConcurrency)
|
|
var wg sync.WaitGroup
|
|
for idx, task := range tasks {
|
|
localIdx, localTask := idx, task
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
status, err := handler.TryRecycle(ctx, pool, podByName[localTask.podName], &recycle.Spec{ID: localTask.sandboxName})
|
|
results[localIdx] = recycleResult{sandboxName: localTask.sandboxName, podName: localTask.podName, status: status, err: err}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// collectRecycleResults aggregates per-task results into the succeed map and delete list.
|
|
func collectRecycleResults(ctx context.Context, results []recycleResult) (map[string][]string, []string, error) {
|
|
log := logf.FromContext(ctx)
|
|
succeedMap := make(map[string][]string)
|
|
var toDeletePods []string
|
|
var errs []error
|
|
|
|
for _, res := range results {
|
|
if res.err != nil {
|
|
log.Error(res.err, "Failed to recycle pod", "pod", res.podName, "sandbox", res.sandboxName)
|
|
errs = append(errs, res.err)
|
|
continue
|
|
}
|
|
if res.status.State == recycle.StateSucceeded {
|
|
succeedMap[res.sandboxName] = append(succeedMap[res.sandboxName], res.podName)
|
|
}
|
|
if res.status.NeedDelete {
|
|
toDeletePods = append(toDeletePods, res.podName)
|
|
}
|
|
}
|
|
return succeedMap, toDeletePods, gerrors.Join(errs...)
|
|
}
|
|
|
|
// doRelease runs the recycle operation for pods to be returned to the pool,
|
|
// then persists the released state to each sandbox's annotation.
|
|
func (r *PoolReconciler) doRelease(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod, toRelease map[string][]string) ([]string, error) {
|
|
log := logf.FromContext(ctx)
|
|
|
|
// 1. Recycle pods.
|
|
succeedMap, toDeletePods, err := r.doRecycle(ctx, pool, batchSandboxes, pods, toRelease)
|
|
if err != nil {
|
|
log.Error(err, "Some errors occurred during recycle")
|
|
r.Recorder.Eventf(pool, corev1.EventTypeWarning, EventReasonFailedRecyclePod, "Failed to recycle some pods: %v", err)
|
|
}
|
|
|
|
// Emit recycle success events.
|
|
var allRecycled []string
|
|
for _, recycledPods := range succeedMap {
|
|
allRecycled = append(allRecycled, recycledPods...)
|
|
}
|
|
if len(allRecycled) > 0 {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonPodRecycled, "Recycled %d pod(s): %v", len(allRecycled), allRecycled)
|
|
}
|
|
|
|
// 2. Compute latest released pods per sandbox (merge current + recycle-succeeded).
|
|
// Also collect orphan pods whose sandboxes no longer exist.
|
|
toSyncMap, orphanPods := r.getLatestReleased(ctx, batchSandboxes, succeedMap)
|
|
|
|
// 3. Concurrently sync each sandbox's Released annotation.
|
|
syncErr := r.syncSandboxConcurrently(ctx, batchSandboxes, toSyncMap, r.Allocator.SyncSandboxReleased, "released")
|
|
if syncErr != nil {
|
|
log.Error(syncErr, "Failed to sync released")
|
|
}
|
|
|
|
// 4. Release in-memory allocations for orphan pods directly (no annotation to persist).
|
|
if len(orphanPods) > 0 {
|
|
r.Allocator.ReleasePodsAllocation(ctx, pool.Namespace, pool.Name, orphanPods)
|
|
}
|
|
|
|
return toDeletePods, gerrors.Join(err, syncErr)
|
|
}
|
|
|
|
// getLatestReleased computes the latest released pods for each sandbox by merging current released with recycle-succeeded pods.
|
|
// It also returns orphanPods: pods from succeedMap whose sandbox no longer exists and should be released directly by the caller.
|
|
func (r *PoolReconciler) getLatestReleased(ctx context.Context, batchSandboxes []*sandboxv1alpha1.BatchSandbox, succeedMap map[string][]string) (map[string][]string, []string) {
|
|
log := logf.FromContext(ctx)
|
|
sandboxByName := make(map[string]*sandboxv1alpha1.BatchSandbox, len(batchSandboxes))
|
|
for _, bs := range batchSandboxes {
|
|
sandboxByName[bs.Name] = bs
|
|
}
|
|
|
|
toSyncMap := make(map[string][]string, len(succeedMap))
|
|
orphanPods := make([]string, 0)
|
|
for sandboxName, succeedPods := range succeedMap {
|
|
if len(succeedPods) == 0 {
|
|
continue
|
|
}
|
|
sandbox, ok := sandboxByName[sandboxName]
|
|
if !ok {
|
|
// Orphan sandbox: deleted before recycle completed. Collect its pods for direct release.
|
|
log.Info("GC: sandbox not found for recycle result, collecting orphan pods", "sandbox", sandboxName, "pods", succeedPods)
|
|
orphanPods = append(orphanPods, succeedPods...)
|
|
continue
|
|
}
|
|
currentReleased, err := r.Allocator.GetSandboxReleased(ctx, sandbox)
|
|
if err != nil {
|
|
log.Error(err, "Failed to get sandbox released", "sandbox", sandboxName)
|
|
continue
|
|
}
|
|
toSyncMap[sandboxName] = append(currentReleased, succeedPods...)
|
|
}
|
|
return toSyncMap, orphanPods
|
|
}
|
|
|
|
func (r *PoolReconciler) scheduleSandbox(ctx context.Context, pool *sandboxv1alpha1.Pool, batchSandboxes []*sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (*ScheduleResult, error) {
|
|
log := logf.FromContext(ctx)
|
|
// 1. Compute scheduling actions.
|
|
spec := &AllocSpec{
|
|
Sandboxes: batchSandboxes,
|
|
Pool: pool,
|
|
Pods: pods,
|
|
}
|
|
allocAction, err := r.Allocator.Schedule(ctx, spec)
|
|
if err != nil {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeWarning, EventReasonAllocationFailed, "Failed to schedule sandboxes: %v", err)
|
|
return nil, err
|
|
}
|
|
log.Info("Allocate action", "pool", pool.Name, "toAllocate", allocAction.ToAllocate, "toRelease", allocAction.ToRelease)
|
|
|
|
// 2. Execute scheduling actions.
|
|
// 2.1 Execute ToAllocate / update in-memory store.
|
|
err = r.doAllocate(ctx, pool, batchSandboxes, pods, allocAction.ToAllocate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Emit allocation events.
|
|
sandboxByName := make(map[string]*sandboxv1alpha1.BatchSandbox, len(batchSandboxes))
|
|
for _, bs := range batchSandboxes {
|
|
sandboxByName[bs.Name] = bs
|
|
}
|
|
for sandboxName, allocPods := range allocAction.ToAllocate {
|
|
if len(allocPods) == 0 {
|
|
continue
|
|
}
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonAllocationSucceeded,
|
|
"Allocated %d pod(s) to sandbox %s: %v", len(allocPods), sandboxName, allocPods)
|
|
if sbx, ok := sandboxByName[sandboxName]; ok {
|
|
r.Recorder.Eventf(sbx, corev1.EventTypeNormal, EventReasonScheduled,
|
|
"Successfully assigned %d pod(s) from pool %s: %v", len(allocPods), pool.Name, allocPods)
|
|
}
|
|
}
|
|
|
|
// 2.2 Execute ToRelease / release in-memory store.
|
|
toDeletePods, err := r.doRelease(ctx, pool, batchSandboxes, pods, allocAction.ToRelease)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 3. Return schedule result
|
|
latestAllocation, err := r.Allocator.GetPoolAllocation(ctx, pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
idlePods := make([]string, 0)
|
|
for _, pod := range pods {
|
|
if _, ok := latestAllocation[pod.Name]; !ok {
|
|
idlePods = append(idlePods, pod.Name)
|
|
}
|
|
}
|
|
result := &ScheduleResult{
|
|
LatestAllocation: latestAllocation,
|
|
IdlePods: idlePods,
|
|
ToDelete: toDeletePods,
|
|
SupplyCnt: allocAction.PodSupplement,
|
|
}
|
|
|
|
log.Info("Schedule result", "pool", pool.Name, "toDeletePods", toDeletePods, "supplyCnt", allocAction.PodSupplement)
|
|
return result, nil
|
|
}
|
|
|
|
func (r *PoolReconciler) updatePool(ctx context.Context, pool *sandboxv1alpha1.Pool, pods []*corev1.Pod, idlePods []string) (*UpdateResult, error) {
|
|
updateRevision, err := r.calculateRevision(pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
strategy := NewPoolUpdateStrategy(pool)
|
|
result := strategy.Compute(ctx, updateRevision, pods, idlePods)
|
|
result.UpdateRevision = updateRevision
|
|
|
|
if len(result.ToDeletePods) > 0 {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonPodUpdated,
|
|
"Rolling update: deleting %d pod(s) for revision %s: %v", len(result.ToDeletePods), updateRevision, result.ToDeletePods)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type scaleArgs struct {
|
|
updateRevision string
|
|
pods []*corev1.Pod
|
|
totalPodCnt int32 // all pods including evicting ones, for PoolMax enforcement
|
|
allocatedCnt int32
|
|
supplyCnt int32 // to create
|
|
idlePods []string
|
|
toDeletePods []string
|
|
}
|
|
|
|
type ScheduleResult struct {
|
|
// LatestAllocation is the most recent pod-to-sandbox allocation map.
|
|
LatestAllocation map[string]string
|
|
// IdlePods contains pods that are not currently allocated to any sandbox.
|
|
IdlePods []string
|
|
// ToDelete contains pods that the recycle handler has decided to delete
|
|
// (e.g. direct deletion or restart failure fallback).
|
|
ToDelete []string
|
|
// SupplyCnt is the number of additional pods the allocator needs but are not yet available.
|
|
SupplyCnt int32
|
|
}
|
|
|
|
type UpdateResult struct {
|
|
UpdateRevision string
|
|
IdlePods []string
|
|
ToDeletePods []string
|
|
// Supply Pods with update revision
|
|
SupplyUpdateRevision int32
|
|
}
|
|
|
|
func (r *PoolReconciler) scalePool(ctx context.Context, pool *sandboxv1alpha1.Pool, args *scaleArgs) error {
|
|
log := logf.FromContext(ctx)
|
|
errs := make([]error, 0)
|
|
pods := args.pods
|
|
if satisfied, unsatisfiedDuration, dirtyPods := PoolScaleExpectations.SatisfiedExpectations(controllerutils.GetControllerKey(pool)); !satisfied {
|
|
if unsatisfiedDuration >= expectations.ExpectationTimeout {
|
|
log.Info("Pool scale expectations timed out, clearing stale expectations",
|
|
"unsatisfiedDuration", unsatisfiedDuration, "dirtyPods", dirtyPods)
|
|
PoolScaleExpectations.DeleteExpectations(controllerutils.GetControllerKey(pool))
|
|
} else {
|
|
log.Info("Pool scale is not ready, requeue", "unsatisfiedDuration", unsatisfiedDuration, "dirtyPods", dirtyPods)
|
|
return fmt.Errorf("pool scale is not ready, %v", pool.Name)
|
|
}
|
|
}
|
|
schedulableCnt := int32(len(args.pods))
|
|
totalPodCnt := args.totalPodCnt
|
|
allocatedCnt := args.allocatedCnt
|
|
supplyCnt := args.supplyCnt
|
|
toDeletePods := args.toDeletePods
|
|
bufferCnt := schedulableCnt - allocatedCnt
|
|
|
|
// Calculate desired buffer cnt.
|
|
desiredBufferCnt := bufferCnt
|
|
if bufferCnt < pool.Spec.CapacitySpec.BufferMin || bufferCnt > pool.Spec.CapacitySpec.BufferMax {
|
|
desiredBufferCnt = (pool.Spec.CapacitySpec.BufferMin + pool.Spec.CapacitySpec.BufferMax) / 2
|
|
}
|
|
|
|
// Calculate desired schedulable cnt.
|
|
desiredSchedulableCnt := max(allocatedCnt+supplyCnt+desiredBufferCnt, pool.Spec.CapacitySpec.PoolMin)
|
|
// Enforce PoolMax: limit new pods based on total running pods (including evicting).
|
|
maxNewPods := max(pool.Spec.CapacitySpec.PoolMax-totalPodCnt, 0)
|
|
|
|
log.Info("Scale pool decision", "pool", pool.Name,
|
|
"totalPodCnt", totalPodCnt, "schedulableCnt", schedulableCnt,
|
|
"allocatedCnt", allocatedCnt, "bufferCnt", bufferCnt,
|
|
"desiredBufferCnt", desiredBufferCnt, "supplyCnt", supplyCnt,
|
|
"desiredSchedulableCnt", desiredSchedulableCnt, "maxNewPods", maxNewPods,
|
|
"toDeletePods", len(toDeletePods), "idlePods", len(args.idlePods))
|
|
|
|
// Scale-up: create new pods if needed and allowed by PoolMax
|
|
if desiredSchedulableCnt > schedulableCnt && maxNewPods > 0 {
|
|
createCnt := min(desiredSchedulableCnt-schedulableCnt, maxNewPods)
|
|
scaleMaxUnavailable := r.getScaleMaxUnavailable(pool, desiredSchedulableCnt)
|
|
notReadyCnt := r.countNotReadyPods(pods)
|
|
limitedCreateCnt := scaleMaxUnavailable - notReadyCnt
|
|
createCnt = max(0, min(createCnt, limitedCreateCnt))
|
|
if createCnt > 0 {
|
|
log.Info("Scaling up pool with constraint", "pool", pool.Name,
|
|
"createCnt", createCnt, "scaleMaxUnavailable", scaleMaxUnavailable,
|
|
"notReadyCnt", notReadyCnt, "desiredSchedulableCnt", desiredSchedulableCnt, "limitedCreateCnt", limitedCreateCnt)
|
|
for range createCnt {
|
|
if err := r.createPoolPod(ctx, pool, args.updateRevision); err != nil {
|
|
log.Error(err, "Failed to create pool pod")
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scale-down: delete redundant or excess pods
|
|
scaleIn := int32(0)
|
|
if desiredSchedulableCnt < schedulableCnt {
|
|
scaleIn = schedulableCnt - desiredSchedulableCnt
|
|
}
|
|
if scaleIn > 0 || len(toDeletePods) > 0 {
|
|
podsToDelete := r.pickPodsToDelete(pods, args.idlePods, args.toDeletePods, scaleIn)
|
|
log.Info("Scaling down pool", "pool", pool.Name, "scaleIn", scaleIn, "toDeletePods", len(toDeletePods), "podsToDelete", len(podsToDelete))
|
|
for _, pod := range podsToDelete {
|
|
log.Info("Deleting pool pod", "pool", pool.Name, "pod", pod.Name)
|
|
if err := r.Delete(ctx, pod); err != nil {
|
|
log.Error(err, "Failed to delete pool pod", "pod", pod.Name)
|
|
r.Recorder.Eventf(pool, corev1.EventTypeWarning, EventReasonFailedDelete, "Failed to delete pool pod %s: %v", pod.Name, err)
|
|
errs = append(errs, err)
|
|
} else {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonSuccessfulDelete, "Deleted pool pod %s (scale-down)", pod.Name)
|
|
}
|
|
}
|
|
}
|
|
return gerrors.Join(errs...)
|
|
}
|
|
|
|
func (r *PoolReconciler) updatePoolStatus(ctx context.Context, updateRevision string, pool *sandboxv1alpha1.Pool, pods []*corev1.Pod, schedulePods []*corev1.Pod, podAllocation map[string]string) error {
|
|
oldStatus := pool.Status.DeepCopy()
|
|
availableCnt := int32(0)
|
|
for _, pod := range schedulePods {
|
|
if _, ok := podAllocation[pod.Name]; ok {
|
|
continue
|
|
}
|
|
if !utils.IsPodReady(pod) {
|
|
continue
|
|
}
|
|
availableCnt++
|
|
}
|
|
updatedCnt := int32(0)
|
|
for _, pod := range pods {
|
|
if pod.Labels[LabelPoolRevision] == updateRevision {
|
|
updatedCnt++
|
|
}
|
|
}
|
|
pool.Status.ObservedGeneration = pool.Generation
|
|
pool.Status.Total = int32(len(pods))
|
|
pool.Status.Allocated = int32(len(podAllocation))
|
|
pool.Status.Available = availableCnt
|
|
pool.Status.Revision = updateRevision
|
|
pool.Status.Updated = updatedCnt
|
|
if equality.Semantic.DeepEqual(*oldStatus, pool.Status) {
|
|
return nil
|
|
}
|
|
log := logf.FromContext(ctx)
|
|
log.Info("Update pool status", "ObservedGeneration", pool.Status.ObservedGeneration, "Total", pool.Status.Total,
|
|
"Allocated", pool.Status.Allocated, "Available", pool.Status.Available, "Revision", pool.Status.Revision, "Updated", pool.Status.Updated)
|
|
if err := r.Status().Update(ctx, pool); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PoolReconciler) pickPodsToDelete(pods []*corev1.Pod, idlePodNames []string, toDeletePodNames []string, scaleIn int32) []*corev1.Pod {
|
|
podMap := make(map[string]*corev1.Pod)
|
|
for _, pod := range pods {
|
|
podMap[pod.Name] = pod
|
|
}
|
|
|
|
var podsToDelete []*corev1.Pod
|
|
for _, name := range toDeletePodNames {
|
|
pod, ok := podMap[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
podsToDelete = append(podsToDelete, pod)
|
|
}
|
|
|
|
var idlePods []*corev1.Pod
|
|
for _, name := range idlePodNames {
|
|
pod, ok := podMap[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
idlePods = append(idlePods, pod)
|
|
}
|
|
sort.Slice(idlePods, func(i, j int) bool {
|
|
return idlePods[i].CreationTimestamp.Before(&idlePods[j].CreationTimestamp)
|
|
})
|
|
for _, pod := range idlePods {
|
|
if scaleIn <= 0 {
|
|
break
|
|
}
|
|
if pod.DeletionTimestamp == nil {
|
|
podsToDelete = append(podsToDelete, pod)
|
|
}
|
|
scaleIn -= 1
|
|
}
|
|
return podsToDelete
|
|
}
|
|
|
|
// getScaleMaxUnavailable returns the resolved maxUnavailable value.
|
|
// If not specified, defaults to 25% of desiredTotal.
|
|
// Minimum return value is 1 to ensure scaling progress.
|
|
func (r *PoolReconciler) getScaleMaxUnavailable(pool *sandboxv1alpha1.Pool, desiredTotal int32) int32 {
|
|
defaultPercentage := intstr.FromString("25%")
|
|
|
|
maxUnavailable := &defaultPercentage
|
|
if pool.Spec.ScaleStrategy != nil && pool.Spec.ScaleStrategy.MaxUnavailable != nil {
|
|
maxUnavailable = pool.Spec.ScaleStrategy.MaxUnavailable
|
|
}
|
|
|
|
result, err := intstr.GetScaledValueFromIntOrPercent(maxUnavailable, int(desiredTotal), true)
|
|
if err != nil || result < 1 {
|
|
result = 1
|
|
}
|
|
return int32(result)
|
|
}
|
|
|
|
// countNotReadyPods returns the count of pods that are not ready.
|
|
// A pod is considered not ready if it doesn't have a Ready condition
|
|
// with status True.
|
|
func (r *PoolReconciler) countNotReadyPods(pods []*corev1.Pod) int32 {
|
|
var count int32
|
|
for _, pod := range pods {
|
|
if !utils.IsPodReady(pod) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (r *PoolReconciler) createPoolPod(ctx context.Context, pool *sandboxv1alpha1.Pool, updateRevision string) error {
|
|
log := logf.FromContext(ctx)
|
|
pod, err := utils.GetPodFromTemplate(pool.Spec.Template, pool, metav1.NewControllerRef(pool, sandboxv1alpha1.SchemeBuilder.GroupVersion.WithKind("Pool")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pod.Namespace = pool.Namespace
|
|
pod.Name = ""
|
|
pod.GenerateName = pool.Name + "-"
|
|
pod.Labels[LabelPoolName] = pool.Name
|
|
pod.Labels[LabelPoolRevision] = updateRevision
|
|
if err := ctrl.SetControllerReference(pool, pod, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
if err := r.Create(ctx, pod); err != nil {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeWarning, EventReasonFailedCreate, "Failed to create pool pod: %v", err)
|
|
return err
|
|
}
|
|
PoolScaleExpectations.ExpectScale(controllerutils.GetControllerKey(pool), expectations.Create, pod.Name)
|
|
log.Info("Created pool pod", "pool", pool.Name, "pod", pod.Name, "revision", updateRevision)
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonSuccessfulCreate, "Created pool pod: %v", pod.Name)
|
|
return nil
|
|
}
|
|
|
|
// handleEviction fetches the current allocation, evicts idle pods marked for eviction,
|
|
// and returns the schedulable pods (excluding evicting idle pods) along with any eviction error.
|
|
// Eviction errors are non-fatal: they are returned to trigger a requeue but do not block the current reconcile.
|
|
func (r *PoolReconciler) handleEviction(ctx context.Context, pool *sandboxv1alpha1.Pool, pods []*corev1.Pod) ([]*corev1.Pod, error) {
|
|
log := logf.FromContext(ctx)
|
|
|
|
podAllocation, err := r.Allocator.GetPoolAllocation(ctx, pool)
|
|
if err != nil {
|
|
log.Error(err, "Failed to get pool allocation")
|
|
return nil, err
|
|
}
|
|
|
|
handler := eviction.NewEvictionHandler(ctx, r.Client, pool)
|
|
|
|
var evictionErrs []error
|
|
filtered := make([]*corev1.Pod, 0, len(pods))
|
|
for _, pod := range pods {
|
|
if !handler.NeedsEviction(pod) {
|
|
filtered = append(filtered, pod)
|
|
continue
|
|
}
|
|
|
|
if sandboxName, allocated := podAllocation[pod.Name]; allocated {
|
|
log.V(1).Info("Skipping eviction for allocated pod", "pod", pod.Name, "sandbox", sandboxName)
|
|
filtered = append(filtered, pod)
|
|
continue
|
|
}
|
|
|
|
// Idle pod marked for eviction: evict and exclude from scheduling
|
|
log.Info("Evicting idle pool pod", "pool", pool.Name, "pod", pod.Name)
|
|
if err := handler.Evict(ctx, pod); err != nil {
|
|
log.Error(err, "Failed to evict pod", "pod", pod.Name)
|
|
evictionErrs = append(evictionErrs, fmt.Errorf("failed to evict pod %s: %w", pod.Name, err))
|
|
} else {
|
|
r.Recorder.Eventf(pool, corev1.EventTypeNormal, EventReasonPodEvicted, "Evicted idle pod: %s", pod.Name)
|
|
}
|
|
}
|
|
|
|
return filtered, gerrors.Join(evictionErrs...)
|
|
}
|