Files
futriix/internal/cluster/node.go
2026-06-10 23:16:57 +03:00

975 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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/node.go
// Назначение: Реализация узла кластера (node) для распределённой СУБД с поддержкой временных меток.
// Полностью lock-free с использованием атомарных операций.
package cluster
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"futriis/internal/log"
"futriis/internal/storage"
"github.com/google/uuid"
)
// NodeStatus представляет состояние узла кластера
type NodeStatus int32
const (
StatusOffline NodeStatus = iota
StatusActive
StatusSyncing
StatusFailed
)
// Node представляет отдельный узел в распределённой системе
type Node struct {
ID string // Уникальный идентификатор узла
IP string // IP-адрес узла
Port int // Порт для коммуникации
Status atomic.Int32 // Атомарный статус узла (NodeStatus)
Storage *storage.Storage
logger *log.Logger
coordinator *RaftCoordinator // Ссылка на координатора (теперь RaftCoordinator)
lastSeen atomic.Int64 // Время последнего heartbeat (Unix миллисекунды)
joinedAt atomic.Int64 // Время присоединения к кластеру
createdAt int64 // Время создания узла
startedAt int64 // Время последнего старта узла
stoppedAt int64 // Время остановки узла
incomingConn chan net.Conn // Канал для входящих соединений (wait-free)
stopChan chan struct{}
requestCount atomic.Uint64 // Счётчик обработанных запросов
bytesRx atomic.Uint64 // Получено байт
bytesTx atomic.Uint64 // Отправлено байт
mu sync.RWMutex // Для синхронизации изменений статуса
// Новые компоненты для production-ready реализации
workerPool *WorkerPool // Пул воркеров для ограничения горутин
replicator *NetworkReplicator // Сетевой репликатор с retry и backoff
connPool sync.Map // Пул соединений к другим узлам
}
// NodeConfig содержит конфигурацию для создания узла
type NodeConfig struct {
IP string
Port int
Storage *storage.Storage
Logger *log.Logger
Coordinator *RaftCoordinator
}
// NewNode создаёт новый экземпляр узла кластера
func NewNode(ip string, port int, store *storage.Storage, logger *log.Logger) *Node {
now := time.Now().UnixMilli()
// Создаём пул воркеров с ограничением (максимум 500 одновременных горутин)
workerPool := NewWorkerPool(500, logger)
// Создаём сетевой репликатор с настройками retry и backoff
replicator := NewNetworkReplicator(DefaultReplicationRetryConfig(), workerPool, logger)
node := &Node{
ID: uuid.New().String(),
IP: ip,
Port: port,
Storage: store,
logger: logger,
incomingConn: make(chan net.Conn, 10000), // Увеличенный буфер
stopChan: make(chan struct{}),
createdAt: now,
startedAt: now,
workerPool: workerPool,
replicator: replicator,
}
node.Status.Store(int32(StatusActive))
node.lastSeen.Store(now)
node.joinedAt.Store(0)
// Безопасный запуск сервера с обработкой паник
SafeGoWithLogger(node.startTCPServer, logger, "TCPServer")
SafeGoWithLogger(node.handleIncomingConnections, logger, "IncomingConnections")
SafeGoWithLogger(node.heartbeatLoop, logger, "HeartbeatLoop")
SafeGoWithLogger(node.connectionHealthMonitor, logger, "ConnectionHealthMonitor")
if logger != nil {
logger.Info(fmt.Sprintf("Node %s created at %s with worker pool (max: 500)", node.ID, node.GetCreatedAtStr()))
}
return node
}
// startTCPServer запускает TCP-сервер для приёма запросов от других узлов
func (n *Node) startTCPServer() {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("TCP server panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем сервер
time.Sleep(5 * time.Second)
SafeGoWithLogger(n.startTCPServer, n.logger, "TCPServer")
}
}()
addr := fmt.Sprintf("%s:%d", n.IP, n.Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s failed to start TCP server: %v", n.ID, err))
}
n.Status.Store(int32(StatusFailed))
return
}
defer listener.Close()
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s listening on %s (started at %s)", n.ID, addr, n.GetStartedAtStr()))
}
for {
select {
case <-n.stopChan:
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s TCP server stopped", n.ID))
}
return
default:
// Устанавливаем таймаут для Accept
if err := listener.(*net.TCPListener).SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
continue
}
conn, err := listener.Accept()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s accept error: %v", n.ID, err))
}
continue
}
// Устанавливаем таймауты на соединение
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
select {
case n.incomingConn <- conn:
n.bytesRx.Add(1)
default:
if n.logger != nil {
n.logger.Warn(fmt.Sprintf("Node %s incoming connection queue full, dropping connection", n.ID))
}
conn.Close()
}
}
}
}
// handleIncomingConnections обрабатывает входящие соединения с использованием пула воркеров
func (n *Node) handleIncomingConnections() {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Incoming connections handler panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем обработчик
SafeGoWithLogger(n.handleIncomingConnections, n.logger, "IncomingConnections")
}
}()
for {
select {
case <-n.stopChan:
return
case conn := <-n.incomingConn:
n.requestCount.Add(1)
// Используем пул воркеров вместо создания неограниченного количества горутин
taskID := fmt.Sprintf("handle_conn_%d_%s", time.Now().UnixNano(), conn.RemoteAddr().String())
err := n.workerPool.SubmitFunc(taskID, func() error {
n.handleNodeRequest(conn)
return nil
})
if err != nil {
if n.logger != nil {
n.logger.Warn(fmt.Sprintf("Failed to submit connection task: %v", err))
}
conn.Close()
}
}
}
}
// handleNodeRequest обрабатывает конкретный запрос от другого узла
func (n *Node) handleNodeRequest(conn net.Conn) {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Request handler panicked: %v\n%s", r, debug.Stack()))
}
}
conn.Close()
}()
// Устанавливаем таймаут на чтение
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
decoder := json.NewDecoder(conn)
var req NodeRequest
if err := decoder.Decode(&req); err != nil {
if err != io.EOF && n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s failed to decode request: %v", n.ID, err))
}
return
}
n.lastSeen.Store(time.Now().UnixMilli())
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Node %s received request type %s from %s at %s",
n.ID, req.Type, req.FromNode, time.UnixMilli(req.Timestamp).Format("15:04:05.000")))
}
switch req.Type {
case "replicate":
n.handleReplicateRequest(req.Data)
case "query":
n.handleQueryRequest(req.Data, conn)
case "sync":
n.handleSyncRequest(req.Data, conn)
case "heartbeat":
n.handleHeartbeatRequest(req, conn)
case "status_sync":
n.handleStatusSyncRequest(req, conn)
default:
if n.logger != nil {
n.logger.Warn(fmt.Sprintf("Node %s unknown request type: %s", n.ID, req.Type))
}
}
}
// handleReplicateRequest обрабатывает запрос на репликацию документа
func (n *Node) handleReplicateRequest(data []byte) {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Replicate request handler panicked: %v\n%s", r, debug.Stack()))
}
}
}()
startTime := time.Now().UnixMilli()
var repData struct {
Database string `json:"database"`
Collection string `json:"collection"`
Document map[string]interface{} `json:"document"`
SourceNode string `json:"source_node"`
ReplicaID string `json:"replica_id"`
}
if err := json.Unmarshal(data, &repData); err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s failed to unmarshal replicate data: %v", n.ID, err))
}
return
}
db, err := n.Storage.GetDatabase(repData.Database)
if err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s database not found for replication: %s", n.ID, repData.Database))
}
return
}
coll, err := db.GetCollection(repData.Collection)
if err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s collection not found for replication: %s", n.ID, repData.Collection))
}
return
}
// Безопасное получение ID документа
docID, ok := repData.Document["_id"].(string)
if !ok {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s document missing _id field", n.ID))
}
return
}
doc := &storage.Document{
ID: docID,
Fields: repData.Document,
}
if err := coll.Insert(doc); err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Node %s failed to replicate document: %v", n.ID, err))
}
} else {
duration := time.Now().UnixMilli() - startTime
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Node %s replicated document %s from %s (took %d ms)",
n.ID, doc.ID, repData.SourceNode, duration))
}
}
}
// handleQueryRequest обрабатывает запрос на чтение данных с узла
func (n *Node) handleQueryRequest(data []byte, conn net.Conn) {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Query request handler panicked: %v\n%s", r, debug.Stack()))
}
n.sendErrorResponse(conn, "Internal server error")
}
}()
startTime := time.Now().UnixMilli()
var queryData struct {
Database string `json:"database"`
Collection string `json:"collection"`
DocumentID string `json:"document_id"`
RequestID string `json:"request_id"`
}
if err := json.Unmarshal(data, &queryData); err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
db, err := n.Storage.GetDatabase(queryData.Database)
if err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
coll, err := db.GetCollection(queryData.Collection)
if err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
doc, err := coll.Find(queryData.DocumentID)
if err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
duration := time.Now().UnixMilli() - startTime
response := map[string]interface{}{
"status": "success",
"data": doc,
"node_id": n.ID,
"request_id": queryData.RequestID,
"duration_ms": duration,
"timestamp": time.Now().UnixMilli(),
}
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
encoder := json.NewEncoder(conn)
if err := encoder.Encode(response); err == nil {
responseData, _ := json.Marshal(response)
n.bytesTx.Add(uint64(len(responseData)))
}
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Node %s handled query for %s.%s:%s (took %d ms)",
n.ID, queryData.Database, queryData.Collection, queryData.DocumentID, duration))
}
}
// handleSyncRequest обрабатывает запрос на синхронизацию всей коллекции
func (n *Node) handleSyncRequest(data []byte, conn net.Conn) {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Sync request handler panicked: %v\n%s", r, debug.Stack()))
}
n.sendErrorResponse(conn, "Internal server error")
}
}()
startTime := time.Now().UnixMilli()
var syncData struct {
Database string `json:"database"`
Collection string `json:"collection"`
RequestID string `json:"request_id"`
Since int64 `json:"since"`
}
if err := json.Unmarshal(data, &syncData); err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
db, err := n.Storage.GetDatabase(syncData.Database)
if err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
coll, err := db.GetCollection(syncData.Collection)
if err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
docs := coll.GetAllDocuments()
if syncData.Since > 0 {
filtered := make([]*storage.Document, 0)
for _, doc := range docs {
if doc.UpdatedAt > syncData.Since {
filtered = append(filtered, doc)
}
}
docs = filtered
}
duration := time.Now().UnixMilli() - startTime
response := map[string]interface{}{
"status": "success",
"docs": docs,
"count": len(docs),
"node_id": n.ID,
"request_id": syncData.RequestID,
"duration_ms": duration,
"timestamp": time.Now().UnixMilli(),
"sync_duration_ms": duration,
}
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
encoder := json.NewEncoder(conn)
if err := encoder.Encode(response); err != nil && n.logger != nil {
n.logger.Error(fmt.Sprintf("Failed to send sync response: %v", err))
}
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s synced %d documents from %s.%s (took %d ms)",
n.ID, len(docs), syncData.Database, syncData.Collection, duration))
}
}
// handleHeartbeatRequest обрабатывает heartbeat запрос
func (n *Node) handleHeartbeatRequest(req NodeRequest, conn net.Conn) {
n.lastSeen.Store(time.Now().UnixMilli())
response := map[string]interface{}{
"status": "alive",
"node_id": n.ID,
"timestamp": time.Now().UnixMilli(),
"uptime_ms": time.Now().UnixMilli() - n.startedAt,
}
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
encoder := json.NewEncoder(conn)
encoder.Encode(response)
}
// handleStatusSyncRequest обрабатывает запрос синхронизации статуса (для split-brain recovery)
func (n *Node) handleStatusSyncRequest(req NodeRequest, conn net.Conn) {
var syncStatus struct {
LeaderID string `json:"leader_id"`
Term uint64 `json:"term"`
ClusterSize int `json:"cluster_size"`
}
if err := json.Unmarshal(req.Data, &syncStatus); err != nil {
n.sendErrorResponse(conn, err.Error())
return
}
if n.coordinator != nil {
n.coordinator.HandleStatusSync(syncStatus.LeaderID, syncStatus.Term, syncStatus.ClusterSize)
}
response := map[string]interface{}{
"status": "synced",
"node_id": n.ID,
"term": n.coordinator.GetCurrentTerm(),
"is_leader": n.coordinator.IsLeader(),
"timestamp": time.Now().UnixMilli(),
}
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
encoder := json.NewEncoder(conn)
encoder.Encode(response)
}
// sendErrorResponse отправляет ошибку в ответ на запрос
func (n *Node) sendErrorResponse(conn net.Conn, errMsg string) {
response := map[string]interface{}{
"status": "error",
"error": errMsg,
"node_id": n.ID,
"timestamp": time.Now().UnixMilli(),
}
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
encoder := json.NewEncoder(conn)
encoder.Encode(response)
}
// heartbeatLoop отправляет периодические сигналы жизни координатору
func (n *Node) heartbeatLoop() {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Heartbeat loop panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем heartbeat loop
time.Sleep(5 * time.Second)
SafeGoWithLogger(n.heartbeatLoop, n.logger, "HeartbeatLoop")
}
}()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-n.stopChan:
return
case <-ticker.C:
if n.coordinator != nil {
n.coordinator.SendHeartbeat(n.ID)
n.lastSeen.Store(time.Now().UnixMilli())
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Node %s sent heartbeat at %s", n.ID, n.GetLastSeenStr()))
}
}
}
}
}
// connectionHealthMonitor мониторит здоровье соединений
func (n *Node) connectionHealthMonitor() {
defer func() {
if r := recover(); r != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Connection health monitor panicked: %v\n%s", r, debug.Stack()))
}
SafeGoWithLogger(n.connectionHealthMonitor, n.logger, "ConnectionHealthMonitor")
}
}()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-n.stopChan:
return
case <-ticker.C:
n.cleanupStaleConnections()
}
}
}
// cleanupStaleConnections очищает старые соединения
func (n *Node) cleanupStaleConnections() {
n.connPool.Range(func(key, value interface{}) bool {
if conn, ok := value.(net.Conn); ok {
// Проверяем, активно ли соединение
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
buf := make([]byte, 1)
_, err := conn.Read(buf)
if err != nil {
conn.Close()
n.connPool.Delete(key)
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Cleaned up stale connection for %v", key))
}
}
}
return true
})
}
// GetNodeStatus возвращает текущий статус узла (атомарно)
func (n *Node) GetNodeStatus() NodeStatus {
return NodeStatus(n.Status.Load())
}
// IsActive проверяет, активен ли узел
func (n *Node) IsActive() bool {
return NodeStatus(n.Status.Load()) == StatusActive
}
// SetStatus устанавливает статус узла с синхронизацией через Raft
func (n *Node) SetStatus(status NodeStatus) error {
n.mu.Lock()
defer n.mu.Unlock()
oldStatus := n.Status.Load()
if oldStatus == int32(status) {
return nil
}
if n.coordinator != nil && n.coordinator.IsLeader() {
if err := n.coordinator.UpdateNodeStatus(n.ID, status); err != nil {
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Failed to update node status via Raft: %v", err))
}
return err
}
}
n.Status.Store(int32(status))
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s status changed from %d to %d at %s",
n.ID, oldStatus, status, time.Now().Format("2006-01-02 15:04:05.000")))
}
return nil
}
// SetCoordinator устанавливает координатора для узла
func (n *Node) SetCoordinator(coord *RaftCoordinator) {
n.coordinator = coord
now := time.Now().UnixMilli()
n.joinedAt.Store(now)
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s joined cluster at %s", n.ID, n.GetJoinedAtStr()))
}
}
// JoinCluster присоединяет узел к кластеру
func (n *Node) JoinCluster(coord *RaftCoordinator) error {
if n.coordinator != nil {
return fmt.Errorf("node already joined to cluster")
}
n.SetCoordinator(coord)
if err := coord.RegisterNode(n); err != nil {
return fmt.Errorf("failed to register node: %v", err)
}
if err := n.SetStatus(StatusActive); err != nil {
return fmt.Errorf("failed to set active status: %v", err)
}
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s successfully joined cluster at %s", n.ID, n.GetJoinedAtStr()))
}
return nil
}
// LeaveCluster покидает кластер
func (n *Node) LeaveCluster() error {
if n.coordinator == nil {
return fmt.Errorf("node not in cluster")
}
if err := n.SetStatus(StatusOffline); err != nil {
n.logger.Warn(fmt.Sprintf("Failed to set offline status: %v", err))
}
if err := n.coordinator.RemoveNode(n.ID); err != nil {
n.logger.Warn(fmt.Sprintf("Failed to remove node from coordinator: %v", err))
}
n.coordinator = nil
n.joinedAt.Store(0)
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s left cluster at %s", n.ID, time.Now().Format("2006-01-02 15:04:05.000")))
}
return nil
}
// GetLastSeen возвращает время последнего контакта
func (n *Node) GetLastSeen() int64 {
return n.lastSeen.Load()
}
// GetLastSeenStr возвращает человекочитаемое время последнего контакта
func (n *Node) GetLastSeenStr() string {
lastSeen := n.lastSeen.Load()
if lastSeen == 0 {
return "never"
}
return time.UnixMilli(lastSeen).Format("2006-01-02 15:04:05.000")
}
// GetJoinedAt возвращает время присоединения к кластеру
func (n *Node) GetJoinedAt() int64 {
return n.joinedAt.Load()
}
// GetJoinedAtStr возвращает человекочитаемое время присоединения
func (n *Node) GetJoinedAtStr() string {
joinedAt := n.joinedAt.Load()
if joinedAt == 0 {
return "not joined"
}
return time.UnixMilli(joinedAt).Format("2006-01-02 15:04:05.000")
}
// GetStartedAt возвращает время старта узла
func (n *Node) GetStartedAt() int64 {
return n.startedAt
}
// GetStartedAtStr возвращает человекочитаемое время старта
func (n *Node) GetStartedAtStr() string {
return time.UnixMilli(n.startedAt).Format("2006-01-02 15:04:05.000")
}
// GetCreatedAt возвращает время создания узла
func (n *Node) GetCreatedAt() int64 {
return n.createdAt
}
// GetCreatedAtStr возвращает человекочитаемое время создания
func (n *Node) GetCreatedAtStr() string {
return time.UnixMilli(n.createdAt).Format("2006-01-02 15:04:05.000")
}
// GetUptime возвращает время работы узла
func (n *Node) GetUptime() time.Duration {
if n.startedAt == 0 {
return 0
}
return time.Duration(time.Now().UnixMilli()-n.startedAt) * time.Millisecond
}
// GetStats возвращает статистику узла
func (n *Node) GetStats() map[string]interface{} {
stats := map[string]interface{}{
"id": n.ID,
"ip": n.IP,
"port": n.Port,
"status": n.GetNodeStatus(),
"created_at": n.GetCreatedAtStr(),
"started_at": n.GetStartedAtStr(),
"joined_at": n.GetJoinedAtStr(),
"last_seen": n.GetLastSeenStr(),
"uptime": n.GetUptime().String(),
"request_count": n.requestCount.Load(),
"bytes_rx": n.bytesRx.Load(),
"bytes_tx": n.bytesTx.Load(),
}
if n.workerPool != nil {
stats["worker_pool"] = n.workerPool.GetStats()
}
if n.replicator != nil {
stats["replication"] = n.replicator.GetStats()
}
return stats
}
// Stop останавливает работу узла
func (n *Node) Stop() {
if n.coordinator != nil && n.coordinator.IsLeader() {
n.SetStatus(StatusOffline)
}
n.Status.Store(int32(StatusOffline))
n.stoppedAt = time.Now().UnixMilli()
close(n.stopChan)
if n.workerPool != nil {
n.workerPool.Stop()
}
if n.replicator != nil {
n.replicator.Close()
}
// Закрываем все соединения в пуле
n.connPool.Range(func(key, value interface{}) bool {
if conn, ok := value.(net.Conn); ok {
conn.Close()
}
return true
})
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Node %s stopped at %s", n.ID, time.UnixMilli(n.stoppedAt).Format("2006-01-02 15:04:05.000")))
}
}
// GetAddress возвращает адрес узла в формате "ip:port"
func (n *Node) GetAddress() string {
return fmt.Sprintf("%s:%d", n.IP, n.Port)
}
// generateReplicationID генерирует уникальный ID для репликации
func generateReplicationID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)
}
// ReplicateDocument отправляет документ на репликацию всем активным узлам с retry и backoff
func (n *Node) ReplicateDocument(database, collection string, doc *storage.Document) error {
if n.coordinator == nil {
if n.logger != nil {
n.logger.Warn("No coordinator set, skipping replication")
}
return fmt.Errorf("no coordinator set")
}
nodes := n.coordinator.GetActiveNodes()
if len(nodes) <= 1 {
if n.logger != nil {
n.logger.Debug("No other nodes for replication")
}
return nil
}
// Подготавливаем данные для репликации
repData := struct {
Database string `json:"database"`
Collection string `json:"collection"`
Document map[string]interface{} `json:"document"`
SourceNode string `json:"source_node"`
ReplicaID string `json:"replica_id"`
}{
Database: database,
Collection: collection,
Document: doc.GetFields(),
SourceNode: n.ID,
ReplicaID: generateReplicationID(),
}
data, err := json.Marshal(repData)
if err != nil {
return fmt.Errorf("failed to marshal replication data: %v", err)
}
startTime := time.Now().UnixMilli()
// Отправляем на все узлы, кроме себя
var wg sync.WaitGroup
var failedCount atomic.Int32
var successCount atomic.Int32
for _, nodeInfo := range nodes {
if nodeInfo.ID == n.ID {
continue
}
wg.Add(1)
targetNodeID := nodeInfo.ID
targetAddress := fmt.Sprintf("%s:%d", nodeInfo.IP, nodeInfo.Port)
// Асинхронная репликация с повторными попытками через пул воркеров
taskID := fmt.Sprintf("replicate_%s_to_%s_%s", doc.ID, targetNodeID, repData.ReplicaID)
err := n.workerPool.SubmitFunc(taskID, func() error {
defer wg.Done()
if n.replicator == nil {
failedCount.Add(1)
return fmt.Errorf("replicator not initialized")
}
err := n.replicator.Replicate(targetNodeID, targetAddress, data)
if err != nil {
failedCount.Add(1)
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Failed to replicate document %s to node %s after retries: %v",
doc.ID, targetNodeID, err))
}
return err
}
successCount.Add(1)
if n.logger != nil {
n.logger.Debug(fmt.Sprintf("Successfully replicated document %s to node %s", doc.ID, targetNodeID))
}
return nil
})
if err != nil {
wg.Done()
failedCount.Add(1)
if n.logger != nil {
n.logger.Error(fmt.Sprintf("Failed to submit replication task for %s to %s: %v",
doc.ID, targetNodeID, err))
}
}
}
// Ждём завершения всех репликаций с таймаутом
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
duration := time.Now().UnixMilli() - startTime
if n.logger != nil {
n.logger.Info(fmt.Sprintf("Replicated document %s to %d/%d nodes (took %d ms)",
doc.ID, successCount.Load(), len(nodes)-1, duration))
}
case <-time.After(30 * time.Second):
if n.logger != nil {
n.logger.Warn(fmt.Sprintf("Replication timeout for document %s after %d ms", doc.ID, time.Now().UnixMilli()-startTime))
}
return fmt.Errorf("replication timeout")
}
if failedCount.Load() > 0 {
return fmt.Errorf("replication partially failed: %d of %d nodes failed", failedCount.Load(), len(nodes)-1)
}
return nil
}
// GetWorkerPoolStats возвращает статистику пула воркеров
func (n *Node) GetWorkerPoolStats() map[string]interface{} {
if n.workerPool == nil {
return map[string]interface{}{"enabled": false}
}
return n.workerPool.GetStats()
}
// GetReplicationStats возвращает статистику репликации
func (n *Node) GetReplicationStats() map[string]interface{} {
if n.replicator == nil {
return map[string]interface{}{"enabled": false}
}
return n.replicator.GetStats()
}