feat(P3): Docker健康检查 + Worker优雅重启 + 前端错误监控

This commit is contained in:
Sisyphus
2026-04-25 16:07:58 +08:00
parent 025be6dd78
commit 3613cfe5a3
5 changed files with 468 additions and 404 deletions

View File

@@ -17,6 +17,7 @@ import (
"time"
amqp "github.com/rabbitmq/amqp091-go"
"gorm.io/gorm"
)
const (
@@ -37,6 +38,21 @@ const (
popularityBindingKey = "video.popularity.*"
)
func connectWithRetry(name string, maxRetries int, fn func() error) {
for i := 0; i < maxRetries; i++ {
if err := fn(); err == nil {
return
}
wait := time.Duration(1<<i) * time.Second
if wait > 30*time.Second {
wait = 30 * time.Second
}
log.Printf("%s 不可用,%v 后重试 (%d/%d)...", name, wait, i+1, maxRetries)
time.Sleep(wait)
}
log.Fatalf("%s: 超过最大重试次数", name)
}
func main() {
// 加载配置
configPath := os.Getenv("CONFIG_PATH")
@@ -53,11 +69,13 @@ func main() {
} else {
log.Printf("Config loaded from file: %s", configPath)
}
// 连接数据库
sqlDB, err := db.NewDB(cfg.Database)
if err != nil {
log.Fatalf("Failed to connect database: %v", err)
}
// 连接数据库(带重试)
var sqlDB *gorm.DB
connectWithRetry("MySQL", 10, func() error {
var err error
sqlDB, err = db.NewDB(cfg.Database)
return err
})
defer db.CloseDB(sqlDB)
// 连接 Redis用于流行度更新
@@ -77,12 +95,14 @@ func main() {
log.Printf("Redis connected (popularity worker enabled)")
}
}
// 连接 RabbitMQ
// 连接 RabbitMQ(带重试)
url := "amqp://" + cfg.RabbitMQ.Username + ":" + cfg.RabbitMQ.Password + "@" + cfg.RabbitMQ.Host + ":" + strconv.Itoa(cfg.RabbitMQ.Port) + "/"
conn, err := amqp.Dial(url)
if err != nil {
log.Fatalf("Failed to connect rabbitmq: %v", err)
}
var conn *amqp.Connection
connectWithRetry("RabbitMQ", 10, func() error {
var err error
conn, err = amqp.Dial(url)
return err
})
defer conn.Close()
// 创建 RabbitMQ 通道
ch, err := conn.Channel()

View File

@@ -71,6 +71,11 @@ services:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- --post-data='{}' --header='Content-Type: application/json' http://localhost:8080/account/findByID || exit 1"]
interval: 10s
timeout: 5s
retries: 3
worker:
build:
@@ -87,6 +92,11 @@ services:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pgrep worker || exit 1"]
interval: 15s
timeout: 5s
retries: 3
frontend:
build:
@@ -97,6 +107,11 @@ services:
- "5173:80"
depends_on:
- backend
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"]
interval: 10s
timeout: 5s
retries: 3
volumes:
mysql_data:

View File

@@ -1,4 +1,5 @@
import { useAuthStore } from '../stores/auth'
import { reportError } from '../utils/error-reporter'
export class ApiError extends Error {
status: number
@@ -51,7 +52,9 @@ export async function postJson<T>(path: string, body: unknown, options?: { authR
data && typeof data === 'object' && (data as ApiErrorBody).error
? String((data as ApiErrorBody).error)
: `请求失败 (${res.status})`
throw new ApiError(msg, res.status, data)
const apiErr = new ApiError(msg, res.status, data)
reportError(apiErr, { path, status: res.status })
throw apiErr
}
return data as T
@@ -92,7 +95,9 @@ export async function postForm<T>(path: string, body: FormData, options?: { auth
data && typeof data === 'object' && (data as ApiErrorBody).error
? String((data as ApiErrorBody).error)
: `请求失败 (${res.status})`
throw new ApiError(msg, res.status, data)
const apiErr = new ApiError(msg, res.status, data)
reportError(apiErr, { path, status: res.status })
throw apiErr
}
return data as T

View File

@@ -3,8 +3,14 @@ import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
import router from './router'
import { reportError } from './utils/error-reporter'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.config.errorHandler = (err, _instance, info) => {
reportError(err instanceof Error ? err : new Error(String(err)), { info })
}
app.mount('#app')

View File

@@ -0,0 +1,18 @@
export function reportError(error: Error, context?: Record<string, unknown>) {
if (import.meta.env.DEV) {
console.error('[ErrorReporter]', error.message, context)
return
}
fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
}),
}).catch(() => {
/* 静默失败,避免错误上报自身导致循环 */
})
}