detached 40d351c

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) {
4444 ProxyURL: proxyURL,
4545 }
4646
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
4755 a, err := cli.NewClientApp(buildInfo)
4856 if err != nil {
4957 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 {
2929 log.Print(err)
3030 }
3131
32- // Initialize and start the reverse proxy
32+ // Initialize and start the reverse proxy using the already loaded configuration
3333 p, err := httpserve.InitializeProxy(a)
3434 if err != nil {
3535 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 proxy32+ // 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 {
1212
1313 // NewClientApp initializes a new App with configuration.
1414 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)
2117 if err != nil {
2218 return nil, fmt.Errorf("failed to load configuration: %w", err)
2319 }
2420
2521 // Initialize App
2622 a := &App{
27- Config: config,
23+ Config: *config,
2824 }
2925
3026 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 AppConfig15+ // 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 App21 // Initialize App
26 a := &App{22 a := &App{
27- Config: config,23+ Config: *config,
28 }24 }
29 25
30 return a, nil26 return a, nil
M internal/common/init_config.go +71 -0
@@ -9,11 +9,19 @@ import (
99 "runtime"
1010 "strconv"
1111 "strings"
12+ "sync"
1213
1314 "github.com/bnema/gordon/pkg/docker"
1415 "github.com/bnema/gordon/pkg/parser"
1516 )
1617
18+// Global singleton configuration instance
19+var (
20+ globalConfig *Config
21+ globalConfigOnce sync.Once
22+ globalConfigMu sync.RWMutex
23+)
24+
1725 type Config struct {
1826 General GeneralConfig `yaml:"General"`
1927 Http HttpConfig `yaml:"Http"`
@@ -83,6 +91,58 @@ var (
8391 // Track whether config logs have been printed already
8492 var configLogsPrinted bool = false
8593
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+
86146 func getConfigDir() (string, error) {
87147 var configDir string
88148
@@ -303,6 +363,17 @@ func loadConfigFromEnv(config *Config, printLogs bool) {
303363 }
304364
305365 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+
306377 configDir, err := getConfigDir()
307378 if err != nil {
308379 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 already91 // Track whether config logs have been printed already
84 var configLogsPrinted bool = false92 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 string147 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 {
6262
6363 // NewProxy creates a new reverse proxy
6464 func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
65+ // Get the configuration from the app interface
66+ // This will use the already loaded global configuration
6567 config := app.GetConfig().ReverseProxy
6668
6769 // Create the routes map
@@ -62,6 +62,8 @@ type requestContext struct {
62 62
63 // NewProxy creates a new reverse proxy63 // 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().ReverseProxy67 config := app.GetConfig().ReverseProxy
66 68
67 // Create the routes map69 // Create the routes map
M internal/server/new_server.go +5 -9
@@ -14,18 +14,14 @@ import (
1414 )
1515
1616 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)
2519 if err != nil {
2620 log.Fatalf("Failed to load configuration: %v", err)
2721 }
2822
23+ log.Info("Starting Gordon server", "version", config.Build.BuildVersion)
24+
2925 // For development environment, use the project root directory
3026 if config.Build.RunEnv == "dev" {
3127 // Get the current working directory as the project root
@@ -68,7 +64,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {
6864 LocYML: bytes,
6965 DBDir: DBDir,
7066 DBFilename: DBFilename,
71- Config: config,
67+ Config: *config,
7268 StartTime: time.Now(),
7369 }
7470
@@ -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 AppConfig17+ // 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 directory25 // 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 root27 // 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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close