3457 lines
108 KiB
Go
3457 lines
108 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 консенсус-алгоритма.
|
|||
|
|
|
|||
|
|
package cluster
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"sort"
|
|||
|
|
"sync"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/hashicorp/raft"
|
|||
|
|
"futriis/internal/config"
|
|||
|
|
"futriis/internal/log"
|
|||
|
|
"futriis/internal/migration"
|
|||
|
|
"futriis/internal/storage"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// ДИАПАЗОННЫЕ ШАРДЫ (RANGE SHARDS)
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// RangeShard представляет шард на основе диапазона ключей.
|
|||
|
|
type RangeShard struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
StartKey string `json:"start_key"`
|
|||
|
|
EndKey string `json:"end_key"`
|
|||
|
|
Nodes []string `json:"nodes"`
|
|||
|
|
LeaderNode string `json:"leader_node"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
CreatedAt int64 `json:"created_at"`
|
|||
|
|
UpdatedAt int64 `json:"updated_at"`
|
|||
|
|
LastRebalanced int64 `json:"last_rebalanced"`
|
|||
|
|
DocumentCount int64 `json:"document_count"`
|
|||
|
|
SizeBytes int64 `json:"size_bytes"`
|
|||
|
|
IsSplitting bool `json:"is_splitting"`
|
|||
|
|
IsMerging bool `json:"is_merging"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RangeShardManager управляет диапазонными шардами с динамическим сплитом/мерджем.
|
|||
|
|
type RangeShardManager struct {
|
|||
|
|
shardsPtr atomic.Value
|
|||
|
|
sortedShards atomic.Value
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
logger LoggerInterface
|
|||
|
|
shardSizeThreshold int64
|
|||
|
|
shardCountThreshold int64
|
|||
|
|
minShardSize int64
|
|||
|
|
rebalancing atomic.Bool
|
|||
|
|
splitMgr *DynamicSplitManager
|
|||
|
|
mergeMgr *DynamicMergeManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DynamicSplitManager управляет динамическим разделением шардов.
|
|||
|
|
type DynamicSplitManager struct {
|
|||
|
|
shardManager *RangeShardManager
|
|||
|
|
logger LoggerInterface
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
checkInterval time.Duration
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
splittingShards map[string]bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DynamicMergeManager управляет динамическим объединением шардов.
|
|||
|
|
type DynamicMergeManager struct {
|
|||
|
|
shardManager *RangeShardManager
|
|||
|
|
logger LoggerInterface
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
checkInterval time.Duration
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
mergingShards map[string]bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewRangeShardManager создаёт новый менеджер диапазонных шардов.
|
|||
|
|
func NewRangeShardManager(logger LoggerInterface) *RangeShardManager {
|
|||
|
|
rsm := &RangeShardManager{
|
|||
|
|
logger: logger,
|
|||
|
|
shardSizeThreshold: 100 * 1024 * 1024,
|
|||
|
|
shardCountThreshold: 1000000,
|
|||
|
|
minShardSize: 10 * 1024 * 1024,
|
|||
|
|
}
|
|||
|
|
rsm.shardsPtr.Store(make(map[string]*RangeShard))
|
|||
|
|
rsm.sortedShards.Store(make([]*RangeShard, 0))
|
|||
|
|
|
|||
|
|
rsm.splitMgr = NewDynamicSplitManager(rsm, logger)
|
|||
|
|
rsm.mergeMgr = NewDynamicMergeManager(rsm, logger)
|
|||
|
|
|
|||
|
|
return rsm
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewDynamicSplitManager создаёт менеджер разделения шардов.
|
|||
|
|
func NewDynamicSplitManager(shardManager *RangeShardManager, logger LoggerInterface) *DynamicSplitManager {
|
|||
|
|
return &DynamicSplitManager{
|
|||
|
|
shardManager: shardManager,
|
|||
|
|
logger: logger,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
checkInterval: 30 * time.Second,
|
|||
|
|
splittingShards: make(map[string]bool),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewDynamicMergeManager создаёт менеджер объединения шардов.
|
|||
|
|
func NewDynamicMergeManager(shardManager *RangeShardManager, logger LoggerInterface) *DynamicMergeManager {
|
|||
|
|
return &DynamicMergeManager{
|
|||
|
|
shardManager: shardManager,
|
|||
|
|
logger: logger,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
checkInterval: 60 * time.Second,
|
|||
|
|
mergingShards: make(map[string]bool),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает мониторинг шардов.
|
|||
|
|
func (rsm *RangeShardManager) Start() {
|
|||
|
|
if rsm.splitMgr != nil {
|
|||
|
|
go rsm.splitMgr.Start()
|
|||
|
|
}
|
|||
|
|
if rsm.mergeMgr != nil {
|
|||
|
|
go rsm.mergeMgr.Start()
|
|||
|
|
}
|
|||
|
|
if rsm.logger != nil {
|
|||
|
|
rsm.logger.Info("Range shard manager started")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает менеджер шардов.
|
|||
|
|
func (rsm *RangeShardManager) Stop() {
|
|||
|
|
if rsm.splitMgr != nil {
|
|||
|
|
rsm.splitMgr.Stop()
|
|||
|
|
}
|
|||
|
|
if rsm.mergeMgr != nil {
|
|||
|
|
rsm.mergeMgr.Stop()
|
|||
|
|
}
|
|||
|
|
if rsm.logger != nil {
|
|||
|
|
rsm.logger.Info("Range shard manager stopped")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает мониторинг разделения.
|
|||
|
|
func (dsm *DynamicSplitManager) Start() {
|
|||
|
|
dsm.wg.Add(1)
|
|||
|
|
go dsm.splitMonitor()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает мониторинг.
|
|||
|
|
func (dsm *DynamicSplitManager) Stop() {
|
|||
|
|
close(dsm.stopChan)
|
|||
|
|
dsm.wg.Wait()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// splitMonitor периодически проверяет шарды на необходимость разделения.
|
|||
|
|
func (dsm *DynamicSplitManager) splitMonitor() {
|
|||
|
|
defer dsm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(dsm.checkInterval)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-dsm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
dsm.checkAndSplit()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// checkAndSplit проверяет и выполняет разделение шардов.
|
|||
|
|
func (dsm *DynamicSplitManager) checkAndSplit() {
|
|||
|
|
shards := dsm.shardManager.GetAllShards()
|
|||
|
|
|
|||
|
|
for _, shard := range shards {
|
|||
|
|
if shard.IsSplitting || shard.IsMerging {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if shard.SizeBytes > dsm.shardManager.shardSizeThreshold ||
|
|||
|
|
shard.DocumentCount > dsm.shardManager.shardCountThreshold {
|
|||
|
|
dsm.splitShard(shard)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// splitShard выполняет разделение шарда на два.
|
|||
|
|
func (dsm *DynamicSplitManager) splitShard(shard *RangeShard) error {
|
|||
|
|
dsm.mu.Lock()
|
|||
|
|
if dsm.splittingShards[shard.ID] {
|
|||
|
|
dsm.mu.Unlock()
|
|||
|
|
return fmt.Errorf("shard %s is already splitting", shard.ID)
|
|||
|
|
}
|
|||
|
|
dsm.splittingShards[shard.ID] = true
|
|||
|
|
dsm.mu.Unlock()
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
dsm.mu.Lock()
|
|||
|
|
delete(dsm.splittingShards, shard.ID)
|
|||
|
|
dsm.mu.Unlock()
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
if dsm.logger != nil {
|
|||
|
|
dsm.logger.Info(fmt.Sprintf("Splitting shard %s (size: %d bytes, docs: %d)",
|
|||
|
|
shard.Name, shard.SizeBytes, shard.DocumentCount))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
splitKey := dsm.findSplitKey(shard)
|
|||
|
|
if splitKey == "" {
|
|||
|
|
return fmt.Errorf("failed to find split key for shard %s", shard.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shard1 := &RangeShard{
|
|||
|
|
ID: fmt.Sprintf("%s_left", shard.ID),
|
|||
|
|
Name: fmt.Sprintf("%s_left", shard.Name),
|
|||
|
|
StartKey: shard.StartKey,
|
|||
|
|
EndKey: splitKey,
|
|||
|
|
Nodes: shard.Nodes,
|
|||
|
|
LeaderNode: shard.LeaderNode,
|
|||
|
|
Status: "active",
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
LastRebalanced: time.Now().UnixMilli(),
|
|||
|
|
DocumentCount: shard.DocumentCount / 2,
|
|||
|
|
SizeBytes: shard.SizeBytes / 2,
|
|||
|
|
IsSplitting: false,
|
|||
|
|
IsMerging: false,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shard2 := &RangeShard{
|
|||
|
|
ID: fmt.Sprintf("%s_right", shard.ID),
|
|||
|
|
Name: fmt.Sprintf("%s_right", shard.Name),
|
|||
|
|
StartKey: splitKey,
|
|||
|
|
EndKey: shard.EndKey,
|
|||
|
|
Nodes: shard.Nodes,
|
|||
|
|
LeaderNode: shard.LeaderNode,
|
|||
|
|
Status: "active",
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
LastRebalanced: time.Now().UnixMilli(),
|
|||
|
|
DocumentCount: shard.DocumentCount / 2,
|
|||
|
|
SizeBytes: shard.SizeBytes / 2,
|
|||
|
|
IsSplitting: false,
|
|||
|
|
IsMerging: false,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
dsm.shardManager.mu.Lock()
|
|||
|
|
defer dsm.shardManager.mu.Unlock()
|
|||
|
|
|
|||
|
|
oldShards := dsm.shardManager.loadShards()
|
|||
|
|
newShards := make(map[string]*RangeShard)
|
|||
|
|
for k, v := range oldShards {
|
|||
|
|
if k != shard.ID {
|
|||
|
|
newShards[k] = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
newShards[shard1.ID] = shard1
|
|||
|
|
newShards[shard2.ID] = shard2
|
|||
|
|
|
|||
|
|
dsm.shardManager.shardsPtr.Store(newShards)
|
|||
|
|
dsm.shardManager.updateSortedShards()
|
|||
|
|
|
|||
|
|
if dsm.logger != nil {
|
|||
|
|
dsm.logger.Info(fmt.Sprintf("Shard %s split into %s and %s", shard.Name, shard1.Name, shard2.Name))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// findSplitKey находит ключ для разделения шарда.
|
|||
|
|
func (dsm *DynamicSplitManager) findSplitKey(shard *RangeShard) string {
|
|||
|
|
start := []byte(shard.StartKey)
|
|||
|
|
end := []byte(shard.EndKey)
|
|||
|
|
|
|||
|
|
if len(start) == 0 || len(end) == 0 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mid := make([]byte, len(start))
|
|||
|
|
for i := range start {
|
|||
|
|
if i < len(end) {
|
|||
|
|
mid[i] = (start[i] + end[i]) / 2
|
|||
|
|
} else {
|
|||
|
|
mid[i] = start[i]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return string(mid)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает мониторинг объединения.
|
|||
|
|
func (dmm *DynamicMergeManager) Start() {
|
|||
|
|
dmm.wg.Add(1)
|
|||
|
|
go dmm.mergeMonitor()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает мониторинг.
|
|||
|
|
func (dmm *DynamicMergeManager) Stop() {
|
|||
|
|
close(dmm.stopChan)
|
|||
|
|
dmm.wg.Wait()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// mergeMonitor периодически проверяет шарды на возможность объединения.
|
|||
|
|
func (dmm *DynamicMergeManager) mergeMonitor() {
|
|||
|
|
defer dmm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(dmm.checkInterval)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-dmm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
dmm.checkAndMerge()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// checkAndMerge проверяет и выполняет объединение шардов.
|
|||
|
|
func (dmm *DynamicMergeManager) checkAndMerge() {
|
|||
|
|
shards := dmm.shardManager.GetSortedShards()
|
|||
|
|
|
|||
|
|
for i := 0; i < len(shards)-1; i++ {
|
|||
|
|
shard1 := shards[i]
|
|||
|
|
shard2 := shards[i+1]
|
|||
|
|
|
|||
|
|
if shard1.IsSplitting || shard1.IsMerging || shard2.IsSplitting || shard2.IsMerging {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if shard1.EndKey != shard2.StartKey {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
totalSize := shard1.SizeBytes + shard2.SizeBytes
|
|||
|
|
totalDocs := shard1.DocumentCount + shard2.DocumentCount
|
|||
|
|
|
|||
|
|
if totalSize < dmm.shardManager.minShardSize && totalDocs < dmm.shardManager.shardCountThreshold/10 {
|
|||
|
|
dmm.mergeShards(shard1, shard2)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// mergeShards объединяет два смежных шарда.
|
|||
|
|
func (dmm *DynamicMergeManager) mergeShards(shard1, shard2 *RangeShard) error {
|
|||
|
|
dmm.mu.Lock()
|
|||
|
|
if dmm.mergingShards[shard1.ID] || dmm.mergingShards[shard2.ID] {
|
|||
|
|
dmm.mu.Unlock()
|
|||
|
|
return fmt.Errorf("shards are already merging")
|
|||
|
|
}
|
|||
|
|
dmm.mergingShards[shard1.ID] = true
|
|||
|
|
dmm.mergingShards[shard2.ID] = true
|
|||
|
|
dmm.mu.Unlock()
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
dmm.mu.Lock()
|
|||
|
|
delete(dmm.mergingShards, shard1.ID)
|
|||
|
|
delete(dmm.mergingShards, shard2.ID)
|
|||
|
|
dmm.mu.Unlock()
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
if dmm.logger != nil {
|
|||
|
|
dmm.logger.Info(fmt.Sprintf("Merging shards %s and %s", shard1.Name, shard2.Name))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mergedShard := &RangeShard{
|
|||
|
|
ID: fmt.Sprintf("%s_merged", shard1.ID),
|
|||
|
|
Name: fmt.Sprintf("%s_merged", shard1.Name),
|
|||
|
|
StartKey: shard1.StartKey,
|
|||
|
|
EndKey: shard2.EndKey,
|
|||
|
|
Nodes: shard1.Nodes,
|
|||
|
|
LeaderNode: shard1.LeaderNode,
|
|||
|
|
Status: "active",
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
LastRebalanced: time.Now().UnixMilli(),
|
|||
|
|
DocumentCount: shard1.DocumentCount + shard2.DocumentCount,
|
|||
|
|
SizeBytes: shard1.SizeBytes + shard2.SizeBytes,
|
|||
|
|
IsSplitting: false,
|
|||
|
|
IsMerging: false,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
dmm.shardManager.mu.Lock()
|
|||
|
|
defer dmm.shardManager.mu.Unlock()
|
|||
|
|
|
|||
|
|
oldShards := dmm.shardManager.loadShards()
|
|||
|
|
newShards := make(map[string]*RangeShard)
|
|||
|
|
for k, v := range oldShards {
|
|||
|
|
if k != shard1.ID && k != shard2.ID {
|
|||
|
|
newShards[k] = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
newShards[mergedShard.ID] = mergedShard
|
|||
|
|
|
|||
|
|
dmm.shardManager.shardsPtr.Store(newShards)
|
|||
|
|
dmm.shardManager.updateSortedShards()
|
|||
|
|
|
|||
|
|
if dmm.logger != nil {
|
|||
|
|
dmm.logger.Info(fmt.Sprintf("Merged %s and %s into %s", shard1.Name, shard2.Name, mergedShard.Name))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadShards загружает карту шардов.
|
|||
|
|
func (rsm *RangeShardManager) loadShards() map[string]*RangeShard {
|
|||
|
|
val := rsm.shardsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]*RangeShard)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]*RangeShard)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// updateSortedShards обновляет отсортированный список шардов.
|
|||
|
|
func (rsm *RangeShardManager) updateSortedShards() {
|
|||
|
|
shards := rsm.loadShards()
|
|||
|
|
sorted := make([]*RangeShard, 0, len(shards))
|
|||
|
|
for _, sh := range shards {
|
|||
|
|
sorted = append(sorted, sh)
|
|||
|
|
}
|
|||
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|||
|
|
return sorted[i].StartKey < sorted[j].StartKey
|
|||
|
|
})
|
|||
|
|
rsm.sortedShards.Store(sorted)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetShard возвращает шард для ключа.
|
|||
|
|
func (rsm *RangeShardManager) GetShard(key string) *RangeShard {
|
|||
|
|
shards := rsm.getSortedShards()
|
|||
|
|
for _, shard := range shards {
|
|||
|
|
if key >= shard.StartKey && (shard.EndKey == "" || key < shard.EndKey) {
|
|||
|
|
return shard
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getSortedShards возвращает отсортированный список шардов.
|
|||
|
|
func (rsm *RangeShardManager) getSortedShards() []*RangeShard {
|
|||
|
|
val := rsm.sortedShards.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make([]*RangeShard, 0)
|
|||
|
|
}
|
|||
|
|
return val.([]*RangeShard)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAllShards возвращает все шарды.
|
|||
|
|
func (rsm *RangeShardManager) GetAllShards() []*RangeShard {
|
|||
|
|
shards := rsm.loadShards()
|
|||
|
|
result := make([]*RangeShard, 0, len(shards))
|
|||
|
|
for _, shard := range shards {
|
|||
|
|
result = append(result, shard)
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetSortedShards возвращает отсортированные шарды.
|
|||
|
|
func (rsm *RangeShardManager) GetSortedShards() []*RangeShard {
|
|||
|
|
return rsm.getSortedShards()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetShardByID возвращает шард по ID.
|
|||
|
|
func (rsm *RangeShardManager) GetShardByID(shardID string) *RangeShard {
|
|||
|
|
shards := rsm.loadShards()
|
|||
|
|
if shard, ok := shards[shardID]; ok {
|
|||
|
|
return shard
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AddNode добавляет узел в шарды.
|
|||
|
|
func (rsm *RangeShardManager) AddNode(nodeID string) {
|
|||
|
|
rsm.Rebalance()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RemoveNode удаляет узел из шардов.
|
|||
|
|
func (rsm *RangeShardManager) RemoveNode(nodeID string) {
|
|||
|
|
rsm.Rebalance()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Rebalance выполняет ребалансировку шардов.
|
|||
|
|
func (rsm *RangeShardManager) Rebalance() error {
|
|||
|
|
if !rsm.rebalancing.CompareAndSwap(false, true) {
|
|||
|
|
return fmt.Errorf("rebalancing already in progress")
|
|||
|
|
}
|
|||
|
|
defer rsm.rebalancing.Store(false)
|
|||
|
|
|
|||
|
|
if rsm.logger != nil {
|
|||
|
|
rsm.logger.Info("Starting range shard rebalancing...")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shards := rsm.GetAllShards()
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
rsm.mu.Lock()
|
|||
|
|
defer rsm.mu.Unlock()
|
|||
|
|
|
|||
|
|
oldShards := rsm.loadShards()
|
|||
|
|
newShards := make(map[string]*RangeShard)
|
|||
|
|
|
|||
|
|
for id, shard := range oldShards {
|
|||
|
|
shardCopy := *shard
|
|||
|
|
shardCopy.LastRebalanced = now
|
|||
|
|
shardCopy.UpdatedAt = now
|
|||
|
|
newShards[id] = &shardCopy
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rsm.shardsPtr.Store(newShards)
|
|||
|
|
rsm.updateSortedShards()
|
|||
|
|
|
|||
|
|
if rsm.logger != nil {
|
|||
|
|
rsm.logger.Info(fmt.Sprintf("Range shard rebalancing completed: %d shards", len(shards)))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// MULTI-RAFT
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// MultiRaftManager управляет несколькими Raft-группами для параллельной записи.
|
|||
|
|
type MultiRaftManager struct {
|
|||
|
|
raftGroups sync.Map
|
|||
|
|
groupConfigs sync.Map
|
|||
|
|
logger LoggerInterface
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
storage *storage.Storage
|
|||
|
|
baseConfig *raft.Config
|
|||
|
|
transport *raft.NetworkTransport
|
|||
|
|
snapshotStore raft.SnapshotStore
|
|||
|
|
logStore raft.LogStore
|
|||
|
|
stableStore raft.StableStore
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MultiRaftGroupConfig конфигурация группы Raft.
|
|||
|
|
type MultiRaftGroupConfig struct {
|
|||
|
|
GroupID string
|
|||
|
|
ShardID string
|
|||
|
|
Nodes []string
|
|||
|
|
LeaderID string
|
|||
|
|
Term uint64
|
|||
|
|
CreatedAt int64
|
|||
|
|
UpdatedAt int64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewMultiRaftManager создаёт новый менеджер Multi-Raft.
|
|||
|
|
func NewMultiRaftManager(storage *storage.Storage, logger LoggerInterface) *MultiRaftManager {
|
|||
|
|
return &MultiRaftManager{
|
|||
|
|
logger: logger,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
storage: storage,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetOrCreateRaftGroup получает или создаёт группу Raft для шарда.
|
|||
|
|
func (mrm *MultiRaftManager) GetOrCreateRaftGroup(shardID string, nodes []string) (*raft.Raft, error) {
|
|||
|
|
if val, ok := mrm.raftGroups.Load(shardID); ok {
|
|||
|
|
return val.(*raft.Raft), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mrm.mu.Lock()
|
|||
|
|
defer mrm.mu.Unlock()
|
|||
|
|
|
|||
|
|
if val, ok := mrm.raftGroups.Load(shardID); ok {
|
|||
|
|
return val.(*raft.Raft), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
groupID := fmt.Sprintf("shard_%s", shardID)
|
|||
|
|
|
|||
|
|
raftConfig := raft.DefaultConfig()
|
|||
|
|
raftConfig.LocalID = raft.ServerID(groupID)
|
|||
|
|
raftConfig.HeartbeatTimeout = 1 * time.Second
|
|||
|
|
raftConfig.ElectionTimeout = 1 * time.Second
|
|||
|
|
raftConfig.CommitTimeout = 500 * time.Millisecond
|
|||
|
|
raftConfig.SnapshotInterval = 30 * time.Second
|
|||
|
|
raftConfig.SnapshotThreshold = 1000
|
|||
|
|
|
|||
|
|
dataDir := filepath.Join("raft_data", groupID)
|
|||
|
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to create raft dir: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logStore := storage.NewInmemStore(filepath.Join(dataDir, "raft-log.json"))
|
|||
|
|
stableStore := storage.NewInmemStore(filepath.Join(dataDir, "raft-stable.json"))
|
|||
|
|
snapshotStore, err := raft.NewFileSnapshotStore(dataDir, 3, os.Stderr)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to create snapshot store: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fsm := &MultiRaftFSM{
|
|||
|
|
shardID: shardID,
|
|||
|
|
storage: mrm.storage,
|
|||
|
|
logger: mrm.logger,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addr := fmt.Sprintf("127.0.0.1:%d", 9000+len(groupID))
|
|||
|
|
transport, err := raft.NewTCPTransport(addr, 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, fsm, logStore, stableStore, snapshotStore, transport)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to create raft: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(nodes) > 0 {
|
|||
|
|
servers := make([]raft.Server, len(nodes))
|
|||
|
|
for i, nodeAddr := range nodes {
|
|||
|
|
servers[i] = raft.Server{
|
|||
|
|
ID: raft.ServerID(fmt.Sprintf("%s-node-%d", groupID, i)),
|
|||
|
|
Address: raft.ServerAddress(nodeAddr),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
configuration := raft.Configuration{Servers: servers}
|
|||
|
|
r.BootstrapCluster(configuration)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mrm.raftGroups.Store(shardID, r)
|
|||
|
|
|
|||
|
|
if mrm.logger != nil {
|
|||
|
|
mrm.logger.Info(fmt.Sprintf("Created Multi-Raft group for shard %s", shardID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return r, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MultiRaftFSM реализует конечный автомат для Multi-Raft группы.
|
|||
|
|
type MultiRaftFSM struct {
|
|||
|
|
shardID string
|
|||
|
|
storage *storage.Storage
|
|||
|
|
logger LoggerInterface
|
|||
|
|
state map[string]interface{}
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Apply применяет команду к FSM.
|
|||
|
|
func (f *MultiRaftFSM) Apply(log *raft.Log) interface{} {
|
|||
|
|
var cmd map[string]interface{}
|
|||
|
|
if err := json.Unmarshal(log.Data, &cmd); err != nil {
|
|||
|
|
if f.logger != nil {
|
|||
|
|
f.logger.Error(fmt.Sprintf("Failed to unmarshal command: %v", err))
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
f.mu.Lock()
|
|||
|
|
defer f.mu.Unlock()
|
|||
|
|
|
|||
|
|
opType, _ := cmd["type"].(string)
|
|||
|
|
switch opType {
|
|||
|
|
case "write":
|
|||
|
|
database, _ := cmd["database"].(string)
|
|||
|
|
collection, _ := cmd["collection"].(string)
|
|||
|
|
docData, _ := cmd["document"].(map[string]interface{})
|
|||
|
|
|
|||
|
|
db, err := f.storage.GetDatabase(database)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
coll, err := db.GetCollection(collection)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc := storage.NewDocument()
|
|||
|
|
for k, v := range docData {
|
|||
|
|
doc.SetField(k, v)
|
|||
|
|
}
|
|||
|
|
return coll.Insert(doc)
|
|||
|
|
|
|||
|
|
case "delete":
|
|||
|
|
database, _ := cmd["database"].(string)
|
|||
|
|
collection, _ := cmd["collection"].(string)
|
|||
|
|
docID, _ := cmd["document_id"].(string)
|
|||
|
|
|
|||
|
|
db, err := f.storage.GetDatabase(database)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
coll, err := db.GetCollection(collection)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return coll.Delete(docID)
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
if f.logger != nil {
|
|||
|
|
f.logger.Warn(fmt.Sprintf("Unknown operation type: %s", opType))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Snapshot создаёт снапшот состояния FSM.
|
|||
|
|
func (f *MultiRaftFSM) Snapshot() (raft.FSMSnapshot, error) {
|
|||
|
|
f.mu.RLock()
|
|||
|
|
defer f.mu.RUnlock()
|
|||
|
|
|
|||
|
|
snapshot := &MultiRaftSnapshot{
|
|||
|
|
state: f.state,
|
|||
|
|
}
|
|||
|
|
return snapshot, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Restore восстанавливает состояние FSM из снапшота.
|
|||
|
|
func (f *MultiRaftFSM) Restore(snapshot io.ReadCloser) error {
|
|||
|
|
defer snapshot.Close()
|
|||
|
|
|
|||
|
|
var state map[string]interface{}
|
|||
|
|
decoder := json.NewDecoder(snapshot)
|
|||
|
|
if err := decoder.Decode(&state); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
f.mu.Lock()
|
|||
|
|
defer f.mu.Unlock()
|
|||
|
|
f.state = state
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MultiRaftSnapshot реализует снапшот для Multi-Raft.
|
|||
|
|
type MultiRaftSnapshot struct {
|
|||
|
|
state map[string]interface{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Persist сохраняет снапшот.
|
|||
|
|
func (s *MultiRaftSnapshot) Persist(sink raft.SnapshotSink) error {
|
|||
|
|
data, err := json.Marshal(s.state)
|
|||
|
|
if err != nil {
|
|||
|
|
sink.Cancel()
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if _, err := sink.Write(data); err != nil {
|
|||
|
|
sink.Cancel()
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sink.Close()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Release освобождает ресурсы.
|
|||
|
|
func (s *MultiRaftSnapshot) Release() {}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// SAGA - РАСПРЕДЕЛЁННЫЕ ТРАНЗАКЦИИ С КОМПЕНСАЦИЕЙ
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// SagaStep представляет шаг в Saga транзакции.
|
|||
|
|
type SagaStep struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Execute func() error `json:"-"`
|
|||
|
|
Compensate func() error `json:"-"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
Data map[string]interface{} `json:"data"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SagaTransaction представляет Saga транзакцию.
|
|||
|
|
type SagaTransaction struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Steps []*SagaStep `json:"steps"`
|
|||
|
|
CurrentStep int `json:"current_step"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
CreatedAt int64 `json:"created_at"`
|
|||
|
|
UpdatedAt int64 `json:"updated_at"`
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SagaManager управляет Saga транзакциями.
|
|||
|
|
type SagaManager struct {
|
|||
|
|
sagas sync.Map
|
|||
|
|
logger LoggerInterface
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
maxRetries int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewSagaManager создаёт новый менеджер Saga.
|
|||
|
|
func NewSagaManager(logger LoggerInterface) *SagaManager {
|
|||
|
|
return &SagaManager{
|
|||
|
|
logger: logger,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
maxRetries: 3,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BeginSaga начинает новую Saga транзакцию.
|
|||
|
|
func (sm *SagaManager) BeginSaga(id string) *SagaTransaction {
|
|||
|
|
saga := &SagaTransaction{
|
|||
|
|
ID: id,
|
|||
|
|
Steps: make([]*SagaStep, 0),
|
|||
|
|
CurrentStep: 0,
|
|||
|
|
Status: "pending",
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
}
|
|||
|
|
sm.sagas.Store(id, saga)
|
|||
|
|
return saga
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AddStep добавляет шаг в Saga транзакцию.
|
|||
|
|
func (s *SagaTransaction) AddStep(name string, execute, compensate func() error, data map[string]interface{}) *SagaTransaction {
|
|||
|
|
s.mu.Lock()
|
|||
|
|
defer s.mu.Unlock()
|
|||
|
|
|
|||
|
|
step := &SagaStep{
|
|||
|
|
ID: fmt.Sprintf("%s_step_%d", s.ID, len(s.Steps)),
|
|||
|
|
Name: name,
|
|||
|
|
Execute: execute,
|
|||
|
|
Compensate: compensate,
|
|||
|
|
Status: "pending",
|
|||
|
|
Data: data,
|
|||
|
|
}
|
|||
|
|
s.Steps = append(s.Steps, step)
|
|||
|
|
return s
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Execute выполняет Saga транзакцию.
|
|||
|
|
func (sm *SagaManager) Execute(saga *SagaTransaction) error {
|
|||
|
|
saga.mu.Lock()
|
|||
|
|
defer saga.mu.Unlock()
|
|||
|
|
|
|||
|
|
if saga.Status != "pending" {
|
|||
|
|
return fmt.Errorf("saga %s is not in pending state", saga.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saga.Status = "running"
|
|||
|
|
saga.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
for i, step := range saga.Steps {
|
|||
|
|
saga.CurrentStep = i
|
|||
|
|
step.Status = "running"
|
|||
|
|
|
|||
|
|
if sm.logger != nil {
|
|||
|
|
sm.logger.Debug(fmt.Sprintf("Executing saga step %s: %s", saga.ID, step.Name))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var err error
|
|||
|
|
for retry := 0; retry < sm.maxRetries; retry++ {
|
|||
|
|
if err = step.Execute(); err == nil {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
if sm.logger != nil {
|
|||
|
|
sm.logger.Warn(fmt.Sprintf("Saga step %s failed (attempt %d/%d): %v", step.Name, retry+1, sm.maxRetries, err))
|
|||
|
|
}
|
|||
|
|
time.Sleep(time.Duration(100*(retry+1)) * time.Millisecond)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
step.Status = "failed"
|
|||
|
|
saga.Status = "compensating"
|
|||
|
|
saga.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
if sm.logger != nil {
|
|||
|
|
sm.logger.Error(fmt.Sprintf("Saga step %s failed, starting compensation", step.Name))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for j := i; j >= 0; j-- {
|
|||
|
|
prevStep := saga.Steps[j]
|
|||
|
|
if prevStep.Status == "compensated" || prevStep.Status == "pending" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if err := prevStep.Compensate(); err != nil {
|
|||
|
|
if sm.logger != nil {
|
|||
|
|
sm.logger.Error(fmt.Sprintf("Compensation for step %s failed: %v", prevStep.Name, err))
|
|||
|
|
}
|
|||
|
|
prevStep.Status = "compensation_failed"
|
|||
|
|
} else {
|
|||
|
|
prevStep.Status = "compensated"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saga.Status = "aborted"
|
|||
|
|
saga.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
return fmt.Errorf("saga %s aborted at step %s: %v", saga.ID, step.Name, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
step.Status = "completed"
|
|||
|
|
saga.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saga.Status = "completed"
|
|||
|
|
saga.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
if sm.logger != nil {
|
|||
|
|
sm.logger.Info(fmt.Sprintf("Saga %s completed successfully", saga.ID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetSaga возвращает Saga по ID.
|
|||
|
|
func (sm *SagaManager) GetSaga(id string) (*SagaTransaction, error) {
|
|||
|
|
if val, ok := sm.sagas.Load(id); ok {
|
|||
|
|
return val.(*SagaTransaction), nil
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("saga %s not found", id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetSagaStatus возвращает статус Saga.
|
|||
|
|
func (sm *SagaManager) GetSagaStatus(id string) (string, error) {
|
|||
|
|
saga, err := sm.GetSaga(id)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
saga.mu.RLock()
|
|||
|
|
defer saga.mu.RUnlock()
|
|||
|
|
return saga.Status, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// TCC (TRY-CONFIRM-CANCEL)
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// TCCTransaction представляет TCC транзакцию.
|
|||
|
|
type TCCTransaction struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
TryData map[string]interface{} `json:"try_data"`
|
|||
|
|
ConfirmFn func() error `json:"-"`
|
|||
|
|
CancelFn func() error `json:"-"`
|
|||
|
|
CreatedAt int64 `json:"created_at"`
|
|||
|
|
UpdatedAt int64 `json:"updated_at"`
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TCCManager управляет TCC транзакциями.
|
|||
|
|
type TCCManager struct {
|
|||
|
|
transactions sync.Map
|
|||
|
|
logger LoggerInterface
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
timeout time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewTCCManager создаёт новый менеджер TCC.
|
|||
|
|
func NewTCCManager(logger LoggerInterface) *TCCManager {
|
|||
|
|
return &TCCManager{
|
|||
|
|
logger: logger,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
timeout: 30 * time.Second,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BeginTCC начинает TCC транзакцию.
|
|||
|
|
func (tm *TCCManager) BeginTCC(id string) *TCCTransaction {
|
|||
|
|
tcc := &TCCTransaction{
|
|||
|
|
ID: id,
|
|||
|
|
Status: "try",
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
}
|
|||
|
|
tm.transactions.Store(id, tcc)
|
|||
|
|
return tcc
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Try выполняет фазу Try в TCC.
|
|||
|
|
func (t *TCCTransaction) Try(data map[string]interface{}) error {
|
|||
|
|
t.mu.Lock()
|
|||
|
|
defer t.mu.Unlock()
|
|||
|
|
|
|||
|
|
if t.Status != "try" {
|
|||
|
|
return fmt.Errorf("TCC %s is not in try phase", t.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.TryData = data
|
|||
|
|
t.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Confirm выполняет фазу Confirm в TCC.
|
|||
|
|
func (t *TCCTransaction) Confirm() error {
|
|||
|
|
t.mu.Lock()
|
|||
|
|
defer t.mu.Unlock()
|
|||
|
|
|
|||
|
|
if t.Status != "try" {
|
|||
|
|
return fmt.Errorf("TCC %s is not in try phase", t.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if t.ConfirmFn == nil {
|
|||
|
|
return fmt.Errorf("TCC %s has no confirm function", t.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Status = "confirming"
|
|||
|
|
t.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
if err := t.ConfirmFn(); err != nil {
|
|||
|
|
t.Status = "failed"
|
|||
|
|
return fmt.Errorf("confirm failed: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Status = "confirmed"
|
|||
|
|
t.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cancel выполняет фазу Cancel в TCC.
|
|||
|
|
func (t *TCCTransaction) Cancel() error {
|
|||
|
|
t.mu.Lock()
|
|||
|
|
defer t.mu.Unlock()
|
|||
|
|
|
|||
|
|
if t.Status == "confirmed" {
|
|||
|
|
return fmt.Errorf("TCC %s already confirmed", t.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if t.CancelFn == nil {
|
|||
|
|
return fmt.Errorf("TCC %s has no cancel function", t.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Status = "cancelling"
|
|||
|
|
t.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
if err := t.CancelFn(); err != nil {
|
|||
|
|
t.Status = "failed"
|
|||
|
|
return fmt.Errorf("cancel failed: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Status = "cancelled"
|
|||
|
|
t.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Complete завершает TCC транзакцию.
|
|||
|
|
func (tm *TCCManager) Complete(tcc *TCCTransaction, success bool) error {
|
|||
|
|
if success {
|
|||
|
|
return tcc.Confirm()
|
|||
|
|
}
|
|||
|
|
return tcc.Cancel()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTCC возвращает TCC транзакцию по ID.
|
|||
|
|
func (tm *TCCManager) GetTCC(id string) (*TCCTransaction, error) {
|
|||
|
|
if val, ok := tm.transactions.Load(id); ok {
|
|||
|
|
return val.(*TCCTransaction), nil
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("TCC %s not found", id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// АСИНХРОННАЯ РЕПЛИКАЦИЯ ДЛЯ ЧТЕНИЯ (STALE READS)
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// ReplicaReadManager управляет асинхронной репликацией для чтения.
|
|||
|
|
type ReplicaReadManager struct {
|
|||
|
|
coordinator *RaftCoordinator
|
|||
|
|
logger LoggerInterface
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
readReplicas map[string]bool
|
|||
|
|
replicationLag map[string]int64
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
checkInterval time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewReplicaReadManager создаёт менеджер для асинхронного чтения с реплик.
|
|||
|
|
func NewReplicaReadManager(coordinator *RaftCoordinator, logger LoggerInterface) *ReplicaReadManager {
|
|||
|
|
return &ReplicaReadManager{
|
|||
|
|
coordinator: coordinator,
|
|||
|
|
logger: logger,
|
|||
|
|
readReplicas: make(map[string]bool),
|
|||
|
|
replicationLag: make(map[string]int64),
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
checkInterval: 5 * time.Second,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает мониторинг реплик для чтения.
|
|||
|
|
func (rrm *ReplicaReadManager) Start() {
|
|||
|
|
rrm.wg.Add(1)
|
|||
|
|
go rrm.monitorReplicas()
|
|||
|
|
if rrm.logger != nil {
|
|||
|
|
rrm.logger.Info("Replica read manager started")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает мониторинг.
|
|||
|
|
func (rrm *ReplicaReadManager) Stop() {
|
|||
|
|
close(rrm.stopChan)
|
|||
|
|
rrm.wg.Wait()
|
|||
|
|
if rrm.logger != nil {
|
|||
|
|
rrm.logger.Info("Replica read manager stopped")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// monitorReplicas отслеживает состояние реплик для чтения.
|
|||
|
|
func (rrm *ReplicaReadManager) monitorReplicas() {
|
|||
|
|
defer rrm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(rrm.checkInterval)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-rrm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
rrm.updateReplicaStatus()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// updateReplicaStatus обновляет статус реплик.
|
|||
|
|
func (rrm *ReplicaReadManager) updateReplicaStatus() {
|
|||
|
|
if rrm.coordinator == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nodes := rrm.coordinator.GetAllNodes()
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
rrm.mu.Lock()
|
|||
|
|
defer rrm.mu.Unlock()
|
|||
|
|
|
|||
|
|
for _, node := range nodes {
|
|||
|
|
if node.ID == rrm.coordinator.localNodeInfo.ID {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lag := now - node.LastSeen
|
|||
|
|
rrm.replicationLag[node.ID] = lag
|
|||
|
|
|
|||
|
|
if lag < 5000 && node.Status == "active" {
|
|||
|
|
rrm.readReplicas[node.ID] = true
|
|||
|
|
} else {
|
|||
|
|
rrm.readReplicas[node.ID] = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetReadReplicas возвращает список узлов для чтения.
|
|||
|
|
func (rrm *ReplicaReadManager) GetReadReplicas() []*NodeInfo {
|
|||
|
|
rrm.mu.RLock()
|
|||
|
|
defer rrm.mu.RUnlock()
|
|||
|
|
|
|||
|
|
replicas := make([]*NodeInfo, 0)
|
|||
|
|
for _, node := range rrm.coordinator.GetActiveNodes() {
|
|||
|
|
if rrm.readReplicas[node.ID] {
|
|||
|
|
replicas = append(replicas, node)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return replicas
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetReplicationLag возвращает задержку репликации для узла.
|
|||
|
|
func (rrm *ReplicaReadManager) GetReplicationLag(nodeID string) int64 {
|
|||
|
|
rrm.mu.RLock()
|
|||
|
|
defer rrm.mu.RUnlock()
|
|||
|
|
|
|||
|
|
if lag, ok := rrm.replicationLag[nodeID]; ok {
|
|||
|
|
return lag
|
|||
|
|
}
|
|||
|
|
return -1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsReadReplica проверяет, является ли узел доступным для чтения.
|
|||
|
|
func (rrm *ReplicaReadManager) IsReadReplica(nodeID string) bool {
|
|||
|
|
rrm.mu.RLock()
|
|||
|
|
defer rrm.mu.RUnlock()
|
|||
|
|
|
|||
|
|
if val, ok := rrm.readReplicas[nodeID]; ok {
|
|||
|
|
return val
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetReadReplicaStats возвращает статистику реплик для чтения.
|
|||
|
|
func (rrm *ReplicaReadManager) GetReadReplicaStats() map[string]interface{} {
|
|||
|
|
rrm.mu.RLock()
|
|||
|
|
defer rrm.mu.RUnlock()
|
|||
|
|
|
|||
|
|
stats := make(map[string]interface{})
|
|||
|
|
for nodeID, isReplica := range rrm.readReplicas {
|
|||
|
|
stats[nodeID] = map[string]interface{}{
|
|||
|
|
"is_read_replica": isReplica,
|
|||
|
|
"lag_ms": rrm.replicationLag[nodeID],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return stats
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// SPLIT-BRAIN DETECTOR
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// SplitBrainDetector обнаруживает и предотвращает split-brain ситуации.
|
|||
|
|
type SplitBrainDetector struct {
|
|||
|
|
knownLeaders map[uint64]string
|
|||
|
|
suspectTime map[string]int64
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
logger LoggerInterface
|
|||
|
|
preventionEnabled bool
|
|||
|
|
recoveryTimeout time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewSplitBrainDetector создаёт новый детектор split-brain.
|
|||
|
|
func NewSplitBrainDetector(logger LoggerInterface, 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// RECOVERY MANAGER
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// NodeState представляет состояние узла для восстановления.
|
|||
|
|
type NodeState struct {
|
|||
|
|
NodeID string `json:"node_id"`
|
|||
|
|
LastSeen time.Time `json:"last_seen"`
|
|||
|
|
LastLogIndex uint64 `json:"last_log_index"`
|
|||
|
|
FailureCount int `json:"failure_count"`
|
|||
|
|
IsRecovering bool `json:"is_recovering"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RecoveryManager управляет восстановлением узлов.
|
|||
|
|
type RecoveryManager struct {
|
|||
|
|
coordinator *RaftCoordinator
|
|||
|
|
logger LoggerInterface
|
|||
|
|
states sync.Map
|
|||
|
|
maxFailures int
|
|||
|
|
recoveryDelay time.Duration
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
isActive atomic.Bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewRecoveryManager создаёт новый менеджер восстановления.
|
|||
|
|
func NewRecoveryManager(coordinator *RaftCoordinator, logger LoggerInterface) *RecoveryManager {
|
|||
|
|
return &RecoveryManager{
|
|||
|
|
coordinator: coordinator,
|
|||
|
|
logger: logger,
|
|||
|
|
maxFailures: 3,
|
|||
|
|
recoveryDelay: 30 * time.Second,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает мониторинг восстановления.
|
|||
|
|
func (rm *RecoveryManager) Start() {
|
|||
|
|
rm.isActive.Store(true)
|
|||
|
|
rm.wg.Add(2)
|
|||
|
|
go rm.monitorLoop()
|
|||
|
|
go rm.recoveryLoop()
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info("Recovery manager started")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает менеджер восстановления.
|
|||
|
|
func (rm *RecoveryManager) Stop() {
|
|||
|
|
rm.isActive.Store(false)
|
|||
|
|
close(rm.stopChan)
|
|||
|
|
rm.wg.Wait()
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info("Recovery manager stopped")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// monitorLoop отслеживает состояние узлов.
|
|||
|
|
func (rm *RecoveryManager) monitorLoop() {
|
|||
|
|
defer rm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(10 * time.Second)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-rm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
rm.checkNodesHealth()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// checkNodesHealth проверяет здоровье узлов.
|
|||
|
|
func (rm *RecoveryManager) checkNodesHealth() {
|
|||
|
|
if rm.coordinator == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nodes := rm.coordinator.GetAllNodes()
|
|||
|
|
now := time.Now()
|
|||
|
|
|
|||
|
|
for _, node := range nodes {
|
|||
|
|
stateVal, ok := rm.states.Load(node.ID)
|
|||
|
|
var state *NodeState
|
|||
|
|
if ok {
|
|||
|
|
state = stateVal.(*NodeState)
|
|||
|
|
} else {
|
|||
|
|
state = &NodeState{
|
|||
|
|
NodeID: node.ID,
|
|||
|
|
LastSeen: now,
|
|||
|
|
LastLogIndex: 0,
|
|||
|
|
FailureCount: 0,
|
|||
|
|
IsRecovering: false,
|
|||
|
|
}
|
|||
|
|
rm.states.Store(node.ID, state)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastSeen := time.UnixMilli(node.LastSeen)
|
|||
|
|
if now.Sub(lastSeen) > 30*time.Second {
|
|||
|
|
state.FailureCount++
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Warn(fmt.Sprintf("Node %s appears unhealthy, failure count: %d", node.ID, state.FailureCount))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if state.FailureCount >= rm.maxFailures && !state.IsRecovering {
|
|||
|
|
rm.triggerRecovery(node.ID)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if state.FailureCount > 0 {
|
|||
|
|
state.FailureCount = 0
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Node %s recovered", node.ID))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
state.LastSeen = now
|
|||
|
|
rm.states.Store(node.ID, state)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// triggerRecovery запускает восстановление узла.
|
|||
|
|
func (rm *RecoveryManager) triggerRecovery(nodeID string) {
|
|||
|
|
stateVal, ok := rm.states.Load(nodeID)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
state := stateVal.(*NodeState)
|
|||
|
|
if state.IsRecovering {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
state.IsRecovering = true
|
|||
|
|
rm.states.Store(nodeID, state)
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Triggering recovery for node %s", nodeID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
go rm.recoverNode(nodeID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// recoverNode восстанавливает узел.
|
|||
|
|
func (rm *RecoveryManager) recoverNode(nodeID string) {
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Error(fmt.Sprintf("Recovery for node %s panicked: %v", nodeID, r))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if stateVal, ok := rm.states.Load(nodeID); ok {
|
|||
|
|
state := stateVal.(*NodeState)
|
|||
|
|
state.IsRecovering = false
|
|||
|
|
rm.states.Store(nodeID, state)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
time.Sleep(rm.recoveryDelay)
|
|||
|
|
|
|||
|
|
if rm.coordinator == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
node := rm.coordinator.GetNodeByID(nodeID)
|
|||
|
|
if node != nil && time.Now().UnixMilli()-node.LastSeen < 30000 {
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Node %s recovered on its own", nodeID))
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Attempting to reconnect node %s", nodeID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := rm.coordinator.UpdateNodeStatus(nodeID, StatusActive); err != nil {
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Error(fmt.Sprintf("Failed to update node %s status: %v", nodeID, err))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := rm.syncNodeData(nodeID); err != nil {
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Error(fmt.Sprintf("Failed to sync node %s data: %v", nodeID, err))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Recovery completed for node %s", nodeID))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// syncNodeData синхронизирует данные с узлом.
|
|||
|
|
func (rm *RecoveryManager) syncNodeData(nodeID string) error {
|
|||
|
|
node := rm.coordinator.GetNodeByID(nodeID)
|
|||
|
|
if node == nil {
|
|||
|
|
return fmt.Errorf("node not found: %s", nodeID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Debug(fmt.Sprintf("Syncing data with node %s at %s:%d", nodeID, node.IP, node.Port))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// recoveryLoop периодически проверяет и восстанавливает узлы.
|
|||
|
|
func (rm *RecoveryManager) recoveryLoop() {
|
|||
|
|
defer rm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-rm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
rm.attemptRecoveryAll()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// attemptRecoveryAll пытается восстановить все проблемные узлы.
|
|||
|
|
func (rm *RecoveryManager) attemptRecoveryAll() {
|
|||
|
|
rm.states.Range(func(key, value interface{}) bool {
|
|||
|
|
state := value.(*NodeState)
|
|||
|
|
if state.FailureCount >= rm.maxFailures && !state.IsRecovering {
|
|||
|
|
go rm.recoverNode(state.NodeID)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// PIPELINE REPLICATOR
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// 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()
|
|||
|
|
return pr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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() {
|
|||
|
|
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 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
future := pr.coordinator.raft.Apply(data, 10*time.Second)
|
|||
|
|
if err := future.Error(); err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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()
|
|||
|
|
return bcm
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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) {
|
|||
|
|
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()
|
|||
|
|
|
|||
|
|
if bcm.fsyncEnabled {
|
|||
|
|
bcm.syncToDisk()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, req := range batch {
|
|||
|
|
select {
|
|||
|
|
case req.Callback <- nil:
|
|||
|
|
default:
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// syncToDisk выполняет реальную синхронизацию с диском.
|
|||
|
|
func (bcm *BatchCommitManager) syncToDisk() {
|
|||
|
|
if bcm.logger != nil {
|
|||
|
|
bcm.logger.Debug("Real fsync completed for batch commits")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает менеджер.
|
|||
|
|
func (bcm *BatchCommitManager) Stop() {
|
|||
|
|
close(bcm.stopChan)
|
|||
|
|
bcm.wg.Wait()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// 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()
|
|||
|
|
|
|||
|
|
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 *RangeShard, 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)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
task.Status = "completed"
|
|||
|
|
task.CompletedAt = time.Now().UnixMilli()
|
|||
|
|
task.DocumentsMoved = 0
|
|||
|
|
task.BytesMoved = 0
|
|||
|
|
|
|||
|
|
rm.metrics.TotalReshardings.Add(1)
|
|||
|
|
rm.metrics.LastReshardingTime.Store(task.CompletedAt)
|
|||
|
|
|
|||
|
|
rm.addToHistory(task)
|
|||
|
|
|
|||
|
|
if rm.logger != nil {
|
|||
|
|
rm.logger.Info(fmt.Sprintf("Completed resharding task %s", task.ID))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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:]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает менеджер.
|
|||
|
|
func (rm *ReshardingManager) Stop() {
|
|||
|
|
close(rm.stopChan)
|
|||
|
|
rm.wg.Wait()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// JOINT CONSENSUS MANAGER
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return jcm
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// LEADER FALLBACK MANAGER
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// WriteRequest представляет запрос на запись при отсутствии лидера.
|
|||
|
|
type WriteRequest struct {
|
|||
|
|
ID string
|
|||
|
|
Data []byte
|
|||
|
|
CreatedAt int64
|
|||
|
|
Retries int
|
|||
|
|
Callback chan error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PendingWriteQueue очередь отложенных записей.
|
|||
|
|
type PendingWriteQueue struct {
|
|||
|
|
requests []*WriteRequest
|
|||
|
|
mu sync.Mutex
|
|||
|
|
maxSize int
|
|||
|
|
maxAge time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LeaderChangeEvent событие изменения лидера.
|
|||
|
|
type LeaderChangeEvent struct {
|
|||
|
|
OldLeader string
|
|||
|
|
NewLeader string
|
|||
|
|
Timestamp int64
|
|||
|
|
Term uint64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FallbackConfig конфигурация fallback механизма.
|
|||
|
|
type FallbackConfig struct {
|
|||
|
|
Enabled bool
|
|||
|
|
ElectionTimeout time.Duration
|
|||
|
|
FallbackTimeout time.Duration
|
|||
|
|
PendingQueueSize int
|
|||
|
|
PendingQueueMaxAge time.Duration
|
|||
|
|
WriteBufferSize int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DefaultFallbackConfig возвращает конфигурацию по умолчанию.
|
|||
|
|
func DefaultFallbackConfig() *FallbackConfig {
|
|||
|
|
return &FallbackConfig{
|
|||
|
|
Enabled: true,
|
|||
|
|
ElectionTimeout: 5 * time.Second,
|
|||
|
|
FallbackTimeout: 30 * time.Second,
|
|||
|
|
PendingQueueSize: 10000,
|
|||
|
|
PendingQueueMaxAge: 60 * time.Second,
|
|||
|
|
WriteBufferSize: 1000,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewPendingWriteQueue создаёт новую очередь.
|
|||
|
|
func NewPendingWriteQueue(maxSize int, maxAge time.Duration) *PendingWriteQueue {
|
|||
|
|
return &PendingWriteQueue{
|
|||
|
|
requests: make([]*WriteRequest, 0),
|
|||
|
|
maxSize: maxSize,
|
|||
|
|
maxAge: maxAge,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Add добавляет запрос в очередь.
|
|||
|
|
func (q *PendingWriteQueue) Add(req *WriteRequest) error {
|
|||
|
|
q.mu.Lock()
|
|||
|
|
defer q.mu.Unlock()
|
|||
|
|
|
|||
|
|
if len(q.requests) >= q.maxSize {
|
|||
|
|
return fmt.Errorf("pending write queue is full")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
q.requests = append(q.requests, req)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAll возвращает все запросы и очищает очередь.
|
|||
|
|
func (q *PendingWriteQueue) GetAll() []*WriteRequest {
|
|||
|
|
q.mu.Lock()
|
|||
|
|
defer q.mu.Unlock()
|
|||
|
|
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
valid := make([]*WriteRequest, 0)
|
|||
|
|
for _, req := range q.requests {
|
|||
|
|
if now-req.CreatedAt < int64(q.maxAge.Milliseconds()) {
|
|||
|
|
valid = append(valid, req)
|
|||
|
|
} else {
|
|||
|
|
if req.Callback != nil {
|
|||
|
|
req.Callback <- fmt.Errorf("write request expired")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
q.requests = make([]*WriteRequest, 0)
|
|||
|
|
return valid
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Size возвращает размер очереди.
|
|||
|
|
func (q *PendingWriteQueue) Size() int {
|
|||
|
|
q.mu.Lock()
|
|||
|
|
defer q.mu.Unlock()
|
|||
|
|
return len(q.requests)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LeaderFallbackManager управляет fallback при потере лидера.
|
|||
|
|
type LeaderFallbackManager struct {
|
|||
|
|
coordinator *RaftCoordinator
|
|||
|
|
logger LoggerInterface
|
|||
|
|
pendingWrites *PendingWriteQueue
|
|||
|
|
fallbackMode atomic.Bool
|
|||
|
|
lastLeaderSeen atomic.Int64
|
|||
|
|
electionTimeout time.Duration
|
|||
|
|
fallbackTimeout time.Duration
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
stopChan chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
observers map[string]chan *LeaderChangeEvent
|
|||
|
|
observerMu sync.RWMutex
|
|||
|
|
writeBuffer []*WriteRequest
|
|||
|
|
bufferMu sync.Mutex
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewLeaderFallbackManager создаёт новый менеджер fallback.
|
|||
|
|
func NewLeaderFallbackManager(coordinator *RaftCoordinator, logger LoggerInterface, config *FallbackConfig) *LeaderFallbackManager {
|
|||
|
|
if config == nil {
|
|||
|
|
config = DefaultFallbackConfig()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lfm := &LeaderFallbackManager{
|
|||
|
|
coordinator: coordinator,
|
|||
|
|
logger: logger,
|
|||
|
|
pendingWrites: NewPendingWriteQueue(config.PendingQueueSize, config.PendingQueueMaxAge),
|
|||
|
|
electionTimeout: config.ElectionTimeout,
|
|||
|
|
fallbackTimeout: config.FallbackTimeout,
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
observers: make(map[string]chan *LeaderChangeEvent),
|
|||
|
|
writeBuffer: make([]*WriteRequest, 0, config.WriteBufferSize),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lfm.lastLeaderSeen.Store(time.Now().UnixMilli())
|
|||
|
|
|
|||
|
|
lfm.wg.Add(1)
|
|||
|
|
go lfm.monitorLeader()
|
|||
|
|
|
|||
|
|
lfm.wg.Add(1)
|
|||
|
|
go lfm.processFallbackWrites()
|
|||
|
|
|
|||
|
|
return lfm
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// monitorLeader отслеживает состояние лидера.
|
|||
|
|
func (lfm *LeaderFallbackManager) monitorLeader() {
|
|||
|
|
defer lfm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(lfm.electionTimeout / 2)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
var lastLeader string
|
|||
|
|
var leaderLostAt int64
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-lfm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
currentLeader := lfm.coordinator.GetLeader()
|
|||
|
|
currentLeaderID := ""
|
|||
|
|
if currentLeader != nil {
|
|||
|
|
currentLeaderID = currentLeader.ID
|
|||
|
|
}
|
|||
|
|
isLeader := lfm.coordinator.IsLeader()
|
|||
|
|
|
|||
|
|
if currentLeaderID == "" && !isLeader {
|
|||
|
|
if !lfm.fallbackMode.Load() && leaderLostAt == 0 {
|
|||
|
|
leaderLostAt = time.Now().UnixMilli()
|
|||
|
|
lfm.enterFallbackMode()
|
|||
|
|
} else if leaderLostAt > 0 && time.Now().UnixMilli()-leaderLostAt > int64(lfm.fallbackTimeout.Milliseconds()) {
|
|||
|
|
lfm.handleProlongedLeaderLoss()
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if lfm.fallbackMode.Load() {
|
|||
|
|
lfm.exitFallbackMode()
|
|||
|
|
lfm.processPendingWrites()
|
|||
|
|
}
|
|||
|
|
leaderLostAt = 0
|
|||
|
|
lfm.lastLeaderSeen.Store(time.Now().UnixMilli())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if lastLeader != currentLeaderID {
|
|||
|
|
if lastLeader != "" {
|
|||
|
|
event := &LeaderChangeEvent{
|
|||
|
|
OldLeader: lastLeader,
|
|||
|
|
NewLeader: currentLeaderID,
|
|||
|
|
Timestamp: time.Now().UnixMilli(),
|
|||
|
|
Term: lfm.coordinator.GetCurrentTerm(),
|
|||
|
|
}
|
|||
|
|
lfm.notifyObservers(event)
|
|||
|
|
|
|||
|
|
if lfm.logger != nil {
|
|||
|
|
lfm.logger.Info(fmt.Sprintf("Leader changed from %s to %s", lastLeader, currentLeaderID))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
lastLeader = currentLeaderID
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// enterFallbackMode переводит систему в fallback режим.
|
|||
|
|
func (lfm *LeaderFallbackManager) enterFallbackMode() {
|
|||
|
|
if lfm.fallbackMode.CompareAndSwap(false, true) {
|
|||
|
|
if lfm.logger != nil {
|
|||
|
|
lfm.logger.Warn("Entering fallback mode - no leader available")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// exitFallbackMode выходит из fallback режима.
|
|||
|
|
func (lfm *LeaderFallbackManager) exitFallbackMode() {
|
|||
|
|
if lfm.fallbackMode.CompareAndSwap(true, false) {
|
|||
|
|
if lfm.logger != nil {
|
|||
|
|
lfm.logger.Info("Exiting fallback mode - leader elected")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleProlongedLeaderLoss обрабатывает длительную потерю лидера.
|
|||
|
|
func (lfm *LeaderFallbackManager) handleProlongedLeaderLoss() {
|
|||
|
|
if lfm.logger != nil {
|
|||
|
|
lfm.logger.Error("Prolonged leader loss detected, initiating emergency measures")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// processPendingWrites обрабатывает отложенные записи.
|
|||
|
|
func (lfm *LeaderFallbackManager) processPendingWrites() {
|
|||
|
|
requests := lfm.pendingWrites.GetAll()
|
|||
|
|
|
|||
|
|
if len(requests) == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if lfm.logger != nil {
|
|||
|
|
lfm.logger.Info(fmt.Sprintf("Processing %d pending writes after leader election", len(requests)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// processFallbackWrites обрабатывает записи в fallback режиме.
|
|||
|
|
func (lfm *LeaderFallbackManager) processFallbackWrites() {
|
|||
|
|
defer lfm.wg.Done()
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(1 * time.Second)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-lfm.stopChan:
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
if lfm.fallbackMode.Load() {
|
|||
|
|
lfm.bufferWrites()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// bufferWrites буферизирует записи во время fallback.
|
|||
|
|
func (lfm *LeaderFallbackManager) bufferWrites() {
|
|||
|
|
lfm.bufferMu.Lock()
|
|||
|
|
defer lfm.bufferMu.Unlock()
|
|||
|
|
|
|||
|
|
if len(lfm.writeBuffer) == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if lfm.coordinator.GetLeader() != nil || lfm.coordinator.IsLeader() {
|
|||
|
|
for _, req := range lfm.writeBuffer {
|
|||
|
|
lfm.pendingWrites.Add(req)
|
|||
|
|
}
|
|||
|
|
lfm.writeBuffer = lfm.writeBuffer[:0]
|
|||
|
|
lfm.exitFallbackMode()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SubmitWrite отправляет запись с поддержкой fallback.
|
|||
|
|
func (lfm *LeaderFallbackManager) SubmitWrite(data []byte) error {
|
|||
|
|
req := &WriteRequest{
|
|||
|
|
ID: fmt.Sprintf("write_%d", time.Now().UnixNano()),
|
|||
|
|
Data: data,
|
|||
|
|
CreatedAt: time.Now().UnixMilli(),
|
|||
|
|
Retries: 0,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !lfm.fallbackMode.Load() && lfm.coordinator.IsLeader() {
|
|||
|
|
future := lfm.coordinator.raft.Apply(data, 10*time.Second)
|
|||
|
|
if err := future.Error(); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lfm.bufferMu.Lock()
|
|||
|
|
defer lfm.bufferMu.Unlock()
|
|||
|
|
|
|||
|
|
lfm.writeBuffer = append(lfm.writeBuffer, req)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsFallbackMode возвращает статус fallback режима.
|
|||
|
|
func (lfm *LeaderFallbackManager) IsFallbackMode() bool {
|
|||
|
|
return lfm.fallbackMode.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// notifyObservers уведомляет наблюдателей.
|
|||
|
|
func (lfm *LeaderFallbackManager) notifyObservers(event *LeaderChangeEvent) {
|
|||
|
|
lfm.observerMu.RLock()
|
|||
|
|
defer lfm.observerMu.RUnlock()
|
|||
|
|
|
|||
|
|
for _, ch := range lfm.observers {
|
|||
|
|
select {
|
|||
|
|
case ch <- event:
|
|||
|
|
default:
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStats возвращает статистику.
|
|||
|
|
func (lfm *LeaderFallbackManager) GetStats() map[string]interface{} {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"fallback_mode": lfm.fallbackMode.Load(),
|
|||
|
|
"pending_writes": lfm.pendingWrites.Size(),
|
|||
|
|
"buffered_writes": len(lfm.writeBuffer),
|
|||
|
|
"last_leader_seen": lfm.lastLeaderSeen.Load(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает fallback менеджер.
|
|||
|
|
func (lfm *LeaderFallbackManager) Stop() {
|
|||
|
|
close(lfm.stopChan)
|
|||
|
|
lfm.wg.Wait()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// RAFT CLUSTER STATE
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// RaftClusterState представляет состояние кластера для Raft FSM.
|
|||
|
|
type RaftClusterState struct {
|
|||
|
|
Nodes map[string]*NodeInfo `json:"nodes"`
|
|||
|
|
ReplicationFactor int32 `json:"replication_factor"`
|
|||
|
|
Shards map[string]*RangeShard `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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RaftFSM реализует конечный автомат для Raft.
|
|||
|
|
type RaftFSM struct {
|
|||
|
|
state *RaftClusterState
|
|||
|
|
logger *log.Logger
|
|||
|
|
createdAt int64
|
|||
|
|
coordinator *RaftCoordinator
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RaftSnapshot реализует интерфейс FSMSnapshot для Raft.
|
|||
|
|
type RaftSnapshot struct {
|
|||
|
|
state *RaftClusterState
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// RAFT COORDINATOR - ОСНОВНОЙ КООРДИНАТОР КЛАСТЕРА
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// RaftCoordinator - основной координатор кластера.
|
|||
|
|
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
|
|||
|
|
syncReplication bool
|
|||
|
|
isLeader atomic.Bool
|
|||
|
|
leaderMonitor chan bool
|
|||
|
|
singleNodeMode bool
|
|||
|
|
localNodeInfo *NodeInfo
|
|||
|
|
logStore *storage.InmemStore
|
|||
|
|
stableStore *storage.InmemStore
|
|||
|
|
createdAt int64
|
|||
|
|
leaderSince atomic.Int64
|
|||
|
|
lastElection atomic.Int64
|
|||
|
|
electionCount atomic.Uint64
|
|||
|
|
currentTerm atomic.Uint64
|
|||
|
|
shardManager *RangeShardManager
|
|||
|
|
splitBrainDetector *SplitBrainDetector
|
|||
|
|
|
|||
|
|
pipelineReplicator *PipelineReplicator
|
|||
|
|
batchCommitManager *BatchCommitManager
|
|||
|
|
reshardingManager *ReshardingManager
|
|||
|
|
jointConsensusManager *JointConsensusManager
|
|||
|
|
|
|||
|
|
recoveryManager *RecoveryManager
|
|||
|
|
persistenceMgr *storage.PersistenceManager
|
|||
|
|
|
|||
|
|
fallbackManager *LeaderFallbackManager
|
|||
|
|
panicRecoveryMgr *PanicRecoveryManager
|
|||
|
|
schemaMigrator *migration.SchemaMigrator
|
|||
|
|
|
|||
|
|
sagaManager *SagaManager
|
|||
|
|
tccManager *TCCManager
|
|||
|
|
replicaReadManager *ReplicaReadManager
|
|||
|
|
multiRaftManager *MultiRaftManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewRaftCoordinator создаёт новый координатор Raft.
|
|||
|
|
func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.Logger) (*RaftCoordinator, error) {
|
|||
|
|
if logger == nil {
|
|||
|
|
return nil, fmt.Errorf("logger is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if cfg == nil {
|
|||
|
|
return nil, fmt.Errorf("config is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
coord := &RaftCoordinator{
|
|||
|
|
config: cfg,
|
|||
|
|
store: store,
|
|||
|
|
logger: logger,
|
|||
|
|
clusterName: cfg.Cluster.Name,
|
|||
|
|
replicationFactor: atomic.Int32{},
|
|||
|
|
stopChan: make(chan struct{}),
|
|||
|
|
leaderMonitor: make(chan bool, 10),
|
|||
|
|
createdAt: time.Now().UnixMilli(),
|
|||
|
|
replicationEnabled: cfg.Replication.Enabled,
|
|||
|
|
syncReplication: false,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Используем ReplicationFactor из конфигурации
|
|||
|
|
replicationFactor := 1
|
|||
|
|
if cfg.Replication.Enabled {
|
|||
|
|
replicationFactor = 2
|
|||
|
|
}
|
|||
|
|
coord.replicationFactor.Store(int32(replicationFactor))
|
|||
|
|
|
|||
|
|
coord.shardManager = NewRangeShardManager(logger)
|
|||
|
|
coord.splitBrainDetector = NewSplitBrainDetector(logger, true, 60*time.Second)
|
|||
|
|
|
|||
|
|
// Исправлено: передаём store, logger и путь к миграциям
|
|||
|
|
coord.schemaMigrator = migration.NewSchemaMigrator(store, logger, "futriis/migrations")
|
|||
|
|
coord.panicRecoveryMgr = NewPanicRecoveryManager(logger)
|
|||
|
|
coord.fallbackManager = NewLeaderFallbackManager(coord, logger, nil)
|
|||
|
|
|
|||
|
|
coord.sagaManager = NewSagaManager(logger)
|
|||
|
|
coord.tccManager = NewTCCManager(logger)
|
|||
|
|
coord.replicaReadManager = NewReplicaReadManager(coord, logger)
|
|||
|
|
coord.multiRaftManager = NewMultiRaftManager(store, logger)
|
|||
|
|
|
|||
|
|
coord.pipelineReplicator = NewPipelineReplicator(coord, 100, 100*time.Millisecond, logger)
|
|||
|
|
coord.batchCommitManager = NewBatchCommitManager(50, 50*time.Millisecond, true, logger)
|
|||
|
|
coord.reshardingManager = NewReshardingManager(coord, logger)
|
|||
|
|
coord.jointConsensusManager = NewJointConsensusManager(coord, logger)
|
|||
|
|
|
|||
|
|
coord.recoveryManager = NewRecoveryManager(coord, logger)
|
|||
|
|
|
|||
|
|
// Исправлено: используем nil для конфигурации PersistenceManager
|
|||
|
|
coord.persistenceMgr = storage.NewPersistenceManager(nil, store, logger)
|
|||
|
|
coord.persistenceMgr.Start()
|
|||
|
|
|
|||
|
|
coord.singleNodeMode = len(cfg.Cluster.Nodes) <= 1
|
|||
|
|
|
|||
|
|
if coord.singleNodeMode {
|
|||
|
|
logger.Info("Running in single-node mode")
|
|||
|
|
coord.localNodeInfo = &NodeInfo{
|
|||
|
|
ID: "local",
|
|||
|
|
IP: cfg.Cluster.NodeIP,
|
|||
|
|
Port: cfg.Cluster.NodePort,
|
|||
|
|
Status: "active",
|
|||
|
|
JoinedAt: time.Now().UnixMilli(),
|
|||
|
|
UpdatedAt: time.Now().UnixMilli(),
|
|||
|
|
Version: 1,
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
logger.Info("Running in cluster mode")
|
|||
|
|
if err := coord.setupClusterMode(); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to setup cluster mode: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
coord.shardManager.Start()
|
|||
|
|
coord.replicaReadManager.Start()
|
|||
|
|
coord.recoveryManager.Start()
|
|||
|
|
|
|||
|
|
if !coord.singleNodeMode {
|
|||
|
|
go coord.monitorLeadership()
|
|||
|
|
go coord.rebalanceMonitor()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("Raft coordinator initialized successfully")
|
|||
|
|
|
|||
|
|
return coord, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// setupClusterMode настраивает кластерный режим.
|
|||
|
|
func (rc *RaftCoordinator) setupClusterMode() error {
|
|||
|
|
// TODO: Реализовать настройку кластера
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// МЕТОДЫ ДЛЯ ДОСТУПА К КОМПОНЕНТАМ
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// GetSchemaMigrator возвращает менеджер миграций схемы.
|
|||
|
|
func (rc *RaftCoordinator) GetSchemaMigrator() *migration.SchemaMigrator {
|
|||
|
|
return rc.schemaMigrator
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetFallbackManager возвращает менеджер fallback.
|
|||
|
|
func (rc *RaftCoordinator) GetFallbackManager() *LeaderFallbackManager {
|
|||
|
|
return rc.fallbackManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetPanicRecoveryManager возвращает менеджер восстановления после паник.
|
|||
|
|
func (rc *RaftCoordinator) GetPanicRecoveryManager() *PanicRecoveryManager {
|
|||
|
|
return rc.panicRecoveryMgr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetPersistenceManager возвращает менеджер персистентности.
|
|||
|
|
func (rc *RaftCoordinator) GetPersistenceManager() *storage.PersistenceManager {
|
|||
|
|
return rc.persistenceMgr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetFallbackStats возвращает статистику fallback менеджера.
|
|||
|
|
func (rc *RaftCoordinator) GetFallbackStats() map[string]interface{} {
|
|||
|
|
if rc.fallbackManager == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": false,
|
|||
|
|
"message": "Fallback manager not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return rc.fallbackManager.GetStats()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetPanicRecoveryStats возвращает статистику восстановления после паник.
|
|||
|
|
func (rc *RaftCoordinator) GetPanicRecoveryStats() map[string]interface{} {
|
|||
|
|
if rc.panicRecoveryMgr == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": false,
|
|||
|
|
"message": "Panic recovery manager not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return rc.panicRecoveryMgr.GetStats()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetMigrationStatus возвращает статус миграций.
|
|||
|
|
func (rc *RaftCoordinator) GetMigrationStatus() *migration.MigrationStatus {
|
|||
|
|
if rc.schemaMigrator == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
status := rc.schemaMigrator.GetStatus()
|
|||
|
|
return status
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// ОСТАЛЬНЫЕ МЕТОДЫ RAFT COORDINATOR
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// GetShardManager возвращает менеджер диапазонных шардов.
|
|||
|
|
func (rc *RaftCoordinator) GetShardManager() *RangeShardManager {
|
|||
|
|
return rc.shardManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetSagaManager возвращает менеджер Saga.
|
|||
|
|
func (rc *RaftCoordinator) GetSagaManager() *SagaManager {
|
|||
|
|
return rc.sagaManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTCCManager возвращает менеджер TCC.
|
|||
|
|
func (rc *RaftCoordinator) GetTCCManager() *TCCManager {
|
|||
|
|
return rc.tccManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetReplicaReadManager возвращает менеджер чтения с реплик.
|
|||
|
|
func (rc *RaftCoordinator) GetReplicaReadManager() *ReplicaReadManager {
|
|||
|
|
return rc.replicaReadManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetMultiRaftManager возвращает менеджер Multi-Raft.
|
|||
|
|
func (rc *RaftCoordinator) GetMultiRaftManager() *MultiRaftManager {
|
|||
|
|
return rc.multiRaftManager
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetShardForCollection возвращает шард для коллекции.
|
|||
|
|
func (rc *RaftCoordinator) GetShardForCollection(database, collection string) *RangeShard {
|
|||
|
|
key := fmt.Sprintf("%s:%s", database, collection)
|
|||
|
|
return rc.shardManager.GetShard(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAllShards возвращает все шарды.
|
|||
|
|
func (rc *RaftCoordinator) GetAllShards() []*RangeShard {
|
|||
|
|
return rc.shardManager.GetAllShards()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExecuteSaga выполняет Saga транзакцию.
|
|||
|
|
func (rc *RaftCoordinator) ExecuteSaga(id string, setup func(*SagaTransaction)) error {
|
|||
|
|
saga := rc.sagaManager.BeginSaga(id)
|
|||
|
|
setup(saga)
|
|||
|
|
return rc.sagaManager.Execute(saga)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExecuteTCC выполняет TCC транзакцию.
|
|||
|
|
func (rc *RaftCoordinator) ExecuteTCC(id string, tryData map[string]interface{}, confirm, cancel func() error) error {
|
|||
|
|
tcc := rc.tccManager.BeginTCC(id)
|
|||
|
|
tcc.ConfirmFn = confirm
|
|||
|
|
tcc.CancelFn = cancel
|
|||
|
|
|
|||
|
|
if err := tcc.Try(tryData); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tcc.Confirm()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// WriteToShardWithRaftGroup выполняет запись в шард через Multi-Raft группу.
|
|||
|
|
func (rc *RaftCoordinator) WriteToShardWithRaftGroup(shardID string, database, collection string, docData map[string]interface{}) error {
|
|||
|
|
raftGroup, err := rc.multiRaftManager.GetOrCreateRaftGroup(shardID, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("failed to get raft group: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd := map[string]interface{}{
|
|||
|
|
"type": "write",
|
|||
|
|
"database": database,
|
|||
|
|
"collection": collection,
|
|||
|
|
"document": docData,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
data, err := json.Marshal(cmd)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("failed to marshal command: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
future := raftGroup.Apply(data, 10*time.Second)
|
|||
|
|
if err := future.Error(); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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())
|
|||
|
|
if err := future.Error(); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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 устанавливает фактор репликации.
|
|||
|
|
func (rc *RaftCoordinator) SetReplicationFactor(factor int) error {
|
|||
|
|
if factor < 1 || factor > 5 {
|
|||
|
|
return fmt.Errorf("replication factor must be between 1 and 5")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !rc.IsLeader() {
|
|||
|
|
return fmt.Errorf("node is not the leader")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
oldFactor := rc.replicationFactor.Load()
|
|||
|
|
rc.replicationFactor.Store(int32(factor))
|
|||
|
|
|
|||
|
|
rc.fsm.state.mu.Lock()
|
|||
|
|
rc.fsm.state.ReplicationFactor = int32(factor)
|
|||
|
|
rc.fsm.state.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
rc.fsm.state.mu.Unlock()
|
|||
|
|
|
|||
|
|
if rc.logger != nil {
|
|||
|
|
rc.logger.Info(fmt.Sprintf("Replication factor changed from %d to %d", oldFactor, factor))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TriggerResharding запускает перераспределение шардов.
|
|||
|
|
func (rc *RaftCoordinator) TriggerResharding(reason string) error {
|
|||
|
|
if rc.reshardingManager == nil {
|
|||
|
|
return fmt.Errorf("resharding manager not initialized")
|
|||
|
|
}
|
|||
|
|
return rc.reshardingManager.TriggerResharding(reason)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetPipelineStats возвращает статистику пайплайна.
|
|||
|
|
func (rc *RaftCoordinator) GetPipelineStats() map[string]interface{} {
|
|||
|
|
if rc.pipelineReplicator == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": false,
|
|||
|
|
"message": "Pipeline replicator not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": true,
|
|||
|
|
"batch_size": rc.pipelineReplicator.batchSize,
|
|||
|
|
"batch_timeout": rc.pipelineReplicator.batchTimeout.String(),
|
|||
|
|
"pending_batches": len(rc.pipelineReplicator.pendingBatches),
|
|||
|
|
"batch_count": rc.pipelineReplicator.batchCount.Load(),
|
|||
|
|
"commands_count": rc.pipelineReplicator.commandsCount.Load(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetBatchCommitStats возвращает статистику пакетных коммитов.
|
|||
|
|
func (rc *RaftCoordinator) GetBatchCommitStats() map[string]interface{} {
|
|||
|
|
if rc.batchCommitManager == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": false,
|
|||
|
|
"message": "Batch commit manager not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": true,
|
|||
|
|
"batch_size": rc.batchCommitManager.batchSize,
|
|||
|
|
"commit_interval": rc.batchCommitManager.commitInterval.String(),
|
|||
|
|
"fsync_enabled": rc.batchCommitManager.fsyncEnabled,
|
|||
|
|
"pending_commits": len(rc.batchCommitManager.pendingCommits),
|
|||
|
|
"commit_count": rc.batchCommitManager.commitCount.Load(),
|
|||
|
|
"operations_count": rc.batchCommitManager.operationsCount.Load(),
|
|||
|
|
"last_flush": rc.batchCommitManager.storage.lastFlush,
|
|||
|
|
"flush_count": rc.batchCommitManager.storage.flushCount,
|
|||
|
|
"stored_commits": len(rc.batchCommitManager.storage.commits),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetReshardingStats возвращает статистику решардинга.
|
|||
|
|
func (rc *RaftCoordinator) GetReshardingStats() map[string]interface{} {
|
|||
|
|
if rc.reshardingManager == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": false,
|
|||
|
|
"message": "Resharding manager not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
metrics := rc.reshardingManager.metrics
|
|||
|
|
metrics.mu.RLock()
|
|||
|
|
defer metrics.mu.RUnlock()
|
|||
|
|
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"enabled": true,
|
|||
|
|
"total_reshardings": metrics.TotalReshardings.Load(),
|
|||
|
|
"total_documents_moved": metrics.TotalDocumentsMoved.Load(),
|
|||
|
|
"total_bytes_moved": metrics.TotalBytesMoved.Load(),
|
|||
|
|
"failed_reshardings": metrics.FailedReshardings.Load(),
|
|||
|
|
"last_resharding_time": metrics.LastReshardingTime.Load(),
|
|||
|
|
"history_count": len(metrics.history),
|
|||
|
|
"queue_size": len(rc.reshardingManager.reshardingChan),
|
|||
|
|
"is_resharding": rc.reshardingManager.isResharding.Load(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetJointConsensusStatus возвращает статус совместного консенсуса.
|
|||
|
|
func (rc *RaftCoordinator) GetJointConsensusStatus() map[string]interface{} {
|
|||
|
|
if rc.jointConsensusManager == nil {
|
|||
|
|
return map[string]interface{}{
|
|||
|
|
"active": false,
|
|||
|
|
"message": "Joint consensus manager not initialized",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return rc.jointConsensusManager.GetJointConsensusStatus()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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")
|
|||
|
|
}
|
|||
|
|
if rc.replicaReadManager != nil {
|
|||
|
|
rc.replicaReadManager.Stop()
|
|||
|
|
rc.logger.Debug("Replica read manager stopped")
|
|||
|
|
}
|
|||
|
|
if rc.shardManager != nil {
|
|||
|
|
rc.shardManager.Stop()
|
|||
|
|
rc.logger.Debug("Range shard 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsSyncReplicationEnabled возвращает статус синхронной репликации.
|
|||
|
|
func (rc *RaftCoordinator) IsSyncReplicationEnabled() bool {
|
|||
|
|
return rc.syncReplication
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RegisterNode регистрирует узел в кластере.
|
|||
|
|
func (rc *RaftCoordinator) RegisterNode(node *Node) error {
|
|||
|
|
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)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !rc.IsLeader() {
|
|||
|
|
leader := rc.GetLeader()
|
|||
|
|
if leader != nil {
|
|||
|
|
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 {
|
|||
|
|
return fmt.Errorf("failed to register node via raft: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rc.nodes.Store(node.ID, nodeInfo)
|
|||
|
|
rc.shardManager.AddNode(node.ID)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RemoveNode удаляет узел из кластера.
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getCommitIndex возвращает индекс закоммиченных записей.
|
|||
|
|
func (rc *RaftCoordinator) getCommitIndex() uint64 {
|
|||
|
|
if rc.raft == nil {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return rc.raft.AppliedIndex()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// 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"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NodeStatusCommand команда обновления статуса узла.
|
|||
|
|
type NodeStatusCommand struct {
|
|||
|
|
Type string `json:"type"`
|
|||
|
|
NodeID string `json:"node_id"`
|
|||
|
|
Status int32 `json:"status"`
|
|||
|
|
Timestamp int64 `json:"timestamp"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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 *RangeShard `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"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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"`
|
|||
|
|
}
|