package transport import ( "context" "fmt" "os/exec" "strconv" "sync" "time" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) // ProviderPool interface defines common functionality for all provider pools type ProviderPool interface { GetHTTPClient() (*ethclient.Client, error) GetWSClient() (*ethclient.Client, error) GetRPCClient(preferWS bool) (*rpc.Client, error) GetStats() map[string]interface{} Close() error } // ReadOnlyProviderPool manages providers optimized for data fetching and real-time events type ReadOnlyProviderPool struct { providers []*Provider config ProviderPoolConfig providerConfigs map[string]ProviderConfig currentProvider int mutex sync.RWMutex healthTicker *time.Ticker stopChan chan struct{} } // NewReadOnlyProviderPool creates a new read-only provider pool func NewReadOnlyProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*ReadOnlyProviderPool, error) { pool := &ReadOnlyProviderPool{ config: poolConfig, providerConfigs: providerConfigs, stopChan: make(chan struct{}), } if err := pool.initializeProviders(); err != nil { return nil, fmt.Errorf("failed to initialize read-only providers: %w", err) } pool.startHealthChecks() return pool, nil } // initializeProviders sets up all configured providers for read-only operations func (p *ReadOnlyProviderPool) initializeProviders() error { p.providers = make([]*Provider, 0, len(p.config.Providers)) for _, providerName := range p.config.Providers { providerConfig, exists := p.providerConfigs[providerName] if !exists { continue // Skip missing providers } provider, err := p.createReadOnlyProvider(providerConfig) if err != nil { // Log error but continue with other providers continue } p.providers = append(p.providers, provider) } if len(p.providers) == 0 { return fmt.Errorf("no read-only providers successfully initialized") } return nil } // createReadOnlyProvider creates a provider optimized for read-only operations func (p *ReadOnlyProviderPool) createReadOnlyProvider(config ProviderConfig) (*Provider, error) { provider, err := createProvider(config) if err != nil { return nil, err } // For read-only pool, prefer WebSocket connections when available if config.WSEndpoint != "" && provider.WSClient == nil { // Try to establish WebSocket connection again with longer timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() wsClient, err := rpc.DialWebsocket(ctx, config.WSEndpoint, "") if err == nil { provider.WSConn = wsClient provider.WSClient = ethclient.NewClient(wsClient) } } return provider, nil } // GetHTTPClient returns an HTTP client optimized for read-only operations func (p *ReadOnlyProviderPool) GetHTTPClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.HTTPClient == nil { return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name) } return provider.HTTPClient, nil } // GetWSClient returns a WebSocket client for real-time events func (p *ReadOnlyProviderPool) GetWSClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() // Prefer providers with WebSocket support based on strategy if p.config.Strategy == "websocket_preferred" { for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.WSClient != nil { return provider.WSClient, nil } } } // Fallback to any healthy provider with WebSocket provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.WSClient == nil { return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name) } return provider.WSClient, nil } // GetRPCClient returns a raw RPC client, preferring WebSocket for real-time operations func (p *ReadOnlyProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } // For read-only operations, prefer WebSocket when available if (preferWS || p.config.Strategy == "websocket_preferred") && provider.WSConn != nil { return provider.WSConn, nil } if provider.HTTPConn != nil { return provider.HTTPConn, nil } return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name) } // getHealthyProvider returns the next healthy provider based on strategy func (p *ReadOnlyProviderPool) getHealthyProvider() (*Provider, error) { if len(p.providers) == 0 { return nil, fmt.Errorf("no providers available") } switch p.config.Strategy { case "websocket_preferred": return p.getWebSocketPreferredProvider() case "round_robin": return p.getRoundRobinProvider() case "priority_based": return p.getPriorityProvider() default: return p.getWebSocketPreferredProvider() } } // getWebSocketPreferredProvider returns a provider, preferring those with WebSocket support func (p *ReadOnlyProviderPool) getWebSocketPreferredProvider() (*Provider, error) { // First try to find a healthy provider with WebSocket for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.WSClient != nil { return provider, nil } } // Fallback to any healthy provider for _, provider := range p.providers { if p.isProviderUsable(provider) { return provider, nil } } return nil, fmt.Errorf("no healthy providers available") } // getRoundRobinProvider implements round-robin selection for read-only pool func (p *ReadOnlyProviderPool) getRoundRobinProvider() (*Provider, error) { startIndex := p.currentProvider for i := 0; i < len(p.providers); i++ { index := (startIndex + i) % len(p.providers) provider := p.providers[index] if p.isProviderUsable(provider) { p.currentProvider = (index + 1) % len(p.providers) return provider, nil } } return nil, fmt.Errorf("no healthy providers available") } // getPriorityProvider returns the highest priority healthy provider func (p *ReadOnlyProviderPool) getPriorityProvider() (*Provider, error) { var bestProvider *Provider highestPriority := int(^uint(0) >> 1) // Max int for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority { bestProvider = provider highestPriority = provider.Config.Priority } } if bestProvider == nil { return nil, fmt.Errorf("no healthy providers available") } return bestProvider, nil } // isProviderUsable checks if a provider is healthy and within rate limits func (p *ReadOnlyProviderPool) isProviderUsable(provider *Provider) bool { provider.mutex.RLock() defer provider.mutex.RUnlock() // Check health status if p.config.FailoverEnabled && !provider.IsHealthy { return false } // Check rate limit if !provider.RateLimiter.Allow() { return false } return true } // startHealthChecks starts periodic health checking func (p *ReadOnlyProviderPool) startHealthChecks() { interval, err := time.ParseDuration(p.config.HealthCheckInterval) if err != nil { interval = time.Minute // Default to 1 minute } p.healthTicker = time.NewTicker(interval) go p.healthCheckLoop() } // healthCheckLoop periodically checks provider health func (p *ReadOnlyProviderPool) healthCheckLoop() { for { select { case <-p.healthTicker.C: p.performHealthChecks() case <-p.stopChan: return } } } // performHealthChecks checks all providers' health func (p *ReadOnlyProviderPool) performHealthChecks() { for _, provider := range p.providers { go p.checkProviderHealth(provider) } } // checkProviderHealth performs a health check on a single provider func (p *ReadOnlyProviderPool) checkProviderHealth(provider *Provider) { if !provider.Config.HealthCheck.Enabled { return } ctx, cancel := context.WithTimeout(context.Background(), provider.Config.HealthCheck.Timeout) defer cancel() start := time.Now() var err error // For read-only pool, test both HTTP and WebSocket if available if provider.WSClient != nil { _, err = provider.WSClient.BlockNumber(ctx) } else if provider.HTTPClient != nil { _, err = provider.HTTPClient.BlockNumber(ctx) } provider.mutex.Lock() provider.LastHealthCheck = time.Now() provider.IsHealthy = (err == nil) if err == nil { provider.AvgResponseTime = time.Since(start) } provider.mutex.Unlock() } // GetStats returns current read-only provider pool statistics func (p *ReadOnlyProviderPool) GetStats() map[string]interface{} { p.mutex.RLock() defer p.mutex.RUnlock() stats := map[string]interface{}{ "pool_type": "read_only", "strategy": p.config.Strategy, "total_providers": len(p.providers), "providers": make(map[string]interface{}), } healthyCount := 0 wsCount := 0 for _, provider := range p.providers { provider.mutex.RLock() providerStats := map[string]interface{}{ "name": provider.Config.Name, "healthy": provider.IsHealthy, "has_websocket": provider.WSClient != nil, "last_health_check": provider.LastHealthCheck, "request_count": provider.RequestCount, "error_count": provider.ErrorCount, "avg_response_time": provider.AvgResponseTime, } provider.mutex.RUnlock() stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats if provider.IsHealthy { healthyCount++ } if provider.WSClient != nil { wsCount++ } } stats["healthy_providers"] = healthyCount stats["websocket_providers"] = wsCount return stats } // Close shuts down the read-only provider pool func (p *ReadOnlyProviderPool) Close() error { close(p.stopChan) if p.healthTicker != nil { p.healthTicker.Stop() } // Close all connections for _, provider := range p.providers { if provider.HTTPConn != nil { provider.HTTPConn.Close() } if provider.WSConn != nil { provider.WSConn.Close() } } return nil } // ExecutionProviderPool manages providers optimized for transaction execution type ExecutionProviderPool struct { providers []*Provider config ProviderPoolConfig providerConfigs map[string]ProviderConfig currentProvider int mutex sync.RWMutex healthTicker *time.Ticker stopChan chan struct{} } // NewExecutionProviderPool creates a new execution provider pool func NewExecutionProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*ExecutionProviderPool, error) { pool := &ExecutionProviderPool{ config: poolConfig, providerConfigs: providerConfigs, stopChan: make(chan struct{}), } if err := pool.initializeProviders(); err != nil { return nil, fmt.Errorf("failed to initialize execution providers: %w", err) } pool.startHealthChecks() return pool, nil } // initializeProviders sets up all configured providers for execution operations func (p *ExecutionProviderPool) initializeProviders() error { p.providers = make([]*Provider, 0, len(p.config.Providers)) for _, providerName := range p.config.Providers { providerConfig, exists := p.providerConfigs[providerName] if !exists { continue // Skip missing providers } provider, err := p.createExecutionProvider(providerConfig) if err != nil { // Log error but continue with other providers continue } p.providers = append(p.providers, provider) } if len(p.providers) == 0 { return fmt.Errorf("no execution providers successfully initialized") } return nil } // createExecutionProvider creates a provider optimized for transaction execution func (p *ExecutionProviderPool) createExecutionProvider(config ProviderConfig) (*Provider, error) { provider, err := createProvider(config) if err != nil { return nil, err } // For execution pool, ensure HTTP connection is stable and secure if provider.HTTPClient == nil { return nil, fmt.Errorf("execution provider %s must have HTTP client", config.Name) } return provider, nil } // GetHTTPClient returns an HTTP client optimized for transaction execution func (p *ExecutionProviderPool) GetHTTPClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.HTTPClient == nil { return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name) } return provider.HTTPClient, nil } // GetWSClient returns a WebSocket client (execution pool typically uses HTTP) func (p *ExecutionProviderPool) GetWSClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.WSClient == nil { return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name) } return provider.WSClient, nil } // GetRPCClient returns a raw RPC client, preferring HTTP for execution reliability func (p *ExecutionProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } // For execution pool, prefer HTTP for reliability unless explicitly requesting WebSocket if preferWS && provider.WSConn != nil { return provider.WSConn, nil } if provider.HTTPConn != nil { return provider.HTTPConn, nil } return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name) } // getHealthyProvider returns the next healthy provider based on reliability strategy func (p *ExecutionProviderPool) getHealthyProvider() (*Provider, error) { if len(p.providers) == 0 { return nil, fmt.Errorf("no providers available") } switch p.config.Strategy { case "reliability_first": return p.getReliabilityFirstProvider() case "priority_based": return p.getPriorityProvider() case "round_robin": return p.getRoundRobinProvider() default: return p.getReliabilityFirstProvider() } } // getReliabilityFirstProvider returns the most reliable provider func (p *ExecutionProviderPool) getReliabilityFirstProvider() (*Provider, error) { var bestProvider *Provider bestReliability := -1.0 for _, provider := range p.providers { if !p.isProviderUsable(provider) { continue } // Calculate reliability score based on error rate and response time reliability := p.calculateReliabilityScore(provider) if reliability > bestReliability { bestProvider = provider bestReliability = reliability } } if bestProvider == nil { return nil, fmt.Errorf("no healthy providers available") } return bestProvider, nil } // calculateReliabilityScore calculates a reliability score for a provider func (p *ExecutionProviderPool) calculateReliabilityScore(provider *Provider) float64 { provider.mutex.RLock() defer provider.mutex.RUnlock() // Base score on health status if !provider.IsHealthy { return 0.0 } score := 1.0 // Factor in error rate if provider.RequestCount > 0 { errorRate := float64(provider.ErrorCount) / float64(provider.RequestCount) score *= (1.0 - errorRate) } // Factor in response time (prefer faster responses) if provider.AvgResponseTime > 0 { // Normalize response time (consider anything under 1 second as good) responseScore := 1.0 - (float64(provider.AvgResponseTime.Milliseconds()) / 1000.0) if responseScore < 0 { responseScore = 0.1 // Minimum score for slow but working providers } score *= responseScore } return score } // getRoundRobinProvider implements round-robin selection for execution pool func (p *ExecutionProviderPool) getRoundRobinProvider() (*Provider, error) { startIndex := p.currentProvider for i := 0; i < len(p.providers); i++ { index := (startIndex + i) % len(p.providers) provider := p.providers[index] if p.isProviderUsable(provider) { p.currentProvider = (index + 1) % len(p.providers) return provider, nil } } return nil, fmt.Errorf("no healthy providers available") } // getPriorityProvider returns the highest priority healthy provider func (p *ExecutionProviderPool) getPriorityProvider() (*Provider, error) { var bestProvider *Provider highestPriority := int(^uint(0) >> 1) // Max int for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority { bestProvider = provider highestPriority = provider.Config.Priority } } if bestProvider == nil { return nil, fmt.Errorf("no healthy providers available") } return bestProvider, nil } // isProviderUsable checks if a provider is healthy and within rate limits func (p *ExecutionProviderPool) isProviderUsable(provider *Provider) bool { provider.mutex.RLock() defer provider.mutex.RUnlock() // Check health status (critical for execution) if !provider.IsHealthy { return false } // Check rate limit if !provider.RateLimiter.Allow() { return false } return true } // startHealthChecks starts periodic health checking for execution pool func (p *ExecutionProviderPool) startHealthChecks() { interval, err := time.ParseDuration(p.config.HealthCheckInterval) if err != nil { interval = 30 * time.Second // Default to 30 seconds for execution pool } p.healthTicker = time.NewTicker(interval) go p.healthCheckLoop() } // healthCheckLoop periodically checks provider health func (p *ExecutionProviderPool) healthCheckLoop() { for { select { case <-p.healthTicker.C: p.performHealthChecks() case <-p.stopChan: return } } } // performHealthChecks checks all providers' health func (p *ExecutionProviderPool) performHealthChecks() { for _, provider := range p.providers { go p.checkProviderHealth(provider) } } // checkProviderHealth performs a health check on a single provider func (p *ExecutionProviderPool) checkProviderHealth(provider *Provider) { if !provider.Config.HealthCheck.Enabled { return } ctx, cancel := context.WithTimeout(context.Background(), provider.Config.HealthCheck.Timeout) defer cancel() start := time.Now() var err error // For execution pool, test HTTP client reliability if provider.HTTPClient != nil { _, err = provider.HTTPClient.BlockNumber(ctx) } provider.mutex.Lock() provider.LastHealthCheck = time.Now() provider.IsHealthy = (err == nil) if err == nil { provider.AvgResponseTime = time.Since(start) } if err != nil { provider.ErrorCount++ } provider.RequestCount++ provider.mutex.Unlock() } // GetStats returns current execution provider pool statistics func (p *ExecutionProviderPool) GetStats() map[string]interface{} { p.mutex.RLock() defer p.mutex.RUnlock() stats := map[string]interface{}{ "pool_type": "execution", "strategy": p.config.Strategy, "total_providers": len(p.providers), "providers": make(map[string]interface{}), } healthyCount := 0 totalRequests := int64(0) totalErrors := int64(0) for _, provider := range p.providers { provider.mutex.RLock() reliability := p.calculateReliabilityScore(provider) providerStats := map[string]interface{}{ "name": provider.Config.Name, "healthy": provider.IsHealthy, "reliability_score": reliability, "last_health_check": provider.LastHealthCheck, "request_count": provider.RequestCount, "error_count": provider.ErrorCount, "avg_response_time": provider.AvgResponseTime, } provider.mutex.RUnlock() stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats if provider.IsHealthy { healthyCount++ } totalRequests += provider.RequestCount totalErrors += provider.ErrorCount } stats["healthy_providers"] = healthyCount stats["total_requests"] = totalRequests stats["total_errors"] = totalErrors if totalRequests > 0 { stats["error_rate"] = float64(totalErrors) / float64(totalRequests) } return stats } // Close shuts down the execution provider pool func (p *ExecutionProviderPool) Close() error { close(p.stopChan) if p.healthTicker != nil { p.healthTicker.Stop() } // Close all connections for _, provider := range p.providers { if provider.HTTPConn != nil { provider.HTTPConn.Close() } if provider.WSConn != nil { provider.WSConn.Close() } } return nil } // TestingProviderPool manages Anvil forked instances for testing type TestingProviderPool struct { providers []*Provider config ProviderPoolConfig providerConfigs map[string]ProviderConfig currentProvider int mutex sync.RWMutex healthTicker *time.Ticker stopChan chan struct{} // Anvil-specific management anvilProcesses map[string]*exec.Cmd processMutex sync.RWMutex } // NewTestingProviderPool creates a new testing provider pool with Anvil support func NewTestingProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*TestingProviderPool, error) { pool := &TestingProviderPool{ config: poolConfig, providerConfigs: providerConfigs, stopChan: make(chan struct{}), anvilProcesses: make(map[string]*exec.Cmd), } if err := pool.initializeProviders(); err != nil { return nil, fmt.Errorf("failed to initialize testing providers: %w", err) } pool.startHealthChecks() return pool, nil } // initializeProviders sets up all configured providers including Anvil instances func (p *TestingProviderPool) initializeProviders() error { p.providers = make([]*Provider, 0, len(p.config.Providers)) for _, providerName := range p.config.Providers { providerConfig, exists := p.providerConfigs[providerName] if !exists { continue // Skip missing providers } provider, err := p.createTestingProvider(providerConfig) if err != nil { // Log error but continue with other providers continue } p.providers = append(p.providers, provider) } if len(p.providers) == 0 { return fmt.Errorf("no testing providers successfully initialized") } return nil } // createTestingProvider creates a provider, starting Anvil if needed func (p *TestingProviderPool) createTestingProvider(config ProviderConfig) (*Provider, error) { // Start Anvil instance if this is an anvil_fork type if config.Type == "anvil_fork" { if err := p.startAnvilInstance(config); err != nil { return nil, fmt.Errorf("failed to start Anvil instance: %w", err) } // Wait for Anvil to start up time.Sleep(2 * time.Second) } provider, err := createProvider(config) if err != nil { // If we started an Anvil instance, clean it up if config.Type == "anvil_fork" { p.stopAnvilInstance(config.Name) } return nil, err } return provider, nil } // startAnvilInstance starts an Anvil forked instance func (p *TestingProviderPool) startAnvilInstance(config ProviderConfig) error { if config.AnvilConfig == nil { return fmt.Errorf("anvil config is required for anvil_fork provider") } p.processMutex.Lock() defer p.processMutex.Unlock() // Check if already running if _, exists := p.anvilProcesses[config.Name]; exists { return nil // Already running } // Build Anvil command args := []string{ "--fork-url", config.AnvilConfig.ForkURL, "--port", strconv.Itoa(config.AnvilConfig.Port), "--chain-id", strconv.Itoa(config.AnvilConfig.ChainID), "--block-time", strconv.Itoa(config.AnvilConfig.BlockTime), "--accounts", "10", // Create 10 test accounts "--balance", "10000", // Each account gets 10,000 ETH } if config.AnvilConfig.AutoImpersonate { args = append(args, "--auto-impersonate") } // Add state interval if specified if config.AnvilConfig.StateInterval > 0 { args = append(args, "--state-interval", strconv.Itoa(config.AnvilConfig.StateInterval)) } // Start the Anvil process cmd := exec.Command("anvil", args...) cmd.Stdout = nil // Could pipe to logger if needed cmd.Stderr = nil if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start anvil process: %w", err) } p.anvilProcesses[config.Name] = cmd // Start a goroutine to monitor the process go p.monitorAnvilProcess(config.Name, cmd) return nil } // stopAnvilInstance stops an Anvil instance func (p *TestingProviderPool) stopAnvilInstance(name string) error { p.processMutex.Lock() defer p.processMutex.Unlock() cmd, exists := p.anvilProcesses[name] if !exists { return nil // Not running } // Kill the process if err := cmd.Process.Kill(); err != nil { return fmt.Errorf("failed to kill anvil process: %w", err) } // Wait for it to exit cmd.Wait() // Remove from map delete(p.anvilProcesses, name) return nil } // monitorAnvilProcess monitors an Anvil process and restarts if needed func (p *TestingProviderPool) monitorAnvilProcess(name string, cmd *exec.Cmd) { err := cmd.Wait() p.processMutex.Lock() defer p.processMutex.Unlock() // Remove from map delete(p.anvilProcesses, name) // If the process exited unexpectedly, we could restart it here // For now, just log the exit if err != nil { // Process exited with error - could implement restart logic } } // GetHTTPClient returns an HTTP client for testing operations func (p *TestingProviderPool) GetHTTPClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.HTTPClient == nil { return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name) } return provider.HTTPClient, nil } // GetWSClient returns a WebSocket client for testing func (p *TestingProviderPool) GetWSClient() (*ethclient.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } if provider.WSClient == nil { return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name) } return provider.WSClient, nil } // GetRPCClient returns a raw RPC client for advanced testing operations func (p *TestingProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) { p.mutex.RLock() defer p.mutex.RUnlock() provider, err := p.getHealthyProvider() if err != nil { return nil, err } // For testing pool, prefer local connections if preferWS && provider.WSConn != nil { return provider.WSConn, nil } if provider.HTTPConn != nil { return provider.HTTPConn, nil } return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name) } // getHealthyProvider returns the next healthy provider based on anvil strategy func (p *TestingProviderPool) getHealthyProvider() (*Provider, error) { if len(p.providers) == 0 { return nil, fmt.Errorf("no providers available") } switch p.config.Strategy { case "anvil_preferred": return p.getAnvilPreferredProvider() case "priority_based": return p.getPriorityProvider() case "round_robin": return p.getRoundRobinProvider() default: return p.getAnvilPreferredProvider() } } // getAnvilPreferredProvider returns an Anvil provider if available, otherwise any healthy provider func (p *TestingProviderPool) getAnvilPreferredProvider() (*Provider, error) { // First try to find a healthy Anvil provider for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.Config.Type == "anvil_fork" { return provider, nil } } // Fallback to any healthy provider for _, provider := range p.providers { if p.isProviderUsable(provider) { return provider, nil } } return nil, fmt.Errorf("no healthy providers available") } // getRoundRobinProvider implements round-robin selection for testing pool func (p *TestingProviderPool) getRoundRobinProvider() (*Provider, error) { startIndex := p.currentProvider for i := 0; i < len(p.providers); i++ { index := (startIndex + i) % len(p.providers) provider := p.providers[index] if p.isProviderUsable(provider) { p.currentProvider = (index + 1) % len(p.providers) return provider, nil } } return nil, fmt.Errorf("no healthy providers available") } // getPriorityProvider returns the highest priority healthy provider func (p *TestingProviderPool) getPriorityProvider() (*Provider, error) { var bestProvider *Provider highestPriority := int(^uint(0) >> 1) // Max int for _, provider := range p.providers { if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority { bestProvider = provider highestPriority = provider.Config.Priority } } if bestProvider == nil { return nil, fmt.Errorf("no healthy providers available") } return bestProvider, nil } // isProviderUsable checks if a provider is healthy and within rate limits func (p *TestingProviderPool) isProviderUsable(provider *Provider) bool { provider.mutex.RLock() defer provider.mutex.RUnlock() // For testing providers, be more lenient with health checks if p.config.FailoverEnabled && !provider.IsHealthy { // For Anvil instances, check if the process is still running if provider.Config.Type == "anvil_fork" { p.processMutex.RLock() _, isRunning := p.anvilProcesses[provider.Config.Name] p.processMutex.RUnlock() return isRunning } return false } // Check rate limit (testing usually has very high limits) if !provider.RateLimiter.Allow() { return false } return true } // startHealthChecks starts periodic health checking for testing pool func (p *TestingProviderPool) startHealthChecks() { interval, err := time.ParseDuration(p.config.HealthCheckInterval) if err != nil { interval = time.Minute // Default to 1 minute for testing pool } p.healthTicker = time.NewTicker(interval) go p.healthCheckLoop() } // healthCheckLoop periodically checks provider health func (p *TestingProviderPool) healthCheckLoop() { for { select { case <-p.healthTicker.C: p.performHealthChecks() case <-p.stopChan: return } } } // performHealthChecks checks all providers' health func (p *TestingProviderPool) performHealthChecks() { for _, provider := range p.providers { go p.checkProviderHealth(provider) } } // checkProviderHealth performs a health check on a single provider func (p *TestingProviderPool) checkProviderHealth(provider *Provider) { if !provider.Config.HealthCheck.Enabled { return } ctx, cancel := context.WithTimeout(context.Background(), provider.Config.HealthCheck.Timeout) defer cancel() start := time.Now() var err error // For testing pool, test the most appropriate client if provider.HTTPClient != nil { _, err = provider.HTTPClient.BlockNumber(ctx) } else if provider.WSClient != nil { _, err = provider.WSClient.BlockNumber(ctx) } provider.mutex.Lock() provider.LastHealthCheck = time.Now() provider.IsHealthy = (err == nil) if err == nil { provider.AvgResponseTime = time.Since(start) } provider.mutex.Unlock() } // GetStats returns current testing provider pool statistics func (p *TestingProviderPool) GetStats() map[string]interface{} { p.mutex.RLock() defer p.mutex.RUnlock() stats := map[string]interface{}{ "pool_type": "testing", "strategy": p.config.Strategy, "total_providers": len(p.providers), "providers": make(map[string]interface{}), } healthyCount := 0 anvilCount := 0 runningAnvilCount := 0 p.processMutex.RLock() defer p.processMutex.RUnlock() for _, provider := range p.providers { provider.mutex.RLock() isAnvil := provider.Config.Type == "anvil_fork" _, anvilRunning := p.anvilProcesses[provider.Config.Name] providerStats := map[string]interface{}{ "name": provider.Config.Name, "type": provider.Config.Type, "healthy": provider.IsHealthy, "is_anvil": isAnvil, "anvil_running": anvilRunning, "last_health_check": provider.LastHealthCheck, "request_count": provider.RequestCount, "error_count": provider.ErrorCount, "avg_response_time": provider.AvgResponseTime, } if isAnvil && provider.Config.AnvilConfig != nil { providerStats["anvil_port"] = provider.Config.AnvilConfig.Port providerStats["anvil_chain_id"] = provider.Config.AnvilConfig.ChainID providerStats["fork_url"] = provider.Config.AnvilConfig.ForkURL } provider.mutex.RUnlock() stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats if provider.IsHealthy { healthyCount++ } if isAnvil { anvilCount++ if anvilRunning { runningAnvilCount++ } } } stats["healthy_providers"] = healthyCount stats["anvil_providers"] = anvilCount stats["running_anvil_instances"] = runningAnvilCount return stats } // CreateSnapshot creates a state snapshot for testing (if supported) func (p *TestingProviderPool) CreateSnapshot() (string, error) { rpcClient, err := p.GetRPCClient(false) if err != nil { return "", err } var snapshotID string err = rpcClient.Call(&snapshotID, "evm_snapshot") if err != nil { return "", fmt.Errorf("failed to create snapshot: %w", err) } return snapshotID, nil } // RevertToSnapshot reverts to a previously created snapshot func (p *TestingProviderPool) RevertToSnapshot(snapshotID string) error { rpcClient, err := p.GetRPCClient(false) if err != nil { return err } var success bool err = rpcClient.Call(&success, "evm_revert", snapshotID) if err != nil { return fmt.Errorf("failed to revert to snapshot: %w", err) } if !success { return fmt.Errorf("snapshot revert was not successful") } return nil } // Close shuts down the testing provider pool and all Anvil instances func (p *TestingProviderPool) Close() error { close(p.stopChan) if p.healthTicker != nil { p.healthTicker.Stop() } // Stop all Anvil instances p.processMutex.Lock() for name := range p.anvilProcesses { p.stopAnvilInstance(name) } p.processMutex.Unlock() // Close all connections for _, provider := range p.providers { if provider.HTTPConn != nil { provider.HTTPConn.Close() } if provider.WSConn != nil { provider.WSConn.Close() } } return nil }