console: use per-function invoke timeout

This commit is contained in:
Naeel
2026-04-27 10:33:40 +03:00
parent e600d6e1e8
commit 01f546e0e8
3 changed files with 68 additions and 13 deletions
+46 -11
View File
@@ -34,6 +34,9 @@ var validFuncName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
// Выше — не имеет смысла для inline функции; лучше использовать Package с URL.
const maxCodeSize = 1 << 20
// defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout.
const defaultFunctionInvokeTimeout = 60 * time.Second
func buildDeployArchive(lang, code string) ([]byte, error) {
switch lang {
case "nodejs":
@@ -47,6 +50,26 @@ func buildDeployArchive(lang, code string) ([]byte, error) {
}
}
func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
if fn != nil {
seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
if err == nil && found && seconds > 0 {
return time.Duration(seconds) * time.Second
}
}
if s.invokeTimeout > 0 {
return s.invokeTimeout
}
return defaultFunctionInvokeTimeout
}
func normalizeFunctionTimeout(seconds int64) int64 {
if seconds <= 0 {
return int64(defaultFunctionInvokeTimeout / time.Second)
}
return seconds
}
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
// GET → список всех функций, POST → создать новую.
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
@@ -184,6 +207,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
req.Route = "/" + req.Route
}
req.Methods = normalizeMethods(req.Methods)
req.Timeout = normalizeFunctionTimeout(req.Timeout)
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
@@ -275,6 +299,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
"metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations},
"spec": map[string]any{
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"functionTimeout": req.Timeout,
"InvokeStrategy": map[string]any{
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
"StrategyType": "execution",
@@ -345,6 +370,10 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
packageName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
environment, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
entrypoint, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "functionName")
functionTimeout, foundTimeout, _ := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
if !foundTimeout || functionTimeout <= 0 {
functionTimeout = int64(defaultFunctionInvokeTimeout / time.Second)
}
// Извлекаем исходный код из Package (пробуем source.literal, потом deployment.literal, потом url)
code := ""
@@ -377,6 +406,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
"environment": environment,
"package": packageName,
"entrypoint": entrypoint,
"timeout": functionTimeout,
"code": code,
"route": route,
"methods": methods,
@@ -452,6 +482,11 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
return
}
if err := unstructured.SetNestedField(fn.Object, normalizeFunctionTimeout(req.Timeout), "spec", "functionTimeout"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function timeout: %v", err))
return
}
// Синхронизируем resourceVersion в Function.spec.package.packageref
// Это триггерит executor перезагрузить код в pool pod
if err := unstructured.SetNestedField(fn.Object, updatedPkg.GetResourceVersion(), "spec", "package", "packageref", "resourceversion"); err != nil {
@@ -482,17 +517,13 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
bodyBytes = []byte("{}")
}
invokeTimeout := s.invokeTimeout
if invokeTimeout <= 0 {
invokeTimeout = 20 * time.Second
}
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
defer cancel()
ns := s.userNS(r)
lookupCtx, lookupCancel := context.WithTimeout(r.Context(), 10*time.Second)
defer lookupCancel()
// Проверяем существование функции до вызова — лучше 404 чем непонятный timeout
if _, err2 := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}); err2 != nil {
fn, err2 := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(lookupCtx, name, metav1.GetOptions{})
if err2 != nil {
if apierrors.IsNotFound(err2) {
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", name))
return
@@ -501,6 +532,10 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
return
}
invokeTimeout := s.resolveInvokeTimeout(fn)
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
defer cancel()
// Ищем HTTPTrigger чтобы получить реальный URL и метод
invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name)
invokeMethod := http.MethodPost
@@ -556,14 +591,14 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
resp, err := s.http.Do(req)
if err != nil {
// Отличаем timeout от сетевой ошибки — timeout часто означает что функция не специализировалась
// Отличаем timeout от сетевой ошибки.
if errors.Is(err, context.DeadlineExceeded) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s: function specialization likely failed (for example, syntax error)", name, invokeTimeout))
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s", name, invokeTimeout))
return
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s: function specialization likely failed (for example, syntax error)", name, invokeTimeout))
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q timeout after %s", name, invokeTimeout))
return
}
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", name, err))
+3 -1
View File
@@ -13,12 +13,14 @@ type CreateFunctionRequest struct {
Entrypoint string `json:"entrypoint"`
Route string `json:"route"`
Methods []string `json:"methods"`
Timeout int64 `json:"timeout"`
TTL string `json:"ttl"` // e.g. "24h", "7d" — пустое = функция не протухает
}
// UpdateCodeRequest — тело PUT /console/api/functions/:name/code.
type UpdateCodeRequest struct {
Code string `json:"code"`
Code string `json:"code"`
Timeout int64 `json:"timeout"`
}
// LangEnvDef описывает Docker-образы для конкретного языка.
+19 -1
View File
@@ -459,6 +459,10 @@
<label>Методы (через запятую)</label>
<input id="c-methods" value="GET">
</div>
<div class="field">
<label>Timeout (сек)</label>
<input id="c-timeout" type="number" min="1" value="60">
</div>
</div>
<div>
<label>Код</label>
@@ -512,6 +516,10 @@
<label>Entrypoint</label>
<input id="e-entry" disabled>
</div>
<div class="field">
<label>Timeout (сек)</label>
<input id="e-timeout" type="number" min="1" value="60">
</div>
</div>
<div>
<label>Код</label>
@@ -627,6 +635,12 @@
return items.length ? Array.from(new Set(items)) : ['GET'];
}
function parseTimeout(v) {
var n = Number(String(v || '').trim());
if (!Number.isFinite(n) || n <= 0) return 60;
return Math.round(n);
}
function triggerByFn(fnName) {
return (S.triggers || []).find(function (t) {
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
@@ -673,6 +687,7 @@
function openCreate() {
document.getElementById('c-lang').value = 'python';
onLangChange();
document.getElementById('c-timeout').value = '60';
document.getElementById('create-modal').classList.add('open');
document.getElementById('c-name').focus();
}
@@ -697,6 +712,7 @@
entrypoint: document.getElementById('c-entry').value.trim(),
route: document.getElementById('c-route').value.trim(),
methods: parseMethods(document.getElementById('c-methods').value),
timeout: parseTimeout(document.getElementById('c-timeout').value),
code: document.getElementById('c-code').value
});
@@ -718,6 +734,7 @@
document.getElementById('e-name').value = name;
document.getElementById('e-env').value = fn.environment || '';
document.getElementById('e-entry').value = fn.entrypoint || '';
document.getElementById('e-timeout').value = String(fn.timeout || 60);
document.getElementById('e-code').value = fn.code || '';
// Определяем язык по имени environment для линтера
var envName = (fn.environment || '').toLowerCase();
@@ -754,7 +771,8 @@
try {
const name = S.currentEdit.name;
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
code: document.getElementById('e-code').value
code: document.getElementById('e-code').value,
timeout: parseTimeout(document.getElementById('e-timeout').value)
});
closeEdit();
progress.stop('\u041a\u043e\u0434 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d: ' + name, 'ok');