refactor: Implement global configuration singleton with thread-safe loading
- Add thread-safe global configuration singleton in `init_config.go` - Modify `GetGlobalConfig()` to ensure single configuration loading - Update client and server app initialization to use global config - Prevent redundant configuration loading across different components - Improve configuration initialization performance and consistency
- commit
- 40d351cf7b80b0c940d7c3b52eaac2d36e27bf5c
- author
- bamen · · 2025-03-09 08:50 UTC
- parent
- 13a9cf1
- signature
- unsigned
6 files · +90 -17
M cmd/gordon.go +8 -0
| @@ -44,6 +44,14 @@ func ExecuteCLI(build, commit, date string) { | |||
| 44 | 44 | ProxyURL: proxyURL, | |
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | + | // Load global configuration once before initializing the apps | |
| 48 | + | _, err := common.GetGlobalConfig(buildInfo) | |
| 49 | + | if err != nil { | |
| 50 | + | fmt.Println("Error initializing global configuration:", err) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // Now initialize the client and server apps, which will use the already loaded config | |
| 47 | 55 | a, err := cli.NewClientApp(buildInfo) | |
| 48 | 56 | if err != nil { | |
| 49 | 57 | fmt.Println("Error initializing app:", err) | |
| @@ -44,6 +44,14 @@ func ExecuteCLI(build, commit, date string) { | |||||
| 44 | ProxyURL: proxyURL, | 44 | ProxyURL: proxyURL, | ||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | + | // Load global configuration once before initializing the apps | |||
| 48 | + | _, err := common.GetGlobalConfig(buildInfo) | |||
| 49 | + | if err != nil { | |||
| 50 | + | fmt.Println("Error initializing global configuration:", err) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // Now initialize the client and server apps, which will use the already loaded config | |||
| 47 | a, err := cli.NewClientApp(buildInfo) | 55 | a, err := cli.NewClientApp(buildInfo) | ||
| 48 | if err != nil { | 56 | if err != nil { | ||
| 49 | fmt.Println("Error initializing app:", err) | 57 | fmt.Println("Error initializing app:", err) | ||
M internal/cli/handler/start_server.go +1 -1
| @@ -29,7 +29,7 @@ func StartServer(a *server.App, port string) error { | |||
| 29 | 29 | log.Print(err) | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | - | // Initialize and start the reverse proxy | |
| 32 | + | // Initialize and start the reverse proxy using the already loaded configuration | |
| 33 | 33 | p, err := httpserve.InitializeProxy(a) | |
| 34 | 34 | if err != nil { | |
| 35 | 35 | log.Error("Failed to initialize reverse proxy:", err) | |
| @@ -29,7 +29,7 @@ func StartServer(a *server.App, port string) error { | |||||
| 29 | log.Print(err) | 29 | log.Print(err) | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | - | // Initialize and start the reverse proxy | 32 | + | // Initialize and start the reverse proxy using the already loaded configuration |
| 33 | p, err := httpserve.InitializeProxy(a) | 33 | p, err := httpserve.InitializeProxy(a) | ||
| 34 | if err != nil { | 34 | if err != nil { | ||
| 35 | log.Error("Failed to initialize reverse proxy:", err) | 35 | log.Error("Failed to initialize reverse proxy:", err) | ||
M internal/cli/new_client.go +3 -7
| @@ -12,19 +12,15 @@ type App struct { | |||
| 12 | 12 | ||
| 13 | 13 | // NewClientApp initializes a new App with configuration. | |
| 14 | 14 | func NewClientApp(buildConfig *common.BuildConfig) (*App, error) { | |
| 15 | - | // Initialize AppConfig | |
| 16 | - | config := common.Config{ | |
| 17 | - | Build: *buildConfig, | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | _, err := config.LoadConfig() | |
| 15 | + | // Get global config singleton instead of loading the config again | |
| 16 | + | config, err := common.GetGlobalConfig(buildConfig) | |
| 21 | 17 | if err != nil { | |
| 22 | 18 | return nil, fmt.Errorf("failed to load configuration: %w", err) | |
| 23 | 19 | } | |
| 24 | 20 | ||
| 25 | 21 | // Initialize App | |
| 26 | 22 | a := &App{ | |
| 27 | - | Config: config, | |
| 23 | + | Config: *config, | |
| 28 | 24 | } | |
| 29 | 25 | ||
| 30 | 26 | return a, nil | |
| @@ -12,19 +12,15 @@ type App struct { | |||||
| 12 | 12 | ||||
| 13 | // NewClientApp initializes a new App with configuration. | 13 | // NewClientApp initializes a new App with configuration. | ||
| 14 | func NewClientApp(buildConfig *common.BuildConfig) (*App, error) { | 14 | func NewClientApp(buildConfig *common.BuildConfig) (*App, error) { | ||
| 15 | - | // Initialize AppConfig | 15 | + | // Get global config singleton instead of loading the config again |
| 16 | - | config := common.Config{ | 16 | + | config, err := common.GetGlobalConfig(buildConfig) |
| 17 | - | Build: *buildConfig, | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | _, err := config.LoadConfig() | |||
| 21 | if err != nil { | 17 | if err != nil { | ||
| 22 | return nil, fmt.Errorf("failed to load configuration: %w", err) | 18 | return nil, fmt.Errorf("failed to load configuration: %w", err) | ||
| 23 | } | 19 | } | ||
| 24 | 20 | ||||
| 25 | // Initialize App | 21 | // Initialize App | ||
| 26 | a := &App{ | 22 | a := &App{ | ||
| 27 | - | Config: config, | 23 | + | Config: *config, |
| 28 | } | 24 | } | ||
| 29 | 25 | ||||
| 30 | return a, nil | 26 | return a, nil | ||
M internal/common/init_config.go +71 -0
| @@ -9,11 +9,19 @@ import ( | |||
| 9 | 9 | "runtime" | |
| 10 | 10 | "strconv" | |
| 11 | 11 | "strings" | |
| 12 | + | "sync" | |
| 12 | 13 | ||
| 13 | 14 | "github.com/bnema/gordon/pkg/docker" | |
| 14 | 15 | "github.com/bnema/gordon/pkg/parser" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| 18 | + | // Global singleton configuration instance | |
| 19 | + | var ( | |
| 20 | + | globalConfig *Config | |
| 21 | + | globalConfigOnce sync.Once | |
| 22 | + | globalConfigMu sync.RWMutex | |
| 23 | + | ) | |
| 24 | + | ||
| 17 | 25 | type Config struct { | |
| 18 | 26 | General GeneralConfig `yaml:"General"` | |
| 19 | 27 | Http HttpConfig `yaml:"Http"` | |
| @@ -83,6 +91,58 @@ var ( | |||
| 83 | 91 | // Track whether config logs have been printed already | |
| 84 | 92 | var configLogsPrinted bool = false | |
| 85 | 93 | ||
| 94 | + | // GetGlobalConfig returns the singleton config instance, loading it if necessary | |
| 95 | + | func GetGlobalConfig(buildConfig *BuildConfig) (*Config, error) { | |
| 96 | + | globalConfigMu.RLock() | |
| 97 | + | if globalConfig != nil { | |
| 98 | + | // If buildConfig is provided, update the Build field | |
| 99 | + | if buildConfig != nil { | |
| 100 | + | globalConfig.Build = *buildConfig | |
| 101 | + | } | |
| 102 | + | config := globalConfig | |
| 103 | + | globalConfigMu.RUnlock() | |
| 104 | + | fmt.Println("Using existing global configuration (already loaded)") | |
| 105 | + | return config, nil | |
| 106 | + | } | |
| 107 | + | globalConfigMu.RUnlock() | |
| 108 | + | ||
| 109 | + | // Need to load the config | |
| 110 | + | globalConfigMu.Lock() | |
| 111 | + | defer globalConfigMu.Unlock() | |
| 112 | + | ||
| 113 | + | // Double-check in case another goroutine loaded the config while we were waiting | |
| 114 | + | if globalConfig != nil { | |
| 115 | + | // If buildConfig is provided, update the Build field | |
| 116 | + | if buildConfig != nil { | |
| 117 | + | globalConfig.Build = *buildConfig | |
| 118 | + | } | |
| 119 | + | fmt.Println("Using existing global configuration (loaded by another goroutine)") | |
| 120 | + | return globalConfig, nil | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | fmt.Println("Loading global configuration for the first time") | |
| 124 | + | ||
| 125 | + | // Initialize config with build info | |
| 126 | + | config := &Config{} | |
| 127 | + | if buildConfig != nil { | |
| 128 | + | config.Build = *buildConfig | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // Load the config once | |
| 132 | + | var err error | |
| 133 | + | globalConfigOnce.Do(func() { | |
| 134 | + | config, err = config.LoadConfig() | |
| 135 | + | if err == nil { | |
| 136 | + | globalConfig = config | |
| 137 | + | fmt.Println("Global configuration loaded successfully") | |
| 138 | + | } else { | |
| 139 | + | fmt.Printf("Error loading global configuration: %v\n", err) | |
| 140 | + | } | |
| 141 | + | }) | |
| 142 | + | ||
| 143 | + | return globalConfig, err | |
| 144 | + | } | |
| 145 | + | ||
| 86 | 146 | func getConfigDir() (string, error) { | |
| 87 | 147 | var configDir string | |
| 88 | 148 | ||
| @@ -303,6 +363,17 @@ func loadConfigFromEnv(config *Config, printLogs bool) { | |||
| 303 | 363 | } | |
| 304 | 364 | ||
| 305 | 365 | func (config *Config) LoadConfig() (*Config, error) { | |
| 366 | + | // Check if we already have a global config loaded | |
| 367 | + | globalConfigMu.RLock() | |
| 368 | + | if globalConfig != nil && config != globalConfig { | |
| 369 | + | // Copy the global config to this config instance | |
| 370 | + | *config = *globalConfig | |
| 371 | + | globalConfigMu.RUnlock() | |
| 372 | + | fmt.Println("Using existing global configuration instead of loading again") | |
| 373 | + | return config, nil | |
| 374 | + | } | |
| 375 | + | globalConfigMu.RUnlock() | |
| 376 | + | ||
| 306 | 377 | configDir, err := getConfigDir() | |
| 307 | 378 | if err != nil { | |
| 308 | 379 | return nil, fmt.Errorf("error getting configuration directory: %w", err) | |
| @@ -9,11 +9,19 @@ import ( | |||||
| 9 | "runtime" | 9 | "runtime" | ||
| 10 | "strconv" | 10 | "strconv" | ||
| 11 | "strings" | 11 | "strings" | ||
| 12 | + | "sync" | |||
| 12 | 13 | ||||
| 13 | "github.com/bnema/gordon/pkg/docker" | 14 | "github.com/bnema/gordon/pkg/docker" | ||
| 14 | "github.com/bnema/gordon/pkg/parser" | 15 | "github.com/bnema/gordon/pkg/parser" | ||
| 15 | ) | 16 | ) | ||
| 16 | 17 | ||||
| 18 | + | // Global singleton configuration instance | |||
| 19 | + | var ( | |||
| 20 | + | globalConfig *Config | |||
| 21 | + | globalConfigOnce sync.Once | |||
| 22 | + | globalConfigMu sync.RWMutex | |||
| 23 | + | ) | |||
| 24 | + | ||||
| 17 | type Config struct { | 25 | type Config struct { | ||
| 18 | General GeneralConfig `yaml:"General"` | 26 | General GeneralConfig `yaml:"General"` | ||
| 19 | Http HttpConfig `yaml:"Http"` | 27 | Http HttpConfig `yaml:"Http"` | ||
| @@ -83,6 +91,58 @@ var ( | |||||
| 83 | // Track whether config logs have been printed already | 91 | // Track whether config logs have been printed already | ||
| 84 | var configLogsPrinted bool = false | 92 | var configLogsPrinted bool = false | ||
| 85 | 93 | ||||
| 94 | + | // GetGlobalConfig returns the singleton config instance, loading it if necessary | |||
| 95 | + | func GetGlobalConfig(buildConfig *BuildConfig) (*Config, error) { | |||
| 96 | + | globalConfigMu.RLock() | |||
| 97 | + | if globalConfig != nil { | |||
| 98 | + | // If buildConfig is provided, update the Build field | |||
| 99 | + | if buildConfig != nil { | |||
| 100 | + | globalConfig.Build = *buildConfig | |||
| 101 | + | } | |||
| 102 | + | config := globalConfig | |||
| 103 | + | globalConfigMu.RUnlock() | |||
| 104 | + | fmt.Println("Using existing global configuration (already loaded)") | |||
| 105 | + | return config, nil | |||
| 106 | + | } | |||
| 107 | + | globalConfigMu.RUnlock() | |||
| 108 | + | ||||
| 109 | + | // Need to load the config | |||
| 110 | + | globalConfigMu.Lock() | |||
| 111 | + | defer globalConfigMu.Unlock() | |||
| 112 | + | ||||
| 113 | + | // Double-check in case another goroutine loaded the config while we were waiting | |||
| 114 | + | if globalConfig != nil { | |||
| 115 | + | // If buildConfig is provided, update the Build field | |||
| 116 | + | if buildConfig != nil { | |||
| 117 | + | globalConfig.Build = *buildConfig | |||
| 118 | + | } | |||
| 119 | + | fmt.Println("Using existing global configuration (loaded by another goroutine)") | |||
| 120 | + | return globalConfig, nil | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | fmt.Println("Loading global configuration for the first time") | |||
| 124 | + | ||||
| 125 | + | // Initialize config with build info | |||
| 126 | + | config := &Config{} | |||
| 127 | + | if buildConfig != nil { | |||
| 128 | + | config.Build = *buildConfig | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // Load the config once | |||
| 132 | + | var err error | |||
| 133 | + | globalConfigOnce.Do(func() { | |||
| 134 | + | config, err = config.LoadConfig() | |||
| 135 | + | if err == nil { | |||
| 136 | + | globalConfig = config | |||
| 137 | + | fmt.Println("Global configuration loaded successfully") | |||
| 138 | + | } else { | |||
| 139 | + | fmt.Printf("Error loading global configuration: %v\n", err) | |||
| 140 | + | } | |||
| 141 | + | }) | |||
| 142 | + | ||||
| 143 | + | return globalConfig, err | |||
| 144 | + | } | |||
| 145 | + | ||||
| 86 | func getConfigDir() (string, error) { | 146 | func getConfigDir() (string, error) { | ||
| 87 | var configDir string | 147 | var configDir string | ||
| 88 | 148 | ||||
| @@ -303,6 +363,17 @@ func loadConfigFromEnv(config *Config, printLogs bool) { | |||||
| 303 | } | 363 | } | ||
| 304 | 364 | ||||
| 305 | func (config *Config) LoadConfig() (*Config, error) { | 365 | func (config *Config) LoadConfig() (*Config, error) { | ||
| 366 | + | // Check if we already have a global config loaded | |||
| 367 | + | globalConfigMu.RLock() | |||
| 368 | + | if globalConfig != nil && config != globalConfig { | |||
| 369 | + | // Copy the global config to this config instance | |||
| 370 | + | *config = *globalConfig | |||
| 371 | + | globalConfigMu.RUnlock() | |||
| 372 | + | fmt.Println("Using existing global configuration instead of loading again") | |||
| 373 | + | return config, nil | |||
| 374 | + | } | |||
| 375 | + | globalConfigMu.RUnlock() | |||
| 376 | + | ||||
| 306 | configDir, err := getConfigDir() | 377 | configDir, err := getConfigDir() | ||
| 307 | if err != nil { | 378 | if err != nil { | ||
| 308 | return nil, fmt.Errorf("error getting configuration directory: %w", err) | 379 | return nil, fmt.Errorf("error getting configuration directory: %w", err) | ||
M internal/proxy/proxy.go +2 -0
| @@ -62,6 +62,8 @@ type requestContext struct { | |||
| 62 | 62 | ||
| 63 | 63 | // NewProxy creates a new reverse proxy | |
| 64 | 64 | func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |
| 65 | + | // Get the configuration from the app interface | |
| 66 | + | // This will use the already loaded global configuration | |
| 65 | 67 | config := app.GetConfig().ReverseProxy | |
| 66 | 68 | ||
| 67 | 69 | // Create the routes map | |
| @@ -62,6 +62,8 @@ type requestContext struct { | |||||
| 62 | 62 | ||||
| 63 | // NewProxy creates a new reverse proxy | 63 | // NewProxy creates a new reverse proxy | ||
| 64 | func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | 64 | func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | ||
| 65 | + | // Get the configuration from the app interface | |||
| 66 | + | // This will use the already loaded global configuration | |||
| 65 | config := app.GetConfig().ReverseProxy | 67 | config := app.GetConfig().ReverseProxy | ||
| 66 | 68 | ||||
| 67 | // Create the routes map | 69 | // Create the routes map | ||
M internal/server/new_server.go +5 -9
| @@ -14,18 +14,14 @@ import ( | |||
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |
| 17 | - | // Initialize AppConfig | |
| 18 | - | config := common.Config{ | |
| 19 | - | Build: *buildConfig, | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | log.Info("Starting Gordon server", "version", config.Build.BuildVersion) | |
| 23 | - | ||
| 24 | - | _, err := config.LoadConfig() | |
| 17 | + | // Get global config singleton instead of loading the config again | |
| 18 | + | config, err := common.GetGlobalConfig(buildConfig) | |
| 25 | 19 | if err != nil { | |
| 26 | 20 | log.Fatalf("Failed to load configuration: %v", err) | |
| 27 | 21 | } | |
| 28 | 22 | ||
| 23 | + | log.Info("Starting Gordon server", "version", config.Build.BuildVersion) | |
| 24 | + | ||
| 29 | 25 | // For development environment, use the project root directory | |
| 30 | 26 | if config.Build.RunEnv == "dev" { | |
| 31 | 27 | // Get the current working directory as the project root | |
| @@ -68,7 +64,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |||
| 68 | 64 | LocYML: bytes, | |
| 69 | 65 | DBDir: DBDir, | |
| 70 | 66 | DBFilename: DBFilename, | |
| 71 | - | Config: config, | |
| 67 | + | Config: *config, | |
| 72 | 68 | StartTime: time.Now(), | |
| 73 | 69 | } | |
| 74 | 70 | ||
| @@ -14,18 +14,14 @@ import ( | |||||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | 16 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | ||
| 17 | - | // Initialize AppConfig | 17 | + | // Get global config singleton instead of loading the config again |
| 18 | - | config := common.Config{ | 18 | + | config, err := common.GetGlobalConfig(buildConfig) |
| 19 | - | Build: *buildConfig, | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | log.Info("Starting Gordon server", "version", config.Build.BuildVersion) | |||
| 23 | - | ||||
| 24 | - | _, err := config.LoadConfig() | |||
| 25 | if err != nil { | 19 | if err != nil { | ||
| 26 | log.Fatalf("Failed to load configuration: %v", err) | 20 | log.Fatalf("Failed to load configuration: %v", err) | ||
| 27 | } | 21 | } | ||
| 28 | 22 | ||||
| 23 | + | log.Info("Starting Gordon server", "version", config.Build.BuildVersion) | |||
| 24 | + | ||||
| 29 | // For development environment, use the project root directory | 25 | // For development environment, use the project root directory | ||
| 30 | if config.Build.RunEnv == "dev" { | 26 | if config.Build.RunEnv == "dev" { | ||
| 31 | // Get the current working directory as the project root | 27 | // Get the current working directory as the project root | ||
| @@ -68,7 +64,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |||||
| 68 | LocYML: bytes, | 64 | LocYML: bytes, | ||
| 69 | DBDir: DBDir, | 65 | DBDir: DBDir, | ||
| 70 | DBFilename: DBFilename, | 66 | DBFilename: DBFilename, | ||
| 71 | - | Config: config, | 67 | + | Config: *config, |
| 72 | StartTime: time.Now(), | 68 | StartTime: time.Now(), | ||
| 73 | } | 69 | } | ||
| 74 | 70 | ||||