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.
|
// Выше — не имеет смысла для inline функции; лучше использовать Package с URL.
|
||||||
const maxCodeSize = 1 << 20
|
const maxCodeSize = 1 << 20
|
||||||
|
|
||||||
|
// defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout.
|
||||||
|
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||||||
|
|
||||||
func buildDeployArchive(lang, code string) ([]byte, error) {
|
func buildDeployArchive(lang, code string) ([]byte, error) {
|
||||||
switch lang {
|
switch lang {
|
||||||
case "nodejs":
|
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 без имени функции.
|
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
|
||||||
// GET → список всех функций, POST → создать новую.
|
// GET → список всех функций, POST → создать новую.
|
||||||
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
|
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.Route = "/" + req.Route
|
||||||
}
|
}
|
||||||
req.Methods = normalizeMethods(req.Methods)
|
req.Methods = normalizeMethods(req.Methods)
|
||||||
|
req.Timeout = normalizeFunctionTimeout(req.Timeout)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||||
defer cancel()
|
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},
|
"metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations},
|
||||||
"spec": map[string]any{
|
"spec": map[string]any{
|
||||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||||
|
"functionTimeout": req.Timeout,
|
||||||
"InvokeStrategy": map[string]any{
|
"InvokeStrategy": map[string]any{
|
||||||
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
|
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
|
||||||
"StrategyType": "execution",
|
"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")
|
packageName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
|
||||||
environment, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
|
environment, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
|
||||||
entrypoint, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "functionName")
|
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)
|
// Извлекаем исходный код из Package (пробуем source.literal, потом deployment.literal, потом url)
|
||||||
code := ""
|
code := ""
|
||||||
@@ -377,6 +406,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
|||||||
"environment": environment,
|
"environment": environment,
|
||||||
"package": packageName,
|
"package": packageName,
|
||||||
"entrypoint": entrypoint,
|
"entrypoint": entrypoint,
|
||||||
|
"timeout": functionTimeout,
|
||||||
"code": code,
|
"code": code,
|
||||||
"route": route,
|
"route": route,
|
||||||
"methods": methods,
|
"methods": methods,
|
||||||
@@ -452,6 +482,11 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
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
|
// Синхронизируем resourceVersion в Function.spec.package.packageref
|
||||||
// Это триггерит executor перезагрузить код в pool pod
|
// Это триггерит executor перезагрузить код в pool pod
|
||||||
if err := unstructured.SetNestedField(fn.Object, updatedPkg.GetResourceVersion(), "spec", "package", "packageref", "resourceversion"); err != nil {
|
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("{}")
|
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)
|
ns := s.userNS(r)
|
||||||
|
lookupCtx, lookupCancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer lookupCancel()
|
||||||
|
|
||||||
// Проверяем существование функции до вызова — лучше 404 чем непонятный timeout
|
// Проверяем существование функции до вызова — лучше 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) {
|
if apierrors.IsNotFound(err2) {
|
||||||
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", name))
|
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", name))
|
||||||
return
|
return
|
||||||
@@ -501,6 +532,10 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invokeTimeout := s.resolveInvokeTimeout(fn)
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// Ищем HTTPTrigger чтобы получить реальный URL и метод
|
// Ищем HTTPTrigger чтобы получить реальный URL и метод
|
||||||
invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name)
|
invokeURL := fmt.Sprintf("%s/fission-function/v2/functions/%s", s.routerURL, name)
|
||||||
invokeMethod := http.MethodPost
|
invokeMethod := http.MethodPost
|
||||||
@@ -556,14 +591,14 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
|
|||||||
|
|
||||||
resp, err := s.http.Do(req)
|
resp, err := s.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Отличаем timeout от сетевой ошибки — timeout часто означает что функция не специализировалась
|
// Отличаем timeout от сетевой ошибки.
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
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
|
return
|
||||||
}
|
}
|
||||||
var netErr net.Error
|
var netErr net.Error
|
||||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
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
|
return
|
||||||
}
|
}
|
||||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", name, err))
|
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke %q: %v", name, err))
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ type CreateFunctionRequest struct {
|
|||||||
Entrypoint string `json:"entrypoint"`
|
Entrypoint string `json:"entrypoint"`
|
||||||
Route string `json:"route"`
|
Route string `json:"route"`
|
||||||
Methods []string `json:"methods"`
|
Methods []string `json:"methods"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
TTL string `json:"ttl"` // e.g. "24h", "7d" — пустое = функция не протухает
|
TTL string `json:"ttl"` // e.g. "24h", "7d" — пустое = функция не протухает
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCodeRequest — тело PUT /console/api/functions/:name/code.
|
// UpdateCodeRequest — тело PUT /console/api/functions/:name/code.
|
||||||
type UpdateCodeRequest struct {
|
type UpdateCodeRequest struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LangEnvDef описывает Docker-образы для конкретного языка.
|
// LangEnvDef описывает Docker-образы для конкретного языка.
|
||||||
|
|||||||
+19
-1
@@ -459,6 +459,10 @@
|
|||||||
<label>Методы (через запятую)</label>
|
<label>Методы (через запятую)</label>
|
||||||
<input id="c-methods" value="GET">
|
<input id="c-methods" value="GET">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Timeout (сек)</label>
|
||||||
|
<input id="c-timeout" type="number" min="1" value="60">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Код</label>
|
<label>Код</label>
|
||||||
@@ -512,6 +516,10 @@
|
|||||||
<label>Entrypoint</label>
|
<label>Entrypoint</label>
|
||||||
<input id="e-entry" disabled>
|
<input id="e-entry" disabled>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Timeout (сек)</label>
|
||||||
|
<input id="e-timeout" type="number" min="1" value="60">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Код</label>
|
<label>Код</label>
|
||||||
@@ -627,6 +635,12 @@
|
|||||||
return items.length ? Array.from(new Set(items)) : ['GET'];
|
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) {
|
function triggerByFn(fnName) {
|
||||||
return (S.triggers || []).find(function (t) {
|
return (S.triggers || []).find(function (t) {
|
||||||
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
|
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
|
||||||
@@ -673,6 +687,7 @@
|
|||||||
function openCreate() {
|
function openCreate() {
|
||||||
document.getElementById('c-lang').value = 'python';
|
document.getElementById('c-lang').value = 'python';
|
||||||
onLangChange();
|
onLangChange();
|
||||||
|
document.getElementById('c-timeout').value = '60';
|
||||||
document.getElementById('create-modal').classList.add('open');
|
document.getElementById('create-modal').classList.add('open');
|
||||||
document.getElementById('c-name').focus();
|
document.getElementById('c-name').focus();
|
||||||
}
|
}
|
||||||
@@ -697,6 +712,7 @@
|
|||||||
entrypoint: document.getElementById('c-entry').value.trim(),
|
entrypoint: document.getElementById('c-entry').value.trim(),
|
||||||
route: document.getElementById('c-route').value.trim(),
|
route: document.getElementById('c-route').value.trim(),
|
||||||
methods: parseMethods(document.getElementById('c-methods').value),
|
methods: parseMethods(document.getElementById('c-methods').value),
|
||||||
|
timeout: parseTimeout(document.getElementById('c-timeout').value),
|
||||||
code: document.getElementById('c-code').value
|
code: document.getElementById('c-code').value
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -718,6 +734,7 @@
|
|||||||
document.getElementById('e-name').value = name;
|
document.getElementById('e-name').value = name;
|
||||||
document.getElementById('e-env').value = fn.environment || '';
|
document.getElementById('e-env').value = fn.environment || '';
|
||||||
document.getElementById('e-entry').value = fn.entrypoint || '';
|
document.getElementById('e-entry').value = fn.entrypoint || '';
|
||||||
|
document.getElementById('e-timeout').value = String(fn.timeout || 60);
|
||||||
document.getElementById('e-code').value = fn.code || '';
|
document.getElementById('e-code').value = fn.code || '';
|
||||||
// Определяем язык по имени environment для линтера
|
// Определяем язык по имени environment для линтера
|
||||||
var envName = (fn.environment || '').toLowerCase();
|
var envName = (fn.environment || '').toLowerCase();
|
||||||
@@ -754,7 +771,8 @@
|
|||||||
try {
|
try {
|
||||||
const name = S.currentEdit.name;
|
const name = S.currentEdit.name;
|
||||||
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
|
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();
|
closeEdit();
|
||||||
progress.stop('\u041a\u043e\u0434 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d: ' + name, 'ok');
|
progress.stop('\u041a\u043e\u0434 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d: ' + name, 'ok');
|
||||||
|
|||||||
Reference in New Issue
Block a user