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 }