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