feat(v0.1.58): ImageExists cache hit, timing analysis, in-cluster registry plan
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// Изменено: 2026-03-18 (добавлены created_at, last_built_at в functionResponse и fnToResponse)
|
||||
// Изменено: 2026-03-21 (fix: DeleteFunction возвращает 404 вместо 204 при отсутствующем объекте)
|
||||
// Изменено: 2026-03-22 (fix: CreateFunction 409 при пересоздании функции — не учитывался DeletionTimestamp)
|
||||
// functions.go — CRUD handlers для Function CRD.
|
||||
// Принимает JSON, создаёт/обновляет/удаляет k8s ресурсы Function.
|
||||
// Namespace берётся из URL: /v1/namespaces/{namespace}/functions/{name}
|
||||
@@ -9,6 +10,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -128,22 +130,59 @@ func (h *Handler) CreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
S3Key: req.S3Key,
|
||||
},
|
||||
}
|
||||
// Пытаемся создать Function CRD в k8s (запись в etcd через controller-runtime).
|
||||
if err := h.K8s.Create(r.Context(), fn); err != nil {
|
||||
|
||||
// IsAlreadyExists = etcd вернул 409 CONFLICT.
|
||||
// Возникает в двух основных сценариях:
|
||||
//
|
||||
// [A] terraform apply -replace (= delete + create за один apply):
|
||||
// 1. terraform DELETE /functions/{name} → API вызывает h.K8s.Delete(fn)
|
||||
// 2. k8s ставит DeletionTimestamp и ждёт снятия finalizer sless.kube5s.ru/finalizer
|
||||
// function_controller.go убивает kaniko Job и снимает finalizer асинхронно
|
||||
// 3. terraform сразу POST /functions/{name} → IsAlreadyExists ← БАГ до этого фикса
|
||||
// → НОВЫЙ КОД: обнаруживает DeletionTimestamp → ждёт исчезновения → создаёт
|
||||
//
|
||||
// [B] Split-brain кеша controller-runtime:
|
||||
// Объект удалён из etcd, но informer-кеш ещё не обновился.
|
||||
// Create падает с IsAlreadyExists из кеша, Get возвращает NotFound → пересоздаём.
|
||||
//
|
||||
// [C] Функция в фазе Failed:
|
||||
// Предыдущий build провалился. Новый apply пытается создать снова.
|
||||
// Удаляем Failed объект и пересоздаём.
|
||||
if errors.IsAlreadyExists(err) {
|
||||
// IsAlreadyExists может прийти из кеша controller-runtime (split-brain):
|
||||
// объект удалён из etcd, но кеш informer ещё южив. Делаем uncached Get:
|
||||
// если реально NotFound — кеш устарел, пересоздаём.
|
||||
// если существует и фаза Failed — тоже пересоздаём (build провалился, терраформ не добавил в state).
|
||||
// если существует и фаза Ready/Building — возвращаем 409 (функция реально есть).
|
||||
|
||||
// Получаем актуальное состояние объекта из etcd (не из кеша informer).
|
||||
existing := &slessv1alpha1.Function{}
|
||||
getErr := h.K8s.Get(r.Context(), client.ObjectKey{Name: req.Name, Namespace: ns}, existing)
|
||||
shouldRecreate := errors.IsNotFound(getErr) ||
|
||||
(getErr == nil && existing.Status.Phase == slessv1alpha1.FunctionPhaseFailed)
|
||||
if shouldRecreate {
|
||||
if getErr == nil {
|
||||
_ = h.K8s.Delete(r.Context(), existing)
|
||||
|
||||
// --- Сценарий A: объект ожидает удаления ---
|
||||
// DeletionTimestamp ≠ zero = k8s принял DELETE, finalizer ещё не снят.
|
||||
// function_controller.go снимает finalizer после cleanup Job — обычно 1-5 сек.
|
||||
if getErr == nil && !existing.DeletionTimestamp.IsZero() {
|
||||
|
||||
deleted := false
|
||||
// Polling каждую секунду, максимум 30 раз (= 30 секунд).
|
||||
// 30 сек — запас на медленный кластер; в норме 1-3 итерации.
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
checkErr := h.K8s.Get(r.Context(), client.ObjectKey{Name: req.Name, Namespace: ns}, existing)
|
||||
// NotFound = finalizer снят, объект исчез из etcd — можно создавать.
|
||||
if errors.IsNotFound(checkErr) {
|
||||
deleted = true
|
||||
break
|
||||
}
|
||||
// Любая другая ошибка (timeout, сбой API) — продолжаем ждать.
|
||||
}
|
||||
// Сбрасываем ResourceVersion — при split-brain etcd считает объект новым
|
||||
|
||||
// Таймаут: объект не исчез за 30 секунд.
|
||||
// Клиент (terraform) получит 409 и должен сделать retry позже.
|
||||
if !deleted {
|
||||
writeJSON(w, http.StatusConflict, errResp("function is being deleted, try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
// Объект исчез — сбрасываем ResourceVersion и создаём как новый.
|
||||
fn.ResourceVersion = ""
|
||||
if createErr := h.K8s.Create(r.Context(), fn); createErr != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(createErr.Error()))
|
||||
@@ -152,6 +191,29 @@ func (h *Handler) CreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, fnToResponse(fn))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Сценарии B и C: split-brain или Failed ---
|
||||
// NotFound при Get = кеш врёт (B); Failed фаза = сломанный build (C).
|
||||
shouldRecreate := errors.IsNotFound(getErr) ||
|
||||
(getErr == nil && existing.Status.Phase == slessv1alpha1.FunctionPhaseFailed)
|
||||
if shouldRecreate {
|
||||
// Если объект реально есть (Failed) — сначала удаляем.
|
||||
// Ошибку Delete игнорируем: Create покажет ошибку сам если что-то пошло не так.
|
||||
if getErr == nil {
|
||||
_ = h.K8s.Delete(r.Context(), existing)
|
||||
}
|
||||
// Сбрасываем ResourceVersion — при split-brain etcd считает объект новым.
|
||||
fn.ResourceVersion = ""
|
||||
if createErr := h.K8s.Create(r.Context(), fn); createErr != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(createErr.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, fnToResponse(fn))
|
||||
return
|
||||
}
|
||||
|
||||
// Объект живой (Ready/Building), DeletionTimestamp=zero.
|
||||
// Легитимный конфликт — клиент пытается создать дубликат.
|
||||
writeJSON(w, http.StatusConflict, errResp("function already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user