fix: бесконечный цикл сборки — idempotency guard через last-built-s3key аннотацию
Проблема: upload handler сбрасывал phase в Pending ПОСЛЕ того как контроллер уже выставил Building → бесконечный цикл (94 job'а). Решение: - controllers/function_controller.go: startBuild сначала ставит аннотацию last-built-s3key = spec.S3Key (idempotency guard), потом status. Reconcile стартует сборку только если spec.S3Key != last-built-s3key. - handler/upload.go: убран Status().Update() — контроллер сам управляет фазой. - builder.go: IsAlreadyExists при создании job не ошибка (parallel reconcile).
This commit is contained in:
@@ -69,24 +69,23 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
|||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем что делать в зависимости от текущей фазы
|
// Есть новый код (s3Key изменился по сравнению с последним запуском сборки)
|
||||||
switch fn.Status.Phase {
|
// и сборка сейчас не идёт — запускаем. Это единственное место где решается "нужна ли сборка".
|
||||||
case "", slessv1alpha1.FunctionPhasePending:
|
// Почему аннотация а не phase: phase может быть Failed/Ready от прошлого кода;
|
||||||
// Новая функция или сброшена в Pending — запускаем сборку образа
|
// новый upload меняет Spec.S3Key → контроллер сам понимает что нужно пересобрать.
|
||||||
|
builtKey := fn.Annotations["sless.kube5s.ru/last-built-s3key"]
|
||||||
|
needsBuild := fn.Spec.S3Key != "" && builtKey != fn.Spec.S3Key
|
||||||
|
|
||||||
|
if needsBuild && fn.Status.Phase != slessv1alpha1.FunctionPhaseBuilding {
|
||||||
logger.Info("starting build", "function", fn.Name)
|
logger.Info("starting build", "function", fn.Name)
|
||||||
return r.startBuild(ctx, fn)
|
return r.startBuild(ctx, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fn.Status.Phase {
|
||||||
case slessv1alpha1.FunctionPhaseBuilding:
|
case slessv1alpha1.FunctionPhaseBuilding:
|
||||||
// Сборка уже запущена — проверяем статус Job'а
|
|
||||||
return r.checkBuild(ctx, fn)
|
return r.checkBuild(ctx, fn)
|
||||||
|
|
||||||
case slessv1alpha1.FunctionPhaseReady:
|
case slessv1alpha1.FunctionPhaseReady:
|
||||||
// Функция готова — проверяем что Deployment существует и актуален
|
|
||||||
return r.ensureDeployment(ctx, fn)
|
return r.ensureDeployment(ctx, fn)
|
||||||
|
|
||||||
case slessv1alpha1.FunctionPhaseFailed:
|
|
||||||
// Ничего не делаем — пользователь должен исправить spec и обновить объект
|
|
||||||
return ctrl.Result{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
return ctrl.Result{}, nil
|
||||||
@@ -94,29 +93,35 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
|||||||
|
|
||||||
const finalizerName = "sless.kube5s.ru/finalizer"
|
const finalizerName = "sless.kube5s.ru/finalizer"
|
||||||
|
|
||||||
// startBuild переводит функцию в фазу Building и запускает kaniko Job.
|
// startBuild запускает kaniko Job и помечает функцию как Building.
|
||||||
|
// Критически важно: СНАЧАЛА сохраняем last-built-s3key аннотацию, ПОТОМ status.
|
||||||
|
// Это предотвращает повторный запуск сборки при параллельных reconcile —
|
||||||
|
// следующий reconcile увидит last-built-s3key == spec.S3Key и не войдёт в startBuild.
|
||||||
func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||||
jobName, err := r.Builder.Build(ctx, fn.Namespace, fn.Name, fn.Spec.S3Key)
|
jobName, err := r.Builder.Build(ctx, fn.Namespace, fn.Name, fn.Spec.S3Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r.setFailed(ctx, fn, fmt.Sprintf("failed to start build: %v", err))
|
return r.setFailed(ctx, fn, fmt.Sprintf("failed to start build: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем имя Job'а в аннотации чтобы отслеживать в следующем reconcile
|
// Обновляем аннотации ПЕРВЫМИ — это idempotency guard.
|
||||||
|
// Как только last-built-s3key == spec.S3Key, дальнейшие reconcile не будут
|
||||||
|
// вызывать startBuild снова, даже если status ещё не обновился.
|
||||||
if fn.Annotations == nil {
|
if fn.Annotations == nil {
|
||||||
fn.Annotations = map[string]string{}
|
fn.Annotations = map[string]string{}
|
||||||
}
|
}
|
||||||
fn.Annotations["sless.kube5s.ru/build-job"] = jobName
|
fn.Annotations["sless.kube5s.ru/build-job"] = jobName
|
||||||
|
fn.Annotations["sless.kube5s.ru/last-built-s3key"] = fn.Spec.S3Key
|
||||||
|
if err := r.Update(ctx, fn); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("update build annotations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статус после аннотаций
|
||||||
fn.Status.Phase = slessv1alpha1.FunctionPhaseBuilding
|
fn.Status.Phase = slessv1alpha1.FunctionPhaseBuilding
|
||||||
fn.Status.Message = "Building image: " + jobName
|
fn.Status.Message = "Building image: " + jobName
|
||||||
if err := r.Status().Update(ctx, fn); err != nil {
|
if err := r.Status().Update(ctx, fn); err != nil {
|
||||||
return ctrl.Result{}, fmt.Errorf("update status to building: %w", err)
|
return ctrl.Result{}, fmt.Errorf("update status to building: %w", err)
|
||||||
}
|
}
|
||||||
if err := r.Update(ctx, fn); err != nil {
|
|
||||||
return ctrl.Result{}, fmt.Errorf("update annotations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перепроверяем через 10 секунд
|
|
||||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,14 +175,6 @@ func (h *Handler) UploadCode(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем phase в Pending чтобы контроллер начал сборку
|
|
||||||
fn.Status.Phase = slessv1alpha1.FunctionPhasePending
|
|
||||||
fn.Status.Message = "code uploaded, build queued"
|
|
||||||
if err := h.K8s.Status().Update(r.Context(), fn); err != nil {
|
|
||||||
// Не фатально — контроллер подберёт через reconcile
|
|
||||||
h.Log.Warn("failed to reset function phase", "name", name, "ns", ns, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]string{
|
writeJSON(w, http.StatusOK, map[string]string{
|
||||||
"s3_key": s3Key,
|
"s3_key": s3Key,
|
||||||
"phase": string(slessv1alpha1.FunctionPhasePending),
|
"phase": string(slessv1alpha1.FunctionPhasePending),
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := b.client.Create(ctx, job); err != nil {
|
if err := b.client.Create(ctx, job); err != nil {
|
||||||
|
// Если job с таким именем уже существует (два reconcile в одну секунду) —
|
||||||
|
// возвращаем имя, контроллер будет следить за ним как обычно
|
||||||
|
if errors.IsAlreadyExists(err) {
|
||||||
|
return jobName, nil
|
||||||
|
}
|
||||||
return "", fmt.Errorf("create build job: %w", err)
|
return "", fmt.Errorf("create build job: %w", err)
|
||||||
}
|
}
|
||||||
return jobName, nil
|
return jobName, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user