mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-05-10 17:43:11 +08:00
133 lines
3.6 KiB
Go
133 lines
3.6 KiB
Go
package fetcher
|
||
|
||
import (
|
||
"database/sql"
|
||
"log"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// SourceManager 代理源管理器(断路器)
|
||
type SourceManager struct {
|
||
db *sql.DB
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
func NewSourceManager(db *sql.DB) *SourceManager {
|
||
return &SourceManager{db: db}
|
||
}
|
||
|
||
// CanUseSource 判断源是否可用
|
||
func (sm *SourceManager) CanUseSource(url string) bool {
|
||
sm.mu.RLock()
|
||
defer sm.mu.RUnlock()
|
||
|
||
var status string
|
||
var disabledUntil sql.NullTime
|
||
err := sm.db.QueryRow(
|
||
`SELECT status, disabled_until FROM source_status WHERE url = ?`,
|
||
url,
|
||
).Scan(&status, &disabledUntil)
|
||
|
||
// 源不存在,默认可用
|
||
if err != nil {
|
||
return true
|
||
}
|
||
|
||
// 检查是否被禁用且还在冷却期
|
||
if status == "disabled" && disabledUntil.Valid {
|
||
if time.Now().Before(disabledUntil.Time) {
|
||
return false
|
||
}
|
||
// 冷却期结束,重置状态
|
||
sm.db.Exec(`UPDATE source_status SET status = 'active', consecutive_fails = 0 WHERE url = ?`, url)
|
||
return true
|
||
}
|
||
|
||
return status != "disabled"
|
||
}
|
||
|
||
// RecordSuccess 记录源抓取成功
|
||
func (sm *SourceManager) RecordSuccess(url string) {
|
||
sm.mu.Lock()
|
||
defer sm.mu.Unlock()
|
||
|
||
sm.db.Exec(`
|
||
INSERT INTO source_status (url, success_count, consecutive_fails, last_success, status)
|
||
VALUES (?, 1, 0, CURRENT_TIMESTAMP, 'active')
|
||
ON CONFLICT(url) DO UPDATE SET
|
||
success_count = success_count + 1,
|
||
consecutive_fails = 0,
|
||
last_success = CURRENT_TIMESTAMP,
|
||
status = 'active'
|
||
`, url)
|
||
}
|
||
|
||
// RecordFail 记录源抓取失败
|
||
func (sm *SourceManager) RecordFail(url string, failThreshold, disableThreshold, cooldownMinutes int) {
|
||
sm.mu.Lock()
|
||
defer sm.mu.Unlock()
|
||
|
||
// 增加失败计数
|
||
sm.db.Exec(`
|
||
INSERT INTO source_status (url, fail_count, consecutive_fails, last_fail)
|
||
VALUES (?, 1, 1, CURRENT_TIMESTAMP)
|
||
ON CONFLICT(url) DO UPDATE SET
|
||
fail_count = fail_count + 1,
|
||
consecutive_fails = consecutive_fails + 1,
|
||
last_fail = CURRENT_TIMESTAMP
|
||
`, url)
|
||
|
||
// 检查是否需要降级或禁用
|
||
var consecutiveFails int
|
||
sm.db.QueryRow(`SELECT consecutive_fails FROM source_status WHERE url = ?`, url).Scan(&consecutiveFails)
|
||
|
||
if consecutiveFails >= disableThreshold {
|
||
// 禁用源
|
||
disabledUntil := time.Now().Add(time.Duration(cooldownMinutes) * time.Minute)
|
||
sm.db.Exec(
|
||
`UPDATE source_status SET status = 'disabled', disabled_until = ? WHERE url = ?`,
|
||
disabledUntil, url,
|
||
)
|
||
log.Printf("[source] ⛔ 禁用源(连续失败%d次): %s (冷却%d分钟)", consecutiveFails, url, cooldownMinutes)
|
||
} else if consecutiveFails >= failThreshold {
|
||
// 降级源
|
||
sm.db.Exec(`UPDATE source_status SET status = 'degraded' WHERE url = ?`, url)
|
||
log.Printf("[source] ⚠️ 降级源(连续失败%d次): %s", consecutiveFails, url)
|
||
}
|
||
}
|
||
|
||
// GetSourceStats 获取所有源的统计信息
|
||
func (sm *SourceManager) GetSourceStats() ([]map[string]interface{}, error) {
|
||
rows, err := sm.db.Query(`
|
||
SELECT url, success_count, fail_count, consecutive_fails,
|
||
last_success, last_fail, status
|
||
FROM source_status
|
||
ORDER BY success_count DESC
|
||
`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var stats []map[string]interface{}
|
||
for rows.Next() {
|
||
var url, status string
|
||
var successCount, failCount, consecutiveFails int
|
||
var lastSuccess, lastFail sql.NullTime
|
||
|
||
rows.Scan(&url, &successCount, &failCount, &consecutiveFails, &lastSuccess, &lastFail, &status)
|
||
|
||
stats = append(stats, map[string]interface{}{
|
||
"url": url,
|
||
"success_count": successCount,
|
||
"fail_count": failCount,
|
||
"consecutive_fails": consecutiveFails,
|
||
"last_success": lastSuccess,
|
||
"last_fail": lastFail,
|
||
"status": status,
|
||
})
|
||
}
|
||
return stats, nil
|
||
}
|