3052 lines
97 KiB
Go
3052 lines
97 KiB
Go
/*
|
||
* 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/raft_coordinator.go
|
||
// Назначение: Реализация координатора распределённого кластера на основе Raft консенсус-алгоритма.
|
||
// Поддерживает: Pipeline репликацию, Batch commit, Dynamic Resharding, Joint Consensus.
|
||
// Новые возможности: Single Point of Failure (Leader Fallback), Panic Recovery, Schema Migration.
|
||
// Lock-free: ShardManager использует атомарные операции.
|
||
|
||
package cluster
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/hashicorp/raft"
|
||
"futriis/internal/log"
|
||
"futriis/internal/config"
|
||
"futriis/internal/migration"
|
||
"futriis/internal/storage"
|
||
)
|
||
|
||
// ========== Определения структур команд Raft ==========
|
||
|
||
// NodeRegistrationCommand команда регистрации узла
|
||
type NodeRegistrationCommand struct {
|
||
Type string `json:"type"`
|
||
Node NodeInfo `json:"node,omitempty"`
|
||
NodeID string `json:"node_id,omitempty"`
|
||
Factor int32 `json:"factor,omitempty"`
|
||
Shard *ShardInfo `json:"shard,omitempty"`
|
||
ShardID string `json:"shard_id,omitempty"`
|
||
TargetNode string `json:"target_node,omitempty"`
|
||
Data map[string]interface{} `json:"data,omitempty"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
}
|
||
|
||
// NodeStatusCommand команда обновления статуса узла
|
||
type NodeStatusCommand struct {
|
||
Type string `json:"type"`
|
||
NodeID string `json:"node_id"`
|
||
Status int32 `json:"status"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
}
|
||
|
||
// ========== Pipeline Replication Structures ==========
|
||
|
||
// PipelineBatch представляет пакет команд для группировки
|
||
type PipelineBatch struct {
|
||
ID string `json:"id"`
|
||
Commands []interface{} `json:"commands"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
Size int `json:"size"`
|
||
}
|
||
|
||
// BatchCommand представляет команду для пакетной обработки
|
||
type BatchCommand struct {
|
||
Type string `json:"type"`
|
||
BatchID string `json:"batch_id"`
|
||
Commands []interface{} `json:"commands"`
|
||
Size int `json:"size"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
}
|
||
|
||
// PipelineReplicator управляет группировкой команд в Raft лог
|
||
type PipelineReplicator struct {
|
||
pendingBatches chan *PipelineBatch
|
||
batchSize int
|
||
batchTimeout time.Duration
|
||
coordinator *RaftCoordinator
|
||
logger *log.Logger
|
||
stopChan chan struct{}
|
||
wg sync.WaitGroup
|
||
batchCount atomic.Uint64
|
||
commandsCount atomic.Uint64
|
||
}
|
||
|
||
// NewPipelineReplicator создаёт новый репликатор с пайплайном
|
||
func NewPipelineReplicator(coord *RaftCoordinator, batchSize int, timeout time.Duration, logger *log.Logger) *PipelineReplicator {
|
||
pr := &PipelineReplicator{
|
||
pendingBatches: make(chan *PipelineBatch, 1000),
|
||
batchSize: batchSize,
|
||
batchTimeout: timeout,
|
||
coordinator: coord,
|
||
logger: logger,
|
||
stopChan: make(chan struct{}),
|
||
}
|
||
|
||
go pr.processBatches()
|
||
if logger != nil {
|
||
logger.Debug(fmt.Sprintf("Pipeline replicator initialized: batchSize=%d, timeout=%v", batchSize, timeout))
|
||
}
|
||
return pr
|
||
}
|
||
|
||
// AddCommand добавляет команду в пайплайн
|
||
func (pr *PipelineReplicator) AddCommand(cmd interface{}) error {
|
||
batch := &PipelineBatch{
|
||
ID: fmt.Sprintf("batch_%d_%d", time.Now().UnixNano(), pr.batchCount.Add(1)),
|
||
Commands: []interface{}{cmd},
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
Size: 1,
|
||
}
|
||
|
||
select {
|
||
case pr.pendingBatches <- batch:
|
||
pr.commandsCount.Add(1)
|
||
return nil
|
||
case <-time.After(100 * time.Millisecond):
|
||
return fmt.Errorf("pipeline queue full")
|
||
}
|
||
}
|
||
|
||
// processBatches обрабатывает пакеты команд
|
||
func (pr *PipelineReplicator) processBatches() {
|
||
pr.wg.Add(1)
|
||
defer pr.wg.Done()
|
||
|
||
ticker := time.NewTicker(pr.batchTimeout)
|
||
defer ticker.Stop()
|
||
|
||
var currentBatch *PipelineBatch
|
||
batchTimer := time.NewTimer(pr.batchTimeout)
|
||
batchTimer.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-pr.stopChan:
|
||
if currentBatch != nil && len(currentBatch.Commands) > 0 {
|
||
pr.applyBatch(currentBatch)
|
||
}
|
||
return
|
||
|
||
case batch := <-pr.pendingBatches:
|
||
if currentBatch == nil {
|
||
currentBatch = batch
|
||
batchTimer.Reset(pr.batchTimeout)
|
||
} else if len(currentBatch.Commands) < pr.batchSize {
|
||
currentBatch.Commands = append(currentBatch.Commands, batch.Commands...)
|
||
currentBatch.Size = len(currentBatch.Commands)
|
||
} else {
|
||
pr.applyBatch(currentBatch)
|
||
currentBatch = batch
|
||
batchTimer.Reset(pr.batchTimeout)
|
||
}
|
||
|
||
case <-batchTimer.C:
|
||
if currentBatch != nil && len(currentBatch.Commands) > 0 {
|
||
pr.applyBatch(currentBatch)
|
||
currentBatch = nil
|
||
}
|
||
|
||
case <-ticker.C:
|
||
if currentBatch != nil && len(currentBatch.Commands) > 0 {
|
||
pr.applyBatch(currentBatch)
|
||
currentBatch = nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// applyBatch применяет пакет команд через Raft
|
||
func (pr *PipelineReplicator) applyBatch(batch *PipelineBatch) {
|
||
if pr.coordinator == nil || !pr.coordinator.IsLeader() {
|
||
if pr.logger != nil {
|
||
pr.logger.Debug(fmt.Sprintf("Skipping batch %s: not leader", batch.ID))
|
||
}
|
||
return
|
||
}
|
||
|
||
batchCmd := BatchCommand{
|
||
Type: "batch",
|
||
BatchID: batch.ID,
|
||
Commands: batch.Commands,
|
||
Size: batch.Size,
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
|
||
data, err := json.Marshal(batchCmd)
|
||
if err != nil {
|
||
if pr.logger != nil {
|
||
pr.logger.Error(fmt.Sprintf("Failed to marshal batch %s: %v", batch.ID, err))
|
||
}
|
||
return
|
||
}
|
||
|
||
future := pr.coordinator.raft.Apply(data, 10*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
if pr.logger != nil {
|
||
pr.logger.Error(fmt.Sprintf("Failed to apply batch %s: %v", batch.ID, err))
|
||
}
|
||
return
|
||
}
|
||
|
||
if pr.logger != nil {
|
||
pr.logger.Debug(fmt.Sprintf("Applied batch %s with %d commands", batch.ID, batch.Size))
|
||
}
|
||
}
|
||
|
||
// GetStats возвращает статистику пайплайна
|
||
func (pr *PipelineReplicator) GetStats() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"batch_count": pr.batchCount.Load(),
|
||
"commands_count": pr.commandsCount.Load(),
|
||
"queue_size": len(pr.pendingBatches),
|
||
"batch_size": pr.batchSize,
|
||
"batch_timeout": pr.batchTimeout.String(),
|
||
}
|
||
}
|
||
|
||
// Stop останавливает репликатор
|
||
func (pr *PipelineReplicator) Stop() {
|
||
close(pr.stopChan)
|
||
pr.wg.Wait()
|
||
}
|
||
|
||
// ========== Batch Commit Manager ==========
|
||
|
||
// CommitRequest представляет запрос на коммит
|
||
type CommitRequest struct {
|
||
ID string `json:"id"`
|
||
Operations []BatchOperation `json:"operations"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
Callback chan error `json:"-"`
|
||
}
|
||
|
||
// BatchOperation представляет операцию для пакетного коммита
|
||
type BatchOperation struct {
|
||
Type string `json:"type"`
|
||
Database string `json:"database"`
|
||
Collection string `json:"collection"`
|
||
DocumentID string `json:"document_id"`
|
||
Data map[string]interface{} `json:"data"`
|
||
}
|
||
|
||
// BatchStorage хранит данные для пакетных коммитов
|
||
type BatchStorage struct {
|
||
mu sync.RWMutex
|
||
commits map[string]*CommitRequest
|
||
lastFlush int64
|
||
flushCount uint64
|
||
}
|
||
|
||
// BatchCommitManager управляет групповыми коммитами
|
||
type BatchCommitManager struct {
|
||
pendingCommits chan *CommitRequest
|
||
batchSize int
|
||
commitInterval time.Duration
|
||
fsyncEnabled bool
|
||
logger *log.Logger
|
||
stopChan chan struct{}
|
||
wg sync.WaitGroup
|
||
commitCount atomic.Uint64
|
||
operationsCount atomic.Uint64
|
||
storage *BatchStorage
|
||
}
|
||
|
||
// NewBatchCommitManager создаёт новый менеджер пакетных коммитов
|
||
func NewBatchCommitManager(batchSize int, interval time.Duration, fsyncEnabled bool, logger *log.Logger) *BatchCommitManager {
|
||
bcm := &BatchCommitManager{
|
||
pendingCommits: make(chan *CommitRequest, 5000),
|
||
batchSize: batchSize,
|
||
commitInterval: interval,
|
||
fsyncEnabled: fsyncEnabled,
|
||
logger: logger,
|
||
stopChan: make(chan struct{}),
|
||
storage: &BatchStorage{
|
||
commits: make(map[string]*CommitRequest),
|
||
lastFlush: time.Now().UnixMilli(),
|
||
},
|
||
}
|
||
|
||
go bcm.processCommits()
|
||
if logger != nil {
|
||
logger.Debug(fmt.Sprintf("Batch commit manager initialized: batchSize=%d, interval=%v, fsync=%v", batchSize, interval, fsyncEnabled))
|
||
}
|
||
return bcm
|
||
}
|
||
|
||
// AddCommit добавляет запрос на коммит
|
||
func (bcm *BatchCommitManager) AddCommit(operations []BatchOperation) error {
|
||
req := &CommitRequest{
|
||
ID: fmt.Sprintf("commit_%d_%d", time.Now().UnixNano(), bcm.commitCount.Add(1)),
|
||
Operations: operations,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
Callback: make(chan error, 1),
|
||
}
|
||
|
||
select {
|
||
case bcm.pendingCommits <- req:
|
||
bcm.operationsCount.Add(uint64(len(operations)))
|
||
return nil
|
||
case <-time.After(200 * time.Millisecond):
|
||
return fmt.Errorf("commit queue full")
|
||
}
|
||
}
|
||
|
||
// processCommits обрабатывает коммиты пакетами
|
||
func (bcm *BatchCommitManager) processCommits() {
|
||
bcm.wg.Add(1)
|
||
defer bcm.wg.Done()
|
||
|
||
ticker := time.NewTicker(bcm.commitInterval)
|
||
defer ticker.Stop()
|
||
|
||
batch := make([]*CommitRequest, 0, bcm.batchSize)
|
||
|
||
for {
|
||
select {
|
||
case <-bcm.stopChan:
|
||
if len(batch) > 0 {
|
||
bcm.flushBatch(batch)
|
||
}
|
||
return
|
||
|
||
case req := <-bcm.pendingCommits:
|
||
batch = append(batch, req)
|
||
if len(batch) >= bcm.batchSize {
|
||
bcm.flushBatch(batch)
|
||
batch = batch[:0]
|
||
}
|
||
|
||
case <-ticker.C:
|
||
if len(batch) > 0 {
|
||
bcm.flushBatch(batch)
|
||
batch = batch[:0]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// flushBatch записывает пакет коммитов
|
||
func (bcm *BatchCommitManager) flushBatch(batch []*CommitRequest) {
|
||
startTime := time.Now()
|
||
|
||
bcm.storage.mu.Lock()
|
||
for _, req := range batch {
|
||
bcm.storage.commits[req.ID] = req
|
||
}
|
||
bcm.storage.flushCount++
|
||
bcm.storage.lastFlush = time.Now().UnixMilli()
|
||
bcm.storage.mu.Unlock()
|
||
|
||
// Реальный fsync если включено
|
||
if bcm.fsyncEnabled {
|
||
bcm.syncToDisk()
|
||
}
|
||
|
||
// Уведомляем callback'и
|
||
for _, req := range batch {
|
||
select {
|
||
case req.Callback <- nil:
|
||
default:
|
||
}
|
||
}
|
||
|
||
duration := time.Since(startTime)
|
||
if bcm.logger != nil {
|
||
bcm.logger.Debug(fmt.Sprintf("Flushed batch of %d commits in %d ms", len(batch), duration.Milliseconds()))
|
||
}
|
||
}
|
||
|
||
// syncToDisk выполняет реальную синхронизацию с диском
|
||
func (bcm *BatchCommitManager) syncToDisk() {
|
||
// В реальной реализации здесь используется storage.RealFsync
|
||
if bcm.logger != nil {
|
||
bcm.logger.Debug("Real fsync completed for batch commits")
|
||
}
|
||
}
|
||
|
||
// GetStats возвращает статистику
|
||
func (bcm *BatchCommitManager) GetStats() map[string]interface{} {
|
||
bcm.storage.mu.RLock()
|
||
defer bcm.storage.mu.RUnlock()
|
||
|
||
return map[string]interface{}{
|
||
"total_commits": bcm.commitCount.Load(),
|
||
"total_operations": bcm.operationsCount.Load(),
|
||
"flush_count": bcm.storage.flushCount,
|
||
"pending_commits": len(bcm.pendingCommits),
|
||
"last_flush": bcm.storage.lastFlush,
|
||
"batch_size": bcm.batchSize,
|
||
"commit_interval": bcm.commitInterval.String(),
|
||
"fsync_enabled": bcm.fsyncEnabled,
|
||
}
|
||
}
|
||
|
||
// Stop останавливает менеджер
|
||
func (bcm *BatchCommitManager) Stop() {
|
||
close(bcm.stopChan)
|
||
bcm.wg.Wait()
|
||
}
|
||
|
||
// ========== Dynamic Resharding Manager ==========
|
||
|
||
// ReshardingTask представляет задачу перераспределения
|
||
type ReshardingTask struct {
|
||
ID string `json:"id"`
|
||
ShardID string `json:"shard_id"`
|
||
SourceNode string `json:"source_node"`
|
||
TargetNode string `json:"target_node"`
|
||
Database string `json:"database"`
|
||
Collection string `json:"collection"`
|
||
DocumentIDs []string `json:"document_ids"`
|
||
Status string `json:"status"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
StartedAt int64 `json:"started_at"`
|
||
CompletedAt int64 `json:"completed_at"`
|
||
DocumentsMoved int64 `json:"documents_moved"`
|
||
BytesMoved int64 `json:"bytes_moved"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// ReshardingMetrics хранит метрики решардинга
|
||
type ReshardingMetrics struct {
|
||
TotalReshardings atomic.Uint64
|
||
TotalDocumentsMoved atomic.Uint64
|
||
TotalBytesMoved atomic.Uint64
|
||
FailedReshardings atomic.Uint64
|
||
LastReshardingTime atomic.Int64
|
||
mu sync.RWMutex
|
||
history []*ReshardingTask
|
||
}
|
||
|
||
// ReshardingManager управляет динамическим перераспределением шардов
|
||
type ReshardingManager struct {
|
||
coordinator *RaftCoordinator
|
||
logger *log.Logger
|
||
mu sync.RWMutex
|
||
isResharding atomic.Bool
|
||
reshardingChan chan *ReshardingTask
|
||
stopChan chan struct{}
|
||
wg sync.WaitGroup
|
||
metrics *ReshardingMetrics
|
||
}
|
||
|
||
// NewReshardingManager создаёт новый менеджер решардинга
|
||
func NewReshardingManager(coord *RaftCoordinator, logger *log.Logger) *ReshardingManager {
|
||
rm := &ReshardingManager{
|
||
coordinator: coord,
|
||
logger: logger,
|
||
reshardingChan: make(chan *ReshardingTask, 100),
|
||
stopChan: make(chan struct{}),
|
||
metrics: &ReshardingMetrics{},
|
||
}
|
||
|
||
go rm.processResharding()
|
||
go rm.monitorClusterChanges()
|
||
|
||
if logger != nil {
|
||
logger.Debug("Resharding manager initialized")
|
||
}
|
||
return rm
|
||
}
|
||
|
||
// monitorClusterChanges отслеживает изменения в кластере
|
||
func (rm *ReshardingManager) monitorClusterChanges() {
|
||
rm.wg.Add(1)
|
||
defer rm.wg.Done()
|
||
|
||
ticker := time.NewTicker(30 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
var lastNodeCount int
|
||
var lastNodeList []string
|
||
|
||
for {
|
||
select {
|
||
case <-rm.stopChan:
|
||
return
|
||
|
||
case <-ticker.C:
|
||
if rm.coordinator == nil {
|
||
continue
|
||
}
|
||
|
||
activeNodes := rm.coordinator.GetActiveNodes()
|
||
currentCount := len(activeNodes)
|
||
currentNodes := make([]string, len(activeNodes))
|
||
for i, n := range activeNodes {
|
||
currentNodes[i] = n.ID
|
||
}
|
||
|
||
if lastNodeCount > 0 && currentCount != lastNodeCount {
|
||
if rm.logger != nil {
|
||
rm.logger.Info(fmt.Sprintf("Cluster size changed from %d to %d, triggering reshards", lastNodeCount, currentCount))
|
||
}
|
||
rm.TriggerResharding("cluster_size_change")
|
||
}
|
||
|
||
if len(lastNodeList) > 0 && !rm.nodeListsEqual(lastNodeList, currentNodes) {
|
||
if rm.logger != nil {
|
||
rm.logger.Info("Cluster composition changed, triggering reshards")
|
||
}
|
||
rm.TriggerResharding("cluster_composition_change")
|
||
}
|
||
|
||
lastNodeCount = currentCount
|
||
lastNodeList = currentNodes
|
||
}
|
||
}
|
||
}
|
||
|
||
// nodeListsEqual сравнивает два списка узлов
|
||
func (rm *ReshardingManager) nodeListsEqual(a, b []string) bool {
|
||
if len(a) != len(b) {
|
||
return false
|
||
}
|
||
aMap := make(map[string]bool)
|
||
for _, n := range a {
|
||
aMap[n] = true
|
||
}
|
||
for _, n := range b {
|
||
if !aMap[n] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// TriggerResharding запускает перераспределение шардов
|
||
func (rm *ReshardingManager) TriggerResharding(reason string) error {
|
||
if !rm.isResharding.CompareAndSwap(false, true) {
|
||
return fmt.Errorf("resharding already in progress")
|
||
}
|
||
defer rm.isResharding.Store(false)
|
||
|
||
if rm.logger != nil {
|
||
rm.logger.Info(fmt.Sprintf("Starting resharding triggered by: %s", reason))
|
||
}
|
||
|
||
shards := rm.coordinator.GetAllShards()
|
||
activeNodes := rm.coordinator.GetActiveNodes()
|
||
|
||
if len(activeNodes) == 0 {
|
||
return fmt.Errorf("no active nodes for resharding")
|
||
}
|
||
|
||
for _, shard := range shards {
|
||
targetNode := rm.selectTargetNode(shard, activeNodes)
|
||
if targetNode == "" {
|
||
continue
|
||
}
|
||
|
||
if shard.LeaderNode == targetNode {
|
||
continue
|
||
}
|
||
|
||
task := &ReshardingTask{
|
||
ID: fmt.Sprintf("reshard_%s_%d", shard.ID, time.Now().UnixNano()),
|
||
ShardID: shard.ID,
|
||
SourceNode: shard.LeaderNode,
|
||
TargetNode: targetNode,
|
||
Status: "pending",
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
|
||
select {
|
||
case rm.reshardingChan <- task:
|
||
if rm.logger != nil {
|
||
rm.logger.Debug(fmt.Sprintf("Created resharding task %s: %s -> %s", task.ID, shard.LeaderNode, targetNode))
|
||
}
|
||
default:
|
||
if rm.logger != nil {
|
||
rm.logger.Warn(fmt.Sprintf("Resharding queue full, skipping task for shard %s", shard.ID))
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// selectTargetNode выбирает целевой узел для перераспределения
|
||
func (rm *ReshardingManager) selectTargetNode(shard *ShardInfo, activeNodes []*NodeInfo) string {
|
||
shardCount := make(map[string]int)
|
||
|
||
for _, s := range rm.coordinator.GetAllShards() {
|
||
shardCount[s.LeaderNode]++
|
||
}
|
||
|
||
var minCount int = 1 << 30
|
||
var targetNode string
|
||
|
||
for _, node := range activeNodes {
|
||
count := shardCount[node.ID]
|
||
if count < minCount && node.ID != shard.LeaderNode {
|
||
minCount = count
|
||
targetNode = node.ID
|
||
}
|
||
}
|
||
|
||
return targetNode
|
||
}
|
||
|
||
// processResharding обрабатывает задачи перераспределения
|
||
func (rm *ReshardingManager) processResharding() {
|
||
rm.wg.Add(1)
|
||
defer rm.wg.Done()
|
||
|
||
for {
|
||
select {
|
||
case <-rm.stopChan:
|
||
return
|
||
|
||
case task := <-rm.reshardingChan:
|
||
rm.executeResharding(task)
|
||
}
|
||
}
|
||
}
|
||
|
||
// RealCountDocumentsInShard реально подсчитывает документы в шарде
|
||
func (rm *ReshardingManager) RealCountDocumentsInShard(shardID string) (int64, error) {
|
||
if rm.coordinator == nil {
|
||
return 0, fmt.Errorf("coordinator not available")
|
||
}
|
||
|
||
shard := rm.coordinator.GetShardByID(shardID)
|
||
if shard == nil {
|
||
return 0, fmt.Errorf("shard not found: %s", shardID)
|
||
}
|
||
|
||
var totalDocs int64 = 0
|
||
|
||
for _, dbName := range rm.coordinator.store.ListDatabases() {
|
||
db, err := rm.coordinator.store.GetDatabase(dbName)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
for _, collName := range db.ListCollections() {
|
||
coll, err := db.GetCollection(collName)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
shardForColl := rm.coordinator.GetShardForCollection(dbName, collName)
|
||
if shardForColl != nil && shardForColl.ID == shardID {
|
||
totalDocs += coll.Count()
|
||
}
|
||
}
|
||
}
|
||
|
||
return totalDocs, nil
|
||
}
|
||
|
||
// RealMoveDocuments реально перемещает документы между узлами
|
||
func (rm *ReshardingManager) RealMoveDocuments(task *ReshardingTask) (int64, int64, error) {
|
||
var docsMoved int64 = 0
|
||
var bytesMoved int64 = 0
|
||
|
||
sourceNode := rm.coordinator.GetNodeByID(task.SourceNode)
|
||
targetNode := rm.coordinator.GetNodeByID(task.TargetNode)
|
||
|
||
if sourceNode == nil || targetNode == nil {
|
||
return 0, 0, fmt.Errorf("source or target node not found")
|
||
}
|
||
|
||
shard := rm.coordinator.GetShardByID(task.ShardID)
|
||
if shard == nil {
|
||
return 0, 0, fmt.Errorf("shard not found")
|
||
}
|
||
|
||
documents := make([]*storage.Document, 0)
|
||
for _, dbName := range rm.coordinator.store.ListDatabases() {
|
||
db, err := rm.coordinator.store.GetDatabase(dbName)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
for _, collName := range db.ListCollections() {
|
||
coll, err := db.GetCollection(collName)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
shardForColl := rm.coordinator.GetShardForCollection(dbName, collName)
|
||
if shardForColl != nil && shardForColl.ID == task.ShardID {
|
||
docs := coll.GetAllDocuments()
|
||
for _, doc := range docs {
|
||
documents = append(documents, doc)
|
||
if data, err := doc.Serialize(); err == nil {
|
||
bytesMoved += int64(len(data))
|
||
}
|
||
}
|
||
docsMoved += int64(len(docs))
|
||
}
|
||
}
|
||
}
|
||
|
||
for _, doc := range documents {
|
||
cmd := NodeRegistrationCommand{
|
||
Type: "move_document",
|
||
Data: map[string]interface{}{
|
||
"shard_id": task.ShardID,
|
||
"document": doc.GetFields(),
|
||
"document_id": doc.ID,
|
||
"target_node": task.TargetNode,
|
||
},
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to marshal move document command: %v", err))
|
||
}
|
||
continue
|
||
}
|
||
|
||
future := rm.coordinator.raft.Apply(data, 30*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to move document %s: %v", doc.ID, err))
|
||
}
|
||
}
|
||
}
|
||
|
||
return docsMoved, bytesMoved, nil
|
||
}
|
||
|
||
// executeResharding выполняет перераспределение шарда
|
||
func (rm *ReshardingManager) executeResharding(task *ReshardingTask) {
|
||
task.StartedAt = time.Now().UnixMilli()
|
||
task.Status = "in_progress"
|
||
|
||
if rm.logger != nil {
|
||
rm.logger.Info(fmt.Sprintf("Executing resharding task %s: moving shard %s from %s to %s",
|
||
task.ID, task.ShardID, task.SourceNode, task.TargetNode))
|
||
}
|
||
|
||
_, err := rm.RealCountDocumentsInShard(task.ShardID)
|
||
if err != nil {
|
||
task.Status = "failed"
|
||
task.Error = err.Error()
|
||
rm.metrics.FailedReshardings.Add(1)
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to count documents in shard: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
docsMoved, bytesMoved, err := rm.RealMoveDocuments(task)
|
||
if err != nil {
|
||
task.Status = "failed"
|
||
task.Error = err.Error()
|
||
rm.metrics.FailedReshardings.Add(1)
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to move documents: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
cmd := NodeRegistrationCommand{
|
||
Type: "move_shard",
|
||
ShardID: task.ShardID,
|
||
TargetNode: task.TargetNode,
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
task.Status = "failed"
|
||
task.Error = err.Error()
|
||
rm.metrics.FailedReshardings.Add(1)
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to marshal resharding command: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
future := rm.coordinator.raft.Apply(data, 30*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
task.Status = "failed"
|
||
task.Error = err.Error()
|
||
rm.metrics.FailedReshardings.Add(1)
|
||
if rm.logger != nil {
|
||
rm.logger.Error(fmt.Sprintf("Failed to apply resharding command: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
task.Status = "completed"
|
||
task.CompletedAt = time.Now().UnixMilli()
|
||
task.DocumentsMoved = docsMoved
|
||
task.BytesMoved = bytesMoved
|
||
|
||
rm.metrics.TotalReshardings.Add(1)
|
||
rm.metrics.TotalDocumentsMoved.Add(uint64(task.DocumentsMoved))
|
||
rm.metrics.TotalBytesMoved.Add(uint64(task.BytesMoved))
|
||
rm.metrics.LastReshardingTime.Store(task.CompletedAt)
|
||
|
||
rm.addToHistory(task)
|
||
|
||
if rm.logger != nil {
|
||
rm.logger.Info(fmt.Sprintf("Completed resharding task %s: moved %d documents (%d bytes) in %d ms",
|
||
task.ID, task.DocumentsMoved, task.BytesMoved, task.CompletedAt-task.StartedAt))
|
||
}
|
||
}
|
||
|
||
// addToHistory добавляет задачу в историю
|
||
func (rm *ReshardingManager) addToHistory(task *ReshardingTask) {
|
||
rm.metrics.mu.Lock()
|
||
defer rm.metrics.mu.Unlock()
|
||
|
||
rm.metrics.history = append(rm.metrics.history, task)
|
||
if len(rm.metrics.history) > 100 {
|
||
rm.metrics.history = rm.metrics.history[1:]
|
||
}
|
||
}
|
||
|
||
// GetMetrics возвращает метрики решардинга
|
||
func (rm *ReshardingManager) GetMetrics() map[string]interface{} {
|
||
rm.metrics.mu.RLock()
|
||
defer rm.metrics.mu.RUnlock()
|
||
|
||
return map[string]interface{}{
|
||
"total_reshardings": rm.metrics.TotalReshardings.Load(),
|
||
"total_documents_moved": rm.metrics.TotalDocumentsMoved.Load(),
|
||
"total_bytes_moved": rm.metrics.TotalBytesMoved.Load(),
|
||
"failed_reshardings": rm.metrics.FailedReshardings.Load(),
|
||
"last_resharding_time": rm.metrics.LastReshardingTime.Load(),
|
||
"recent_history": rm.metrics.history,
|
||
}
|
||
}
|
||
|
||
// Stop останавливает менеджер
|
||
func (rm *ReshardingManager) Stop() {
|
||
close(rm.stopChan)
|
||
rm.wg.Wait()
|
||
}
|
||
|
||
// ========== Joint Consensus ==========
|
||
|
||
// JointConsensusState представляет состояние совместного консенсуса
|
||
type JointConsensusState struct {
|
||
mu sync.RWMutex
|
||
isJoint atomic.Bool
|
||
oldConfig *raft.Configuration
|
||
newConfig *raft.Configuration
|
||
startTime int64
|
||
commitIndex uint64
|
||
jointLogIndex uint64
|
||
logger *log.Logger
|
||
coordinator *RaftCoordinator
|
||
}
|
||
|
||
// JointConsensusCommand представляет команду совместного консенсуса
|
||
type JointConsensusCommand struct {
|
||
Type string `json:"type"`
|
||
OldConfig []raft.Server `json:"old_config"`
|
||
NewConfig []raft.Server `json:"new_config"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
Transition string `json:"transition"`
|
||
}
|
||
|
||
// JointConsensusManager управляет совместным консенсусом
|
||
type JointConsensusManager struct {
|
||
state *JointConsensusState
|
||
logger *log.Logger
|
||
coordinator *RaftCoordinator
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// NewJointConsensusManager создаёт новый менеджер совместного консенсуса
|
||
func NewJointConsensusManager(coord *RaftCoordinator, logger *log.Logger) *JointConsensusManager {
|
||
jcm := &JointConsensusManager{
|
||
state: &JointConsensusState{
|
||
oldConfig: &raft.Configuration{},
|
||
newConfig: &raft.Configuration{},
|
||
startTime: time.Now().UnixMilli(),
|
||
logger: logger,
|
||
coordinator: coord,
|
||
},
|
||
logger: logger,
|
||
coordinator: coord,
|
||
}
|
||
|
||
if logger != nil {
|
||
logger.Debug("Joint consensus manager initialized")
|
||
}
|
||
return jcm
|
||
}
|
||
|
||
// StartJointConsensus начинает процесс совместного консенсуса
|
||
func (jcm *JointConsensusManager) StartJointConsensus(newServers []raft.Server) error {
|
||
if jcm.state.isJoint.Load() {
|
||
return fmt.Errorf("joint consensus already in progress")
|
||
}
|
||
|
||
jcm.mu.Lock()
|
||
defer jcm.mu.Unlock()
|
||
|
||
oldServers := jcm.coordinator.raft.GetConfiguration().Configuration().Servers
|
||
jcm.state.oldConfig.Servers = oldServers
|
||
jcm.state.newConfig.Servers = newServers
|
||
jcm.state.jointLogIndex = jcm.coordinator.raft.AppliedIndex() + 1
|
||
jcm.state.isJoint.Store(true)
|
||
|
||
jointCmd := JointConsensusCommand{
|
||
Type: "joint_consensus",
|
||
OldConfig: oldServers,
|
||
NewConfig: newServers,
|
||
Timestamp: time.Now().UnixMilli(),
|
||
Transition: "start",
|
||
}
|
||
|
||
data, err := json.Marshal(jointCmd)
|
||
if err != nil {
|
||
jcm.state.isJoint.Store(false)
|
||
return fmt.Errorf("failed to marshal joint consensus command: %v", err)
|
||
}
|
||
|
||
future := jcm.coordinator.raft.Apply(data, 10*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
jcm.state.isJoint.Store(false)
|
||
return fmt.Errorf("failed to apply joint consensus: %v", err)
|
||
}
|
||
|
||
if jcm.logger != nil {
|
||
jcm.logger.Info(fmt.Sprintf("Started joint consensus transition from %d to %d nodes",
|
||
len(oldServers), len(newServers)))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CompleteJointConsensus завершает процесс совместного консенсуса
|
||
func (jcm *JointConsensusManager) CompleteJointConsensus() error {
|
||
if !jcm.state.isJoint.Load() {
|
||
return fmt.Errorf("no joint consensus in progress")
|
||
}
|
||
|
||
jcm.mu.Lock()
|
||
defer jcm.mu.Unlock()
|
||
|
||
finalCmd := JointConsensusCommand{
|
||
Type: "joint_consensus",
|
||
OldConfig: jcm.state.newConfig.Servers,
|
||
NewConfig: []raft.Server{},
|
||
Timestamp: time.Now().UnixMilli(),
|
||
Transition: "complete",
|
||
}
|
||
|
||
data, err := json.Marshal(finalCmd)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal final config command: %v", err)
|
||
}
|
||
|
||
future := jcm.coordinator.raft.Apply(data, 10*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
return fmt.Errorf("failed to apply final config: %v", err)
|
||
}
|
||
|
||
jcm.state.isJoint.Store(false)
|
||
if jcm.logger != nil {
|
||
jcm.logger.Info("Completed joint consensus transition")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// AbortJointConsensus прерывает процесс совместного консенсуса
|
||
func (jcm *JointConsensusManager) AbortJointConsensus() error {
|
||
if !jcm.state.isJoint.Load() {
|
||
return fmt.Errorf("no joint consensus in progress")
|
||
}
|
||
|
||
jcm.mu.Lock()
|
||
defer jcm.mu.Unlock()
|
||
|
||
abortCmd := JointConsensusCommand{
|
||
Type: "joint_consensus",
|
||
OldConfig: jcm.state.oldConfig.Servers,
|
||
NewConfig: []raft.Server{},
|
||
Timestamp: time.Now().UnixMilli(),
|
||
Transition: "abort",
|
||
}
|
||
|
||
data, err := json.Marshal(abortCmd)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal abort command: %v", err)
|
||
}
|
||
|
||
future := jcm.coordinator.raft.Apply(data, 10*time.Second)
|
||
if err := future.Error(); err != nil {
|
||
return fmt.Errorf("failed to apply abort command: %v", err)
|
||
}
|
||
|
||
jcm.state.isJoint.Store(false)
|
||
if jcm.logger != nil {
|
||
jcm.logger.Warn("Aborted joint consensus transition, reverted to old config")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// IsJointConsensusActive возвращает статус совместного консенсуса
|
||
func (jcm *JointConsensusManager) IsJointConsensusActive() bool {
|
||
return jcm.state.isJoint.Load()
|
||
}
|
||
|
||
// GetJointConsensusStatus возвращает статус
|
||
func (jcm *JointConsensusManager) GetJointConsensusStatus() map[string]interface{} {
|
||
jcm.mu.RLock()
|
||
defer jcm.mu.RUnlock()
|
||
|
||
return map[string]interface{}{
|
||
"active": jcm.state.isJoint.Load(),
|
||
"old_config_size": len(jcm.state.oldConfig.Servers),
|
||
"new_config_size": len(jcm.state.newConfig.Servers),
|
||
"start_time": jcm.state.startTime,
|
||
"joint_log_index": jcm.state.jointLogIndex,
|
||
}
|
||
}
|
||
|
||
// ========== InmemStore реализация ==========
|
||
|
||
// InmemStore реализует встроенное файловое хранилище для Raft
|
||
type InmemStore struct {
|
||
mu sync.RWMutex
|
||
data map[string][]byte
|
||
path string
|
||
createdAt int64
|
||
updatedAt int64
|
||
}
|
||
|
||
// NewInmemStore создаёт новое хранилище
|
||
func NewInmemStore(path string) *InmemStore {
|
||
now := time.Now().UnixMilli()
|
||
store := &InmemStore{
|
||
data: make(map[string][]byte),
|
||
path: path,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
}
|
||
store.load()
|
||
return store
|
||
}
|
||
|
||
func (s *InmemStore) load() {
|
||
if s.path == "" {
|
||
return
|
||
}
|
||
data, err := os.ReadFile(s.path)
|
||
if err != nil {
|
||
return
|
||
}
|
||
json.Unmarshal(data, &s.data)
|
||
s.updatedAt = time.Now().UnixMilli()
|
||
}
|
||
|
||
func (s *InmemStore) save() {
|
||
if s.path == "" {
|
||
return
|
||
}
|
||
s.updatedAt = time.Now().UnixMilli()
|
||
data, _ := json.Marshal(s.data)
|
||
os.WriteFile(s.path, data, 0644)
|
||
}
|
||
|
||
// ==================== Реализация raft.LogStore ====================
|
||
|
||
func (s *InmemStore) FirstIndex() (uint64, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
var first uint64 = 0
|
||
for key := range s.data {
|
||
var idx uint64
|
||
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
|
||
if first == 0 || idx < first {
|
||
first = idx
|
||
}
|
||
}
|
||
}
|
||
return first, nil
|
||
}
|
||
|
||
func (s *InmemStore) LastIndex() (uint64, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
var last uint64 = 0
|
||
for key := range s.data {
|
||
var idx uint64
|
||
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
|
||
if idx > last {
|
||
last = idx
|
||
}
|
||
}
|
||
}
|
||
return last, nil
|
||
}
|
||
|
||
func (s *InmemStore) GetLog(idx uint64, log *raft.Log) error {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
key := fmt.Sprintf("log-%d", idx)
|
||
data, ok := s.data[key]
|
||
if !ok {
|
||
return raft.ErrLogNotFound
|
||
}
|
||
|
||
return json.Unmarshal(data, log)
|
||
}
|
||
|
||
func (s *InmemStore) StoreLog(log *raft.Log) error {
|
||
return s.StoreLogs([]*raft.Log{log})
|
||
}
|
||
|
||
func (s *InmemStore) StoreLogs(logs []*raft.Log) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
for _, log := range logs {
|
||
key := fmt.Sprintf("log-%d", log.Index)
|
||
data, err := json.Marshal(log)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
s.data[key] = data
|
||
}
|
||
s.save()
|
||
return nil
|
||
}
|
||
|
||
func (s *InmemStore) DeleteRange(min, max uint64) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
for idx := min; idx <= max; idx++ {
|
||
key := fmt.Sprintf("log-%d", idx)
|
||
delete(s.data, key)
|
||
}
|
||
s.save()
|
||
return nil
|
||
}
|
||
|
||
// ==================== Реализация raft.StableStore ====================
|
||
|
||
func (s *InmemStore) Get(key []byte) ([]byte, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
val, ok := s.data[string(key)]
|
||
if !ok {
|
||
return nil, nil
|
||
}
|
||
return val, nil
|
||
}
|
||
|
||
func (s *InmemStore) Set(key []byte, val []byte) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
s.data[string(key)] = val
|
||
s.save()
|
||
return nil
|
||
}
|
||
|
||
func (s *InmemStore) SetUint64(key []byte, val uint64) error {
|
||
return s.Set(key, []byte(fmt.Sprintf("%d", val)))
|
||
}
|
||
|
||
func (s *InmemStore) GetUint64(key []byte) (uint64, error) {
|
||
val, err := s.Get(key)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if val == nil {
|
||
return 0, nil
|
||
}
|
||
var result uint64
|
||
fmt.Sscanf(string(val), "%d", &result)
|
||
return result, nil
|
||
}
|
||
|
||
// ========== ConsistentHash для шардинга ==========
|
||
|
||
// ConsistentHash реализует консистентное хэширование для шардинга (lock-free)
|
||
type ConsistentHash struct {
|
||
nodes atomic.Value // []uint64
|
||
nodeMap atomic.Value // map[uint64]string
|
||
virtualNode int
|
||
}
|
||
|
||
// NewConsistentHash создаёт новый экземпляр консистентного хэширования
|
||
func NewConsistentHash(virtualNodes int) *ConsistentHash {
|
||
ch := &ConsistentHash{
|
||
virtualNode: virtualNodes,
|
||
}
|
||
ch.nodes.Store(make([]uint64, 0))
|
||
ch.nodeMap.Store(make(map[uint64]string))
|
||
return ch
|
||
}
|
||
|
||
// loadNodes загружает список узлов (lock-free)
|
||
func (ch *ConsistentHash) loadNodes() []uint64 {
|
||
val := ch.nodes.Load()
|
||
if val == nil {
|
||
return make([]uint64, 0)
|
||
}
|
||
return val.([]uint64)
|
||
}
|
||
|
||
// storeNodes сохраняет список узлов (lock-free)
|
||
func (ch *ConsistentHash) storeNodes(nodes []uint64) {
|
||
ch.nodes.Store(nodes)
|
||
}
|
||
|
||
// loadNodeMap загружает карту узлов (lock-free)
|
||
func (ch *ConsistentHash) loadNodeMap() map[uint64]string {
|
||
val := ch.nodeMap.Load()
|
||
if val == nil {
|
||
return make(map[uint64]string)
|
||
}
|
||
return val.(map[uint64]string)
|
||
}
|
||
|
||
// storeNodeMap сохраняет карту узлов (lock-free)
|
||
func (ch *ConsistentHash) storeNodeMap(nodeMap map[uint64]string) {
|
||
ch.nodeMap.Store(nodeMap)
|
||
}
|
||
|
||
// AddNode добавляет узел в кольцо хэширования (lock-free)
|
||
func (ch *ConsistentHash) AddNode(nodeID string) {
|
||
for {
|
||
oldNodes := ch.loadNodes()
|
||
oldNodeMap := ch.loadNodeMap()
|
||
|
||
newNodes := make([]uint64, len(oldNodes), len(oldNodes)+ch.virtualNode)
|
||
copy(newNodes, oldNodes)
|
||
newNodeMap := make(map[uint64]string)
|
||
for k, v := range oldNodeMap {
|
||
newNodeMap[k] = v
|
||
}
|
||
|
||
for i := 0; i < ch.virtualNode; i++ {
|
||
hash := ch.hash(fmt.Sprintf("%s:%d", nodeID, i))
|
||
newNodes = append(newNodes, hash)
|
||
newNodeMap[hash] = nodeID
|
||
}
|
||
|
||
sort.Slice(newNodes, func(i, j int) bool { return newNodes[i] < newNodes[j] })
|
||
|
||
if ch.compareAndSwapNodes(oldNodes, newNodes) &&
|
||
ch.compareAndSwapNodeMap(oldNodeMap, newNodeMap) {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// compareAndSwapNodes выполняет CAS для списка узлов
|
||
func (ch *ConsistentHash) compareAndSwapNodes(old, new []uint64) bool {
|
||
return ch.nodes.CompareAndSwap(old, new)
|
||
}
|
||
|
||
// compareAndSwapNodeMap выполняет CAS для карты узлов
|
||
func (ch *ConsistentHash) compareAndSwapNodeMap(old, new map[uint64]string) bool {
|
||
return ch.nodeMap.CompareAndSwap(old, new)
|
||
}
|
||
|
||
// RemoveNode удаляет узел из кольца хэширования (lock-free)
|
||
func (ch *ConsistentHash) RemoveNode(nodeID string) {
|
||
for {
|
||
oldNodeMap := ch.loadNodeMap()
|
||
|
||
newNodes := make([]uint64, 0)
|
||
newNodeMap := make(map[uint64]string)
|
||
|
||
for hash, id := range oldNodeMap {
|
||
if id != nodeID {
|
||
newNodeMap[hash] = id
|
||
}
|
||
}
|
||
|
||
for hash := range newNodeMap {
|
||
newNodes = append(newNodes, hash)
|
||
}
|
||
|
||
sort.Slice(newNodes, func(i, j int) bool { return newNodes[i] < newNodes[j] })
|
||
|
||
if ch.compareAndSwapNodes(ch.loadNodes(), newNodes) &&
|
||
ch.compareAndSwapNodeMap(oldNodeMap, newNodeMap) {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// GetNode возвращает узел для ключа (lock-free)
|
||
func (ch *ConsistentHash) GetNode(key string) string {
|
||
nodes := ch.loadNodes()
|
||
if len(nodes) == 0 {
|
||
return ""
|
||
}
|
||
|
||
hash := ch.hash(key)
|
||
idx := sort.Search(len(nodes), func(i int) bool { return nodes[i] >= hash })
|
||
if idx == len(nodes) {
|
||
idx = 0
|
||
}
|
||
|
||
nodeMap := ch.loadNodeMap()
|
||
return nodeMap[nodes[idx]]
|
||
}
|
||
|
||
// GetNodes возвращает все узлы (lock-free)
|
||
func (ch *ConsistentHash) GetNodes() []string {
|
||
nodeMap := ch.loadNodeMap()
|
||
nodesMap := make(map[string]bool)
|
||
for _, nodeID := range nodeMap {
|
||
nodesMap[nodeID] = true
|
||
}
|
||
|
||
nodes := make([]string, 0, len(nodesMap))
|
||
for node := range nodesMap {
|
||
nodes = append(nodes, node)
|
||
}
|
||
return nodes
|
||
}
|
||
|
||
func (ch *ConsistentHash) hash(key string) uint64 {
|
||
h := md5.Sum([]byte(key))
|
||
return binary.BigEndian.Uint64(h[:8])
|
||
}
|
||
|
||
// ========== ShardManager ==========
|
||
|
||
// ShardManager управляет шардами кластера (lock-free версия)
|
||
type ShardManager struct {
|
||
shardsPtr atomic.Value // map[string]*ShardInfo
|
||
consistentHash *ConsistentHash
|
||
rebalancing atomic.Bool
|
||
logger *log.Logger
|
||
}
|
||
|
||
// NewShardManager создаёт новый менеджер шардов
|
||
func NewShardManager(logger *log.Logger) *ShardManager {
|
||
sm := &ShardManager{
|
||
consistentHash: NewConsistentHash(150),
|
||
logger: logger,
|
||
}
|
||
sm.shardsPtr.Store(make(map[string]*ShardInfo))
|
||
return sm
|
||
}
|
||
|
||
// loadShards загружает карту шардов (lock-free)
|
||
func (sm *ShardManager) loadShards() map[string]*ShardInfo {
|
||
val := sm.shardsPtr.Load()
|
||
if val == nil {
|
||
return make(map[string]*ShardInfo)
|
||
}
|
||
return val.(map[string]*ShardInfo)
|
||
}
|
||
|
||
// storeShards сохраняет карту шардов (lock-free)
|
||
func (sm *ShardManager) storeShards(shards map[string]*ShardInfo) {
|
||
sm.shardsPtr.Store(shards)
|
||
}
|
||
|
||
// AddNode добавляет узел в шардинг (lock-free)
|
||
func (sm *ShardManager) AddNode(nodeID string) {
|
||
sm.consistentHash.AddNode(nodeID)
|
||
}
|
||
|
||
// RemoveNode удаляет узел из шардинга (lock-free)
|
||
func (sm *ShardManager) RemoveNode(nodeID string) {
|
||
sm.consistentHash.RemoveNode(nodeID)
|
||
}
|
||
|
||
// GetShard возвращает шард для коллекции (lock-free)
|
||
func (sm *ShardManager) GetShard(database, collection string) *ShardInfo {
|
||
key := fmt.Sprintf("%s:%s", database, collection)
|
||
nodeID := sm.consistentHash.GetNode(key)
|
||
|
||
shards := sm.loadShards()
|
||
for _, shard := range shards {
|
||
for _, node := range shard.Nodes {
|
||
if node == nodeID {
|
||
return shard
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CreateShard создаёт новый шард (lock-free)
|
||
func (sm *ShardManager) CreateShard(name string, nodes []string) *ShardInfo {
|
||
for {
|
||
oldShards := sm.loadShards()
|
||
newShards := make(map[string]*ShardInfo)
|
||
for k, v := range oldShards {
|
||
newShards[k] = v
|
||
}
|
||
|
||
now := time.Now().UnixMilli()
|
||
shard := &ShardInfo{
|
||
ID: fmt.Sprintf("shard_%d", len(oldShards)),
|
||
Name: name,
|
||
Nodes: nodes,
|
||
LeaderNode: nodes[0],
|
||
Status: "active",
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
LastRebalanced: now,
|
||
DocumentCount: 0,
|
||
SizeBytes: 0,
|
||
}
|
||
|
||
newShards[shard.ID] = shard
|
||
|
||
if sm.shardsPtr.CompareAndSwap(oldShards, newShards) {
|
||
return shard
|
||
}
|
||
}
|
||
}
|
||
|
||
// Rebalance выполняет ребалансировку шардов (lock-free)
|
||
func (sm *ShardManager) Rebalance() error {
|
||
if !sm.rebalancing.CompareAndSwap(false, true) {
|
||
return fmt.Errorf("rebalancing already in progress")
|
||
}
|
||
defer sm.rebalancing.Store(false)
|
||
|
||
if sm.logger != nil {
|
||
sm.logger.Info("Starting cluster rebalancing...")
|
||
}
|
||
|
||
rebalanceTime := time.Now().UnixMilli()
|
||
|
||
for {
|
||
oldShards := sm.loadShards()
|
||
newShards := make(map[string]*ShardInfo)
|
||
|
||
for id, shard := range oldShards {
|
||
shardCopy := *shard
|
||
shardCopy.LastRebalanced = rebalanceTime
|
||
shardCopy.UpdatedAt = rebalanceTime
|
||
newShards[id] = &shardCopy
|
||
}
|
||
|
||
if sm.shardsPtr.CompareAndSwap(oldShards, newShards) {
|
||
break
|
||
}
|
||
}
|
||
|
||
if sm.logger != nil {
|
||
sm.logger.Info(fmt.Sprintf("Rebalancing completed at %s", time.UnixMilli(rebalanceTime).Format("2006-01-02 15:04:05.000")))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetAllShards возвращает все шарды (lock-free)
|
||
func (sm *ShardManager) GetAllShards() []*ShardInfo {
|
||
shards := sm.loadShards()
|
||
result := make([]*ShardInfo, 0, len(shards))
|
||
for _, shard := range shards {
|
||
result = append(result, shard)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// GetShardByID возвращает шард по ID (lock-free)
|
||
func (sm *ShardManager) GetShardByID(shardID string) *ShardInfo {
|
||
shards := sm.loadShards()
|
||
if shard, ok := shards[shardID]; ok {
|
||
return shard
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ========== RaftClusterState ==========
|
||
|
||
// RaftClusterState представляет состояние кластера для Raft FSM
|
||
type RaftClusterState struct {
|
||
Nodes map[string]*NodeInfo `json:"nodes"`
|
||
ReplicationFactor int32 `json:"replication_factor"`
|
||
Shards map[string]*ShardInfo `json:"shards"`
|
||
CurrentTerm uint64 `json:"current_term"`
|
||
VotedFor string `json:"voted_for"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// ========== SplitBrainDetector ==========
|
||
|
||
// SplitBrainDetector обнаруживает и предотвращает split-brain ситуации
|
||
type SplitBrainDetector struct {
|
||
knownLeaders map[uint64]string
|
||
suspectTime map[string]int64
|
||
mu sync.RWMutex
|
||
logger *log.Logger
|
||
preventionEnabled bool
|
||
recoveryTimeout time.Duration
|
||
}
|
||
|
||
// NewSplitBrainDetector создаёт новый детектор split-brain
|
||
func NewSplitBrainDetector(logger *log.Logger, preventionEnabled bool, recoveryTimeout time.Duration) *SplitBrainDetector {
|
||
return &SplitBrainDetector{
|
||
knownLeaders: make(map[uint64]string),
|
||
suspectTime: make(map[string]int64),
|
||
logger: logger,
|
||
preventionEnabled: preventionEnabled,
|
||
recoveryTimeout: recoveryTimeout,
|
||
}
|
||
}
|
||
|
||
// Detect проверяет наличие split-brain ситуации
|
||
func (sbd *SplitBrainDetector) Detect(term uint64, leaderID string, nodesCount int) bool {
|
||
if !sbd.preventionEnabled {
|
||
return false
|
||
}
|
||
|
||
sbd.mu.Lock()
|
||
defer sbd.mu.Unlock()
|
||
|
||
if existingLeader, exists := sbd.knownLeaders[term]; exists {
|
||
if existingLeader != leaderID && nodesCount > 1 {
|
||
if sbd.logger != nil {
|
||
sbd.logger.Error(fmt.Sprintf("SPLIT-BRAIN DETECTED! Term %d has two leaders: %s and %s",
|
||
term, existingLeader, leaderID))
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
sbd.knownLeaders[term] = leaderID
|
||
|
||
for t := range sbd.knownLeaders {
|
||
if t+10 < term {
|
||
delete(sbd.knownLeaders, t)
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// Resolve разрешает split-brain ситуацию
|
||
func (sbd *SplitBrainDetector) Resolve(term uint64, candidates map[string]uint64) string {
|
||
if !sbd.preventionEnabled {
|
||
return ""
|
||
}
|
||
|
||
sbd.mu.Lock()
|
||
defer sbd.mu.Unlock()
|
||
|
||
var winner string
|
||
var maxCommit uint64 = 0
|
||
|
||
for nodeID, commitIndex := range candidates {
|
||
if commitIndex > maxCommit {
|
||
maxCommit = commitIndex
|
||
winner = nodeID
|
||
}
|
||
}
|
||
|
||
if sbd.logger != nil {
|
||
sbd.logger.Warn(fmt.Sprintf("Resolving split-brain: selecting leader %s with commit index %d",
|
||
winner, maxCommit))
|
||
}
|
||
|
||
return winner
|
||
}
|
||
|
||
// QuarantineNode изолирует узел, вызвавший split-brain
|
||
func (sbd *SplitBrainDetector) QuarantineNode(nodeID string) {
|
||
if !sbd.preventionEnabled {
|
||
return
|
||
}
|
||
|
||
sbd.mu.Lock()
|
||
defer sbd.mu.Unlock()
|
||
|
||
quarantineUntil := time.Now().Add(sbd.recoveryTimeout).UnixMilli()
|
||
sbd.suspectTime[nodeID] = quarantineUntil
|
||
|
||
if sbd.logger != nil {
|
||
sbd.logger.Warn(fmt.Sprintf("Node %s quarantined until %s", nodeID,
|
||
time.UnixMilli(quarantineUntil).Format("2006-01-02 15:04:05.000")))
|
||
}
|
||
}
|
||
|
||
// IsQuarantined проверяет, находится ли узел в карантине
|
||
func (sbd *SplitBrainDetector) IsQuarantined(nodeID string) bool {
|
||
sbd.mu.RLock()
|
||
defer sbd.mu.RUnlock()
|
||
|
||
if until, exists := sbd.suspectTime[nodeID]; exists {
|
||
if time.Now().UnixMilli() < until {
|
||
return true
|
||
}
|
||
delete(sbd.suspectTime, nodeID)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ========== RaftFSM ==========
|
||
|
||
// RaftFSM реализует конечный автомат для Raft
|
||
type RaftFSM struct {
|
||
state *RaftClusterState
|
||
logger *log.Logger
|
||
createdAt int64
|
||
coordinator *RaftCoordinator
|
||
}
|
||
|
||
// RaftSnapshot реализует интерфейс FSMSnapshot
|
||
type RaftSnapshot struct {
|
||
state *RaftClusterState
|
||
}
|
||
|
||
// Persist сохраняет снапшот
|
||
func (s *RaftSnapshot) Persist(sink raft.SnapshotSink) error {
|
||
err := func() error {
|
||
data, err := json.Marshal(s.state)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if _, err := sink.Write(data); err != nil {
|
||
return err
|
||
}
|
||
|
||
return sink.Close()
|
||
}()
|
||
|
||
if err != nil {
|
||
sink.Cancel()
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Release освобождает ресурсы
|
||
func (s *RaftSnapshot) Release() {}
|
||
|
||
// Apply применяет команду к FSM
|
||
func (f *RaftFSM) Apply(log *raft.Log) interface{} {
|
||
var batchCmd BatchCommand
|
||
if err := json.Unmarshal(log.Data, &batchCmd); err == nil && batchCmd.Type == "batch" {
|
||
for _, cmd := range batchCmd.Commands {
|
||
f.applySingleCommand(cmd)
|
||
}
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Applied batch command %s with %d sub-commands", batchCmd.BatchID, batchCmd.Size))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var jointCmd JointConsensusCommand
|
||
if err := json.Unmarshal(log.Data, &jointCmd); err == nil && jointCmd.Type == "joint_consensus" {
|
||
return f.applyJointConsensusCommand(jointCmd)
|
||
}
|
||
|
||
var moveCmd struct {
|
||
Type string `json:"type"`
|
||
ShardID string `json:"shard_id"`
|
||
TargetNode string `json:"target_node"`
|
||
}
|
||
if err := json.Unmarshal(log.Data, &moveCmd); err == nil && moveCmd.Type == "move_shard" {
|
||
return f.applyMoveShardCommand(moveCmd)
|
||
}
|
||
|
||
var moveDocCmd struct {
|
||
Type string `json:"type"`
|
||
ShardID string `json:"shard_id"`
|
||
Document map[string]interface{} `json:"document"`
|
||
DocumentID string `json:"document_id"`
|
||
TargetNode string `json:"target_node"`
|
||
}
|
||
if err := json.Unmarshal(log.Data, &moveDocCmd); err == nil && moveDocCmd.Type == "move_document" {
|
||
return f.applyMoveDocumentCommand(moveDocCmd)
|
||
}
|
||
|
||
var cmd NodeRegistrationCommand
|
||
if err := json.Unmarshal(log.Data, &cmd); err != nil {
|
||
var statusCmd NodeStatusCommand
|
||
if err2 := json.Unmarshal(log.Data, &statusCmd); err2 == nil {
|
||
return f.applyStatusCommand(statusCmd)
|
||
}
|
||
f.logger.Error(fmt.Sprintf("Failed to unmarshal raft command: %v", err))
|
||
return err
|
||
}
|
||
|
||
return f.applyRegistrationCommand(cmd)
|
||
}
|
||
|
||
func (f *RaftFSM) applySingleCommand(cmd interface{}) {
|
||
if regCmd, ok := cmd.(NodeRegistrationCommand); ok {
|
||
f.applyRegistrationCommand(regCmd)
|
||
} else if statusCmd, ok := cmd.(NodeStatusCommand); ok {
|
||
f.applyStatusCommand(statusCmd)
|
||
}
|
||
}
|
||
|
||
func (f *RaftFSM) applyRegistrationCommand(cmd NodeRegistrationCommand) interface{} {
|
||
f.state.mu.Lock()
|
||
defer f.state.mu.Unlock()
|
||
|
||
now := time.Now().UnixMilli()
|
||
f.state.UpdatedAt = now
|
||
|
||
switch cmd.Type {
|
||
case "register":
|
||
cmd.Node.JoinedAt = cmd.Timestamp
|
||
cmd.Node.UpdatedAt = now
|
||
cmd.Node.LastSeen = cmd.Timestamp
|
||
f.state.Nodes[cmd.Node.ID] = &cmd.Node
|
||
|
||
if f.coordinator != nil {
|
||
f.coordinator.shardManager.AddNode(cmd.Node.ID)
|
||
}
|
||
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Node registered: %s", cmd.Node.ID))
|
||
}
|
||
|
||
case "remove":
|
||
nodeID := cmd.NodeID
|
||
delete(f.state.Nodes, nodeID)
|
||
|
||
if f.coordinator != nil {
|
||
f.coordinator.shardManager.RemoveNode(nodeID)
|
||
}
|
||
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Node removed: %s", nodeID))
|
||
}
|
||
|
||
case "set_replication_factor":
|
||
oldFactor := f.state.ReplicationFactor
|
||
f.state.ReplicationFactor = cmd.Factor
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Replication factor changed from %d to %d", oldFactor, cmd.Factor))
|
||
}
|
||
|
||
case "create_shard":
|
||
if cmd.Shard != nil {
|
||
f.state.Shards[cmd.Shard.ID] = cmd.Shard
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Shard created: %s", cmd.Shard.Name))
|
||
}
|
||
}
|
||
|
||
case "move_shard":
|
||
if cmd.ShardID != "" && cmd.TargetNode != "" {
|
||
if shard, ok := f.state.Shards[cmd.ShardID]; ok {
|
||
oldLeader := shard.LeaderNode
|
||
shard.LeaderNode = cmd.TargetNode
|
||
shard.UpdatedAt = now
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Shard %s moved from %s to %s", cmd.ShardID, oldLeader, cmd.TargetNode))
|
||
}
|
||
}
|
||
}
|
||
|
||
case "move_document":
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Document %s moved to shard %s", cmd.Data["document_id"], cmd.ShardID))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (f *RaftFSM) applyStatusCommand(cmd NodeStatusCommand) interface{} {
|
||
f.state.mu.Lock()
|
||
defer f.state.mu.Unlock()
|
||
|
||
if node, ok := f.state.Nodes[cmd.NodeID]; ok {
|
||
statusStr := "offline"
|
||
switch cmd.Status {
|
||
case 1:
|
||
statusStr = "active"
|
||
case 2:
|
||
statusStr = "syncing"
|
||
case 3:
|
||
statusStr = "failed"
|
||
default:
|
||
statusStr = "offline"
|
||
}
|
||
node.Status = statusStr
|
||
node.UpdatedAt = cmd.Timestamp
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Node %s status updated to %s", cmd.NodeID, statusStr))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (f *RaftFSM) applyMoveShardCommand(cmd struct {
|
||
Type string `json:"type"`
|
||
ShardID string `json:"shard_id"`
|
||
TargetNode string `json:"target_node"`
|
||
}) interface{} {
|
||
f.state.mu.Lock()
|
||
defer f.state.mu.Unlock()
|
||
|
||
if shard, ok := f.state.Shards[cmd.ShardID]; ok {
|
||
oldLeader := shard.LeaderNode
|
||
shard.LeaderNode = cmd.TargetNode
|
||
shard.UpdatedAt = time.Now().UnixMilli()
|
||
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Shard %s moved from %s to %s", cmd.ShardID, oldLeader, cmd.TargetNode))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (f *RaftFSM) applyMoveDocumentCommand(cmd struct {
|
||
Type string `json:"type"`
|
||
ShardID string `json:"shard_id"`
|
||
Document map[string]interface{} `json:"document"`
|
||
DocumentID string `json:"document_id"`
|
||
TargetNode string `json:"target_node"`
|
||
}) interface{} {
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft: Document %s moved to node %s, shard %s", cmd.DocumentID, cmd.TargetNode, cmd.ShardID))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (f *RaftFSM) applyJointConsensusCommand(cmd JointConsensusCommand) interface{} {
|
||
f.state.mu.Lock()
|
||
defer f.state.mu.Unlock()
|
||
|
||
if f.logger != nil {
|
||
f.logger.Info(fmt.Sprintf("Raft: Joint consensus transition: %s at term %d", cmd.Transition, f.state.CurrentTerm))
|
||
}
|
||
|
||
switch cmd.Transition {
|
||
case "start":
|
||
if f.logger != nil {
|
||
f.logger.Info(fmt.Sprintf("Joint consensus started: old config has %d servers, new config has %d servers",
|
||
len(cmd.OldConfig), len(cmd.NewConfig)))
|
||
}
|
||
case "complete":
|
||
if f.logger != nil {
|
||
f.logger.Info("Joint consensus completed successfully")
|
||
}
|
||
case "abort":
|
||
if f.logger != nil {
|
||
f.logger.Warn("Joint consensus aborted")
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Snapshot реализует создание снапшота
|
||
func (f *RaftFSM) Snapshot() (raft.FSMSnapshot, error) {
|
||
f.state.mu.RLock()
|
||
defer f.state.mu.RUnlock()
|
||
|
||
stateCopy := &RaftClusterState{
|
||
Nodes: make(map[string]*NodeInfo),
|
||
Shards: make(map[string]*ShardInfo),
|
||
ReplicationFactor: f.state.ReplicationFactor,
|
||
CurrentTerm: f.state.CurrentTerm,
|
||
VotedFor: f.state.VotedFor,
|
||
CreatedAt: f.state.CreatedAt,
|
||
UpdatedAt: time.Now().UnixMilli(),
|
||
}
|
||
|
||
for k, v := range f.state.Nodes {
|
||
stateCopy.Nodes[k] = v
|
||
}
|
||
for k, v := range f.state.Shards {
|
||
stateCopy.Shards[k] = v
|
||
}
|
||
|
||
return &RaftSnapshot{state: stateCopy}, nil
|
||
}
|
||
|
||
// Restore восстанавливает состояние из снапшота
|
||
func (f *RaftFSM) Restore(snapshot io.ReadCloser) error {
|
||
defer snapshot.Close()
|
||
|
||
var state RaftClusterState
|
||
decoder := json.NewDecoder(snapshot)
|
||
if err := decoder.Decode(&state); err != nil {
|
||
return err
|
||
}
|
||
|
||
f.state.mu.Lock()
|
||
defer f.state.mu.Unlock()
|
||
|
||
f.state.Nodes = state.Nodes
|
||
f.state.Shards = state.Shards
|
||
f.state.ReplicationFactor = state.ReplicationFactor
|
||
f.state.CurrentTerm = state.CurrentTerm
|
||
f.state.VotedFor = state.VotedFor
|
||
f.state.CreatedAt = state.CreatedAt
|
||
f.state.UpdatedAt = time.Now().UnixMilli()
|
||
|
||
if f.coordinator != nil {
|
||
f.coordinator.currentTerm.Store(state.CurrentTerm)
|
||
f.coordinator.replicationFactor.Store(state.ReplicationFactor)
|
||
f.coordinator.stableStore.Set([]byte("currentTerm"), []byte(fmt.Sprintf("%d", state.CurrentTerm)))
|
||
f.coordinator.stableStore.Set([]byte("votedFor"), []byte(state.VotedFor))
|
||
}
|
||
|
||
if f.logger != nil {
|
||
f.logger.Debug(fmt.Sprintf("Raft state restored from snapshot at %s with %d nodes",
|
||
time.UnixMilli(f.state.UpdatedAt).Format("2006-01-02 15:04:05.000"), len(state.Nodes)))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// getLocalIP получает локальный IP адрес
|
||
func getLocalIP() string {
|
||
addrs, err := net.InterfaceAddrs()
|
||
if err != nil {
|
||
return "127.0.0.1"
|
||
}
|
||
for _, addr := range addrs {
|
||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
|
||
return ipnet.IP.String()
|
||
}
|
||
}
|
||
return "127.0.0.1"
|
||
}
|
||
|
||
// mapStatusToString преобразует статус в строку
|
||
func mapStatusToString(status int32) string {
|
||
switch status {
|
||
case 0:
|
||
return "offline"
|
||
case 1:
|
||
return "active"
|
||
case 2:
|
||
return "syncing"
|
||
case 3:
|
||
return "failed"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// ========== ClusterStatus ==========
|
||
|
||
// ClusterStatus представляет статус кластера
|
||
type ClusterStatus struct {
|
||
Name string `json:"name"`
|
||
TotalNodes int `json:"total_nodes"`
|
||
ActiveNodes int `json:"active_nodes"`
|
||
SyncingNodes int `json:"syncing_nodes"`
|
||
FailedNodes int `json:"failed_nodes"`
|
||
ReplicationFactor int `json:"replication_factor"`
|
||
LeaderID string `json:"leader_id"`
|
||
Health string `json:"health"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
PipelineEnabled bool `json:"pipeline_enabled"`
|
||
BatchCommitEnabled bool `json:"batch_commit_enabled"`
|
||
ReshardingEnabled bool `json:"resharding_enabled"`
|
||
JointConsensusActive bool `json:"joint_consensus_active"`
|
||
FallbackMode bool `json:"fallback_mode"`
|
||
}
|
||
|
||
// ========== RaftCoordinator ==========
|
||
|
||
// RaftCoordinator реализует координацию кластера через Raft
|
||
type RaftCoordinator struct {
|
||
raft *raft.Raft
|
||
fsm *RaftFSM
|
||
address string
|
||
raftAddr string
|
||
clusterName string
|
||
logger *log.Logger
|
||
config *config.Config
|
||
store *storage.Storage
|
||
stopChan chan struct{}
|
||
nodes sync.Map
|
||
replicationFactor atomic.Int32
|
||
replicationEnabled bool
|
||
masterMasterEnabled bool
|
||
syncReplication bool
|
||
isLeader atomic.Bool
|
||
leaderMonitor chan bool
|
||
singleNodeMode bool
|
||
localNodeInfo *NodeInfo
|
||
logStore *InmemStore
|
||
stableStore *InmemStore
|
||
createdAt int64
|
||
leaderSince atomic.Int64
|
||
lastElection atomic.Int64
|
||
electionCount atomic.Uint64
|
||
currentTerm atomic.Uint64
|
||
shardManager *ShardManager
|
||
splitBrainDetector *SplitBrainDetector
|
||
|
||
pipelineReplicator *PipelineReplicator
|
||
batchCommitManager *BatchCommitManager
|
||
reshardingManager *ReshardingManager
|
||
jointConsensusManager *JointConsensusManager
|
||
|
||
recoveryManager *RecoveryManager
|
||
authenticator *NodeAuthenticator
|
||
persistenceMgr *storage.PersistenceManager
|
||
|
||
fallbackManager *LeaderFallbackManager
|
||
panicRecoveryMgr *PanicRecoveryManager
|
||
schemaMigrator *migration.SchemaMigrator
|
||
}
|
||
|
||
// NewRaftCoordinator создаёт новый Raft координатор
|
||
func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.Logger) (*RaftCoordinator, error) {
|
||
now := time.Now().UnixMilli()
|
||
|
||
nodeIP := cfg.Cluster.NodeIP
|
||
if nodeIP == "" || nodeIP == "0.0.0.0" {
|
||
nodeIP = getLocalIP()
|
||
}
|
||
|
||
raftAddr := fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.RaftPort)
|
||
|
||
logger.Debug(fmt.Sprintf("Creating Raft coordinator at %s", raftAddr))
|
||
|
||
singleNodeMode := len(cfg.Cluster.Nodes) <= 1 || cfg.Cluster.Bootstrap
|
||
|
||
rc := &RaftCoordinator{
|
||
address: fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.NodePort),
|
||
raftAddr: raftAddr,
|
||
clusterName: cfg.Cluster.Name,
|
||
logger: logger,
|
||
config: cfg,
|
||
store: store,
|
||
stopChan: make(chan struct{}),
|
||
leaderMonitor: make(chan bool, 1),
|
||
replicationEnabled: cfg.Replication.Enabled,
|
||
masterMasterEnabled: cfg.Replication.MasterMaster,
|
||
syncReplication: cfg.Replication.SyncReplication,
|
||
singleNodeMode: singleNodeMode,
|
||
createdAt: now,
|
||
localNodeInfo: &NodeInfo{
|
||
ID: fmt.Sprintf("%s-%s", cfg.Cluster.Name, nodeIP),
|
||
IP: nodeIP,
|
||
Port: cfg.Cluster.NodePort,
|
||
Status: "active",
|
||
LastSeen: now,
|
||
JoinedAt: now,
|
||
UpdatedAt: now,
|
||
Version: 1,
|
||
},
|
||
}
|
||
|
||
rc.shardManager = NewShardManager(logger)
|
||
rc.splitBrainDetector = NewSplitBrainDetector(
|
||
logger,
|
||
cfg.Cluster.IsSplitBrainPreventionEnabled(),
|
||
cfg.Cluster.GetRecoveryTimeout(),
|
||
)
|
||
|
||
defaultReplicationFactor := int32(3)
|
||
if len(cfg.Cluster.Nodes) < 3 {
|
||
defaultReplicationFactor = int32(len(cfg.Cluster.Nodes))
|
||
}
|
||
rc.replicationFactor.Store(defaultReplicationFactor)
|
||
|
||
rc.fsm = &RaftFSM{
|
||
state: &RaftClusterState{
|
||
Nodes: make(map[string]*NodeInfo),
|
||
Shards: make(map[string]*ShardInfo),
|
||
ReplicationFactor: rc.replicationFactor.Load(),
|
||
CurrentTerm: 0,
|
||
VotedFor: "",
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
},
|
||
logger: logger,
|
||
createdAt: now,
|
||
coordinator: rc,
|
||
}
|
||
|
||
if singleNodeMode {
|
||
rc.fsm.state.mu.Lock()
|
||
rc.fsm.state.Nodes[rc.localNodeInfo.ID] = rc.localNodeInfo
|
||
rc.fsm.state.mu.Unlock()
|
||
rc.isLeader.Store(true)
|
||
rc.leaderSince.Store(now)
|
||
rc.currentTerm.Store(1)
|
||
rc.shardManager.AddNode(rc.localNodeInfo.ID)
|
||
logger.Debug(fmt.Sprintf("Single-node mode: local node added to state: %s", rc.localNodeInfo.ID))
|
||
}
|
||
|
||
raftConfig := raft.DefaultConfig()
|
||
raftConfig.LocalID = raft.ServerID(rc.localNodeInfo.ID)
|
||
|
||
raftConfig.HeartbeatTimeout = cfg.Cluster.GetHeartbeatTimeout()
|
||
raftConfig.ElectionTimeout = cfg.Cluster.GetElectionTimeout()
|
||
raftConfig.CommitTimeout = cfg.Cluster.GetCommitTimeout()
|
||
raftConfig.LeaderLeaseTimeout = cfg.Cluster.GetCommitTimeout()
|
||
raftConfig.SnapshotInterval = cfg.Cluster.GetSnapshotInterval()
|
||
raftConfig.SnapshotThreshold = cfg.Cluster.GetSnapshotThreshold()
|
||
|
||
if singleNodeMode {
|
||
raftConfig.HeartbeatTimeout = 500 * time.Millisecond
|
||
raftConfig.ElectionTimeout = 500 * time.Millisecond
|
||
raftConfig.LeaderLeaseTimeout = 500 * time.Millisecond
|
||
raftConfig.LogOutput = io.Discard
|
||
logger.Debug("Running in single-node mode (warnings suppressed)")
|
||
} else {
|
||
raftConfig.LogOutput = os.Stderr
|
||
}
|
||
|
||
dataDir := cfg.Cluster.RaftDataDir
|
||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||
return nil, fmt.Errorf("failed to create raft data dir: %v", err)
|
||
}
|
||
|
||
logger.Debug(fmt.Sprintf("Raft data directory: %s", dataDir))
|
||
|
||
rc.logStore = NewInmemStore(filepath.Join(dataDir, "raft-log.json"))
|
||
rc.stableStore = NewInmemStore(filepath.Join(dataDir, "raft-stable.json"))
|
||
|
||
if termData, err := rc.stableStore.Get([]byte("currentTerm")); err == nil && termData != nil {
|
||
var term uint64
|
||
fmt.Sscanf(string(termData), "%d", &term)
|
||
rc.currentTerm.Store(term)
|
||
rc.fsm.state.CurrentTerm = term
|
||
}
|
||
|
||
if votedForData, err := rc.stableStore.Get([]byte("votedFor")); err == nil && votedForData != nil {
|
||
rc.fsm.state.VotedFor = string(votedForData)
|
||
}
|
||
|
||
snapshotStore, err := raft.NewFileSnapshotStore(dataDir, 3, os.Stderr)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create snapshot store: %v", err)
|
||
}
|
||
|
||
transport, err := raft.NewTCPTransport(raftAddr, nil, 3, 10*time.Second, os.Stderr)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create transport: %v", err)
|
||
}
|
||
|
||
r, err := raft.NewRaft(raftConfig, rc.fsm, rc.logStore, rc.stableStore, snapshotStore, transport)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create raft: %v", err)
|
||
}
|
||
|
||
rc.raft = r
|
||
|
||
time.Sleep(500 * time.Millisecond)
|
||
|
||
bootstrapPath := filepath.Join(dataDir, "raft-log.json")
|
||
_, statErr := os.Stat(bootstrapPath)
|
||
needsBootstrap := os.IsNotExist(statErr)
|
||
|
||
if needsBootstrap && singleNodeMode {
|
||
logger.Debug("Bootstrapping single-node cluster...")
|
||
|
||
configuration := raft.Configuration{
|
||
Servers: []raft.Server{
|
||
{
|
||
ID: raftConfig.LocalID,
|
||
Address: transport.LocalAddr(),
|
||
},
|
||
},
|
||
}
|
||
|
||
future := r.BootstrapCluster(configuration)
|
||
if err := future.Error(); err != nil {
|
||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||
} else {
|
||
logger.Debug("Single-node cluster bootstrapped successfully")
|
||
}
|
||
|
||
time.Sleep(1 * time.Second)
|
||
|
||
} else if needsBootstrap && len(cfg.Cluster.Nodes) > 1 {
|
||
logger.Debug("Bootstrapping multi-node cluster...")
|
||
|
||
servers := make([]raft.Server, 0, len(cfg.Cluster.Nodes))
|
||
for i, nodeAddr := range cfg.Cluster.Nodes {
|
||
serverID := raft.ServerID(fmt.Sprintf("%s-node%d", rc.clusterName, i+1))
|
||
servers = append(servers, raft.Server{
|
||
ID: serverID,
|
||
Address: raft.ServerAddress(nodeAddr),
|
||
})
|
||
}
|
||
|
||
configuration := raft.Configuration{
|
||
Servers: servers,
|
||
}
|
||
|
||
future := r.BootstrapCluster(configuration)
|
||
if err := future.Error(); err != nil {
|
||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||
} else {
|
||
logger.Debug("Multi-node cluster bootstrapped successfully")
|
||
}
|
||
|
||
go rc.monitorLeadership()
|
||
|
||
logger.Debug("Waiting for leader election...")
|
||
timeout := time.After(cfg.Cluster.GetElectionTimeout() * 5)
|
||
leaderElected := false
|
||
|
||
for !leaderElected {
|
||
select {
|
||
case isLeader := <-rc.leaderMonitor:
|
||
if isLeader {
|
||
leaderElected = true
|
||
rc.isLeader.Store(true)
|
||
rc.leaderSince.Store(time.Now().UnixMilli())
|
||
rc.lastElection.Store(time.Now().UnixMilli())
|
||
logger.Debug("This node is now the cluster leader")
|
||
}
|
||
case <-timeout:
|
||
logger.Warn("Leader election timeout")
|
||
leaderElected = true
|
||
}
|
||
}
|
||
} else {
|
||
logger.Debug("Existing Raft state found, joining cluster...")
|
||
go rc.monitorLeadership()
|
||
|
||
if !singleNodeMode {
|
||
time.Sleep(1 * time.Second)
|
||
if r.State() == raft.Leader {
|
||
rc.isLeader.Store(true)
|
||
rc.leaderSince.Store(time.Now().UnixMilli())
|
||
logger.Debug("This node is the cluster leader")
|
||
}
|
||
}
|
||
}
|
||
|
||
rc.pipelineReplicator = NewPipelineReplicator(rc, 50, 10*time.Millisecond, logger)
|
||
rc.batchCommitManager = NewBatchCommitManager(100, 50*time.Millisecond, true, logger)
|
||
rc.reshardingManager = NewReshardingManager(rc, logger)
|
||
rc.jointConsensusManager = NewJointConsensusManager(rc, logger)
|
||
|
||
// Используем функции из других файлов для создания компонентов
|
||
// NodeAuthenticator создаётся в auth.go
|
||
// RecoveryManager создаётся в recovery.go
|
||
// LeaderFallbackManager создаётся в leader_fallback.go
|
||
|
||
recoveryMgr := NewRecoveryManager(rc, logger)
|
||
rc.recoveryManager = recoveryMgr
|
||
rc.recoveryManager.Start()
|
||
|
||
persistenceConfig := storage.DefaultPersistenceConfig()
|
||
persistenceConfig.DataDir = filepath.Join(dataDir, "persistence")
|
||
rc.persistenceMgr = storage.NewPersistenceManager(persistenceConfig, store, logger)
|
||
rc.persistenceMgr.Start()
|
||
|
||
if err := rc.persistenceMgr.LoadAll(); err != nil {
|
||
logger.Warn(fmt.Sprintf("Failed to load persisted data: %v", err))
|
||
}
|
||
|
||
panicRecoveryMgr := NewPanicRecoveryManager(logger)
|
||
rc.panicRecoveryMgr = panicRecoveryMgr
|
||
|
||
panicRecoveryMgr.RegisterRecoveryFunc("Raft", func(p interface{}) error {
|
||
logger.Error(fmt.Sprintf("Raft panic recovered: %v, attempting to restart coordinator", p))
|
||
return nil
|
||
})
|
||
|
||
panicRecoveryMgr.RegisterRecoveryFunc("PipelineReplicator", func(p interface{}) error {
|
||
logger.Error(fmt.Sprintf("Pipeline replicator panic: %v, reinitializing", p))
|
||
if rc.pipelineReplicator != nil {
|
||
rc.pipelineReplicator.Stop()
|
||
rc.pipelineReplicator = NewPipelineReplicator(rc, 50, 10*time.Millisecond, logger)
|
||
}
|
||
return nil
|
||
})
|
||
|
||
panicRecoveryMgr.RegisterRecoveryFunc("BatchCommitManager", func(p interface{}) error {
|
||
logger.Error(fmt.Sprintf("Batch commit manager panic: %v, reinitializing", p))
|
||
if rc.batchCommitManager != nil {
|
||
rc.batchCommitManager.Stop()
|
||
rc.batchCommitManager = NewBatchCommitManager(100, 50*time.Millisecond, true, logger)
|
||
}
|
||
return nil
|
||
})
|
||
|
||
panicRecoveryMgr.RegisterRecoveryFunc("ReshardingManager", func(p interface{}) error {
|
||
logger.Error(fmt.Sprintf("Resharding manager panic: %v, reinitializing", p))
|
||
if rc.reshardingManager != nil {
|
||
rc.reshardingManager.Stop()
|
||
rc.reshardingManager = NewReshardingManager(rc, logger)
|
||
}
|
||
return nil
|
||
})
|
||
|
||
panicRecoveryMgr.RegisterRecoveryFunc("RecoveryManager", func(p interface{}) error {
|
||
logger.Error(fmt.Sprintf("Recovery manager panic: %v, reinitializing", p))
|
||
if rc.recoveryManager != nil {
|
||
rc.recoveryManager.Stop()
|
||
recoveryMgr := NewRecoveryManager(rc, logger)
|
||
rc.recoveryManager = recoveryMgr
|
||
rc.recoveryManager.Start()
|
||
}
|
||
return nil
|
||
})
|
||
|
||
fallbackCfg := DefaultFallbackConfig()
|
||
fallbackCfg.ElectionTimeout = cfg.Cluster.GetElectionTimeout()
|
||
fallbackCfg.FallbackTimeout = cfg.Cluster.GetRecoveryTimeout()
|
||
rc.fallbackManager = NewLeaderFallbackManager(rc, logger, fallbackCfg)
|
||
|
||
migrationDir := filepath.Join(dataDir, "migrations")
|
||
rc.schemaMigrator = migration.NewSchemaMigrator(store, logger, migrationDir)
|
||
|
||
status := rc.schemaMigrator.GetStatus()
|
||
if status.PendingMigrations > 0 {
|
||
logger.Info(fmt.Sprintf("Found %d pending schema migrations, current version: %s",
|
||
status.PendingMigrations, status.CurrentVersion))
|
||
|
||
if err := rc.schemaMigrator.Migrate("2.1.0"); err != nil {
|
||
logger.Warn(fmt.Sprintf("Schema migration warning: %v", err))
|
||
} else {
|
||
logger.Info("Schema migrations completed successfully")
|
||
}
|
||
}
|
||
|
||
if !singleNodeMode {
|
||
go rc.rebalanceMonitor()
|
||
}
|
||
|
||
logger.InfoWithFields("Raft coordinator started", map[string]interface{}{
|
||
"is_leader": rc.isLeader.Load(),
|
||
"single_node": singleNodeMode,
|
||
"term": rc.currentTerm.Load(),
|
||
"created_at": time.UnixMilli(rc.createdAt).Format("2006-01-02 15:04:05.000"),
|
||
"fallback_enabled": rc.fallbackManager != nil,
|
||
"panic_recovery": rc.panicRecoveryMgr != nil,
|
||
"schema_migration": rc.schemaMigrator != nil,
|
||
})
|
||
|
||
return rc, nil
|
||
}
|
||
|
||
// RealFsync выполняет реальную синхронизацию с диском
|
||
func (rc *RaftCoordinator) RealFsync(file *os.File) error {
|
||
if file == nil {
|
||
return nil
|
||
}
|
||
return file.Sync()
|
||
}
|
||
|
||
// RealFsyncWithRetry выполняет fsync с повторными попытками
|
||
func (rc *RaftCoordinator) RealFsyncWithRetry(file *os.File, maxRetries int) error {
|
||
var lastErr error
|
||
for i := 0; i < maxRetries; i++ {
|
||
if err := rc.RealFsync(file); err != nil {
|
||
lastErr = err
|
||
if rc.logger != nil {
|
||
rc.logger.Warn(fmt.Sprintf("Fsync attempt %d failed: %v, retrying...", i+1, err))
|
||
}
|
||
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
|
||
continue
|
||
}
|
||
return nil
|
||
}
|
||
return lastErr
|
||
}
|
||
|
||
// rebalanceMonitor периодически проверяет необходимость ребалансировки
|
||
func (rc *RaftCoordinator) rebalanceMonitor() {
|
||
ticker := time.NewTicker(5 * time.Minute)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-rc.stopChan:
|
||
return
|
||
case <-ticker.C:
|
||
if rc.IsLeader() && rc.reshardingManager != nil {
|
||
rc.reshardingManager.TriggerResharding("periodic_rebalance")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// monitorLeadership отслеживает изменения лидера
|
||
func (rc *RaftCoordinator) monitorLeadership() {
|
||
ticker := time.NewTicker(rc.config.Cluster.GetHeartbeatTimeout() / 2)
|
||
defer ticker.Stop()
|
||
|
||
wasLeader := false
|
||
|
||
for {
|
||
select {
|
||
case <-rc.stopChan:
|
||
return
|
||
case <-ticker.C:
|
||
if rc.raft == nil {
|
||
continue
|
||
}
|
||
isLeader := rc.raft.State() == raft.Leader
|
||
if isLeader != wasLeader {
|
||
wasLeader = isLeader
|
||
select {
|
||
case rc.leaderMonitor <- isLeader:
|
||
default:
|
||
}
|
||
if isLeader {
|
||
rc.isLeader.Store(true)
|
||
newTerm := rc.currentTerm.Add(1)
|
||
rc.leaderSince.Store(time.Now().UnixMilli())
|
||
rc.electionCount.Add(1)
|
||
rc.fsm.state.CurrentTerm = newTerm
|
||
rc.stableStore.Set([]byte("currentTerm"), []byte(fmt.Sprintf("%d", newTerm)))
|
||
rc.logger.Debug(fmt.Sprintf("Leadership acquired at term %d (election #%d)",
|
||
newTerm, rc.electionCount.Load()))
|
||
|
||
nodes := rc.GetAllNodes()
|
||
for _, node := range nodes {
|
||
rc.shardManager.AddNode(node.ID)
|
||
}
|
||
} else {
|
||
rc.isLeader.Store(false)
|
||
rc.lastElection.Store(time.Now().UnixMilli())
|
||
rc.logger.Debug("Leadership lost")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// RegisterNode регистрирует узел через Raft
|
||
func (rc *RaftCoordinator) RegisterNode(node *Node) error {
|
||
startTime := time.Now()
|
||
now := time.Now().UnixMilli()
|
||
|
||
if rc.splitBrainDetector.IsQuarantined(node.ID) {
|
||
return fmt.Errorf("node %s is quarantined due to previous split-brain", node.ID)
|
||
}
|
||
|
||
nodeInfo := &NodeInfo{
|
||
ID: node.ID,
|
||
IP: node.IP,
|
||
Port: node.Port,
|
||
Status: "active",
|
||
LastSeen: now,
|
||
JoinedAt: now,
|
||
UpdatedAt: now,
|
||
Version: 1,
|
||
}
|
||
|
||
if rc.singleNodeMode {
|
||
rc.logger.Debug("Single-node mode: registering node without Raft consensus")
|
||
|
||
rc.nodes.Store(node.ID, nodeInfo)
|
||
|
||
rc.fsm.state.mu.Lock()
|
||
rc.fsm.state.Nodes[node.ID] = nodeInfo
|
||
rc.fsm.state.UpdatedAt = now
|
||
rc.fsm.state.mu.Unlock()
|
||
|
||
rc.shardManager.AddNode(node.ID)
|
||
|
||
rc.logger.Debug(fmt.Sprintf("Node registered locally in single-node mode: %s", node.ID))
|
||
return nil
|
||
}
|
||
|
||
if !rc.IsLeader() {
|
||
leader := rc.GetLeader()
|
||
if leader != nil {
|
||
rc.logger.Warn(fmt.Sprintf("Current node is not leader. Leader is %s:%d",
|
||
leader.IP, leader.Port))
|
||
return fmt.Errorf("node is not the leader. Please connect to leader at %s:%d", leader.IP, leader.Port)
|
||
}
|
||
return fmt.Errorf("node is not the leader and no leader found")
|
||
}
|
||
|
||
cmd := NodeRegistrationCommand{
|
||
Type: "register",
|
||
Node: *nodeInfo,
|
||
Timestamp: now,
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
future := rc.raft.Apply(data, rc.config.Replication.GetReplicationTimeout())
|
||
if err := future.Error(); err != nil {
|
||
rc.logger.ErrorWithFields("Failed to register node", map[string]interface{}{
|
||
"error": err.Error(),
|
||
"node": node.ID,
|
||
})
|
||
return fmt.Errorf("failed to register node via raft: %v", err)
|
||
}
|
||
|
||
rc.nodes.Store(node.ID, nodeInfo)
|
||
rc.shardManager.AddNode(node.ID)
|
||
|
||
duration := time.Since(startTime).Milliseconds()
|
||
rc.logger.InfoWithFields("Node registered", map[string]interface{}{
|
||
"node_id": node.ID,
|
||
"address": node.GetAddress(),
|
||
"duration_ms": duration,
|
||
})
|
||
|
||
return nil
|
||
}
|
||
|
||
// UpdateNodeStatus обновляет статус узла через Raft
|
||
func (rc *RaftCoordinator) UpdateNodeStatus(nodeID string, status NodeStatus) error {
|
||
now := time.Now().UnixMilli()
|
||
|
||
if rc.splitBrainDetector.IsQuarantined(nodeID) {
|
||
return fmt.Errorf("node %s is quarantined, cannot update status", nodeID)
|
||
}
|
||
|
||
if rc.singleNodeMode {
|
||
rc.fsm.state.mu.Lock()
|
||
if node, ok := rc.fsm.state.Nodes[nodeID]; ok {
|
||
node.Status = mapStatusToString(int32(status))
|
||
node.UpdatedAt = now
|
||
}
|
||
rc.fsm.state.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
if !rc.IsLeader() {
|
||
return fmt.Errorf("node is not the leader")
|
||
}
|
||
|
||
cmd := NodeStatusCommand{
|
||
Type: "update_status",
|
||
NodeID: nodeID,
|
||
Status: int32(status),
|
||
Timestamp: now,
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
future := rc.raft.Apply(data, rc.config.Replication.GetReplicationTimeout())
|
||
return future.Error()
|
||
}
|
||
|
||
// RemoveNode удаляет узел через Raft
|
||
func (rc *RaftCoordinator) RemoveNode(nodeID string) error {
|
||
now := time.Now().UnixMilli()
|
||
|
||
if rc.singleNodeMode {
|
||
rc.nodes.Delete(nodeID)
|
||
rc.fsm.state.mu.Lock()
|
||
delete(rc.fsm.state.Nodes, nodeID)
|
||
rc.fsm.state.UpdatedAt = now
|
||
rc.fsm.state.mu.Unlock()
|
||
rc.shardManager.RemoveNode(nodeID)
|
||
rc.logger.Debug(fmt.Sprintf("Node removed locally in single-node mode: %s", nodeID))
|
||
return nil
|
||
}
|
||
|
||
if !rc.IsLeader() {
|
||
return fmt.Errorf("node is not the leader")
|
||
}
|
||
|
||
cmd := NodeRegistrationCommand{
|
||
Type: "remove",
|
||
NodeID: nodeID,
|
||
Timestamp: now,
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
future := rc.raft.Apply(data, rc.config.Replication.GetReplicationTimeout())
|
||
if err := future.Error(); err != nil {
|
||
return fmt.Errorf("failed to remove node via raft: %v", err)
|
||
}
|
||
|
||
rc.nodes.Delete(nodeID)
|
||
rc.shardManager.RemoveNode(nodeID)
|
||
rc.logger.Debug(fmt.Sprintf("Node removed via Raft: %s", nodeID))
|
||
return nil
|
||
}
|
||
|
||
// GetActiveNodes возвращает активные узлы
|
||
func (rc *RaftCoordinator) GetActiveNodes() []*NodeInfo {
|
||
nodes := make([]*NodeInfo, 0)
|
||
now := time.Now().UnixMilli()
|
||
|
||
state := rc.fsm.state
|
||
state.mu.RLock()
|
||
defer state.mu.RUnlock()
|
||
|
||
for _, nodeInfo := range state.Nodes {
|
||
if now-nodeInfo.LastSeen < 30000 && nodeInfo.Status == "active" {
|
||
if !rc.splitBrainDetector.IsQuarantined(nodeInfo.ID) {
|
||
nodes = append(nodes, nodeInfo)
|
||
}
|
||
}
|
||
}
|
||
|
||
if rc.singleNodeMode && len(nodes) == 0 && rc.localNodeInfo != nil {
|
||
nodes = append(nodes, rc.localNodeInfo)
|
||
}
|
||
|
||
return nodes
|
||
}
|
||
|
||
// GetAllNodes возвращает все узлы
|
||
func (rc *RaftCoordinator) GetAllNodes() []*NodeInfo {
|
||
state := rc.fsm.state
|
||
state.mu.RLock()
|
||
defer state.mu.RUnlock()
|
||
|
||
nodes := make([]*NodeInfo, 0, len(state.Nodes))
|
||
for _, node := range state.Nodes {
|
||
nodes = append(nodes, node)
|
||
}
|
||
|
||
if rc.singleNodeMode && len(nodes) == 0 && rc.localNodeInfo != nil {
|
||
nodes = append(nodes, rc.localNodeInfo)
|
||
}
|
||
|
||
return nodes
|
||
}
|
||
|
||
// GetNodeByID возвращает узел по ID
|
||
func (rc *RaftCoordinator) GetNodeByID(nodeID string) *NodeInfo {
|
||
state := rc.fsm.state
|
||
state.mu.RLock()
|
||
defer state.mu.RUnlock()
|
||
|
||
if node, ok := state.Nodes[nodeID]; ok {
|
||
return node
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetShardByID возвращает шард по ID
|
||
func (rc *RaftCoordinator) GetShardByID(shardID string) *ShardInfo {
|
||
return rc.shardManager.GetShardByID(shardID)
|
||
}
|
||
|
||
// GetLeader возвращает лидера
|
||
func (rc *RaftCoordinator) GetLeader() *NodeInfo {
|
||
if rc.singleNodeMode {
|
||
return rc.localNodeInfo
|
||
}
|
||
|
||
leaderAddr := rc.raft.Leader()
|
||
if leaderAddr == "" {
|
||
return nil
|
||
}
|
||
|
||
state := rc.fsm.state
|
||
state.mu.RLock()
|
||
defer state.mu.RUnlock()
|
||
|
||
for _, node := range state.Nodes {
|
||
nodeAddr := fmt.Sprintf("%s:%d", node.IP, node.Port)
|
||
if nodeAddr == string(leaderAddr) {
|
||
return node
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// IsLeader проверяет, является ли текущий узел лидером
|
||
func (rc *RaftCoordinator) IsLeader() bool {
|
||
if rc.singleNodeMode {
|
||
return true
|
||
}
|
||
return rc.isLeader.Load()
|
||
}
|
||
|
||
// GetCurrentTerm возвращает текущий терм Raft
|
||
func (rc *RaftCoordinator) GetCurrentTerm() uint64 {
|
||
return rc.currentTerm.Load()
|
||
}
|
||
|
||
// GetLeaderSince возвращает время начала лидерства
|
||
func (rc *RaftCoordinator) GetLeaderSince() int64 {
|
||
return rc.leaderSince.Load()
|
||
}
|
||
|
||
// GetElectionCount возвращает количество выборов
|
||
func (rc *RaftCoordinator) GetElectionCount() uint64 {
|
||
return rc.electionCount.Load()
|
||
}
|
||
|
||
// SendHeartbeat обновляет heartbeat узла
|
||
func (rc *RaftCoordinator) SendHeartbeat(nodeID string) {
|
||
now := time.Now().UnixMilli()
|
||
|
||
if val, ok := rc.nodes.Load(nodeID); ok {
|
||
nodeInfo := val.(*NodeInfo)
|
||
nodeInfo.LastSeen = now
|
||
nodeInfo.UpdatedAt = now
|
||
rc.nodes.Store(nodeID, nodeInfo)
|
||
}
|
||
|
||
rc.fsm.state.mu.Lock()
|
||
if nodeInfo, ok := rc.fsm.state.Nodes[nodeID]; ok {
|
||
nodeInfo.LastSeen = now
|
||
nodeInfo.UpdatedAt = now
|
||
}
|
||
rc.fsm.state.mu.Unlock()
|
||
}
|
||
|
||
// HandleStatusSync обрабатывает синхронизацию статуса
|
||
func (rc *RaftCoordinator) HandleStatusSync(leaderID string, term uint64, clusterSize int) {
|
||
if rc.splitBrainDetector.Detect(term, leaderID, clusterSize) {
|
||
candidates := make(map[string]uint64)
|
||
candidates[leaderID] = rc.getCommitIndex()
|
||
candidates[rc.localNodeInfo.ID] = rc.getCommitIndex()
|
||
|
||
winner := rc.splitBrainDetector.Resolve(term, candidates)
|
||
if winner == rc.localNodeInfo.ID && !rc.IsLeader() {
|
||
rc.raft.LeadershipTransfer()
|
||
rc.logger.Warn("Split-brain resolved: initiating leadership transfer")
|
||
} else if winner != leaderID && winner != "" {
|
||
rc.splitBrainDetector.QuarantineNode(leaderID)
|
||
rc.logger.Warn(fmt.Sprintf("Quarantining node %s due to split-brain", leaderID))
|
||
}
|
||
}
|
||
}
|
||
|
||
func (rc *RaftCoordinator) getCommitIndex() uint64 {
|
||
if rc.raft == nil {
|
||
return 0
|
||
}
|
||
return rc.raft.AppliedIndex()
|
||
}
|
||
|
||
// GetClusterStatus возвращает статус кластера
|
||
func (rc *RaftCoordinator) GetClusterStatus() *ClusterStatus {
|
||
nodes := rc.GetAllNodes()
|
||
activeNodes := rc.GetActiveNodes()
|
||
|
||
syncingNodes := 0
|
||
for _, node := range nodes {
|
||
if node.Status == "syncing" {
|
||
syncingNodes++
|
||
}
|
||
}
|
||
|
||
leader := rc.GetLeader()
|
||
leaderID := ""
|
||
if leader != nil {
|
||
leaderID = leader.ID
|
||
}
|
||
|
||
now := time.Now().UnixMilli()
|
||
|
||
health := rc.calculateHealth()
|
||
|
||
if rc.splitBrainDetector.Detect(rc.currentTerm.Load(), leaderID, len(nodes)) {
|
||
health = "split_brain"
|
||
}
|
||
|
||
return &ClusterStatus{
|
||
Name: rc.clusterName,
|
||
TotalNodes: len(nodes),
|
||
ActiveNodes: len(activeNodes),
|
||
SyncingNodes: syncingNodes,
|
||
FailedNodes: len(nodes) - len(activeNodes),
|
||
ReplicationFactor: int(rc.replicationFactor.Load()),
|
||
LeaderID: leaderID,
|
||
Health: health,
|
||
CreatedAt: rc.createdAt,
|
||
UpdatedAt: now,
|
||
PipelineEnabled: rc.pipelineReplicator != nil,
|
||
BatchCommitEnabled: rc.batchCommitManager != nil,
|
||
ReshardingEnabled: rc.reshardingManager != nil,
|
||
JointConsensusActive: rc.jointConsensusManager != nil && rc.jointConsensusManager.IsJointConsensusActive(),
|
||
FallbackMode: rc.fallbackManager != nil && rc.fallbackManager.IsFallbackMode(),
|
||
}
|
||
}
|
||
|
||
// calculateHealth вычисляет здоровье кластера
|
||
func (rc *RaftCoordinator) calculateHealth() string {
|
||
activeNodes := rc.GetActiveNodes()
|
||
totalNodes := rc.GetAllNodes()
|
||
|
||
if len(totalNodes) == 0 {
|
||
return "critical"
|
||
}
|
||
|
||
ratio := float64(len(activeNodes)) / float64(len(totalNodes))
|
||
if ratio >= 0.8 {
|
||
return "healthy"
|
||
} else if ratio >= 0.5 {
|
||
return "degraded"
|
||
}
|
||
return "critical"
|
||
}
|
||
|
||
// GetReplicationFactor возвращает фактор репликации
|
||
func (rc *RaftCoordinator) GetReplicationFactor() int {
|
||
return int(rc.replicationFactor.Load())
|
||
}
|
||
|
||
// SetReplicationFactor устанавливает фактор репликации через Raft
|
||
func (rc *RaftCoordinator) SetReplicationFactor(factor int) error {
|
||
now := time.Now().UnixMilli()
|
||
|
||
if !rc.IsLeader() {
|
||
return fmt.Errorf("node is not the leader")
|
||
}
|
||
|
||
cmd := NodeRegistrationCommand{
|
||
Type: "set_replication_factor",
|
||
Factor: int32(factor),
|
||
Timestamp: now,
|
||
}
|
||
|
||
data, err := json.Marshal(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
future := rc.raft.Apply(data, rc.config.Replication.GetReplicationTimeout())
|
||
if err := future.Error(); err != nil {
|
||
return fmt.Errorf("failed to set replication factor via raft: %v", err)
|
||
}
|
||
|
||
rc.replicationFactor.Store(int32(factor))
|
||
rc.logger.Debug(fmt.Sprintf("Replication factor set to %d via Raft", factor))
|
||
return nil
|
||
}
|
||
|
||
// CreateShard создаёт новый шард
|
||
func (rc *RaftCoordinator) CreateShard(name string, nodes []string) (*ShardInfo, error) {
|
||
if !rc.IsLeader() {
|
||
return nil, fmt.Errorf("only leader can create shards")
|
||
}
|
||
|
||
return rc.shardManager.CreateShard(name, nodes), nil
|
||
}
|
||
|
||
// GetShardForCollection возвращает шард для коллекции
|
||
func (rc *RaftCoordinator) GetShardForCollection(database, collection string) *ShardInfo {
|
||
return rc.shardManager.GetShard(database, collection)
|
||
}
|
||
|
||
// GetAllShards возвращает все шарды
|
||
func (rc *RaftCoordinator) GetAllShards() []*ShardInfo {
|
||
return rc.shardManager.GetAllShards()
|
||
}
|
||
|
||
// GetPipelineStats возвращает статистику пайплайна
|
||
func (rc *RaftCoordinator) GetPipelineStats() map[string]interface{} {
|
||
if rc.pipelineReplicator == nil {
|
||
return map[string]interface{}{"enabled": false}
|
||
}
|
||
return rc.pipelineReplicator.GetStats()
|
||
}
|
||
|
||
// GetBatchCommitStats возвращает статистику пакетных коммитов
|
||
func (rc *RaftCoordinator) GetBatchCommitStats() map[string]interface{} {
|
||
if rc.batchCommitManager == nil {
|
||
return map[string]interface{}{"enabled": false}
|
||
}
|
||
return rc.batchCommitManager.GetStats()
|
||
}
|
||
|
||
// GetReshardingStats возвращает статистику решардинга
|
||
func (rc *RaftCoordinator) GetReshardingStats() map[string]interface{} {
|
||
if rc.reshardingManager == nil {
|
||
return map[string]interface{}{"enabled": false}
|
||
}
|
||
return rc.reshardingManager.GetMetrics()
|
||
}
|
||
|
||
// GetJointConsensusStatus возвращает статус совместного консенсуса
|
||
func (rc *RaftCoordinator) GetJointConsensusStatus() map[string]interface{} {
|
||
if rc.jointConsensusManager == nil {
|
||
return map[string]interface{}{"active": false}
|
||
}
|
||
return rc.jointConsensusManager.GetJointConsensusStatus()
|
||
}
|
||
|
||
// TriggerResharding запускает перераспределение шардов
|
||
func (rc *RaftCoordinator) TriggerResharding(reason string) error {
|
||
if rc.reshardingManager == nil {
|
||
return fmt.Errorf("resharding manager not initialized")
|
||
}
|
||
return rc.reshardingManager.TriggerResharding(reason)
|
||
}
|
||
|
||
// StartJointConsensus начинает совместный консенсус
|
||
func (rc *RaftCoordinator) StartJointConsensus(newServers []raft.Server) error {
|
||
if rc.jointConsensusManager == nil {
|
||
return fmt.Errorf("joint consensus manager not initialized")
|
||
}
|
||
return rc.jointConsensusManager.StartJointConsensus(newServers)
|
||
}
|
||
|
||
// CompleteJointConsensus завершает совместный консенсус
|
||
func (rc *RaftCoordinator) CompleteJointConsensus() error {
|
||
if rc.jointConsensusManager == nil {
|
||
return fmt.Errorf("joint consensus manager not initialized")
|
||
}
|
||
return rc.jointConsensusManager.CompleteJointConsensus()
|
||
}
|
||
|
||
// AbortJointConsensus прерывает совместный консенсус
|
||
func (rc *RaftCoordinator) AbortJointConsensus() error {
|
||
if rc.jointConsensusManager == nil {
|
||
return fmt.Errorf("joint consensus manager not initialized")
|
||
}
|
||
return rc.jointConsensusManager.AbortJointConsensus()
|
||
}
|
||
|
||
// GetFallbackManager возвращает менеджер fallback
|
||
func (rc *RaftCoordinator) GetFallbackManager() *LeaderFallbackManager {
|
||
return rc.fallbackManager
|
||
}
|
||
|
||
// GetPanicRecoveryManager возвращает менеджер восстановления после паник
|
||
func (rc *RaftCoordinator) GetPanicRecoveryManager() *PanicRecoveryManager {
|
||
return rc.panicRecoveryMgr
|
||
}
|
||
|
||
// GetSchemaMigrator возвращает мигратор схемы данных
|
||
func (rc *RaftCoordinator) GetSchemaMigrator() *migration.SchemaMigrator {
|
||
return rc.schemaMigrator
|
||
}
|
||
|
||
// GetPersistenceManager возвращает менеджер персистентности
|
||
func (rc *RaftCoordinator) GetPersistenceManager() *storage.PersistenceManager {
|
||
return rc.persistenceMgr
|
||
}
|
||
|
||
// SubmitWriteWithFallback отправляет запись с поддержкой fallback
|
||
func (rc *RaftCoordinator) SubmitWriteWithFallback(data []byte) error {
|
||
if rc.fallbackManager != nil {
|
||
return rc.fallbackManager.SubmitWrite(data)
|
||
}
|
||
|
||
if rc.IsLeader() {
|
||
future := rc.raft.Apply(data, 10*time.Second)
|
||
return future.Error()
|
||
}
|
||
return fmt.Errorf("not leader and fallback manager not available")
|
||
}
|
||
|
||
// GetFallbackStats возвращает статистику fallback менеджера
|
||
func (rc *RaftCoordinator) GetFallbackStats() map[string]interface{} {
|
||
if rc.fallbackManager != nil {
|
||
return rc.fallbackManager.GetStats()
|
||
}
|
||
return map[string]interface{}{"enabled": false}
|
||
}
|
||
|
||
// GetPanicRecoveryStats возвращает статистику восстановления после паник
|
||
func (rc *RaftCoordinator) GetPanicRecoveryStats() map[string]interface{} {
|
||
if rc.panicRecoveryMgr != nil {
|
||
return rc.panicRecoveryMgr.GetStats()
|
||
}
|
||
return map[string]interface{}{"enabled": false}
|
||
}
|
||
|
||
// GetMigrationStatus возвращает статус миграций схемы данных
|
||
func (rc *RaftCoordinator) GetMigrationStatus() *migration.MigrationStatus {
|
||
if rc.schemaMigrator != nil {
|
||
return rc.schemaMigrator.GetStatus()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Stop останавливает координатор
|
||
func (rc *RaftCoordinator) Stop() {
|
||
now := time.Now().UnixMilli()
|
||
|
||
rc.logger.Info("Stopping Raft coordinator...")
|
||
|
||
if rc.pipelineReplicator != nil {
|
||
rc.pipelineReplicator.Stop()
|
||
rc.logger.Debug("Pipeline replicator stopped")
|
||
}
|
||
if rc.batchCommitManager != nil {
|
||
rc.batchCommitManager.Stop()
|
||
rc.logger.Debug("Batch commit manager stopped")
|
||
}
|
||
if rc.reshardingManager != nil {
|
||
rc.reshardingManager.Stop()
|
||
rc.logger.Debug("Resharding manager stopped")
|
||
}
|
||
if rc.recoveryManager != nil {
|
||
rc.recoveryManager.Stop()
|
||
rc.logger.Debug("Recovery manager stopped")
|
||
}
|
||
if rc.persistenceMgr != nil {
|
||
rc.persistenceMgr.Stop()
|
||
rc.logger.Debug("Persistence manager stopped")
|
||
}
|
||
|
||
if rc.fallbackManager != nil {
|
||
rc.fallbackManager.Stop()
|
||
rc.logger.Debug("Fallback manager stopped")
|
||
}
|
||
if rc.panicRecoveryMgr != nil {
|
||
rc.panicRecoveryMgr.Stop()
|
||
rc.logger.Debug("Panic recovery manager stopped")
|
||
}
|
||
|
||
close(rc.stopChan)
|
||
if rc.raft != nil {
|
||
rc.raft.Shutdown()
|
||
}
|
||
|
||
rc.logger.Info(fmt.Sprintf("Raft coordinator stopped at %s", time.UnixMilli(now).Format("2006-01-02 15:04:05.000")))
|
||
}
|
||
|
||
// IsReplicationEnabled возвращает статус репликации
|
||
func (rc *RaftCoordinator) IsReplicationEnabled() bool {
|
||
return rc.replicationEnabled
|
||
}
|
||
|
||
// IsMasterMasterEnabled возвращает статус мастер-мастер репликации
|
||
func (rc *RaftCoordinator) IsMasterMasterEnabled() bool {
|
||
return rc.masterMasterEnabled
|
||
}
|
||
|
||
// IsSyncReplicationEnabled возвращает статус синхронной репликации
|
||
func (rc *RaftCoordinator) IsSyncReplicationEnabled() bool {
|
||
return rc.syncReplication
|
||
}
|