Upload files to "internal/cluster"
This commit is contained in:
904
internal/cluster/network_replicator.go
Normal file
904
internal/cluster/network_replicator.go
Normal file
@@ -0,0 +1,904 @@
|
|||||||
|
/*
|
||||||
|
* 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/network_replicator.go
|
||||||
|
// Назначение: Сетевой репликатор для отправки данных между узлами кластера.
|
||||||
|
// Реализует надёжную доставку с повторными попытками, экспоненциальным backoff,
|
||||||
|
// джиттером и асинхронной отправкой через пул воркеров.
|
||||||
|
|
||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"futriis/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КОНФИГУРАЦИЯ РЕПЛИКАЦИИ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicationRetryConfig определяет конфигурацию повторных попыток для репликации.
|
||||||
|
type ReplicationRetryConfig struct {
|
||||||
|
MaxRetries int // Максимальное количество попыток
|
||||||
|
InitialBackoff time.Duration // Начальная задержка между попытками
|
||||||
|
MaxBackoff time.Duration // Максимальная задержка
|
||||||
|
BackoffFactor float64 // Множитель для экспоненциального увеличения задержки
|
||||||
|
JitterEnabled bool // Включение случайного джиттера
|
||||||
|
JitterPercent float64 // Процент джиттера (0.0 - 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultReplicationRetryConfig возвращает конфигурацию по умолчанию.
|
||||||
|
func DefaultReplicationRetryConfig() *ReplicationRetryConfig {
|
||||||
|
return &ReplicationRetryConfig{
|
||||||
|
MaxRetries: 5,
|
||||||
|
InitialBackoff: 100 * time.Millisecond,
|
||||||
|
MaxBackoff: 10 * time.Second,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
|
JitterEnabled: true,
|
||||||
|
JitterPercent: 0.3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// СТАТИСТИКА РЕПЛИКАЦИИ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicationStats содержит статистику работы репликатора.
|
||||||
|
type ReplicationStats struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
TotalReplications uint64 // Всего репликаций
|
||||||
|
SuccessfulReplicas uint64 // Успешных репликаций
|
||||||
|
FailedReplicas uint64 // Неудачных репликаций
|
||||||
|
RetriedReplicas uint64 // Репликаций с повторными попытками
|
||||||
|
ReplicationDuration time.Duration // Общая длительность репликаций
|
||||||
|
TargetStats map[string]*TargetStats // Статистика по целевым узлам
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetStats содержит статистику для конкретного узла.
|
||||||
|
type TargetStats struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
TotalAttempts uint64
|
||||||
|
Successful uint64
|
||||||
|
Failed uint64
|
||||||
|
LastFailure int64
|
||||||
|
LastSuccess int64
|
||||||
|
FailureCount uint64
|
||||||
|
SuccessiveFails uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicationStats создаёт новую статистику.
|
||||||
|
func NewReplicationStats() *ReplicationStats {
|
||||||
|
return &ReplicationStats{
|
||||||
|
TargetStats: make(map[string]*TargetStats),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrCreateTargetStats возвращает или создаёт статистику для целевого узла.
|
||||||
|
func (rs *ReplicationStats) GetOrCreateTargetStats(targetID string) *TargetStats {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
|
if stats, ok := rs.TargetStats[targetID]; ok {
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := &TargetStats{}
|
||||||
|
rs.TargetStats[targetID] = stats
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// РЕПЛИКАЦИОННОЕ СОЕДИНЕНИЕ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicationConnection представляет соединение для репликации.
|
||||||
|
type ReplicationConnection struct {
|
||||||
|
targetID string
|
||||||
|
targetAddr string
|
||||||
|
conn net.Conn
|
||||||
|
mu sync.Mutex
|
||||||
|
lastUsed int64
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicationConnection создаёт новое соединение.
|
||||||
|
func NewReplicationConnection(targetID, targetAddr string) *ReplicationConnection {
|
||||||
|
return &ReplicationConnection{
|
||||||
|
targetID: targetID,
|
||||||
|
targetAddr: targetAddr,
|
||||||
|
lastUsed: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect устанавливает соединение.
|
||||||
|
func (rc *ReplicationConnection) Connect(timeout time.Duration) error {
|
||||||
|
rc.mu.Lock()
|
||||||
|
defer rc.mu.Unlock()
|
||||||
|
|
||||||
|
if rc.closed {
|
||||||
|
return fmt.Errorf("connection already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.conn != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", rc.targetAddr, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to %s: %v", rc.targetAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.conn = conn
|
||||||
|
rc.lastUsed = time.Now().UnixMilli()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send отправляет данные через соединение.
|
||||||
|
func (rc *ReplicationConnection) Send(data []byte, timeout time.Duration) error {
|
||||||
|
rc.mu.Lock()
|
||||||
|
defer rc.mu.Unlock()
|
||||||
|
|
||||||
|
if rc.closed {
|
||||||
|
return fmt.Errorf("connection already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.conn == nil {
|
||||||
|
return fmt.Errorf("connection not established")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rc.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return fmt.Errorf("failed to set write deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем длину сообщения (4 байта, big-endian)
|
||||||
|
length := uint32(len(data))
|
||||||
|
lenBuf := []byte{
|
||||||
|
byte(length >> 24),
|
||||||
|
byte(length >> 16),
|
||||||
|
byte(length >> 8),
|
||||||
|
byte(length),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := rc.conn.Write(lenBuf); err != nil {
|
||||||
|
return fmt.Errorf("failed to send length: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем данные
|
||||||
|
_, err := rc.conn.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.lastUsed = time.Now().UnixMilli()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает соединение.
|
||||||
|
func (rc *ReplicationConnection) Close() error {
|
||||||
|
rc.mu.Lock()
|
||||||
|
defer rc.mu.Unlock()
|
||||||
|
|
||||||
|
if rc.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.closed = true
|
||||||
|
if rc.conn != nil {
|
||||||
|
return rc.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsActive проверяет, активно ли соединение.
|
||||||
|
func (rc *ReplicationConnection) IsActive() bool {
|
||||||
|
rc.mu.Lock()
|
||||||
|
defer rc.mu.Unlock()
|
||||||
|
|
||||||
|
if rc.closed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rc.conn == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка соединения
|
||||||
|
rc.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := rc.conn.Read(buf)
|
||||||
|
rc.conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ОСНОВНОЙ РЕПЛИКАТОР
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// NetworkReplicator реализует сетевую репликацию данных между узлами кластера.
|
||||||
|
// Поддерживает:
|
||||||
|
// - Асинхронную отправку через пул воркеров
|
||||||
|
// - Экспоненциальный backoff с джиттером
|
||||||
|
// - Повторные попытки при ошибках
|
||||||
|
// - Управление соединениями с keep-alive
|
||||||
|
// - Подробную статистику
|
||||||
|
type NetworkReplicator struct {
|
||||||
|
config *ReplicationRetryConfig
|
||||||
|
workerPool *WorkerPool
|
||||||
|
logger *log.Logger
|
||||||
|
stats *ReplicationStats
|
||||||
|
connections sync.Map // map[string]*ReplicationConnection
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mu sync.RWMutex
|
||||||
|
enabled atomic.Bool
|
||||||
|
connectionTTL time.Duration
|
||||||
|
cleanupTicker *time.Ticker
|
||||||
|
requestTimeout time.Duration
|
||||||
|
|
||||||
|
// Счётчики для метрик
|
||||||
|
bytesSent atomic.Uint64
|
||||||
|
bytesReceived atomic.Uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNetworkReplicator создаёт новый экземпляр сетевого репликатора.
|
||||||
|
func NewNetworkReplicator(config *ReplicationRetryConfig, workerPool *WorkerPool, logger *log.Logger) *NetworkReplicator {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultReplicationRetryConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем максимальное количество попыток
|
||||||
|
if config.MaxRetries < 1 {
|
||||||
|
config.MaxRetries = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем начальную задержку
|
||||||
|
if config.InitialBackoff < 10*time.Millisecond {
|
||||||
|
config.InitialBackoff = 10 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем максимальную задержку
|
||||||
|
if config.MaxBackoff < config.InitialBackoff {
|
||||||
|
config.MaxBackoff = config.InitialBackoff * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем фактор backoff
|
||||||
|
if config.BackoffFactor < 1.0 {
|
||||||
|
config.BackoffFactor = 1.5
|
||||||
|
}
|
||||||
|
if config.BackoffFactor > 10.0 {
|
||||||
|
config.BackoffFactor = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем процент джиттера
|
||||||
|
if config.JitterPercent < 0 {
|
||||||
|
config.JitterPercent = 0
|
||||||
|
}
|
||||||
|
if config.JitterPercent > 1.0 {
|
||||||
|
config.JitterPercent = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
nr := &NetworkReplicator{
|
||||||
|
config: config,
|
||||||
|
workerPool: workerPool,
|
||||||
|
logger: logger,
|
||||||
|
stats: NewReplicationStats(),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
connectionTTL: 30 * time.Second,
|
||||||
|
requestTimeout: 10 * time.Second,
|
||||||
|
cleanupTicker: time.NewTicker(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
nr.enabled.Store(true)
|
||||||
|
|
||||||
|
// Запускаем горутину для очистки неактивных соединений
|
||||||
|
nr.wg.Add(1)
|
||||||
|
go nr.cleanupLoop()
|
||||||
|
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info(fmt.Sprintf("Network replicator created: max_retries=%d, initial_backoff=%v, max_backoff=%v",
|
||||||
|
config.MaxRetries, config.InitialBackoff, config.MaxBackoff))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nr
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ОСНОВНЫЕ МЕТОДЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Replicate выполняет репликацию данных на целевой узел.
|
||||||
|
// Использует механизм повторных попыток с экспоненциальным backoff.
|
||||||
|
func (nr *NetworkReplicator) Replicate(targetNodeID, targetAddress string, data []byte) error {
|
||||||
|
if !nr.enabled.Load() {
|
||||||
|
return fmt.Errorf("replicator is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAddress == "" {
|
||||||
|
return fmt.Errorf("target address is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("data is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// Обновляем статистику
|
||||||
|
nr.stats.mu.Lock()
|
||||||
|
nr.stats.TotalReplications++
|
||||||
|
nr.stats.mu.Unlock()
|
||||||
|
|
||||||
|
// Получаем или создаём статистику для целевого узла
|
||||||
|
targetStats := nr.stats.GetOrCreateTargetStats(targetNodeID)
|
||||||
|
|
||||||
|
// Выполняем репликацию с повторными попытками
|
||||||
|
var lastErr error
|
||||||
|
var attempt int
|
||||||
|
|
||||||
|
for attempt = 0; attempt < nr.config.MaxRetries; attempt++ {
|
||||||
|
targetStats.mu.Lock()
|
||||||
|
targetStats.TotalAttempts++
|
||||||
|
targetStats.mu.Unlock()
|
||||||
|
|
||||||
|
// Проверяем, не остановлен ли репликатор
|
||||||
|
select {
|
||||||
|
case <-nr.stopChan:
|
||||||
|
return fmt.Errorf("replicator stopped")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем попытку репликации
|
||||||
|
err := nr.doReplicate(targetNodeID, targetAddress, data)
|
||||||
|
if err == nil {
|
||||||
|
// Успешно
|
||||||
|
targetStats.mu.Lock()
|
||||||
|
targetStats.Successful++
|
||||||
|
targetStats.LastSuccess = time.Now().UnixMilli()
|
||||||
|
targetStats.SuccessiveFails = 0
|
||||||
|
targetStats.mu.Unlock()
|
||||||
|
|
||||||
|
nr.stats.mu.Lock()
|
||||||
|
nr.stats.SuccessfulReplicas++
|
||||||
|
nr.stats.ReplicationDuration += time.Since(startTime)
|
||||||
|
nr.stats.mu.Unlock()
|
||||||
|
|
||||||
|
nr.bytesSent.Add(uint64(len(data)))
|
||||||
|
|
||||||
|
if nr.logger != nil && attempt > 0 {
|
||||||
|
nr.logger.Debug(fmt.Sprintf("Replication to %s succeeded after %d attempts", targetNodeID, attempt+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = err
|
||||||
|
|
||||||
|
targetStats.mu.Lock()
|
||||||
|
targetStats.Failed++
|
||||||
|
targetStats.LastFailure = time.Now().UnixMilli()
|
||||||
|
targetStats.SuccessiveFails++
|
||||||
|
targetStats.mu.Unlock()
|
||||||
|
|
||||||
|
nr.stats.mu.Lock()
|
||||||
|
nr.stats.FailedReplicas++
|
||||||
|
nr.stats.mu.Unlock()
|
||||||
|
|
||||||
|
// Если это была последняя попытка, выходим
|
||||||
|
if attempt == nr.config.MaxRetries-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем задержку перед следующей попыткой с джиттером
|
||||||
|
delay := nr.calculateBackoff(attempt)
|
||||||
|
|
||||||
|
if nr.logger != nil && attempt < 3 {
|
||||||
|
nr.logger.Debug(fmt.Sprintf("Replication to %s failed (attempt %d/%d): %v, retrying in %v",
|
||||||
|
targetNodeID, attempt+1, nr.config.MaxRetries, err, delay))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем перед следующей попыткой
|
||||||
|
select {
|
||||||
|
case <-nr.stopChan:
|
||||||
|
return fmt.Errorf("replicator stopped during retry")
|
||||||
|
case <-time.After(delay):
|
||||||
|
// Продолжаем
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все попытки исчерпаны
|
||||||
|
if nr.logger != nil {
|
||||||
|
nr.logger.Error(fmt.Sprintf("Replication to %s failed after %d attempts: %v",
|
||||||
|
targetNodeID, nr.config.MaxRetries, lastErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("replication failed after %d attempts: %v", nr.config.MaxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doReplicate выполняет одну попытку репликации.
|
||||||
|
func (nr *NetworkReplicator) doReplicate(targetNodeID, targetAddress string, data []byte) error {
|
||||||
|
// Получаем или создаём соединение
|
||||||
|
conn, err := nr.getOrCreateConnection(targetNodeID, targetAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём запрос
|
||||||
|
req := ReplicationRequest{
|
||||||
|
Type: "replicate",
|
||||||
|
FromNode: targetNodeID,
|
||||||
|
Data: data,
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
reqData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем данные
|
||||||
|
if err := conn.Send(reqData, nr.requestTimeout); err != nil {
|
||||||
|
// Закрываем соединение при ошибке
|
||||||
|
conn.Close()
|
||||||
|
nr.connections.Delete(targetNodeID)
|
||||||
|
return fmt.Errorf("send failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем ответ
|
||||||
|
response, err := nr.readResponse(conn)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
nr.connections.Delete(targetNodeID)
|
||||||
|
return fmt.Errorf("response failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем ответ
|
||||||
|
if response.Status != "success" {
|
||||||
|
return fmt.Errorf("remote error: %s", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readResponse читает ответ от удалённого узла.
|
||||||
|
func (nr *NetworkReplicator) readResponse(conn *ReplicationConnection) (*ReplicationResponse, error) {
|
||||||
|
conn.mu.Lock()
|
||||||
|
rawConn := conn.conn
|
||||||
|
conn.mu.Unlock()
|
||||||
|
|
||||||
|
if rawConn == nil {
|
||||||
|
return nil, fmt.Errorf("connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем таймаут на чтение
|
||||||
|
if err := rawConn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set read deadline: %v", err)
|
||||||
|
}
|
||||||
|
defer rawConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
// Читаем длину сообщения (4 байта)
|
||||||
|
lenBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(rawConn, lenBuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read message length: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
length := uint32(lenBuf[0])<<24 | uint32(lenBuf[1])<<16 | uint32(lenBuf[2])<<8 | uint32(lenBuf[3])
|
||||||
|
if length > 10*1024*1024 { // 10 MB лимит
|
||||||
|
return nil, fmt.Errorf("message too large: %d bytes", length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем данные
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(rawConn, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nr.bytesReceived.Add(uint64(len(data)))
|
||||||
|
|
||||||
|
// Декодируем ответ
|
||||||
|
var response ReplicationResponse
|
||||||
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// УПРАВЛЕНИЕ СОЕДИНЕНИЯМИ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// getOrCreateConnection получает существующее или создаёт новое соединение.
|
||||||
|
func (nr *NetworkReplicator) getOrCreateConnection(targetID, targetAddr string) (*ReplicationConnection, error) {
|
||||||
|
// Пытаемся получить существующее соединение
|
||||||
|
if val, ok := nr.connections.Load(targetID); ok {
|
||||||
|
conn := val.(*ReplicationConnection)
|
||||||
|
if conn.IsActive() {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
// Соединение неактивно, удаляем и создаём новое
|
||||||
|
nr.connections.Delete(targetID)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новое соединение
|
||||||
|
conn := NewReplicationConnection(targetID, targetAddr)
|
||||||
|
if err := conn.Connect(5 * time.Second); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nr.connections.Store(targetID, conn)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupLoop периодически очищает неактивные соединения.
|
||||||
|
func (nr *NetworkReplicator) cleanupLoop() {
|
||||||
|
defer nr.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-nr.stopChan:
|
||||||
|
return
|
||||||
|
case <-nr.cleanupTicker.C:
|
||||||
|
nr.cleanupConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupConnections удаляет неактивные и устаревшие соединения.
|
||||||
|
func (nr *NetworkReplicator) cleanupConnections() {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
ttl := int64(nr.connectionTTL.Milliseconds())
|
||||||
|
|
||||||
|
var toDelete []string
|
||||||
|
|
||||||
|
nr.connections.Range(func(key, value interface{}) bool {
|
||||||
|
targetID := key.(string)
|
||||||
|
conn := value.(*ReplicationConnection)
|
||||||
|
|
||||||
|
// Проверяем время последнего использования
|
||||||
|
if now-conn.lastUsed > ttl {
|
||||||
|
toDelete = append(toDelete, targetID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем активность
|
||||||
|
if !conn.IsActive() {
|
||||||
|
toDelete = append(toDelete, targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, targetID := range toDelete {
|
||||||
|
if val, ok := nr.connections.Load(targetID); ok {
|
||||||
|
conn := val.(*ReplicationConnection)
|
||||||
|
conn.Close()
|
||||||
|
nr.connections.Delete(targetID)
|
||||||
|
if nr.logger != nil {
|
||||||
|
nr.logger.Debug(fmt.Sprintf("Cleaned up connection to %s", targetID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// calculateBackoff вычисляет задержку для повторной попытки.
|
||||||
|
// Использует экспоненциальный backoff с джиттером.
|
||||||
|
func (nr *NetworkReplicator) calculateBackoff(attempt int) time.Duration {
|
||||||
|
// Экспоненциальный backoff: initial * factor^attempt
|
||||||
|
backoff := float64(nr.config.InitialBackoff) * math.Pow(nr.config.BackoffFactor, float64(attempt))
|
||||||
|
|
||||||
|
// Ограничиваем максимальной задержкой
|
||||||
|
if backoff > float64(nr.config.MaxBackoff) {
|
||||||
|
backoff = float64(nr.config.MaxBackoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяем джиттер
|
||||||
|
if nr.config.JitterEnabled {
|
||||||
|
jitter := float64(time.Duration(backoff)) * nr.config.JitterPercent
|
||||||
|
backoff += (rand.Float64()*2 - 1) * jitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем минимальной задержкой
|
||||||
|
if backoff < float64(nr.config.InitialBackoff) {
|
||||||
|
backoff = float64(nr.config.InitialBackoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// УПРАВЛЕНИЕ СОСТОЯНИЕМ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Enable включает репликатор.
|
||||||
|
func (nr *NetworkReplicator) Enable() {
|
||||||
|
nr.enabled.Store(true)
|
||||||
|
if nr.logger != nil {
|
||||||
|
nr.logger.Info("Network replicator enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable отключает репликатор.
|
||||||
|
func (nr *NetworkReplicator) Disable() {
|
||||||
|
nr.enabled.Store(false)
|
||||||
|
if nr.logger != nil {
|
||||||
|
nr.logger.Info("Network replicator disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled возвращает состояние репликатора.
|
||||||
|
func (nr *NetworkReplicator) IsEnabled() bool {
|
||||||
|
return nr.enabled.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает репликатор и освобождает ресурсы.
|
||||||
|
func (nr *NetworkReplicator) Close() error {
|
||||||
|
nr.enabled.Store(false)
|
||||||
|
|
||||||
|
close(nr.stopChan)
|
||||||
|
|
||||||
|
// Закрываем все соединения
|
||||||
|
nr.connections.Range(func(key, value interface{}) bool {
|
||||||
|
conn := value.(*ReplicationConnection)
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
nr.wg.Wait()
|
||||||
|
|
||||||
|
if nr.logger != nil {
|
||||||
|
nr.logger.Info("Network replicator closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// СТАТИСТИКА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// GetStats возвращает статистику работы репликатора.
|
||||||
|
func (nr *NetworkReplicator) GetStats() map[string]interface{} {
|
||||||
|
nr.stats.mu.RLock()
|
||||||
|
defer nr.stats.mu.RUnlock()
|
||||||
|
|
||||||
|
targetStats := make(map[string]interface{})
|
||||||
|
for targetID, stats := range nr.stats.TargetStats {
|
||||||
|
stats.mu.RLock()
|
||||||
|
targetStats[targetID] = map[string]interface{}{
|
||||||
|
"total_attempts": stats.TotalAttempts,
|
||||||
|
"successful": stats.Successful,
|
||||||
|
"failed": stats.Failed,
|
||||||
|
"last_failure": stats.LastFailure,
|
||||||
|
"last_success": stats.LastSuccess,
|
||||||
|
"successive_fails": stats.SuccessiveFails,
|
||||||
|
}
|
||||||
|
stats.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"enabled": nr.enabled.Load(),
|
||||||
|
"total_replications": nr.stats.TotalReplications,
|
||||||
|
"successful_replicas": nr.stats.SuccessfulReplicas,
|
||||||
|
"failed_replicas": nr.stats.FailedReplicas,
|
||||||
|
"retried_replicas": nr.stats.RetriedReplicas,
|
||||||
|
"bytes_sent": nr.bytesSent.Load(),
|
||||||
|
"bytes_received": nr.bytesReceived.Load(),
|
||||||
|
"active_connections": nr.getActiveConnectionsCount(),
|
||||||
|
"target_stats": targetStats,
|
||||||
|
"config": map[string]interface{}{
|
||||||
|
"max_retries": nr.config.MaxRetries,
|
||||||
|
"initial_backoff": nr.config.InitialBackoff.String(),
|
||||||
|
"max_backoff": nr.config.MaxBackoff.String(),
|
||||||
|
"backoff_factor": nr.config.BackoffFactor,
|
||||||
|
"jitter_enabled": nr.config.JitterEnabled,
|
||||||
|
"connection_ttl": nr.connectionTTL.String(),
|
||||||
|
"request_timeout": nr.requestTimeout.String(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getActiveConnectionsCount возвращает количество активных соединений.
|
||||||
|
func (nr *NetworkReplicator) getActiveConnectionsCount() int {
|
||||||
|
count := 0
|
||||||
|
nr.connections.Range(func(key, value interface{}) bool {
|
||||||
|
conn := value.(*ReplicationConnection)
|
||||||
|
if conn.IsActive() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetStats возвращает статистику для конкретного целевого узла.
|
||||||
|
func (nr *NetworkReplicator) GetTargetStats(targetID string) map[string]interface{} {
|
||||||
|
stats := nr.stats.GetOrCreateTargetStats(targetID)
|
||||||
|
|
||||||
|
stats.mu.RLock()
|
||||||
|
defer stats.mu.RUnlock()
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_attempts": stats.TotalAttempts,
|
||||||
|
"successful": stats.Successful,
|
||||||
|
"failed": stats.Failed,
|
||||||
|
"last_failure": stats.LastFailure,
|
||||||
|
"last_success": stats.LastSuccess,
|
||||||
|
"successive_fails": stats.SuccessiveFails,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ВСПОМОГАТЕЛЬНЫЕ СТРУКТУРЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicationRequest представляет запрос репликации.
|
||||||
|
type ReplicationRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
FromNode string `json:"from_node"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicationResponse представляет ответ на запрос репликации.
|
||||||
|
type ReplicationResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// БЫСТРАЯ РЕПЛИКАЦИЯ (СИНХРОННАЯ)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicateSync выполняет синхронную репликацию с подтверждением от целевого узла.
|
||||||
|
// Используется для критически важных операций, требующих гарантированной доставки.
|
||||||
|
func (nr *NetworkReplicator) ReplicateSync(targetNodeID, targetAddress string, data []byte, timeout time.Duration) error {
|
||||||
|
if !nr.enabled.Load() {
|
||||||
|
return fmt.Errorf("replicator is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAddress == "" {
|
||||||
|
return fmt.Errorf("target address is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("data is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём контекст с таймаутом
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Канал для результата
|
||||||
|
resultChan := make(chan error, 1)
|
||||||
|
|
||||||
|
// Выполняем репликацию в горутине
|
||||||
|
go func() {
|
||||||
|
resultChan <- nr.Replicate(targetNodeID, targetAddress, data)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Ожидаем результат или таймаут
|
||||||
|
select {
|
||||||
|
case err := <-resultChan:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("replication timeout after %v", timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// АСИНХРОННАЯ РЕПЛИКАЦИЯ (НЕБЛОКИРУЮЩАЯ)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReplicateAsync выполняет асинхронную репликацию.
|
||||||
|
// Возвращает сразу, не дожидаясь подтверждения.
|
||||||
|
func (nr *NetworkReplicator) ReplicateAsync(targetNodeID, targetAddress string, data []byte, callback func(error)) error {
|
||||||
|
if !nr.enabled.Load() {
|
||||||
|
return fmt.Errorf("replicator is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAddress == "" {
|
||||||
|
return fmt.Errorf("target address is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("data is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём задачу для пула воркеров
|
||||||
|
taskID := fmt.Sprintf("replicate_async_%s_%s_%d", targetNodeID, targetAddress, time.Now().UnixNano())
|
||||||
|
|
||||||
|
err := nr.workerPool.SubmitFunc(taskID, func() error {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
err := nr.Replicate(targetNodeID, targetAddress, data)
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
if nr.logger != nil && duration > 5*time.Second {
|
||||||
|
nr.logger.Warn(fmt.Sprintf("Slow async replication to %s took %v", targetNodeID, duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to submit async replication: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nr.stats.mu.Lock()
|
||||||
|
nr.stats.RetriedReplicas++
|
||||||
|
nr.stats.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ПАКЕТНАЯ РЕПЛИКАЦИЯ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// BatchReplicationData содержит данные для пакетной репликации.
|
||||||
|
type BatchReplicationData struct {
|
||||||
|
Documents []map[string]interface{} `json:"documents"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
Collection string `json:"collection"`
|
||||||
|
BatchID string `json:"batch_id"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicateBatch выполняет пакетную репликацию нескольких документов.
|
||||||
|
func (nr *NetworkReplicator) ReplicateBatch(targetNodeID, targetAddress string, batch *BatchReplicationData) error {
|
||||||
|
if !nr.enabled.Load() {
|
||||||
|
return fmt.Errorf("replicator is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if batch == nil || len(batch.Documents) == 0 {
|
||||||
|
return fmt.Errorf("empty batch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сериализуем пакет
|
||||||
|
data, err := json.Marshal(batch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal batch: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём запрос с типом "batch_replicate"
|
||||||
|
req := ReplicationRequest{
|
||||||
|
Type: "batch_replicate",
|
||||||
|
FromNode: targetNodeID,
|
||||||
|
Data: data,
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
reqData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal batch request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем репликацию
|
||||||
|
return nr.Replicate(targetNodeID, targetAddress, reqData)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user