From d4984cf473d3efc259b0a1ce742b2ec51abc8fe6 Mon Sep 17 00:00:00 2001
From: zhengkunwang223 <1paneldev@sina.com>
Date: Tue, 28 Oct 2025 15:28:48 +0800
Subject: [PATCH] feat: Website Load balancer config support fail_timeout
---
agent/app/dto/nginx.go | 13 +-
agent/app/service/website.go | 88 +-----
agent/app/service/website_utils.go | 87 ++++++
frontend/src/api/interface/website.ts | 3 +-
frontend/src/lang/modules/en.ts | 3 +
frontend/src/lang/modules/es-es.ts | 3 +
frontend/src/lang/modules/ja.ts | 3 +
frontend/src/lang/modules/ko.ts | 3 +
frontend/src/lang/modules/ms.ts | 3 +
frontend/src/lang/modules/pt-br.ts | 3 +
frontend/src/lang/modules/ru.ts | 3 +
frontend/src/lang/modules/tr.ts | 3 +
frontend/src/lang/modules/zh-Hant.ts | 3 +
frontend/src/lang/modules/zh.ts | 3 +
.../config/basic/load-balance/index.vue | 4 +-
.../basic/load-balance/operate/index.vue | 258 +++++++++++++-----
16 files changed, 317 insertions(+), 166 deletions(-)
diff --git a/agent/app/dto/nginx.go b/agent/app/dto/nginx.go
index df1f09821f60..36308105d0aa 100644
--- a/agent/app/dto/nginx.go
+++ b/agent/app/dto/nginx.go
@@ -73,12 +73,13 @@ type NginxUpstream struct {
}
type NginxUpstreamServer struct {
- Server string `json:"server"`
- Weight int `json:"weight"`
- FailTimeout string `json:"failTimeout"`
- MaxFails int `json:"maxFails"`
- MaxConns int `json:"maxConns"`
- Flag string `json:"flag"`
+ Server string `json:"server"`
+ Weight int `json:"weight"`
+ FailTimeout int `json:"failTimeout"`
+ FailTimeoutUnit string `json:"failTimeoutUnit"`
+ MaxFails int `json:"maxFails"`
+ MaxConns int `json:"maxConns"`
+ Flag string `json:"flag"`
}
var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
diff --git a/agent/app/service/website.go b/agent/app/service/website.go
index e02dd45a11fe..f7b92b496168 100644
--- a/agent/app/service/website.go
+++ b/agent/app/service/website.go
@@ -3094,39 +3094,7 @@ func (w WebsiteService) GetLoadBalances(id uint) ([]dto.NginxUpstream, error) {
upstream.Algorithm = dName
}
}
- var servers []dto.NginxUpstreamServer
- for _, ups := range up.UpstreamServers {
- server := dto.NginxUpstreamServer{
- Server: ups.Address,
- }
- parameters := ups.Parameters
- if weight, ok := parameters["weight"]; ok {
- num, err := strconv.Atoi(weight)
- if err == nil {
- server.Weight = num
- }
- }
- if maxFails, ok := parameters["max_fails"]; ok {
- num, err := strconv.Atoi(maxFails)
- if err == nil {
- server.MaxFails = num
- }
- }
- if failTimeout, ok := parameters["fail_timeout"]; ok {
- server.FailTimeout = failTimeout
- }
- if maxConns, ok := parameters["max_conns"]; ok {
- num, err := strconv.Atoi(maxConns)
- if err == nil {
- server.MaxConns = num
- }
- }
- for _, flag := range ups.Flags {
- server.Flag = flag
- }
- servers = append(servers, server)
- }
- upstream.Servers = servers
+ upstream.Servers = getNginxUpstreamServers(up.UpstreamServers)
}
}
res = append(res, upstream)
@@ -3160,33 +3128,7 @@ func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) error {
if req.Algorithm != "default" {
upstream.UpdateDirective(req.Algorithm, []string{})
}
-
- servers := make([]*components.UpstreamServer, 0)
-
- for _, server := range req.Servers {
- upstreamServer := &components.UpstreamServer{
- Address: server.Server,
- }
- parameters := make(map[string]string)
- if server.Weight > 0 {
- parameters["weight"] = strconv.Itoa(server.Weight)
- }
- if server.MaxFails > 0 {
- parameters["max_fails"] = strconv.Itoa(server.MaxFails)
- }
- if server.FailTimeout != "" {
- parameters["fail_timeout"] = server.FailTimeout
- }
- if server.MaxConns > 0 {
- parameters["max_conns"] = strconv.Itoa(server.MaxConns)
- }
- if server.Flag != "" {
- upstreamServer.Flags = []string{server.Flag}
- }
- upstreamServer.Parameters = parameters
- servers = append(servers, upstreamServer)
- }
- upstream.UpstreamServers = servers
+ upstream.UpstreamServers = parseUpstreamServers(req.Servers)
config.Block.Directives = append(config.Block.Directives, &upstream)
defer func() {
@@ -3245,31 +3187,7 @@ func (w WebsiteService) UpdateLoadBalance(req request.WebsiteLBUpdate) error {
if req.Algorithm != "default" {
up.UpdateDirective(req.Algorithm, []string{})
}
- var servers []*components.UpstreamServer
- for _, server := range req.Servers {
- upstreamServer := &components.UpstreamServer{
- Address: server.Server,
- }
- parameters := make(map[string]string)
- if server.Weight > 0 {
- parameters["weight"] = strconv.Itoa(server.Weight)
- }
- if server.MaxFails > 0 {
- parameters["max_fails"] = strconv.Itoa(server.MaxFails)
- }
- if server.FailTimeout != "" {
- parameters["fail_timeout"] = server.FailTimeout
- }
- if server.MaxConns > 0 {
- parameters["max_conns"] = strconv.Itoa(server.MaxConns)
- }
- if server.Flag != "" {
- upstreamServer.Flags = []string{server.Flag}
- }
- upstreamServer.Parameters = parameters
- servers = append(servers, upstreamServer)
- }
- up.UpstreamServers = servers
+ up.UpstreamServers = parseUpstreamServers(req.Servers)
}
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go
index e1e26d6f001e..e7ab54059b2a 100644
--- a/agent/app/service/website_utils.go
+++ b/agent/app/service/website_utils.go
@@ -9,6 +9,7 @@ import (
"os"
"path"
"path/filepath"
+ "regexp"
"strconv"
"strings"
"syscall"
@@ -1533,3 +1534,89 @@ func getServer(website model.Website) (*components.Server, error) {
server := servers[0]
return server, nil
}
+
+func parseTimeString(input string) (int, string, error) {
+ input = strings.TrimSpace(input)
+
+ re := regexp.MustCompile(`^(\d+)([smhdw]?)$`)
+ matches := re.FindStringSubmatch(input)
+
+ if len(matches) < 2 {
+ return 0, "", fmt.Errorf("invalid time format: %s", input)
+ }
+
+ value, err := strconv.Atoi(matches[1])
+ if err != nil {
+ return 0, "", fmt.Errorf("invalid number: %s", matches[1])
+ }
+
+ unit := matches[2]
+ if unit == "" {
+ unit = "s"
+ }
+ return value, unit, nil
+}
+
+func parseUpstreamServers(reqServers []dto.NginxUpstreamServer) []*components.UpstreamServer {
+ var servers []*components.UpstreamServer
+ for _, server := range reqServers {
+ upstreamServer := &components.UpstreamServer{
+ Address: server.Server,
+ }
+ parameters := make(map[string]string)
+ if server.Weight > 0 {
+ parameters["weight"] = strconv.Itoa(server.Weight)
+ }
+ if server.MaxFails > 0 {
+ parameters["max_fails"] = strconv.Itoa(server.MaxFails)
+ }
+ if server.FailTimeout > 0 {
+ parameters["fail_timeout"] = fmt.Sprintf("%d%s", server.FailTimeout, server.FailTimeoutUnit)
+ }
+ if server.MaxConns > 0 {
+ parameters["max_conns"] = strconv.Itoa(server.MaxConns)
+ }
+ if server.Flag != "" {
+ upstreamServer.Flags = []string{server.Flag}
+ }
+ upstreamServer.Parameters = parameters
+ servers = append(servers, upstreamServer)
+ }
+ return servers
+}
+
+func getNginxUpstreamServers(upstreamServers []*components.UpstreamServer) []dto.NginxUpstreamServer {
+ var servers []dto.NginxUpstreamServer
+ for _, ups := range upstreamServers {
+ server := dto.NginxUpstreamServer{
+ Server: ups.Address,
+ }
+ parameters := ups.Parameters
+ if weight, ok := parameters["weight"]; ok {
+ num, err := strconv.Atoi(weight)
+ if err == nil {
+ server.Weight = num
+ }
+ }
+ if maxFails, ok := parameters["max_fails"]; ok {
+ num, err := strconv.Atoi(maxFails)
+ if err == nil {
+ server.MaxFails = num
+ }
+ }
+ if failTimeout, ok := parameters["fail_timeout"]; ok {
+ server.FailTimeout, server.FailTimeoutUnit, _ = parseTimeString(failTimeout)
+ }
+ if maxConns, ok := parameters["max_conns"]; ok {
+ num, err := strconv.Atoi(maxConns)
+ if err == nil {
+ server.MaxConns = num
+ }
+ }
+ for _, flag := range ups.Flags {
+ server.Flag = flag
+ }
+ servers = append(servers, server)
+ }
+ return servers
+}
diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts
index bafb7a2f3f99..ed998d282943 100644
--- a/frontend/src/api/interface/website.ts
+++ b/frontend/src/api/interface/website.ts
@@ -607,7 +607,8 @@ export namespace Website {
interface NginxUpstreamServer {
server: string;
weight: number;
- failTimeout: string;
+ failTimeout: number;
+ failTimeoutUnit: string;
maxFails: number;
maxConns: number;
flag: string;
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 0d832f8b9064..0e59ceab6380 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -2600,6 +2600,9 @@ const message = {
strategyDown: 'Down',
strategyBackup: 'Backup',
ipHashBackupErr: 'IP hash does not support backup nodes',
+ failTimeout: 'Failure timeout',
+ failTimeoutHelper:
+ 'The time window length for server health checks. When the cumulative number of failures reaches the threshold within this period, the server will be temporarily removed and retried after the same duration. Default 10 seconds',
staticChangePHPHelper: 'Currently a static website, you can switch to a PHP website',
proxyCache: 'Reverse Proxy Cache',
diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts
index 0a325b8396a6..7847c3d52306 100644
--- a/frontend/src/lang/modules/es-es.ts
+++ b/frontend/src/lang/modules/es-es.ts
@@ -2585,6 +2585,9 @@ const message = {
strategyDown: 'Baja',
strategyBackup: 'Backup',
ipHashBackupErr: 'IP hash does not support backup nodes',
+ failTimeout: 'Tiempo de espera de fallo',
+ failTimeoutHelper:
+ 'La duración de la ventana de tiempo para las comprobaciones de estado del servidor. Cuando el número acumulado de fallos alcanza el umbral dentro de este período, el servidor se eliminará temporalmente y se reintentará después de la misma duración. Por defecto 10 segundos',
staticChangePHPHelper: 'Actualmente es un sitio estático, puedes cambiarlo a PHP',
proxyCache: 'Caché de proxy inverso',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index 8b6c26cc9622..c891b8e6730f 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -2517,6 +2517,9 @@ const message = {
strategyDown: '無効',
strategyBackup: 'バックアップ',
ipHashBackupErr: 'IPハッシュはバックアップノードをサポートしていません',
+ failTimeout: '障害タイムアウト',
+ failTimeoutHelper:
+ 'サーバーのヘルスチェックの時間ウィンドウの長さ。この期間内に累積障害回数がしきい値に達すると、サーバーは一時的に削除され、同じ時間経過後に再試行されます。デフォルト 10 秒',
staticChangePHPHelper: '現在は静的ウェブサイトですが、PHPウェブサイトに切り替えることができます。',
proxyCache: 'リバースプロキシキャッシュ',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index a68ed9a84461..9a875897123f 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -2473,6 +2473,9 @@ const message = {
strategyDown: '비활성화',
strategyBackup: '백업',
ipHashBackupErr: 'IP 해시는 백업 노드를 지원하지 않습니다',
+ failTimeout: '장애 시간 초과',
+ failTimeoutHelper:
+ '서버 상태 점검 시간 창 길이. 이 기간 동안 누적 실패 횟수가 임계값에 도달하면 서버가 일시적으로 제거되고 동일한 시간 후에 재시도됩니다. 기본값 10초',
staticChangePHPHelper: '현재 정적 웹사이트이며 PHP 웹사이트로 전환할 수 있습니다.',
proxyCache: '리버스 프록시 캐시',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index e51cceb99718..c6f164c97941 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -2577,6 +2577,9 @@ const message = {
strategyDown: 'Lumpuh',
strategyBackup: 'Sandaran',
ipHashBackupErr: 'Hash IP tidak menyokong nod sandaran',
+ failTimeout: 'Masa tamat kegagalan',
+ failTimeoutHelper:
+ 'Panjang tetingkap masa untuk pemeriksaan kesihatan pelayan. Apabila bilangan kegagalan terkumpul mencapai ambang dalam tempoh ini, pelayan akan dikeluarkan buat sementara waktu dan dicuba semula selepas tempoh yang sama. Lalai 10 saat',
staticChangePHPHelper: 'Kini laman web statik, boleh ditukar ke laman web PHP.',
proxyCache: 'Cache Proksi Terbalik',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index 24dfd1d5eeae..2fa2c9543dfe 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -2581,6 +2581,9 @@ const message = {
strategyDown: 'Desativar',
strategyBackup: 'Backup',
ipHashBackupErr: 'Hash IP não suporta nós de backup',
+ failTimeout: 'Tempo limite de falha',
+ failTimeoutHelper:
+ 'O comprimento da janela de tempo para verificações de integridade do servidor. Quando o número acumulado de falhas atinge o limite dentro deste período, o servidor será removido temporariamente e repetido após a mesma duração. Padrão 10 segundos',
staticChangePHPHelper: 'Atualmente um site estático, pode ser alterado para um site PHP.',
proxyCache: 'Cache de Proxy Reverso',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 9476c1f83367..7286882a9953 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -2578,6 +2578,9 @@ const message = {
strategyDown: 'Отключить',
strategyBackup: 'Резервный',
ipHashBackupErr: 'IP хэш не поддерживает резервные узлы',
+ failTimeout: 'Таймаут отказа',
+ failTimeoutHelper:
+ 'Длина временного окна для проверки работоспособности сервера. Когда кумулятивное количество отказов достигает порога в течение этого периода, сервер будет временно удален и повторно проверен через тот же промежуток времени. По умолчанию 10 секунд',
staticChangePHPHelper: 'В настоящее время статический сайт, можно переключить на PHP сайт.',
proxyCache: 'Кэш Обратного Прокси',
diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts
index ca4ed313fc72..b93a18861450 100644
--- a/frontend/src/lang/modules/tr.ts
+++ b/frontend/src/lang/modules/tr.ts
@@ -2634,6 +2634,9 @@ const message = {
strategyDown: 'Kapalı',
strategyBackup: 'Yedek',
ipHashBackupErr: 'IP 哈希不支持备用节点',
+ failTimeout: 'Hata zaman aşımı',
+ failTimeoutHelper:
+ 'Sunucu sağlık kontrolleri için zaman penceresi uzunluğu. Bu süre içinde biriken hata sayısı eşiğe ulaştığında, sunucu geçici olarak kaldırılacak ve aynı süre sonunda yeniden denenir. Varsayılan 10 saniye',
staticChangePHPHelper: 'Şu anda statik bir web sitesi, PHP web sitesine geçiş yapabilirsiniz',
proxyCache: 'Ters Vekil Önbelleği',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 847298c936c3..b87604daa94d 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -2418,6 +2418,9 @@ const message = {
strategyDown: '停用',
strategyBackup: '備用',
ipHashBackupErr: 'IP 雜湊不支援備用節點',
+ failTimeout: '故障超時',
+ failTimeoutHelper:
+ '服務器健康檢查的時間窗口長度。在該時間段內累計失敗次數達到閾值時,服務器將被暫時移除,並在經過相同時長後重新嘗試。默認 10 秒',
staticChangePHPHelper: '目前為靜態網站,可切換為 PHP 網站',
proxyCache: '反向代理快取',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index e27053cd2936..608e2a7e63cc 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -2411,6 +2411,9 @@ const message = {
strategyDown: '停用',
strategyBackup: '备用',
ipHashBackupErr: 'IP 哈希不支持备用节点',
+ failTimeout: '故障超时',
+ failTimeoutHelper:
+ '服务器健康检查的时间窗口长度。在该时间段内累计失败次数达到阈值时,服务器将被暂时移除,并在经过相同时长后重新尝试。默认 10 秒',
staticChangePHPHelper: '当前为静态网站,可以切换为 PHP 网站',
proxyCache: '反代缓存',
diff --git a/frontend/src/views/website/website/config/basic/load-balance/index.vue b/frontend/src/views/website/website/config/basic/load-balance/index.vue
index 73fc9f73e425..6e039b36d05f 100644
--- a/frontend/src/views/website/website/config/basic/load-balance/index.vue
+++ b/frontend/src/views/website/website/config/basic/load-balance/index.vue
@@ -30,7 +30,9 @@