console: use per-function invoke timeout
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user