Files
spore-gateway/internal/mock/discovery.go
Patrick Balsiger 3c3fb886a3 feat: mock gateway
2025-10-24 14:24:14 +02:00

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")
}
}