Files
futriix/internal/cluster/panic_recovery.go

365 lines
11 KiB
Go
Raw Permalink Normal View History

2026-06-11 17:38:45 +00:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/panic_recovery.go
// Назначение: Автоматическое восстановление после паники по всему коду
package cluster
import (
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"time"
)
// PanicInfo содержит информацию о панике
type PanicInfo struct {
ID string
GoroutineID int64
PanicValue interface{}
StackTrace string
Timestamp int64
Recovered bool
RecoveryTime int64
}
// PanicRecoveryManager управляет восстановлением после паник
type PanicRecoveryManager struct {
mu sync.RWMutex
panics map[string]*PanicInfo
maxPanics int
recoveryFuncs map[string]func(interface{}) error
logger LoggerInterface
stopChan chan struct{}
wg sync.WaitGroup
totalPanics atomic.Uint64
recoveredFrom atomic.Uint64
failedRecoveries atomic.Uint64
}
// NewPanicRecoveryManager создаёт новый менеджер восстановления
func NewPanicRecoveryManager(logger LoggerInterface) *PanicRecoveryManager {
prm := &PanicRecoveryManager{
panics: make(map[string]*PanicInfo),
maxPanics: 1000,
recoveryFuncs: make(map[string]func(interface{}) error),
logger: logger,
stopChan: make(chan struct{}),
}
prm.registerDefaultRecoveryFuncs()
// Запускаем периодическую очистку старых паник
prm.wg.Add(1)
go prm.cleanupOldPanics()
if logger != nil {
logger.Info("Panic recovery manager initialized")
}
return prm
}
// getGoroutineID возвращает ID текущей горутины
func getGoroutineID() int64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
var id int64
fmt.Sscanf(string(buf[:n]), "goroutine %d", &id)
return id
}
// RegisterRecoveryFunc регистрирует функцию восстановления для компонента
func (prm *PanicRecoveryManager) RegisterRecoveryFunc(component string, fn func(interface{}) error) {
prm.mu.Lock()
defer prm.mu.Unlock()
prm.recoveryFuncs[component] = fn
}
// registerDefaultRecoveryFuncs регистрирует стандартные функции восстановления
func (prm *PanicRecoveryManager) registerDefaultRecoveryFuncs() {
// Восстановление для TCP сервера
prm.recoveryFuncs["TCPServer"] = func(p interface{}) error {
return nil // Базовое восстановление
}
// Восстановление для обработчика соединений
prm.recoveryFuncs["ConnectionHandler"] = func(p interface{}) error {
return nil
}
// Восстановление для репликации
prm.recoveryFuncs["Replication"] = func(p interface{}) error {
return nil
}
// Восстановление для WAL
prm.recoveryFuncs["WAL"] = func(p interface{}) error {
return nil
}
// Восстановление для Raft
prm.recoveryFuncs["Raft"] = func(p interface{}) error {
return nil
}
}
// Recover обрабатывает панику и пытается восстановиться
func (prm *PanicRecoveryManager) Recover(component string, context map[string]interface{}) {
if r := recover(); r != nil {
prm.totalPanics.Add(1)
panicInfo := &PanicInfo{
ID: fmt.Sprintf("panic_%d_%d", time.Now().UnixNano(), prm.totalPanics.Load()),
GoroutineID: getGoroutineID(),
PanicValue: r,
StackTrace: string(debug.Stack()),
Timestamp: time.Now().UnixMilli(),
Recovered: false,
}
prm.mu.Lock()
// Ограничиваем количество сохраняемых паник
if len(prm.panics) >= prm.maxPanics {
for k := range prm.panics {
delete(prm.panics, k)
break
}
}
prm.panics[panicInfo.ID] = panicInfo
prm.mu.Unlock()
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("PANIC in component %s: %v\n%s", component, r, panicInfo.StackTrace))
}
// Пытаемся восстановиться
if recoveryFn, ok := prm.recoveryFuncs[component]; ok {
if err := recoveryFn(r); err != nil {
prm.failedRecoveries.Add(1)
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("Failed to recover from panic in %s: %v", component, err))
}
} else {
panicInfo.Recovered = true
panicInfo.RecoveryTime = time.Now().UnixMilli()
prm.recoveredFrom.Add(1)
if prm.logger != nil {
prm.logger.Info(fmt.Sprintf("Successfully recovered from panic in %s", component))
}
}
} else {
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("No recovery function registered for component %s", component))
}
}
}
}
// RecoverWithRetry пытается восстановиться с повторными попытками
func (prm *PanicRecoveryManager) RecoverWithRetry(component string, retries int, fn func() error) error {
var lastErr error
for i := 0; i < retries; i++ {
func() {
defer prm.Recover(component, map[string]interface{}{
"attempt": i + 1,
"retries": retries,
})
lastErr = fn()
}()
if lastErr == nil {
return nil
}
if i < retries-1 {
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
}
}
return lastErr
}
// SafeGo безопасно запускает горутину с восстановлением
func (prm *PanicRecoveryManager) SafeGo(component string, fn func()) {
go func() {
defer prm.Recover(component, nil)
fn()
}()
}
// SafeGoWithContext безопасно запускает горутину с контекстом
func (prm *PanicRecoveryManager) SafeGoWithContext(component string, ctx map[string]interface{}, fn func()) {
go func() {
defer prm.Recover(component, ctx)
fn()
}()
}
// cleanupOldPanics периодически очищает старые записи о паниках
func (prm *PanicRecoveryManager) cleanupOldPanics() {
defer prm.wg.Done()
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-prm.stopChan:
return
case <-ticker.C:
prm.mu.Lock()
now := time.Now().UnixMilli()
for id, info := range prm.panics {
// Удаляем паники старше 24 часов
if now-info.Timestamp > 24*3600*1000 {
delete(prm.panics, id)
}
}
prm.mu.Unlock()
}
}
}
// GetPanicInfo возвращает информацию о панике
func (prm *PanicRecoveryManager) GetPanicInfo(id string) *PanicInfo {
prm.mu.RLock()
defer prm.mu.RUnlock()
return prm.panics[id]
}
// GetAllPanics возвращает все паники
func (prm *PanicRecoveryManager) GetAllPanics() []*PanicInfo {
prm.mu.RLock()
defer prm.mu.RUnlock()
result := make([]*PanicInfo, 0, len(prm.panics))
for _, info := range prm.panics {
result = append(result, info)
}
return result
}
// GetStats возвращает статистику
func (prm *PanicRecoveryManager) GetStats() map[string]interface{} {
return map[string]interface{}{
"total_panics": prm.totalPanics.Load(),
"recovered_from": prm.recoveredFrom.Load(),
"failed_recoveries": prm.failedRecoveries.Load(),
"active_panics": len(prm.panics),
"max_panics": prm.maxPanics,
}
}
// Stop останавливает менеджер
func (prm *PanicRecoveryManager) Stop() {
close(prm.stopChan)
prm.wg.Wait()
}
// RecoverableRoutine обёртка для восстанавливаемых горутин
type RecoverableRoutine struct {
name string
fn func() error
mgr *PanicRecoveryManager
maxRetries int
stopChan chan struct{}
running atomic.Bool
mu sync.Mutex
}
// NewRecoverableRoutine создаёт новую восстанавливаемую горутину
func NewRecoverableRoutine(name string, fn func() error, mgr *PanicRecoveryManager, maxRetries int) *RecoverableRoutine {
return &RecoverableRoutine{
name: name,
fn: fn,
mgr: mgr,
maxRetries: maxRetries,
stopChan: make(chan struct{}),
}
}
// Start запускает горутину с автоматическим восстановлением
func (rr *RecoverableRoutine) Start() {
if !rr.running.CompareAndSwap(false, true) {
return
}
go rr.run()
}
// Stop останавливает горутину
func (rr *RecoverableRoutine) Stop() {
close(rr.stopChan)
rr.running.Store(false)
}
// run запускает основной цикл с восстановлением
func (rr *RecoverableRoutine) run() {
defer rr.running.Store(false)
retries := 0
for {
select {
case <-rr.stopChan:
return
default:
err := rr.runWithRecovery()
if err == nil {
retries = 0
} else {
retries++
if retries > rr.maxRetries {
if rr.mgr.logger != nil {
rr.mgr.logger.Error(fmt.Sprintf("Routine %s exceeded max retries (%d)", rr.name, rr.maxRetries))
}
return
}
backoff := time.Duration(100*retries) * time.Millisecond
if backoff > 5*time.Second {
backoff = 5 * time.Second
}
select {
case <-rr.stopChan:
return
case <-time.After(backoff):
}
}
}
}
}
// runWithRecovery запускает функцию с защитой от паники
func (rr *RecoverableRoutine) runWithRecovery() (err error) {
defer func() {
if r := recover(); r != nil {
rr.mgr.Recover(rr.name, map[string]interface{}{
"routine": rr.name,
})
err = fmt.Errorf("panic recovered: %v", r)
}
}()
return rr.fn()
}
// IsRunning возвращает статус работы
func (rr *RecoverableRoutine) IsRunning() bool {
return rr.running.Load()
}