Files
futriix/internal/cluster/raft_coordinator.go
2026-07-12 01:39:26 +03:00

3457 lines
108 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/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"`
}