Upload files to "internal/cluster"

This commit is contained in:
2026-07-31 13:38:40 +00:00
parent efb67c0ab6
commit dadeab5c64

View 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)
}