mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 01:11:31 +08:00
🐛 fix(app): 为稳定期首次连接增加瞬时网络重试保护
This commit is contained in:
@@ -603,7 +603,7 @@ func (a *App) connectDatabaseWithStartupRetry(rawConfig connection.ConnectionCon
|
||||
|
||||
if err := dbInst.Connect(connectConfig); err == nil {
|
||||
if attempt > 1 {
|
||||
logger.Warnf("数据库连接在启动保护重试后成功:%s 缓存Key=%s 尝试=%d/%d", formatConnSummary(effectiveConfig), cacheKey, attempt, startupConnectRetryAttempts)
|
||||
logger.Warnf("数据库连接在重试后成功:%s 缓存Key=%s 尝试=%d/%d", formatConnSummary(effectiveConfig), cacheKey, attempt, startupConnectRetryAttempts)
|
||||
}
|
||||
return dbInst, effectiveConfig, nil
|
||||
} else {
|
||||
@@ -611,10 +611,10 @@ func (a *App) connectDatabaseWithStartupRetry(rawConfig connection.ConnectionCon
|
||||
wrapped := wrapConnectError(effectiveConfig, err)
|
||||
lastErr = wrapped
|
||||
logger.Error(wrapped, "建立数据库连接失败:%s 缓存Key=%s", formatConnSummary(effectiveConfig), cacheKey)
|
||||
if !a.shouldRetryStartupConnect(err, attempt) {
|
||||
if !a.shouldRetryConnect(err, attempt) {
|
||||
return nil, effectiveConfig, wrapped
|
||||
}
|
||||
logger.Warnf("检测到启动期瞬时网络失败,准备重试连接:%s 缓存Key=%s 尝试=%d/%d 延迟=%s 原因=%s",
|
||||
logger.Warnf("检测到瞬时网络失败,准备重试连接:%s 缓存Key=%s 尝试=%d/%d 延迟=%s 原因=%s",
|
||||
formatConnSummary(effectiveConfig), cacheKey, attempt, startupConnectRetryAttempts, startupConnectRetryDelay, normalizeErrorMessage(err))
|
||||
time.Sleep(startupConnectRetryDelay)
|
||||
}
|
||||
@@ -645,18 +645,21 @@ func (a *App) startupPhaseLabel() string {
|
||||
return fmt.Sprintf("稳定期(age=%s)", age)
|
||||
}
|
||||
|
||||
func (a *App) shouldRetryStartupConnect(err error, attempt int) bool {
|
||||
func (a *App) shouldRetryConnect(err error, attempt int) bool {
|
||||
if attempt >= startupConnectRetryAttempts {
|
||||
return false
|
||||
}
|
||||
if a == nil || a.startedAt.IsZero() {
|
||||
if !isTransientStartupConnectError(err) {
|
||||
return false
|
||||
}
|
||||
age := time.Since(a.startedAt)
|
||||
if age < 0 || age > startupConnectRetryWindow {
|
||||
return false
|
||||
if a != nil && !a.startedAt.IsZero() {
|
||||
age := time.Since(a.startedAt)
|
||||
if age >= 0 && age <= startupConnectRetryWindow {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return isTransientStartupConnectError(err)
|
||||
// Outside startup window, still grant one retry for transient network glitches.
|
||||
return attempt == 1
|
||||
}
|
||||
|
||||
func isTransientStartupConnectError(err error) bool {
|
||||
|
||||
@@ -2,11 +2,14 @@ package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/db"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
)
|
||||
|
||||
type fakeStartupRetryDB struct {
|
||||
@@ -106,7 +109,7 @@ func TestConnectDatabaseWithStartupRetry_RetriesTransientFailureAndReappliesGlob
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectDatabaseWithStartupRetry_DoesNotRetryOutsideStartupWindow(t *testing.T) {
|
||||
func TestConnectDatabaseWithStartupRetry_RetriesOnceOutsideStartupWindow(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
@@ -130,12 +133,165 @@ func TestConnectDatabaseWithStartupRetry_DoesNotRetryOutsideStartupWindow(t *tes
|
||||
a := &App{startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second)}
|
||||
rawConfig := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
|
||||
|
||||
_, _, err := a.connectDatabaseWithStartupRetry(rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if connectCalls != 2 {
|
||||
t.Fatalf("expected 2 connect attempts outside startup window, got %d", connectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectDatabaseWithStartupRetry_DoesNotRetryOutsideStartupWindowForNonTransientError(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
}()
|
||||
|
||||
connectCalls := 0
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return &fakeStartupRetryDB{
|
||||
connect: func(config connection.ConnectionConfig) error {
|
||||
connectCalls++
|
||||
return errors.New("pq: password authentication failed")
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
a := &App{startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second)}
|
||||
rawConfig := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
|
||||
|
||||
_, _, err := a.connectDatabaseWithStartupRetry(rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if connectCalls != 1 {
|
||||
t.Fatalf("expected 1 connect attempt outside startup window, got %d", connectCalls)
|
||||
t.Fatalf("expected 1 connect attempt outside startup window for non-transient error, got %d", connectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectDatabaseWithStartupRetry_LogsRetryHintOutsideStartupWindow(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
}()
|
||||
|
||||
logPath := logger.Path()
|
||||
beforeSize := int64(0)
|
||||
if fi, err := os.Stat(logPath); err == nil {
|
||||
beforeSize = fi.Size()
|
||||
}
|
||||
|
||||
connectCalls := 0
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return &fakeStartupRetryDB{
|
||||
connect: func(config connection.ConnectionConfig) error {
|
||||
connectCalls++
|
||||
if connectCalls == 1 {
|
||||
return errors.New("dial tcp 10.1.131.86:5432: connect: no route to host")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
a := &App{startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second)}
|
||||
rawConfig := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
|
||||
|
||||
_, _, err := a.connectDatabaseWithStartupRetry(rawConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("expected success after retry, got error: %v", err)
|
||||
}
|
||||
if connectCalls != 2 {
|
||||
t.Fatalf("expected 2 connect attempts, got %d", connectCalls)
|
||||
}
|
||||
|
||||
logContent, readErr := os.ReadFile(logPath)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read log failed: %v", readErr)
|
||||
}
|
||||
if int64(len(logContent)) < beforeSize {
|
||||
t.Fatalf("expected log file to grow, before=%d after=%d", beforeSize, len(logContent))
|
||||
}
|
||||
appended := string(logContent[beforeSize:])
|
||||
if !strings.Contains(appended, "检测到瞬时网络失败,准备重试连接") {
|
||||
t.Fatalf("expected retry hint log in appended segment, got: %s", appended)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectDatabaseWithStartupRetry_OutsideStartupWindowTransientFailureStopsAfterOneRetry(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
}()
|
||||
|
||||
connectCalls := 0
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return &fakeStartupRetryDB{
|
||||
connect: func(config connection.ConnectionConfig) error {
|
||||
connectCalls++
|
||||
return errors.New("dial tcp 10.1.131.86:5432: connect: no route to host")
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
a := &App{startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second)}
|
||||
rawConfig := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
|
||||
|
||||
_, _, err := a.connectDatabaseWithStartupRetry(rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if connectCalls != 2 {
|
||||
t.Fatalf("expected 2 connect attempts outside startup window for transient error, got %d", connectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectDatabaseWithStartupRetry_StartupWindowTransientFailureUsesFullRetryBudget(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
defer func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
}()
|
||||
|
||||
connectCalls := 0
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return &fakeStartupRetryDB{
|
||||
connect: func(config connection.ConnectionConfig) error {
|
||||
connectCalls++
|
||||
return errors.New("dial tcp 10.1.131.86:5432: connect: no route to host")
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
a := &App{startedAt: time.Now()}
|
||||
rawConfig := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
|
||||
|
||||
_, _, err := a.connectDatabaseWithStartupRetry(rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if connectCalls != startupConnectRetryAttempts {
|
||||
t.Fatalf("expected %d connect attempts in startup window, got %d", startupConnectRetryAttempts, connectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user