275 lines
6.8 KiB
Go
275 lines
6.8 KiB
Go
package mock
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"spore-gateway/internal/discovery"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// MockNodeDiscovery simulates node discovery with mock nodes
|
|
type MockNodeDiscovery struct {
|
|
mockNodes map[string]*discovery.NodeInfo
|
|
primaryNode string
|
|
mutex sync.RWMutex
|
|
callbacks []discovery.NodeUpdateCallback
|
|
heartbeatRate time.Duration
|
|
shutdownChan chan struct{}
|
|
shutdownOnce sync.Once
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewMockNodeDiscovery creates a new mock node discovery instance
|
|
func NewMockNodeDiscovery(numNodes int, heartbeatRate time.Duration) *MockNodeDiscovery {
|
|
mnd := &MockNodeDiscovery{
|
|
mockNodes: make(map[string]*discovery.NodeInfo),
|
|
heartbeatRate: heartbeatRate,
|
|
shutdownChan: make(chan struct{}),
|
|
logger: log.New(),
|
|
}
|
|
|
|
// Generate mock nodes with realistic firmware versions
|
|
// These versions should match the firmware available in the registry
|
|
now := time.Now()
|
|
for i := 0; i < numNodes; i++ {
|
|
ip := fmt.Sprintf("192.168.1.%d", 100+i)
|
|
hostname := fmt.Sprintf("spore-node-%d", i+1)
|
|
|
|
// Distribute nodes across different firmware versions
|
|
// Most nodes on stable versions, some on beta
|
|
var version string
|
|
switch i % 5 {
|
|
case 0, 1:
|
|
version = "1.0.0" // 40% on oldest stable
|
|
case 2, 3:
|
|
version = "1.1.0" // 40% on newer stable
|
|
case 4:
|
|
version = "1.2.0" // 20% on beta
|
|
}
|
|
|
|
// Determine stability based on version
|
|
stable := "true"
|
|
env := "production"
|
|
if version == "1.2.0" {
|
|
stable = "false"
|
|
env = "beta"
|
|
}
|
|
|
|
nodeInfo := &discovery.NodeInfo{
|
|
IP: ip,
|
|
Port: 80,
|
|
Hostname: hostname,
|
|
Status: discovery.NodeStatusActive,
|
|
DiscoveredAt: now.Add(-time.Duration(i*5) * time.Minute),
|
|
LastSeen: now,
|
|
Uptime: fmt.Sprintf("%dh%dm", 10+i, rand.Intn(60)),
|
|
Labels: map[string]string{
|
|
"version": version,
|
|
"stable": stable,
|
|
"env": env,
|
|
"zone": fmt.Sprintf("zone-%d", (i%3)+1),
|
|
"type": "spore-node",
|
|
},
|
|
Latency: int64(10 + rand.Intn(50)),
|
|
Resources: map[string]interface{}{
|
|
"freeHeap": 32768 + rand.Intn(32768),
|
|
"cpuFreqMHz": 80 + rand.Intn(160),
|
|
"flashChipSize": 4194304,
|
|
},
|
|
}
|
|
|
|
mnd.mockNodes[ip] = nodeInfo
|
|
}
|
|
|
|
// Set first node as primary
|
|
if numNodes > 0 {
|
|
mnd.primaryNode = fmt.Sprintf("192.168.1.%d", 100)
|
|
}
|
|
|
|
mnd.logger.WithField("nodes", numNodes).Info("Mock discovery initialized with nodes")
|
|
return mnd
|
|
}
|
|
|
|
// Start starts the mock discovery (simulates periodic heartbeats)
|
|
func (mnd *MockNodeDiscovery) Start() error {
|
|
mnd.logger.Info("Starting mock node discovery")
|
|
|
|
// Simulate periodic node updates
|
|
go mnd.simulateHeartbeats()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the mock discovery
|
|
func (mnd *MockNodeDiscovery) Shutdown(ctx context.Context) error {
|
|
mnd.shutdownOnce.Do(func() {
|
|
mnd.logger.Info("Shutting down mock node discovery")
|
|
close(mnd.shutdownChan)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// simulateHeartbeats simulates periodic node heartbeats and updates
|
|
func (mnd *MockNodeDiscovery) simulateHeartbeats() {
|
|
ticker := time.NewTicker(mnd.heartbeatRate)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-mnd.shutdownChan:
|
|
return
|
|
case <-ticker.C:
|
|
mnd.updateMockNodes()
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateMockNodes simulates node updates (version changes, status changes, etc.)
|
|
func (mnd *MockNodeDiscovery) updateMockNodes() {
|
|
mnd.mutex.Lock()
|
|
defer mnd.mutex.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
for ip, node := range mnd.mockNodes {
|
|
// Update last seen time
|
|
node.LastSeen = now
|
|
|
|
// Randomly update some metrics
|
|
if rand.Float32() < 0.3 { // 30% chance
|
|
node.Latency = int64(10 + rand.Intn(50))
|
|
// Update resources map
|
|
node.Resources["freeHeap"] = 32768 + rand.Intn(32768)
|
|
}
|
|
|
|
// Occasionally notify about updates
|
|
if rand.Float32() < 0.1 { // 10% chance
|
|
mnd.notifyCallbacks(ip, "heartbeat")
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetNodes returns a copy of all mock nodes
|
|
func (mnd *MockNodeDiscovery) GetNodes() map[string]*discovery.NodeInfo {
|
|
mnd.mutex.RLock()
|
|
defer mnd.mutex.RUnlock()
|
|
|
|
nodes := make(map[string]*discovery.NodeInfo)
|
|
for ip, node := range mnd.mockNodes {
|
|
// Create a copy
|
|
nodeCopy := *node
|
|
nodes[ip] = &nodeCopy
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// GetPrimaryNode returns the current primary node IP
|
|
func (mnd *MockNodeDiscovery) GetPrimaryNode() string {
|
|
mnd.mutex.RLock()
|
|
defer mnd.mutex.RUnlock()
|
|
return mnd.primaryNode
|
|
}
|
|
|
|
// SetPrimaryNode manually sets the primary node
|
|
func (mnd *MockNodeDiscovery) SetPrimaryNode(ip string) error {
|
|
mnd.mutex.Lock()
|
|
defer mnd.mutex.Unlock()
|
|
|
|
if _, exists := mnd.mockNodes[ip]; !exists {
|
|
return fmt.Errorf("node %s not found", ip)
|
|
}
|
|
|
|
oldPrimary := mnd.primaryNode
|
|
mnd.primaryNode = ip
|
|
mnd.logger.WithFields(log.Fields{
|
|
"old_primary": oldPrimary,
|
|
"new_primary": ip,
|
|
}).Info("Primary node changed")
|
|
|
|
mnd.notifyCallbacks(ip, "primary_changed")
|
|
return nil
|
|
}
|
|
|
|
// SelectRandomPrimaryNode selects a random active node as primary
|
|
func (mnd *MockNodeDiscovery) SelectRandomPrimaryNode() string {
|
|
mnd.mutex.Lock()
|
|
defer mnd.mutex.Unlock()
|
|
|
|
if len(mnd.mockNodes) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Get active nodes
|
|
var activeNodes []string
|
|
for ip, node := range mnd.mockNodes {
|
|
if node.Status == discovery.NodeStatusActive {
|
|
activeNodes = append(activeNodes, ip)
|
|
}
|
|
}
|
|
|
|
if len(activeNodes) == 0 {
|
|
return mnd.primaryNode
|
|
}
|
|
|
|
// Select random node
|
|
randomIndex := rand.Intn(len(activeNodes))
|
|
randomNode := activeNodes[randomIndex]
|
|
|
|
oldPrimary := mnd.primaryNode
|
|
mnd.primaryNode = randomNode
|
|
mnd.logger.WithFields(log.Fields{
|
|
"old_primary": oldPrimary,
|
|
"new_primary": randomNode,
|
|
}).Info("Randomly selected new primary node")
|
|
|
|
mnd.notifyCallbacks(randomNode, "primary_changed")
|
|
return randomNode
|
|
}
|
|
|
|
// AddCallback registers a callback for node updates
|
|
func (mnd *MockNodeDiscovery) AddCallback(callback discovery.NodeUpdateCallback) {
|
|
mnd.mutex.Lock()
|
|
defer mnd.mutex.Unlock()
|
|
mnd.callbacks = append(mnd.callbacks, callback)
|
|
}
|
|
|
|
// GetClusterStatus returns current cluster status
|
|
func (mnd *MockNodeDiscovery) GetClusterStatus() discovery.ClusterStatus {
|
|
mnd.mutex.RLock()
|
|
defer mnd.mutex.RUnlock()
|
|
|
|
return discovery.ClusterStatus{
|
|
PrimaryNode: mnd.primaryNode,
|
|
TotalNodes: len(mnd.mockNodes),
|
|
UDPPort: "4210",
|
|
ServerRunning: true,
|
|
}
|
|
}
|
|
|
|
// notifyCallbacks notifies all registered callbacks about node changes
|
|
func (mnd *MockNodeDiscovery) notifyCallbacks(nodeIP, action string) {
|
|
for _, callback := range mnd.callbacks {
|
|
go callback(nodeIP, action)
|
|
}
|
|
}
|
|
|
|
// UpdateNodeVersion simulates updating a node's version (for testing rollouts)
|
|
func (mnd *MockNodeDiscovery) UpdateNodeVersion(ip, version string) {
|
|
mnd.mutex.Lock()
|
|
defer mnd.mutex.Unlock()
|
|
|
|
if node, exists := mnd.mockNodes[ip]; exists {
|
|
node.Labels["version"] = version
|
|
mnd.logger.WithFields(log.Fields{
|
|
"ip": ip,
|
|
"version": version,
|
|
}).Info("Updated node version")
|
|
mnd.notifyCallbacks(ip, "version_updated")
|
|
}
|
|
}
|