new version with saga

This commit is contained in:
2026-07-12 01:39:26 +03:00
commit 10f6ed0bda
55 changed files with 40117 additions and 0 deletions

349
internal/cluster/auth.go Normal file
View File

@@ -0,0 +1,349 @@
/*
* 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/auth.go
// Назначение: Аутентификация и авторизация между узлами кластера
package cluster
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// NodeAuthConfig конфигурация аутентификации узлов
type NodeAuthConfig struct {
Enabled bool `json:"enabled"`
TokenTTL time.Duration `json:"token_ttl"`
PrivateKeyPath string `json:"private_key_path"`
PublicKeyPath string `json:"public_key_path"`
AllowedNodes []string `json:"allowed_nodes"`
RequireMTLS bool `json:"require_mtls"`
}
// NodeAuthToken представляет токен аутентификации узла
type NodeAuthToken struct {
NodeID string `json:"node_id"`
IssuedAt int64 `json:"issued_at"`
ExpiresAt int64 `json:"expires_at"`
Signature string `json:"signature"`
}
// NodeAuthenticator управляет аутентификацией между узлами
type NodeAuthenticator struct {
config *NodeAuthConfig
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
tokens sync.Map
mu sync.RWMutex
logger LoggerInterface // Используем LoggerInterface из node.go
}
// NewNodeAuthenticator создаёт новый аутентификатор узлов
func NewNodeAuthenticator(config *NodeAuthConfig, logger LoggerInterface) (*NodeAuthenticator, error) {
if config == nil {
config = &NodeAuthConfig{
Enabled: true,
TokenTTL: 24 * time.Hour,
AllowedNodes: make([]string, 0),
RequireMTLS: false,
}
}
na := &NodeAuthenticator{
config: config,
logger: logger,
}
if config.Enabled {
if err := na.loadKeys(); err != nil {
if err := na.generateKeys(); err != nil {
return nil, err
}
}
}
return na, nil
}
// generateKeys генерирует RSA ключи для аутентификации
func (na *NodeAuthenticator) generateKeys() error {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
na.privateKey = privateKey
na.publicKey = &privateKey.PublicKey
// Сохраняем ключи
if err := na.saveKeys(); err != nil {
return err
}
if na.logger != nil {
na.logger.Info("Generated new RSA keys for node authentication")
}
return nil
}
// loadKeys загружает RSA ключи с диска
func (na *NodeAuthenticator) loadKeys() error {
// Загружаем приватный ключ
privData, err := os.ReadFile(na.config.PrivateKeyPath)
if err != nil {
return err
}
block, _ := pem.Decode(privData)
if block == nil {
return fmt.Errorf("failed to decode private key")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return err
}
var ok bool
na.privateKey, ok = privateKey.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("invalid private key type")
}
// Загружаем публичный ключ
pubData, err := os.ReadFile(na.config.PublicKeyPath)
if err != nil {
return err
}
pubBlock, _ := pem.Decode(pubData)
if pubBlock == nil {
return fmt.Errorf("failed to decode public key")
}
publicKey, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
if err != nil {
return err
}
na.publicKey, ok = publicKey.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("invalid public key type")
}
return nil
}
// saveKeys сохраняет RSA ключи на диск
func (na *NodeAuthenticator) saveKeys() error {
if na.privateKey == nil {
return fmt.Errorf("no private key to save")
}
// Сохраняем приватный ключ
privBytes, err := x509.MarshalPKCS8PrivateKey(na.privateKey)
if err != nil {
return err
}
privPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
})
if err := os.MkdirAll(filepath.Dir(na.config.PrivateKeyPath), 0700); err != nil {
return err
}
if err := os.WriteFile(na.config.PrivateKeyPath, privPEM, 0600); err != nil {
return err
}
// Сохраняем публичный ключ
pubBytes, err := x509.MarshalPKIXPublicKey(na.publicKey)
if err != nil {
return err
}
pubPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubBytes,
})
if err := os.WriteFile(na.config.PublicKeyPath, pubPEM, 0644); err != nil {
return err
}
return nil
}
// GenerateToken генерирует токен для узла
func (na *NodeAuthenticator) GenerateToken(nodeID string) (string, error) {
if !na.config.Enabled {
return "", nil
}
now := time.Now().UnixMilli()
token := &NodeAuthToken{
NodeID: nodeID,
IssuedAt: now,
ExpiresAt: now + na.config.TokenTTL.Milliseconds(),
}
// Подписываем токен
data, err := json.Marshal(token)
if err != nil {
return "", err
}
hash := sha256.Sum256(data)
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
token.Signature = base64.StdEncoding.EncodeToString(signature)
// Сохраняем токен
tokenData, err := json.Marshal(token)
if err != nil {
return "", err
}
tokenString := base64.StdEncoding.EncodeToString(tokenData)
na.tokens.Store(nodeID, token)
return tokenString, nil
}
// VerifyToken проверяет токен узла
func (na *NodeAuthenticator) VerifyToken(tokenString string) (string, error) {
if !na.config.Enabled {
return "", nil
}
data, err := base64.StdEncoding.DecodeString(tokenString)
if err != nil {
return "", fmt.Errorf("invalid token encoding: %v", err)
}
var token NodeAuthToken
if err := json.Unmarshal(data, &token); err != nil {
return "", fmt.Errorf("invalid token format: %v", err)
}
// Проверяем срок действия
now := time.Now().UnixMilli()
if token.ExpiresAt < now {
return "", fmt.Errorf("token expired")
}
// Проверяем подпись
sigData, err := base64.StdEncoding.DecodeString(token.Signature)
if err != nil {
return "", fmt.Errorf("invalid signature: %v", err)
}
tokenCopy := token
tokenCopy.Signature = ""
dataWithoutSig, _ := json.Marshal(tokenCopy)
hash := sha256.Sum256(dataWithoutSig)
if err := rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData); err != nil {
return "", fmt.Errorf("invalid signature: %v", err)
}
// Проверяем, разрешён ли узел
if len(na.config.AllowedNodes) > 0 {
allowed := false
for _, node := range na.config.AllowedNodes {
if node == token.NodeID {
allowed = true
break
}
}
if !allowed {
return "", fmt.Errorf("node %s not allowed", token.NodeID)
}
}
return token.NodeID, nil
}
// AuthenticateRequest аутентифицирует запрос
func (na *NodeAuthenticator) AuthenticateRequest(data []byte, signature string) bool {
if !na.config.Enabled {
return true
}
sigData, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false
}
hash := sha256.Sum256(data)
err = rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData)
return err == nil
}
// SignRequest подписывает запрос
func (na *NodeAuthenticator) SignRequest(data []byte) (string, error) {
if !na.config.Enabled {
return "", nil
}
hash := sha256.Sum256(data)
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// AddAllowedNode добавляет разрешённый узел
func (na *NodeAuthenticator) AddAllowedNode(nodeID string) {
na.mu.Lock()
defer na.mu.Unlock()
for _, n := range na.config.AllowedNodes {
if n == nodeID {
return
}
}
na.config.AllowedNodes = append(na.config.AllowedNodes, nodeID)
}
// RemoveAllowedNode удаляет разрешённый узел
func (na *NodeAuthenticator) RemoveAllowedNode(nodeID string) {
na.mu.Lock()
defer na.mu.Unlock()
newList := make([]string, 0)
for _, n := range na.config.AllowedNodes {
if n != nodeID {
newList = append(newList, n)
}
}
na.config.AllowedNodes = newList
}

View File

@@ -0,0 +1,301 @@
/*
* 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/backpressure.go
// Назначение: Backpressure при перегрузке системы
package cluster
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// BackpressureLevel представляет уровень перегрузки
type BackpressureLevel int
const (
LevelNone BackpressureLevel = iota // Нет перегрузки
LevelLow // Низкая перегрузка - задержки
LevelMedium // Средняя перегрузка - отклонение части запросов
LevelHigh // Высокая перегрузка - отклонение большинства
LevelCritical // Критическая - только чтение
)
// BackpressureManager управляет backpressure
type BackpressureManager struct {
mu sync.RWMutex
currentLevel BackpressureLevel
cpuThreshold float64
memoryThreshold float64
queueSizeThreshold int
connectionThreshold int
currentCPU atomic.Uint64
currentMemory atomic.Uint64
currentQueueSize atomic.Int64
currentConnections atomic.Int64
rejectedCount atomic.Uint64
delayedCount atomic.Uint64
lastCheck time.Time
checkInterval time.Duration
logger LoggerInterface // Используем LoggerInterface из node.go
enabled bool
writeAllowed bool
readAllowed bool
rejectProbability atomic.Uint32
delayDuration atomic.Int64
}
// BackpressureConfig содержит настройки backpressure
type BackpressureConfig struct {
Enabled bool `json:"enabled"`
CPUThreshold float64 `json:"cpu_threshold"`
MemoryThreshold float64 `json:"memory_threshold"`
QueueSizeThreshold int `json:"queue_size_threshold"`
ConnectionThreshold int `json:"connection_threshold"`
CheckIntervalMs int `json:"check_interval_ms"`
LowDelayMs int64 `json:"low_delay_ms"`
MediumRejectProb uint32 `json:"medium_reject_prob"`
HighRejectProb uint32 `json:"high_reject_prob"`
}
// DefaultBackpressureConfig возвращает конфигурацию по умолчанию
func DefaultBackpressureConfig() *BackpressureConfig {
return &BackpressureConfig{
Enabled: true,
CPUThreshold: 0.8,
MemoryThreshold: 0.85,
QueueSizeThreshold: 10000,
ConnectionThreshold: 5000,
CheckIntervalMs: 1000,
LowDelayMs: 100,
MediumRejectProb: 30,
HighRejectProb: 70,
}
}
// NewBackpressureManager создаёт новый менеджер backpressure
func NewBackpressureManager(cfg *BackpressureConfig, logger LoggerInterface) *BackpressureManager {
if cfg == nil {
cfg = DefaultBackpressureConfig()
}
bpm := &BackpressureManager{
currentLevel: LevelNone,
cpuThreshold: cfg.CPUThreshold,
memoryThreshold: cfg.MemoryThreshold,
queueSizeThreshold: cfg.QueueSizeThreshold,
connectionThreshold: cfg.ConnectionThreshold,
checkInterval: time.Duration(cfg.CheckIntervalMs) * time.Millisecond,
logger: logger,
enabled: cfg.Enabled,
writeAllowed: true,
readAllowed: true,
rejectProbability: atomic.Uint32{},
delayDuration: atomic.Int64{},
}
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
if cfg.Enabled {
go bpm.monitorLoop()
}
if logger != nil {
logger.Debug("Backpressure manager initialized")
}
return bpm
}
// monitorLoop периодически проверяет метрики
func (bpm *BackpressureManager) monitorLoop() {
ticker := time.NewTicker(bpm.checkInterval)
defer ticker.Stop()
for range ticker.C {
bpm.updateLevel()
}
}
// updateLevel обновляет уровень перегрузки
func (bpm *BackpressureManager) updateLevel() {
cpu := float64(bpm.currentCPU.Load()) / 100.0
memory := float64(bpm.currentMemory.Load()) / 100.0
queueSize := bpm.currentQueueSize.Load()
connections := bpm.currentConnections.Load()
newLevel := LevelNone
if cpu >= bpm.cpuThreshold || memory >= bpm.memoryThreshold {
newLevel = LevelHigh
} else if queueSize > int64(bpm.queueSizeThreshold) {
if queueSize > int64(bpm.queueSizeThreshold*2) {
newLevel = LevelCritical
} else {
newLevel = LevelMedium
}
} else if connections > int64(bpm.connectionThreshold) {
newLevel = LevelLow
}
bpm.mu.Lock()
oldLevel := bpm.currentLevel
bpm.currentLevel = newLevel
bpm.mu.Unlock()
// Применяем политики в зависимости от уровня
bpm.applyPolicies(newLevel)
if oldLevel != newLevel && bpm.logger != nil {
bpm.logger.Info(fmt.Sprintf("Backpressure level changed from %v to %v (cpu=%.2f%%, mem=%.2f%%, queue=%d, conns=%d)",
bpm.levelToString(oldLevel), bpm.levelToString(newLevel), cpu*100, memory*100, queueSize, connections))
}
}
// applyPolicies применяет политики в зависимости от уровня
func (bpm *BackpressureManager) applyPolicies(level BackpressureLevel) {
bpm.mu.Lock()
defer bpm.mu.Unlock()
switch level {
case LevelNone:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
case LevelLow:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(100) // 100ms задержка
case LevelMedium:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(30) // 30% отклонение
bpm.delayDuration.Store(200)
case LevelHigh:
bpm.writeAllowed = false // Запись запрещена
bpm.readAllowed = true
bpm.rejectProbability.Store(70) // 70% отклонение
bpm.delayDuration.Store(500)
case LevelCritical:
bpm.writeAllowed = false
bpm.readAllowed = true // Только чтение
bpm.rejectProbability.Store(90)
bpm.delayDuration.Store(1000)
}
}
// BeforeRequest вызывается перед обработкой запроса
func (bpm *BackpressureManager) BeforeRequest(isWrite bool) error {
if !bpm.enabled {
return nil
}
bpm.mu.RLock()
level := bpm.currentLevel
writeAllowed := bpm.writeAllowed
readAllowed := bpm.readAllowed
rejectProb := bpm.rejectProbability.Load()
delayDur := bpm.delayDuration.Load()
bpm.mu.RUnlock()
// Проверяем разрешение на операцию
if isWrite && !writeAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("write operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
if !isWrite && !readAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("read operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
// Вероятностное отклонение
if rejectProb > 0 {
// Простая вероятностная проверка
if uint32(time.Now().UnixNano()%100) < rejectProb {
bpm.rejectedCount.Add(1)
return fmt.Errorf("request rejected due to backpressure (probability: %d%%)", rejectProb)
}
}
// Добавляем задержку если нужно
if delayDur > 0 {
bpm.delayedCount.Add(1)
time.Sleep(time.Duration(delayDur) * time.Millisecond)
}
return nil
}
// AfterRequest вызывается после обработки запроса
func (bpm *BackpressureManager) AfterRequest(duration time.Duration, success bool) {
// Можно использовать для дополнительной статистики
}
// UpdateMetrics обновляет метрики для backpressure
func (bpm *BackpressureManager) UpdateMetrics(cpuPercent, memoryPercent uint64, queueSize, connections int64) {
bpm.currentCPU.Store(cpuPercent)
bpm.currentMemory.Store(memoryPercent)
bpm.currentQueueSize.Store(queueSize)
bpm.currentConnections.Store(connections)
}
// GetCurrentLevel возвращает текущий уровень перегрузки
func (bpm *BackpressureManager) GetCurrentLevel() BackpressureLevel {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return bpm.currentLevel
}
// GetStats возвращает статистику backpressure
func (bpm *BackpressureManager) GetStats() map[string]interface{} {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return map[string]interface{}{
"current_level": bpm.levelToString(bpm.currentLevel),
"write_allowed": bpm.writeAllowed,
"read_allowed": bpm.readAllowed,
"reject_probability": bpm.rejectProbability.Load(),
"delay_ms": bpm.delayDuration.Load(),
"rejected_count": bpm.rejectedCount.Load(),
"delayed_count": bpm.delayedCount.Load(),
"cpu_threshold": bpm.cpuThreshold,
"memory_threshold": bpm.memoryThreshold,
"queue_threshold": bpm.queueSizeThreshold,
"conn_threshold": bpm.connectionThreshold,
}
}
func (bpm *BackpressureManager) levelToString(level BackpressureLevel) string {
switch level {
case LevelNone:
return "none"
case LevelLow:
return "low"
case LevelMedium:
return "medium"
case LevelHigh:
return "high"
case LevelCritical:
return "critical"
default:
return "unknown"
}
}

1208
internal/cluster/node.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
/*
* 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() {
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
}
prm.recoveryFuncs["WAL"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["Raft"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["PipelineReplicator"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["BatchCommitManager"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["ReshardingManager"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["RecoveryManager"] = 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,
})
if fn != nil {
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 {
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)
}
}()
if rr.fn != nil {
return rr.fn()
}
return nil
}
// IsRunning возвращает статус работы
func (rr *RecoverableRoutine) IsRunning() bool {
return rr.running.Load()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
/*
* 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/worker_pool.go
// Назначение: Lock-free пул воркеров для ограничения количества горутин.
// Использует атомарные операции и lock-free структуры данных.
package cluster
import (
"fmt"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"futriis/internal/log"
)
// WorkerPool представляет lock-free пул воркеров для выполнения задач
type WorkerPool struct {
maxWorkers int32 // Максимальное количество воркеров
activeWorkers atomic.Int32 // Текущее количество активных воркеров
tasks *LockFreeQueue // Lock-free очередь задач
stopChan chan struct{}
wg sync.WaitGroup
logger *log.Logger
// Статистика (атомарные счётчики)
submittedTasks atomic.Uint64
completedTasks atomic.Uint64
failedTasks atomic.Uint64
rejectedTasks atomic.Uint64
lastSubmitTime atomic.Int64
lastCompleteTime atomic.Int64
}
// Task представляет задачу для выполнения в пуле
type Task struct {
ID string
Execute func() error
CreatedAt int64
RetryCount int32
}
// LockFreeQueue представляет lock-free очередь на основе CAS операций
type LockFreeQueue struct {
head atomic.Value // *queueNode
tail atomic.Value // *queueNode
size atomic.Int64
}
type queueNode struct {
value *Task
next atomic.Value // *queueNode
}
// NewLockFreeQueue создаёт новую lock-free очередь
func NewLockFreeQueue() *LockFreeQueue {
q := &LockFreeQueue{}
dummy := &queueNode{}
q.head.Store(dummy)
q.tail.Store(dummy)
return q
}
// Enqueue добавляет задачу в очередь (lock-free)
func (q *LockFreeQueue) Enqueue(task *Task) bool {
newNode := &queueNode{value: task}
for {
tailVal := q.tail.Load()
if tailVal == nil {
continue
}
tail := tailVal.(*queueNode)
nextVal := tail.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if tailVal != q.tail.Load() {
continue
}
if next != nil {
q.tail.CompareAndSwap(tailVal, next)
continue
}
if tail.next.CompareAndSwap(nil, newNode) {
q.tail.CompareAndSwap(tailVal, newNode)
q.size.Add(1)
return true
}
}
}
// Dequeue извлекает задачу из очереди (lock-free)
func (q *LockFreeQueue) Dequeue() *Task {
for {
headVal := q.head.Load()
if headVal == nil {
return nil
}
head := headVal.(*queueNode)
tailVal := q.tail.Load()
if tailVal == nil {
return nil
}
tail := tailVal.(*queueNode)
nextVal := head.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if headVal != q.head.Load() {
continue
}
if head == tail {
if next == nil {
return nil
}
q.tail.CompareAndSwap(tailVal, next)
continue
}
if next == nil {
return nil
}
task := next.value
if q.head.CompareAndSwap(headVal, next) {
q.size.Add(-1)
return task
}
}
}
// Size возвращает текущий размер очереди (lock-free)
func (q *LockFreeQueue) Size() int64 {
return q.size.Load()
}
// NewWorkerPool создаёт новый lock-free пул воркеров
func NewWorkerPool(maxWorkers int, logger *log.Logger) *WorkerPool {
if maxWorkers <= 0 {
maxWorkers = 500
}
wp := &WorkerPool{
maxWorkers: int32(maxWorkers),
tasks: NewLockFreeQueue(),
stopChan: make(chan struct{}),
logger: logger,
}
// Запускаем диспетчер задач
go wp.dispatcher()
if logger != nil {
logger.Debug(fmt.Sprintf("Lock-free worker pool created: maxWorkers=%d", maxWorkers))
}
return wp
}
// dispatcher управляет воркерами
func (wp *WorkerPool) dispatcher() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker pool dispatcher panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем диспетчер
go wp.dispatcher()
}
}()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-wp.stopChan:
return
case <-ticker.C:
// Динамически регулируем количество воркеров
wp.adjustWorkers()
}
}
}
// adjustWorkers динамически регулирует количество воркеров
func (wp *WorkerPool) adjustWorkers() {
queueSize := wp.tasks.Size()
activeWorkers := wp.activeWorkers.Load()
// Если есть задачи и есть место для новых воркеров
if queueSize > 0 && activeWorkers < wp.maxWorkers {
if activeWorkers == 0 || queueSize > int64(activeWorkers)*2 {
if wp.activeWorkers.CompareAndSwap(activeWorkers, activeWorkers+1) {
wp.wg.Add(1)
go wp.worker()
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Added worker, active: %d/%d", activeWorkers+1, wp.maxWorkers))
}
}
}
}
// Уменьшаем количество воркеров, если нет задач
if queueSize == 0 && activeWorkers > 0 {
if activeWorkers > 1 {
lastComplete := wp.lastCompleteTime.Load()
if time.Now().UnixMilli()-lastComplete > 5000 {
task := &Task{
ID: fmt.Sprintf("stop_worker_%d", time.Now().UnixNano()),
Execute: func() error { return nil },
CreatedAt: time.Now().UnixMilli(),
}
if wp.tasks.Enqueue(task) {
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Stop signal sent to worker, active: %d/%d", activeWorkers-1, wp.maxWorkers))
}
}
}
}
}
}
// worker выполняет задачи из очереди
func (wp *WorkerPool) worker() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker panicked: %v\n%s", r, debug.Stack()))
}
wp.activeWorkers.Add(-1)
wp.wg.Done()
// Создаём нового воркера вместо упавшего
if wp.activeWorkers.Load() < wp.maxWorkers {
wp.activeWorkers.Add(1)
wp.wg.Add(1)
go wp.worker()
}
}
}()
for {
select {
case <-wp.stopChan:
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
default:
task := wp.tasks.Dequeue()
if task == nil {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Проверяем специальную задачу остановки
if len(task.ID) >= 12 && task.ID[:12] == "stop_worker_" {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Выполняем задачу
startTime := time.Now().UnixMilli()
err := task.Execute()
duration := time.Now().UnixMilli() - startTime
if err != nil {
wp.failedTasks.Add(1)
if wp.logger != nil && duration > 100 {
wp.logger.Warn(fmt.Sprintf("Task %s failed after %dms: %v", task.ID, duration, err))
}
} else {
wp.completedTasks.Add(1)
wp.lastCompleteTime.Store(time.Now().UnixMilli())
if wp.logger != nil && duration > 1000 {
wp.logger.Debug(fmt.Sprintf("Task %s completed in %dms", task.ID, duration))
}
}
}
}
}
// Submit отправляет задачу в пул
func (wp *WorkerPool) Submit(task *Task) error {
if task.Execute == nil {
return fmt.Errorf("task execute function is nil")
}
task.CreatedAt = time.Now().UnixMilli()
if !wp.tasks.Enqueue(task) {
wp.rejectedTasks.Add(1)
return fmt.Errorf("failed to enqueue task (queue full)")
}
wp.submittedTasks.Add(1)
wp.lastSubmitTime.Store(task.CreatedAt)
// Асинхронно добавляем воркера при необходимости
if wp.activeWorkers.Load() == 0 {
if wp.activeWorkers.CompareAndSwap(0, 1) {
wp.wg.Add(1)
go wp.worker()
}
}
return nil
}
// SubmitFunc отправляет функцию как задачу
func (wp *WorkerPool) SubmitFunc(id string, fn func() error) error {
return wp.Submit(&Task{
ID: id,
Execute: fn,
})
}
// GetStats возвращает статистику пула
func (wp *WorkerPool) GetStats() map[string]interface{} {
return map[string]interface{}{
"max_workers": wp.maxWorkers,
"active_workers": wp.activeWorkers.Load(),
"queue_size": wp.tasks.Size(),
"submitted_tasks": wp.submittedTasks.Load(),
"completed_tasks": wp.completedTasks.Load(),
"failed_tasks": wp.failedTasks.Load(),
"rejected_tasks": wp.rejectedTasks.Load(),
"last_submit_time": wp.lastSubmitTime.Load(),
"last_complete_time": wp.lastCompleteTime.Load(),
}
}
// Stop останавливает пул воркеров
func (wp *WorkerPool) Stop() {
close(wp.stopChan)
done := make(chan struct{})
go func() {
wp.wg.Wait()
close(done)
}()
select {
case <-done:
if wp.logger != nil {
wp.logger.Debug("Worker pool stopped gracefully")
}
case <-time.After(10 * time.Second):
if wp.logger != nil {
wp.logger.Warn("Worker pool stop timeout, forcing shutdown")
}
}
}