new version with saga
This commit is contained in:
349
internal/cluster/auth.go
Normal file
349
internal/cluster/auth.go
Normal 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
|
||||
}
|
||||
301
internal/cluster/backpressure.go
Normal file
301
internal/cluster/backpressure.go
Normal 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
1208
internal/cluster/node.go
Normal file
File diff suppressed because it is too large
Load Diff
370
internal/cluster/panic_recovery.go
Normal file
370
internal/cluster/panic_recovery.go
Normal 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()
|
||||
}
|
||||
3456
internal/cluster/raft_coordinator.go
Normal file
3456
internal/cluster/raft_coordinator.go
Normal file
File diff suppressed because it is too large
Load Diff
375
internal/cluster/worker_pool.go
Normal file
375
internal/cluster/worker_pool.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user