/ go-proxy-cache / cmd / rpc-cache / composition_root.go
composition_root.go
  1  package main
  2  
  3  import (
  4  	"fmt"
  5  	"io"
  6  	"os"
  7  
  8  	"go.uber.org/zap"
  9  
 10  	proxyCache "github.com/status-im/proxy-common/cache"
 11  	"github.com/status-im/proxy-common/cache/l1"
 12  	"github.com/status-im/proxy-common/cache/l2"
 13  	"github.com/status-im/proxy-common/cache/noop"
 14  
 15  	"go-proxy-cache/internal/cache"
 16  	"go-proxy-cache/internal/cache/service"
 17  	"go-proxy-cache/internal/cache_rules"
 18  	"go-proxy-cache/internal/config"
 19  	"go-proxy-cache/internal/httpserver"
 20  	"go-proxy-cache/internal/interfaces"
 21  )
 22  
 23  // CompositionRoot holds all application dependencies and provides a centralized
 24  // place for dependency injection and service initialization.
 25  // This pattern helps with:
 26  // - Centralized dependency management
 27  // - Easier testing (can inject mocks)
 28  // - Clear separation of concerns
 29  // - Proper resource cleanup
 30  type CompositionRoot struct {
 31  	// Configuration
 32  	Config     *config.Config
 33  	Logger     *zap.Logger
 34  	CacheRules interfaces.CacheRulesClassifier
 35  
 36  	// Cache components
 37  	L1Cache    proxyCache.Cache
 38  	L2Cache    proxyCache.Cache
 39  	KeyBuilder interfaces.KeyBuilder
 40  	Metrics    *PrometheusMetrics
 41  
 42  	// Services
 43  	CacheService  *service.CacheService
 44  	HTTPServer    *httpserver.Server
 45  	MetricsServer *httpserver.MetricsServer
 46  }
 47  
 48  // NewCompositionRoot creates and initializes all application dependencies.
 49  // It follows the dependency injection pattern where all services are created
 50  // and wired together in the correct order.
 51  //
 52  // Initialization order:
 53  // 1. Logger (needed by all other components)
 54  // 2. Configuration (defines how components should be configured)
 55  // 3. Cache rules (defines caching policies)
 56  // 4. Cache components (L1, L2, KeyBuilder)
 57  // 5. Services (CacheService with metrics)
 58  // 6. HTTP Server (uses all above components)
 59  func NewCompositionRoot() (*CompositionRoot, error) {
 60  	root := &CompositionRoot{}
 61  
 62  	// Initialize logger first
 63  	if err := root.initLogger(); err != nil {
 64  		return nil, fmt.Errorf("failed to initialize logger: %w", err)
 65  	}
 66  
 67  	// Load configuration
 68  	if err := root.loadConfig(); err != nil {
 69  		return nil, fmt.Errorf("failed to load configuration: %w", err)
 70  	}
 71  
 72  	// Load cache rules
 73  	if err := root.loadCacheRules(); err != nil {
 74  		return nil, fmt.Errorf("failed to load cache rules: %w", err)
 75  	}
 76  
 77  	// Initialize cache components
 78  	if err := root.initCacheComponents(); err != nil {
 79  		return nil, fmt.Errorf("failed to initialize cache components: %w", err)
 80  	}
 81  
 82  	// Initialize services
 83  	if err := root.initServices(); err != nil {
 84  		return nil, fmt.Errorf("failed to initialize services: %w", err)
 85  	}
 86  
 87  	// Initialize HTTP server
 88  	if err := root.initHTTPServer(); err != nil {
 89  		return nil, fmt.Errorf("failed to initialize HTTP server: %w", err)
 90  	}
 91  
 92  	// Initialize metrics server
 93  	if err := root.initMetricsServer(); err != nil {
 94  		return nil, fmt.Errorf("failed to initialize metrics server: %w", err)
 95  	}
 96  
 97  	return root, nil
 98  }
 99  
