forked from ryyst/kalzu-value-store
refactor: extract clustering system to cluster package
- Create cluster/merkle.go with Merkle tree operations - Create cluster/gossip.go with gossip protocol implementation - Create cluster/sync.go with data synchronization logic - Create cluster/bootstrap.go with cluster joining functionality Major clustering functionality now properly separated: * MerkleService: Tree building, hashing, filtering * GossipService: Member discovery, health checking, list merging * SyncService: Merkle-based synchronization between nodes * BootstrapService: Seed node joining and initial sync Build tested and verified working. Ready for main.go integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
145
cluster/bootstrap.go
Normal file
145
cluster/bootstrap.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"kvs/types"
|
||||
)
|
||||
|
||||
// BootstrapService handles cluster joining and initial synchronization
|
||||
type BootstrapService struct {
|
||||
config *types.Config
|
||||
gossipService *GossipService
|
||||
syncService *SyncService
|
||||
logger *logrus.Logger
|
||||
setMode func(string) // Callback to set server mode
|
||||
}
|
||||
|
||||
// NewBootstrapService creates a new bootstrap service
|
||||
func NewBootstrapService(config *types.Config, gossipService *GossipService, syncService *SyncService, logger *logrus.Logger, setMode func(string)) *BootstrapService {
|
||||
return &BootstrapService{
|
||||
config: config,
|
||||
gossipService: gossipService,
|
||||
syncService: syncService,
|
||||
logger: logger,
|
||||
setMode: setMode,
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap joins cluster using seed nodes
|
||||
func (s *BootstrapService) Bootstrap() {
|
||||
if len(s.config.SeedNodes) == 0 {
|
||||
s.logger.Info("No seed nodes configured, running as standalone")
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("Starting bootstrap process")
|
||||
s.setMode("syncing")
|
||||
|
||||
// Try to join cluster via each seed node
|
||||
joined := false
|
||||
for _, seedAddr := range s.config.SeedNodes {
|
||||
if s.attemptJoin(seedAddr) {
|
||||
joined = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !joined {
|
||||
s.logger.Warn("Failed to join cluster via seed nodes, running as standalone")
|
||||
s.setMode("normal")
|
||||
return
|
||||
}
|
||||
|
||||
// Wait a bit for member discovery
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Perform gradual sync (now Merkle-based)
|
||||
s.performGradualSync()
|
||||
|
||||
// Switch to normal mode
|
||||
s.setMode("normal")
|
||||
s.logger.Info("Bootstrap completed, entering normal mode")
|
||||
}
|
||||
|
||||
// attemptJoin attempts to join cluster via a seed node
|
||||
func (s *BootstrapService) attemptJoin(seedAddr string) bool {
|
||||
joinReq := types.JoinRequest{
|
||||
ID: s.config.NodeID,
|
||||
Address: fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.Port),
|
||||
JoinedTimestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(joinReq)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to marshal join request")
|
||||
return false
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
url := fmt.Sprintf("http://%s/members/join", seedAddr)
|
||||
|
||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"seed": seedAddr,
|
||||
"error": err.Error(),
|
||||
}).Warn("Failed to contact seed node")
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"seed": seedAddr,
|
||||
"status": resp.StatusCode,
|
||||
}).Warn("Seed node rejected join request")
|
||||
return false
|
||||
}
|
||||
|
||||
// Process member list response
|
||||
var memberList []types.Member
|
||||
if err := json.NewDecoder(resp.Body).Decode(&memberList); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to decode member list from seed")
|
||||
return false
|
||||
}
|
||||
|
||||
// Add all members to our local list
|
||||
for _, member := range memberList {
|
||||
if member.ID != s.config.NodeID {
|
||||
s.gossipService.AddMember(&member)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"seed": seedAddr,
|
||||
"member_count": len(memberList),
|
||||
}).Info("Successfully joined cluster")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// performGradualSync performs gradual sync (Merkle-based version)
|
||||
func (s *BootstrapService) performGradualSync() {
|
||||
s.logger.Info("Starting gradual sync (Merkle-based)")
|
||||
|
||||
members := s.gossipService.GetHealthyMembers()
|
||||
if len(members) == 0 {
|
||||
s.logger.Info("No healthy members for gradual sync")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, just do a few rounds of Merkle sync
|
||||
for i := 0; i < 3; i++ {
|
||||
s.syncService.performMerkleSync()
|
||||
time.Sleep(time.Duration(s.config.ThrottleDelayMs) * time.Millisecond)
|
||||
}
|
||||
|
||||
s.logger.Info("Gradual sync completed")
|
||||
}
|
Reference in New Issue
Block a user