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
639 lines
23 KiB
Go
639 lines
23 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"
|
|
"encoding/json"
|
|
gerrors "errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"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/sets"
|
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
|
"k8s.io/client-go/tools/record"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
|
|
poolassign "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/poolassign"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/strategy"
|
|
taskscheduler "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/scheduler"
|
|
"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"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/requeueduration"
|
|
)
|
|
|
|
var (
|
|
BatchSandboxScaleExpectations = expectations.NewScaleExpectations()
|
|
DurationStore = requeueduration.DurationStore{}
|
|
)
|
|
|
|
const batchSandboxFirstPodIndex = 0
|
|
|
|
type taskScheduleResult struct {
|
|
Running, Failed, Succeed, Unknown, Pending int32
|
|
}
|
|
|
|
// BatchSandboxReconciler reconciles a BatchSandbox object
|
|
type BatchSandboxReconciler struct {
|
|
client.Client
|
|
Scheme *runtime.Scheme
|
|
Recorder record.EventRecorder
|
|
ProfileStore *poolassign.ProfileStore
|
|
taskSchedulers sync.Map
|
|
StatusRVExpectation expectations.ResourceVersionExpectation
|
|
// ResumePullSecret is the K8s Secret name for pulling snapshot images during resume.
|
|
ResumePullSecret string
|
|
}
|
|
|
|
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes/status,verbs=get;update;patch
|
|
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes/finalizers,verbs=update
|
|
|
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
|
// move the current state of the cluster closer to the desired state.
|
|
// TODO(user): Modify the Reconcile function to compare the state specified by
|
|
// the BatchSandbox object against the actual cluster state, and then
|
|
// perform operations to make the cluster state reflect the state specified by
|
|
// the user.
|
|
//
|
|
// For more details, check Reconcile and its Result here:
|
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
|
|
func (r *BatchSandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
|
|
log := logf.FromContext(ctx)
|
|
start := time.Now()
|
|
var aggErrors []error
|
|
defer func() {
|
|
_ = DurationStore.Pop(req.String())
|
|
log.Info("Reconcile finished", "duration", time.Since(start).String(), "requeueAfter", result.RequeueAfter.String(), "error", retErr)
|
|
}()
|
|
batchSbx := &sandboxv1alpha1.BatchSandbox{}
|
|
if err := r.Get(ctx, client.ObjectKey{
|
|
Namespace: req.Namespace,
|
|
Name: req.Name,
|
|
}, batchSbx); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
// handle expire
|
|
if expireAt := batchSbx.Spec.ExpireTime; expireAt != nil {
|
|
now := time.Now()
|
|
if expireAt.Time.Before(now) {
|
|
if batchSbx.DeletionTimestamp == nil {
|
|
log.Info("batch sandbox expired, delete", "expireAt", expireAt)
|
|
if err := r.Delete(ctx, batchSbx); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
} else {
|
|
DurationStore.Push(types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String(), expireAt.Time.Sub(now))
|
|
}
|
|
}
|
|
|
|
// task schedule
|
|
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
|
|
|
|
// pool strategy
|
|
poolStrategy := strategy.NewPoolStrategy(batchSbx)
|
|
|
|
if profileName := poolStrategy.AssignProfile(); profileName != "" {
|
|
updated, err := r.assignPool(ctx, batchSbx, profileName)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("failed to auto-assign pool: %w", err)
|
|
}
|
|
if updated {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
}
|
|
|
|
// handle finalizers
|
|
if batchSbx.DeletionTimestamp == nil {
|
|
if taskStrategy.NeedTaskScheduling() {
|
|
if !controllerutil.ContainsFinalizer(batchSbx, FinalizerTaskCleanup) {
|
|
err := utils.UpdateFinalizer(r.Client, batchSbx, utils.AddFinalizerOpType, FinalizerTaskCleanup)
|
|
if err != nil {
|
|
log.Error(err, "failed to add finalizer", "finalizer", FinalizerTaskCleanup)
|
|
} else {
|
|
log.Info("added finalizer", "finalizer", FinalizerTaskCleanup)
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
} else {
|
|
if !taskStrategy.NeedTaskScheduling() {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
}
|
|
|
|
// Pause/Resume dispatch: handles pause/resume intent before normal scaling.
|
|
if result, handled, err := r.dispatchPauseResume(ctx, batchSbx); handled {
|
|
return result, err
|
|
}
|
|
|
|
// dispatchPauseResume may patch BatchSandbox spec/state (for example resume detaches a pooled
|
|
// sandbox from its pool). Recompute strategies from the latest object before listing pods so
|
|
// normal reconciliation does not keep using a stale pre-dispatch view.
|
|
taskStrategy = strategy.NewTaskSchedulingStrategy(batchSbx)
|
|
|
|
pods, err := r.listPods(ctx, poolStrategy, batchSbx)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("failed to list pods %w", err)
|
|
}
|
|
podIndex, err := calPodIndex(poolStrategy, batchSbx, pods)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("failed to cal pod index %w", err)
|
|
}
|
|
slices.SortStableFunc(pods, utils.MultiPodSorter([]func(a, b *corev1.Pod) int{
|
|
utils.WithPodIndexSorter(podIndex),
|
|
utils.PodNameSorter,
|
|
}).Sort)
|
|
// Normal mode owns pod lifecycle except while a sandbox is fully paused. In Paused, the
|
|
// snapshot-backed runtime is quiesced and pods must stay absent until resume rewrites the
|
|
// template images and transitions back through Resuming.
|
|
if !poolStrategy.IsPooledMode() && batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhasePaused {
|
|
err := r.scaleBatchSandbox(ctx, batchSbx, batchSbx.Spec.Template, pods)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("failed to scale batch sandbox %w", err)
|
|
}
|
|
}
|
|
|
|
runtimeView := buildRuntimeView(batchSbx, pods)
|
|
// Ensure PauseObservedGeneration is up-to-date so the status patch ACKs the
|
|
// current generation without requiring a dedicated API call.
|
|
// Skip during Resuming: a newer generation may carry a queued pause request
|
|
// that must remain unacknowledged until resume completes and handlePause runs.
|
|
if batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhaseResuming &&
|
|
runtimeView.status.PauseObservedGeneration < batchSbx.Generation {
|
|
runtimeView.status.PauseObservedGeneration = batchSbx.Generation
|
|
}
|
|
|
|
if batchSbx.Status.Phase == sandboxv1alpha1.BatchSandboxPhasePaused {
|
|
r.deleteTaskScheduler(ctx, batchSbx)
|
|
}
|
|
|
|
if taskStrategy.NeedTaskScheduling() && batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhasePaused {
|
|
ts, err := r.reconcileTasks(ctx, batchSbx, pods)
|
|
if err != nil {
|
|
aggErrors = append(aggErrors, err)
|
|
} else if ts != nil {
|
|
runtimeView.status.TaskRunning = ts.Running
|
|
runtimeView.status.TaskFailed = ts.Failed
|
|
runtimeView.status.TaskSucceed = ts.Succeed
|
|
runtimeView.status.TaskUnknown = ts.Unknown
|
|
runtimeView.status.TaskPending = ts.Pending
|
|
}
|
|
}
|
|
|
|
requeue, persistErrors := r.persistRuntimeView(ctx, batchSbx, runtimeView)
|
|
aggErrors = append(aggErrors, persistErrors...)
|
|
|
|
requeueAfter := DurationStore.Pop(req.String())
|
|
if requeue > 0 && (requeueAfter == 0 || requeue < requeueAfter) {
|
|
requeueAfter = requeue
|
|
}
|
|
return reconcile.Result{RequeueAfter: requeueAfter}, gerrors.Join(aggErrors...)
|
|
}
|
|
|
|
func calPodIndex(poolStrategy strategy.PoolStrategy, batchSbx *sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (map[string]int, error) {
|
|
podIndex := map[string]int{}
|
|
if poolStrategy.IsPooledMode() {
|
|
// cal index from pool alloc result while using pooling
|
|
alloc, err := parseSandboxAllocation(batchSbx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range alloc.Pods {
|
|
podIndex[alloc.Pods[i]] = i
|
|
}
|
|
} else {
|
|
for i := range pods {
|
|
po := pods[i]
|
|
idx, err := parseIndex(po)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("batchsandbox: failed to parse %s/%s index %w", po.Namespace, po.Name, err)
|
|
}
|
|
podIndex[po.Name] = idx
|
|
}
|
|
}
|
|
return podIndex, nil
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) reconcileTasks(
|
|
ctx context.Context,
|
|
batchSbx *sandboxv1alpha1.BatchSandbox,
|
|
pods []*corev1.Pod,
|
|
) (*taskScheduleResult, error) {
|
|
log := logf.FromContext(ctx)
|
|
|
|
sch, err := r.getTaskScheduler(ctx, batchSbx, pods)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Because tasks are in-memory and there is no event mechanism, periodic reconciliation is required.
|
|
DurationStore.Push(types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String(), 3*time.Second)
|
|
|
|
if batchSbx.DeletionTimestamp != nil {
|
|
stoppingTasks := sch.StopTask()
|
|
if len(stoppingTasks) > 0 {
|
|
log.Info("stopping tasks", "count", len(stoppingTasks))
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
ts, err := r.scheduleTasks(ctx, sch, batchSbx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to schedule tasks, err %w", err)
|
|
}
|
|
log.Info("schedule tasks completed", "costMs", time.Since(now).Milliseconds(), "task schedule result", utils.DumpJSON(ts))
|
|
|
|
// check task cleanup is finished
|
|
if batchSbx.DeletionTimestamp != nil {
|
|
unfinishedTasks := r.getTasksCleanupUnfinished(batchSbx, sch)
|
|
if len(unfinishedTasks) > 0 {
|
|
log.Info("tasks cleanup is unfinished", "unfinishedCount", len(unfinishedTasks))
|
|
} else {
|
|
var cleanupErr error
|
|
if controllerutil.ContainsFinalizer(batchSbx, FinalizerTaskCleanup) {
|
|
cleanupErr = utils.UpdateFinalizer(r.Client, batchSbx, utils.RemoveFinalizerOpType, FinalizerTaskCleanup)
|
|
if cleanupErr != nil {
|
|
if errors.IsNotFound(cleanupErr) {
|
|
cleanupErr = nil
|
|
} else {
|
|
log.Error(cleanupErr, "failed to remove finalizer", "finalizer", FinalizerTaskCleanup)
|
|
}
|
|
}
|
|
}
|
|
if cleanupErr == nil {
|
|
r.deleteTaskScheduler(ctx, batchSbx)
|
|
log.Info("task cleanup is finished, removed finalizer", "finalizer", FinalizerTaskCleanup)
|
|
}
|
|
// all tasks are cleaned up; skip returning task schedule result so the caller doesn't overwrite status
|
|
return nil, cleanupErr
|
|
}
|
|
}
|
|
|
|
return ts, nil
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) listPods(ctx context.Context, poolStrategy strategy.PoolStrategy, batchSbx *sandboxv1alpha1.BatchSandbox) ([]*corev1.Pod, error) {
|
|
var ret []*corev1.Pod
|
|
if poolStrategy.IsPooledMode() {
|
|
var (
|
|
allocSet = make(sets.Set[string])
|
|
releasedSet = make(sets.Set[string])
|
|
)
|
|
alloc, err := parseSandboxAllocation(batchSbx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allocSet.Insert(alloc.Pods...)
|
|
|
|
released, err := parseSandboxReleased(batchSbx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
releasedSet.Insert(released.Pods...)
|
|
|
|
activePods := allocSet.Difference(releasedSet)
|
|
for name := range activePods {
|
|
pod := &corev1.Pod{}
|
|
// TODO maybe performance is problem
|
|
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: batchSbx.Namespace, Name: name}, pod); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
ret = append(ret, pod)
|
|
}
|
|
} else {
|
|
podList := &corev1.PodList{}
|
|
if err := r.Client.List(ctx, podList, &client.ListOptions{
|
|
Namespace: batchSbx.Namespace,
|
|
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForOwnerRefUID: string(batchSbx.UID)}),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range podList.Items {
|
|
ret = append(ret, &podList.Items[i])
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) getTaskScheduler(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (taskscheduler.TaskScheduler, error) {
|
|
log := logf.FromContext(ctx)
|
|
var tSch taskscheduler.TaskScheduler
|
|
key := types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String()
|
|
val, ok := r.taskSchedulers.Load(key)
|
|
// The reconciler guarantees that it will not concurrently reconcile the same BatchSandbox.
|
|
if !ok {
|
|
policy := sandboxv1alpha1.TaskResourcePolicyRetain
|
|
if batchSbx.Spec.TaskResourcePolicyWhenCompleted != nil {
|
|
policy = *batchSbx.Spec.TaskResourcePolicyWhenCompleted
|
|
}
|
|
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
|
|
taskSpecs, err := taskStrategy.GenerateTaskSpecs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sc, err := taskscheduler.NewTaskScheduler(key, taskSpecs, pods, policy, log)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new task scheduler err %w", err)
|
|
}
|
|
log.Info("successfully created task scheduler")
|
|
tSch = sc
|
|
r.taskSchedulers.Store(key, sc)
|
|
} else {
|
|
tSch, ok = (val.(taskscheduler.TaskScheduler))
|
|
if !ok {
|
|
return nil, gerrors.New("invalid scheduler type stored")
|
|
}
|
|
// Update the pods list for this scheduler
|
|
tSch.UpdatePods(pods)
|
|
// Handle scale-out: register task specs for any replicas added since the
|
|
// scheduler was first created. Already-tracked task names are skipped.
|
|
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
|
|
taskSpecs, err := taskStrategy.GenerateTaskSpecs()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate task specs for scale-out: %w", err)
|
|
}
|
|
if err := tSch.AddTasks(taskSpecs); err != nil {
|
|
return nil, fmt.Errorf("failed to add tasks on scale-out: %w", err)
|
|
}
|
|
}
|
|
return tSch, nil
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) deleteTaskScheduler(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox) {
|
|
key := types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String()
|
|
if _, ok := r.taskSchedulers.LoadAndDelete(key); ok {
|
|
log := logf.FromContext(ctx)
|
|
log.Info("delete task scheduler")
|
|
}
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) scheduleTasks(ctx context.Context, tSch taskscheduler.TaskScheduler, batchSbx *sandboxv1alpha1.BatchSandbox) (*taskScheduleResult, error) {
|
|
log := logf.FromContext(ctx)
|
|
if err := tSch.Schedule(); err != nil {
|
|
return nil, err
|
|
}
|
|
tasks := tSch.ListTask()
|
|
toReleasedPods := []string{}
|
|
var (
|
|
running, failed, succeed, unknown int32
|
|
pending int32
|
|
)
|
|
for i := range len(tasks) {
|
|
task := tasks[i]
|
|
if task.GetPodName() == "" {
|
|
pending++
|
|
} else {
|
|
state := task.GetState()
|
|
if task.IsResourceReleased() {
|
|
toReleasedPods = append(toReleasedPods, task.GetPodName())
|
|
}
|
|
switch state {
|
|
case taskscheduler.RunningTaskState:
|
|
running++
|
|
case taskscheduler.SucceedTaskState:
|
|
succeed++
|
|
case taskscheduler.FailedTaskState:
|
|
failed++
|
|
case taskscheduler.UnknownTaskState:
|
|
unknown++
|
|
}
|
|
}
|
|
}
|
|
if len(toReleasedPods) > 0 {
|
|
log.Info("try to release Pods", "count", len(toReleasedPods))
|
|
if err := r.releasePods(ctx, batchSbx, toReleasedPods); err != nil {
|
|
return nil, err
|
|
}
|
|
log.Info("successfully released Pods", "count", len(toReleasedPods))
|
|
}
|
|
return &taskScheduleResult{
|
|
Running: running,
|
|
Failed: failed,
|
|
Succeed: succeed,
|
|
Unknown: unknown,
|
|
Pending: pending,
|
|
}, nil
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) getTasksCleanupUnfinished(batchSbx *sandboxv1alpha1.BatchSandbox, tSch taskscheduler.TaskScheduler) []taskscheduler.Task {
|
|
var notReleased []taskscheduler.Task
|
|
for _, task := range tSch.ListTask() {
|
|
if !task.IsResourceReleased() {
|
|
notReleased = append(notReleased, task)
|
|
}
|
|
}
|
|
return notReleased
|
|
}
|
|
|
|
func (r *BatchSandboxReconciler) releasePods(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, toReleasePods []string) error {
|
|
releasedSet := make(sets.Set[string])
|
|
released, err := parseSandboxReleased(batchSbx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
releasedSet.Insert(released.Pods...)
|
|
releasedSet.Insert(toReleasePods...)
|
|
newRelease := AllocationRelease{
|
|
Pods: sets.List(releasedSet),
|
|
}
|
|
raw, err := json.Marshal(newRelease)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to marshal released pod names: %v", err)
|
|
}
|
|
body := utils.DumpJSON(struct {
|
|
MetaData metav1.ObjectMeta `json:"metadata"`
|
|
}{
|
|
MetaData: metav1.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
AnnoAllocReleaseKey: string(raw),
|
|
},
|
|
},
|
|
})
|
|
b := &sandboxv1alpha1.BatchSandbox{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: batchSbx.Namespace,
|
|
Name: batchSbx.Name,
|
|
},
|
|
}
|
|
if err := r.Client.Patch(ctx, b, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
|
r.Recorder.Eventf(batchSbx, corev1.EventTypeWarning, EventReasonFailedRelease, "Failed to release pods: %v", err)
|
|
return err
|
|
}
|
|
if len(toReleasePods) > 0 {
|
|
r.Recorder.Eventf(batchSbx, corev1.EventTypeNormal, EventReasonPodReleased, "Released %d pod(s) back to pool: %v", len(toReleasePods), toReleasePods)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Normal Mode
|
|
func (r *BatchSandboxReconciler) scaleBatchSandbox(ctx context.Context, batchSandbox *sandboxv1alpha1.BatchSandbox, podTemplateSpec *corev1.PodTemplateSpec, pods []*corev1.Pod) error {
|
|
log := logf.FromContext(ctx)
|
|
indexedPodMap := map[int]*corev1.Pod{}
|
|
for i := range pods {
|
|
pod := pods[i]
|
|
BatchSandboxScaleExpectations.ObserveScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
|
|
idx, err := parseIndex(pod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse idx Pod %s, err %w", pod.Name, err)
|
|
}
|
|
indexedPodMap[idx] = pod
|
|
}
|
|
if satisfied, unsatisfiedDuration, dirtyPods := BatchSandboxScaleExpectations.SatisfiedExpectations(controllerutils.GetControllerKey(batchSandbox)); !satisfied {
|
|
log.Info("scale expectation is not satisfied", "unsatisfiedDuration", unsatisfiedDuration, "dirtyPods", dirtyPods)
|
|
DurationStore.Push(types.NamespacedName{Namespace: batchSandbox.Namespace, Name: batchSandbox.Name}.String(), expectations.ExpectationTimeout-unsatisfiedDuration)
|
|
return nil
|
|
}
|
|
// TODO consider supply Pods if Pods is deleted unexpectedly
|
|
var needCreateIndex []int
|
|
// TODO var needDeleteIndex []int
|
|
for i := 0; i < int(*batchSandbox.Spec.Replicas); i++ {
|
|
_, ok := indexedPodMap[i]
|
|
if !ok {
|
|
needCreateIndex = append(needCreateIndex, i)
|
|
}
|
|
}
|
|
// scale
|
|
if len(needCreateIndex) > 0 {
|
|
log.Info("try to create Pods", "count", len(needCreateIndex), "indexes", needCreateIndex)
|
|
}
|
|
for _, idx := range needCreateIndex {
|
|
pod, err := utils.GetPodFromTemplate(podTemplateSpec, batchSandbox, metav1.NewControllerRef(batchSandbox, sandboxv1alpha1.SchemeBuilder.GroupVersion.WithKind("BatchSandbox")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Apply shard patch if available for this index
|
|
if len(batchSandbox.Spec.ShardPatches) > 0 && idx < len(batchSandbox.Spec.ShardPatches) {
|
|
podBytes, err := json.Marshal(pod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal pod: %w", err)
|
|
}
|
|
patch := batchSandbox.Spec.ShardPatches[idx]
|
|
modifiedPodBytes, err := strategicpatch.StrategicMergePatch(podBytes, patch.Raw, &corev1.Pod{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to apply shard patch for index %d: %w", idx, err)
|
|
}
|
|
if err := json.Unmarshal(modifiedPodBytes, pod); err != nil {
|
|
return fmt.Errorf("failed to unmarshal patched pod for index %d: %w", idx, err)
|
|
}
|
|
}
|
|
if err := ctrl.SetControllerReference(pod, batchSandbox, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
pod.Labels[LabelBatchSandboxPodIndexKey] = strconv.Itoa(idx)
|
|
pod.Labels[LabelBatchSandboxNameKey] = batchSandbox.Name
|
|
pod.Namespace = batchSandbox.Namespace
|
|
pod.Name = fmt.Sprintf("%s-%d", batchSandbox.Name, idx)
|
|
BatchSandboxScaleExpectations.ExpectScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
|
|
if err := r.Create(ctx, pod); err != nil {
|
|
BatchSandboxScaleExpectations.ObserveScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
|
|
r.Recorder.Eventf(batchSandbox, corev1.EventTypeWarning, EventReasonFailedCreate, "failed to create pod: %v, pod: %v", err, utils.DumpJSON(pod))
|
|
return err
|
|
}
|
|
r.Recorder.Eventf(batchSandbox, corev1.EventTypeNormal, EventReasonSuccessfulCreate, "succeed to create pod %s", pod.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseIndex(pod *corev1.Pod) (int, error) {
|
|
if v := pod.Labels[LabelBatchSandboxPodIndexKey]; v != "" {
|
|
return strconv.Atoi(v)
|
|
}
|
|
idx := strings.LastIndex(pod.Name, "-")
|
|
if idx == -1 {
|
|
return -1, gerrors.New("batchsandbox: Invalid pod Name")
|
|
}
|
|
return strconv.Atoi(pod.Name[idx+1:])
|
|
}
|
|
|
|
// assignPool selects a Pool for the BatchSandbox using the assign package and writes
|
|
// the result back to spec.poolRef. Returns (true, nil) when the update was applied,
|
|
// which triggers a new reconcile with the concrete poolRef.
|
|
func (r *BatchSandboxReconciler) assignPool(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, profileName string) (bool, error) {
|
|
log := logf.FromContext(ctx)
|
|
|
|
poolList := &sandboxv1alpha1.PoolList{}
|
|
if err := r.List(ctx, poolList, client.InNamespace(batchSbx.Namespace)); err != nil {
|
|
return false, fmt.Errorf("failed to list pools: %w", err)
|
|
}
|
|
|
|
pools := make([]*sandboxv1alpha1.Pool, 0, len(poolList.Items))
|
|
for i := range poolList.Items {
|
|
pools = append(pools, &poolList.Items[i])
|
|
}
|
|
|
|
profile := r.ProfileStore.GetProfile(profileName)
|
|
assigner := poolassign.NewDefaultAssigner(profile)
|
|
|
|
poolName, err := assigner.AssignPool(ctx, batchSbx, pools)
|
|
if err != nil {
|
|
r.Recorder.Eventf(batchSbx, corev1.EventTypeWarning, EventReasonFailedPoolAssign, "Failed to assign pool: %v", err)
|
|
return false, err
|
|
}
|
|
|
|
oldSbx := batchSbx.DeepCopy()
|
|
batchSbx.Spec.PoolRef = poolName
|
|
patch := client.MergeFrom(oldSbx)
|
|
if err := r.Patch(ctx, batchSbx, patch); err != nil {
|
|
return false, fmt.Errorf("failed to patch poolRef: %w", err)
|
|
}
|
|
|
|
log.Info("auto-assigned pool", "pool", poolName)
|
|
r.Recorder.Eventf(batchSbx, corev1.EventTypeNormal, EventReasonPoolAssigned, "Assigned to pool %s", poolName)
|
|
return true, nil
|
|
}
|
|
|
|
// SetupWithManager sets up the controller with the Manager.
|
|
func (r *BatchSandboxReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int) error {
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
For(&sandboxv1alpha1.BatchSandbox{}).
|
|
Named("batchsandbox").
|
|
Owns(&corev1.Pod{}).
|
|
Owns(&sandboxv1alpha1.SandboxSnapshot{}).
|
|
WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}).
|
|
Complete(r)
|
|
}
|