100  // initLogger initializes the application logger
101  func (r *CompositionRoot) initLogger() error {
102  	logger, err := zap.NewProduction()
103  	if err != nil {
104  		return err
105  	}
106  	r.Logger = logger
107  	return nil
108  }
109  
110  // loadConfig loads the application configuration
111  func (r *CompositionRoot) loadConfig() error {
112  	configPath := os.Getenv("CACHE_CONFIG_FILE")
113  	if configPath == "" {
114  		configPath = "/app/cache_config.yaml"
115  	}
116  
117  	cfg, err := config.LoadConfig(configPath, r.Logger)
118  	if err != nil {
119  		return err
120  	}
121  
122  	r.Config = cfg
123  	return nil
124  }
125  
126  // loadCacheRules loads cache rules configuration
127  func (r *CompositionRoot) loadCacheRules() error {
128  	rulesPath := os.Getenv("CACHE_RULES_FILE")
129  	if rulesPath == "" {
130  		rulesPath = "/app/cache_rules.yaml"
131  	}
132  
133  	cacheRules, err := cache_rules.LoadCacheRulesConfig(rulesPath, r.Logger)
134  	if err != nil {
135  		return err
136  	}
137  
138  	// Create classifier from the loaded config
139  	r.CacheRules = cache_rules.NewClassifier(r.Logger, cacheRules)
140  
141  	// Initialize metrics with allowed methods
142  	r.Metrics = NewPrometheusMetrics()
143  	methods := cacheRules.GetAllMethods()
144  	r.Metrics.InitializeAllowedMethods(methods)
145  	r.Logger.Info("Metrics initialized", zap.Int("allowed_methods_count", len(methods)), zap.Strings("methods", methods))
146  
147  	return nil
148  }
149  
150  // initCacheComponents initializes all cache-related components
151  func (r *CompositionRoot) initCacheComponents() error {
152  	// Initialize L1 cache (BigCache)
153  	if err := r.initL1Cache(); err != nil {
154  		return fmt.Errorf("failed to initialize L1 cache: %w", err)
155  	}
156  
157  	// Initialize L2 cache (KeyDB)
158  	if err := r.initL2Cache(); err != nil {
159  		return fmt.Errorf("failed to initialize L2 cache: %w", err)
160  	}
161  
162  	// Initialize key builder
163  	r.KeyBuilder = cache.NewKeyBuilder()
164  
165  	return nil
166  }
167  
168  // initL1Cache initializes the L1 cache (BigCache)
169  func (r *CompositionRoot) initL1Cache() error {
170  	if r.Config.BigCache.Enabled {
171  		l1Cache, err := l1.NewBigCache(
172  			&r.Config.BigCache,
173  			l1.WithLogger(NewZapLogger(r.Logger)),
174  			l1.WithMetrics(r.Metrics),
175  		)
176  		if err != nil {
177  			return err
178  		}
179  		r.L1Cache = l1Cache
180  		r.Logger.Info("BigCache (L1) initialized", zap.Int("size_mb", r.Config.BigCache.Size))
181  	} else {
182  		r.L1Cache = noop.NewNoOpCache()
183  		r.Logger.Info("BigCache (L1) disabled")
184  	}
185  	return nil
186  }
187  
188  // initL2Cache initializes the L2 cache (KeyDB)
189  func (r *CompositionRoot) initL2Cache() error {
190  	if r.Config.KeyDB.Enabled {
191  		keydbURL := GetKeyDBURL(r.Logger)
192  
193  		// Create KeyDB client
194  		keydbClient, err := l2.NewRedisKeyDbClient(
195  			&r.Config.KeyDB,
196  			keydbURL,
197  			l2.WithClientLogger(NewZapLogger(r.Logger)),
198  		)
199  		if err != nil {
200  			r.Logger.Warn("Failed to connect to KeyDB, falling back to no L2 cache",
201  				zap.String("keydb_url", keydbURL),
202  				zap.Error(err))
203  			r.L2Cache = noop.NewNoOpCache()
204  			return nil
205  		}
206  
207  		// Create L2 cache with the client
208  		r.L2Cache = l2.NewKeyDBCache(
209  			&r.Config.KeyDB,
210  			keydbClient,
211  			l2.WithLogger(NewZapLogger(r.Logger)),
212  			l2.WithMetrics(r.Metrics),
213  		)
214  		r.Logger.Info("KeyDB (L2) initialized", zap.String("keydb_url", keydbURL))
215  	} else {
216  		r.L2Cache = noop.NewNoOpCache()
217  		r.Logger.Info("KeyDB (L2) disabled")
218  	}
219  	return nil
220  }
221  
222  // initServices initializes application services
223  func (r *CompositionRoot) initServices() error {
224  	// Initialize cache service
225  	r.CacheService = service.NewCacheService(
226  		r.L1Cache,
227  		r.L2Cache,
228  		r.CacheRules,
229  		r.Config.MultiCache.EnablePropagation,
230  		r.Logger,
231  		r.Metrics,
232  	)
233  
234  	return nil
235  }
236  
237  // initHTTPServer initializes the HTTP server
238  func (r *CompositionRoot) initHTTPServer() error {
239  	r.HTTPServer = httpserver.NewServer(
240  		r.CacheService,
241  		r.Logger,
242  	)
243  
244  	return nil
245  }
246  
247  // initMetricsServer initializes the metrics HTTP server
248  func (r *CompositionRoot) initMetricsServer() error {
249  	r.MetricsServer = httpserver.NewMetricsServer(r.Logger)
250  	return nil
251  }
252  
253  // Cleanup performs cleanup of all resources
254  func (r *CompositionRoot) Cleanup() error {
255  	var errors []error
256  
257  	// Sync logger
258  	if r.Logger != nil {
259  		if err := r.Logger.Sync(); err != nil {
260  			errors = append(errors, fmt.Errorf("failed to sync logger: %w", err))
261  		}
262  	}
263  
264  	// Close L1 cache
265  	if r.L1Cache != nil {
266  		if closer, ok := r.L1Cache.(io.Closer); ok {
267  			if err := closer.Close(); err != nil {
268  				errors = append(errors, fmt.Errorf("failed to close L1 cache: %w", err))
269  			}
270  		}
271  	}
272  
273  	// Close L2 cache
274  	if r.L2Cache != nil {
275  		if closer, ok := r.L2Cache.(io.Closer); ok {
276  			if err := closer.Close(); err != nil {
277  				errors = append(errors, fmt.Errorf("failed to close L2 cache: %w", err))
278  			}
279  		}
280  	}
281  
282  	// Return first error if any
283  	if len(errors) > 0 {
284  		return errors[0]
285  	}
286  
287  	return nil
288  }
289  
290  // GetSocketPath returns the Unix socket path for the server
291  func (r *CompositionRoot) GetSocketPath() string {
292  	socketPath := os.Getenv("CACHE_SOCKET_PATH")
293  	if socketPath == "" {
294  		socketPath = "/tmp/cache.sock"
295  	}
296  	return socketPath
297  }
298  
299  // GetMetricsPort returns the port for the metrics HTTP server
300  func (r *CompositionRoot) GetMetricsPort() string {
301  	port := os.Getenv("CACHE_METRICS_PORT")
302  	if port == "" {
303  		port = "8080"
304  	}
305  	return port
306  }