mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Route registration, client URL builders, and log lines all move to /v1/ (wallet primary api, chains/rpc handler_manager+chain_integration, xvm client+wallet_client, xsvm api client, multi-network example, manager logs). Zero /ext/ literals remain. One endpoint namespace, no transition alias. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
325 lines
9.3 KiB
Go
325 lines
9.3 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package rpc provides robust RPC handler registration with retries, health checks, and clear debugging.
|
|
// Follows Go principles: fail fast with clear errors, single responsibility, minimal dependencies.
|
|
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/node/server/http"
|
|
)
|
|
|
|
var (
|
|
// Errors follow Go convention: lowercase, descriptive, actionable
|
|
errNilHandler = errors.New("handler is nil")
|
|
errNilServer = errors.New("server is nil")
|
|
errEmptyEndpoint = errors.New("endpoint is empty")
|
|
errRegistrationFailed = errors.New("handler registration failed")
|
|
errHealthCheckFailed = errors.New("health check failed")
|
|
)
|
|
|
|
// HandlerManager manages RPC handler registration with robust error handling and health checks.
|
|
// Single responsibility: reliable handler registration with observability.
|
|
type HandlerManager struct {
|
|
server server.Server
|
|
log log.Logger
|
|
mu sync.RWMutex
|
|
routes map[string]*RouteInfo // chainID -> route info
|
|
retries int // max registration retries
|
|
retryWait time.Duration // initial retry wait time
|
|
}
|
|
|
|
// RouteInfo contains complete information about a registered route.
|
|
// Everything needed for debugging in one place.
|
|
type RouteInfo struct {
|
|
ChainID ids.ID
|
|
ChainAlias string
|
|
Base string // e.g., "bc/C" or "bc/<chainID>"
|
|
Endpoints []string // e.g., ["/rpc", "/ws"]
|
|
Handler http.Handler
|
|
Healthy bool
|
|
LastCheck time.Time
|
|
}
|
|
|
|
// NewHandlerManager creates a handler manager with sensible defaults.
|
|
// Simple factory, no magic.
|
|
func NewHandlerManager(server server.Server, logger log.Logger) *HandlerManager {
|
|
return &HandlerManager{
|
|
server: server,
|
|
log: logger,
|
|
routes: make(map[string]*RouteInfo),
|
|
retries: 3,
|
|
retryWait: 100 * time.Millisecond,
|
|
}
|
|
}
|
|
|
|
// RegisterChainHandlers registers all handlers for a chain with retry logic and health checks.
|
|
// This is the main entry point - handles everything needed for robust registration.
|
|
func (m *HandlerManager) RegisterChainHandlers(
|
|
ctx context.Context,
|
|
chainID ids.ID,
|
|
chainAlias string,
|
|
handlers map[string]http.Handler,
|
|
) error {
|
|
if m.server == nil {
|
|
return errNilServer
|
|
}
|
|
|
|
m.log.Info("Starting chain handler registration",
|
|
log.Stringer("chainID", chainID),
|
|
log.String("alias", chainAlias),
|
|
log.Int("handlerCount", len(handlers)))
|
|
|
|
// Validate handlers first - fail fast
|
|
if err := m.validateHandlers(handlers); err != nil {
|
|
return fmt.Errorf("handler validation failed: %w", err)
|
|
}
|
|
|
|
// Build route info
|
|
info := &RouteInfo{
|
|
ChainID: chainID,
|
|
ChainAlias: chainAlias,
|
|
Endpoints: make([]string, 0, len(handlers)),
|
|
}
|
|
|
|
// Determine base paths
|
|
bases := m.getBasePaths(chainID, chainAlias)
|
|
|
|
// Register each handler with retries
|
|
var registrationErrors []error
|
|
for endpoint, handler := range handlers {
|
|
info.Endpoints = append(info.Endpoints, endpoint)
|
|
|
|
for _, base := range bases {
|
|
if err := m.registerWithRetry(ctx, base, endpoint, handler); err != nil {
|
|
registrationErrors = append(registrationErrors,
|
|
fmt.Errorf("failed to register %s%s: %w", base, endpoint, err))
|
|
m.log.Error("Handler registration failed",
|
|
log.String("base", base),
|
|
log.String("endpoint", endpoint),
|
|
log.Err(err))
|
|
} else {
|
|
m.log.Info("Handler registered successfully",
|
|
log.String("route", fmt.Sprintf("/v1/%s%s", base, endpoint)),
|
|
log.Stringer("chainID", chainID))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store route info for monitoring
|
|
m.mu.Lock()
|
|
info.Base = bases[0] // Primary base
|
|
info.Handler = handlers["/rpc"] // Store primary handler for health checks
|
|
m.routes[chainID.String()] = info
|
|
m.mu.Unlock()
|
|
|
|
// Run health checks
|
|
if err := m.healthCheckRoute(info); err != nil {
|
|
m.log.Warn("Health check failed for newly registered chain",
|
|
log.Stringer("chainID", chainID),
|
|
log.Err(err))
|
|
}
|
|
|
|
// Return aggregate error if any registrations failed
|
|
if len(registrationErrors) > 0 {
|
|
return fmt.Errorf("%w: %v", errRegistrationFailed, registrationErrors)
|
|
}
|
|
|
|
m.log.Info("Chain handler registration completed",
|
|
log.Stringer("chainID", chainID),
|
|
log.String("routes", strings.Join(m.getFullRoutes(bases, info.Endpoints), ", ")))
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateHandlers ensures all handlers are valid before attempting registration.
|
|
// Fail fast with clear errors - no silent failures.
|
|
func (m *HandlerManager) validateHandlers(handlers map[string]http.Handler) error {
|
|
if len(handlers) == 0 {
|
|
return errors.New("no handlers provided")
|
|
}
|
|
|
|
for endpoint, handler := range handlers {
|
|
if handler == nil {
|
|
return fmt.Errorf("%w for endpoint %s", errNilHandler, endpoint)
|
|
}
|
|
if endpoint == "" {
|
|
return errEmptyEndpoint
|
|
}
|
|
// Ensure endpoint starts with /
|
|
if !strings.HasPrefix(endpoint, "/") {
|
|
return fmt.Errorf("endpoint %s must start with /", endpoint)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getBasePaths returns all base paths for a chain (with and without alias).
|
|
// Single source of truth for path construction.
|
|
func (m *HandlerManager) getBasePaths(chainID ids.ID, chainAlias string) []string {
|
|
bases := []string{}
|
|
|
|
// If we have an alias (like "C" for C-Chain), use it as primary
|
|
if chainAlias != "" && chainAlias != chainID.String() {
|
|
bases = append(bases, fmt.Sprintf("bc/%s", chainAlias))
|
|
}
|
|
|
|
// Always include the full chain ID path
|
|
bases = append(bases, fmt.Sprintf("bc/%s", chainID.String()))
|
|
|
|
return bases
|
|
}
|
|
|
|
// getFullRoutes constructs full route paths for logging.
|
|
// Clear, complete information for operators.
|
|
func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []string {
|
|
routes := []string{}
|
|
for _, base := range bases {
|
|
for _, endpoint := range endpoints {
|
|
routes = append(routes, fmt.Sprintf("/v1/%s%s", base, endpoint))
|
|
}
|
|
}
|
|
return routes
|
|
}
|
|
|
|
// registerWithRetry attempts registration with exponential backoff.
|
|
// Handles transient failures gracefully.
|
|
func (m *HandlerManager) registerWithRetry(
|
|
ctx context.Context,
|
|
base string,
|
|
endpoint string,
|
|
handler http.Handler,
|
|
) error {
|
|
wait := m.retryWait
|
|
var lastErr error
|
|
|
|
for attempt := 0; attempt < m.retries; attempt++ {
|
|
// Check context cancellation
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Try registration
|
|
if err := m.server.AddRoute(handler, base, endpoint); err == nil {
|
|
return nil // Success!
|
|
} else {
|
|
lastErr = err
|
|
m.log.Debug("Registration attempt failed, retrying",
|
|
log.Int("attempt", attempt+1),
|
|
log.String("base", base),
|
|
log.String("endpoint", endpoint),
|
|
log.Err(err))
|
|
}
|
|
|
|
// Don't wait after last attempt
|
|
if attempt < m.retries-1 {
|
|
select {
|
|
case <-time.After(wait):
|
|
wait *= 2 // Exponential backoff
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed after %d attempts: %w", m.retries, lastErr)
|
|
}
|
|
|
|
// healthCheckRoute performs a basic health check on a registered route.
|
|
// Validates that handlers are actually responding.
|
|
func (m *HandlerManager) healthCheckRoute(info *RouteInfo) error {
|
|
if info.Handler == nil {
|
|
return fmt.Errorf("no handler to check for chain %s", info.ChainID)
|
|
}
|
|
|
|
// Create a test request
|
|
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Record the response
|
|
recorder := httptest.NewRecorder()
|
|
|
|
// Call the handler
|
|
info.Handler.ServeHTTP(recorder, req)
|
|
|
|
// Check response
|
|
info.LastCheck = time.Now()
|
|
if recorder.Code == http.StatusOK || recorder.Code == http.StatusMethodNotAllowed {
|
|
info.Healthy = true
|
|
m.log.Debug("Health check passed",
|
|
log.Stringer("chainID", info.ChainID),
|
|
log.Int("status", recorder.Code))
|
|
return nil
|
|
}
|
|
|
|
info.Healthy = false
|
|
return fmt.Errorf("%w: status %d", errHealthCheckFailed, recorder.Code)
|
|
}
|
|
|
|
// GetRouteInfo returns information about a registered chain's routes.
|
|
// Useful for debugging and monitoring.
|
|
func (m *HandlerManager) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
info, exists := m.routes[chainID.String()]
|
|
return info, exists
|
|
}
|
|
|
|
// GetAllRoutes returns all registered route information.
|
|
// Complete visibility for operators.
|
|
func (m *HandlerManager) GetAllRoutes() map[string]*RouteInfo {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
// Return a copy to prevent external modification
|
|
routes := make(map[string]*RouteInfo, len(m.routes))
|
|
for k, v := range m.routes {
|
|
routes[k] = v
|
|
}
|
|
return routes
|
|
}
|
|
|
|
// HealthCheckAll performs health checks on all registered routes.
|
|
// Batch operation for monitoring systems.
|
|
func (m *HandlerManager) HealthCheckAll() map[string]bool {
|
|
m.mu.RLock()
|
|
routes := make([]*RouteInfo, 0, len(m.routes))
|
|
for _, info := range m.routes {
|
|
routes = append(routes, info)
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
results := make(map[string]bool)
|
|
for _, info := range routes {
|
|
err := m.healthCheckRoute(info)
|
|
results[info.ChainID.String()] = err == nil
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// SetRetryConfig allows customization of retry behavior.
|
|
// Flexibility for different deployment scenarios.
|
|
func (m *HandlerManager) SetRetryConfig(maxRetries int, initialWait time.Duration) {
|
|
if maxRetries > 0 {
|
|
m.retries = maxRetries
|
|
}
|
|
if initialWait > 0 {
|
|
m.retryWait = initialWait
|
|
}
|
|
}
|