-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Refactor WatchConfig to panic inline instead of os.Exit() in a goroutine on init fail (#2095) #2096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b1b8f81
ed1917e
f86aae5
eb5ac2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,6 @@ import ( | |
| "slices" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/fsnotify/fsnotify" | ||
|
|
@@ -276,80 +275,78 @@ func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) { | |
| } | ||
|
|
||
| // WatchConfig starts watching a config file for changes. | ||
| // If there is an error watching the config file during setup, WatchConfig will panic. | ||
| // Errors encountered after setup, during watching will be logged. | ||
| func WatchConfig() { v.WatchConfig() } | ||
|
|
||
| // WatchConfig starts watching a config file for changes. | ||
| // If there is an error watching the config file during setup, WatchConfig will panic. | ||
| // Errors encountered after setup, during watching will be logged. | ||
| func (v *Viper) WatchConfig() { | ||
| initWG := sync.WaitGroup{} | ||
| initWG.Add(1) | ||
|
|
||
| watcher, err := fsnotify.NewWatcher() | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to create watcher: %w", err) | ||
| v.logger.Error(err.Error()) | ||
| panic(err) | ||
| } | ||
| // we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way | ||
| filename, err := v.getConfigFile() | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to get config file: %w", err) | ||
| v.logger.Error(err.Error()) | ||
|
deefdragon marked this conversation as resolved.
|
||
| watcher.Close() | ||
| panic(err) | ||
| } | ||
|
|
||
| configFile := filepath.Clean(filename) | ||
| configDir := filepath.Dir(configFile) | ||
| realConfigFile, _ := filepath.EvalSymlinks(filename) | ||
|
|
||
| err = watcher.Add(configDir) | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to add config dir to watcher: %w", err) | ||
| v.logger.Error(err.Error()) | ||
| watcher.Close() | ||
| panic(err) | ||
| } | ||
|
|
||
| go func() { | ||
| watcher, err := fsnotify.NewWatcher() | ||
| if err != nil { | ||
| v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err)) | ||
| os.Exit(1) | ||
| } | ||
| defer watcher.Close() | ||
| // we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way | ||
| filename, err := v.getConfigFile() | ||
| if err != nil { | ||
| v.logger.Error(fmt.Sprintf("get config file: %s", err)) | ||
| initWG.Done() | ||
| return | ||
| } | ||
|
|
||
| configFile := filepath.Clean(filename) | ||
| configDir, _ := filepath.Split(configFile) | ||
| realConfigFile, _ := filepath.EvalSymlinks(filename) | ||
|
|
||
| eventsWG := sync.WaitGroup{} | ||
| eventsWG.Add(1) | ||
| go func() { | ||
| for { | ||
| select { | ||
| case event, ok := <-watcher.Events: | ||
| if !ok { // 'Events' channel is closed | ||
| eventsWG.Done() | ||
| return | ||
| for { | ||
| select { | ||
| case event, ok := <-watcher.Events: | ||
| if !ok { // 'Events' channel is closed | ||
| return | ||
| } | ||
| currentConfigFile, _ := filepath.EvalSymlinks(filename) | ||
| // we only care about the config file with the following cases: | ||
| // 1 - if the config file was modified or created | ||
| // 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement) | ||
| if (filepath.Clean(event.Name) == configFile && | ||
| (event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) || | ||
| (currentConfigFile != "" && currentConfigFile != realConfigFile) { | ||
| realConfigFile = currentConfigFile | ||
| err := v.ReadInConfig() | ||
| if err != nil { | ||
| v.logger.Error(fmt.Sprintf("read config file: %s", err)) | ||
| } | ||
| currentConfigFile, _ := filepath.EvalSymlinks(filename) | ||
| // we only care about the config file with the following cases: | ||
| // 1 - if the config file was modified or created | ||
| // 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement) | ||
| if (filepath.Clean(event.Name) == configFile && | ||
| (event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) || | ||
| (currentConfigFile != "" && currentConfigFile != realConfigFile) { | ||
| realConfigFile = currentConfigFile | ||
| err := v.ReadInConfig() | ||
| if err != nil { | ||
| v.logger.Error(fmt.Sprintf("read config file: %s", err)) | ||
| } | ||
| if v.onConfigChange != nil { | ||
| v.onConfigChange(event) | ||
| } | ||
| } else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) { | ||
| eventsWG.Done() | ||
| return | ||
| if v.onConfigChange != nil { | ||
| v.onConfigChange(event) | ||
| } | ||
|
|
||
| case err, ok := <-watcher.Errors: | ||
| if ok { // 'Errors' channel is not closed | ||
| v.logger.Error(fmt.Sprintf("watcher error: %s", err)) | ||
| } | ||
| eventsWG.Done() | ||
| } else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) { | ||
| return | ||
| } | ||
|
|
||
| case err, ok := <-watcher.Errors: | ||
| if ok { // 'Errors' channel is not closed | ||
| v.logger.Error(fmt.Sprintf("watcher error: %s", err)) | ||
| } | ||
| return | ||
| } | ||
| }() | ||
| err = watcher.Add(configDir) | ||
| if err != nil { | ||
| v.logger.Error(fmt.Sprintf("failed to add watcher: %s", err)) | ||
| initWG.Done() | ||
| return | ||
| } | ||
| initWG.Done() // done initializing the watch in this go routine, so the parent routine can move on... | ||
| eventsWG.Wait() // now, wait for event loop to end in this go-routine... | ||
| }() | ||
| initWG.Wait() // make sure that the go routine above fully ended before returning | ||
|
Comment on lines
-350
to
-352
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised by the changes about wait group here, and all the changes you did in the file to do not use waitgroup I would have kept the change minimal to support removal the os.Exit, and that's it. The other changes, which are good, I feel, are making the current PR uneasy to review. I would have split this in 2 PRs one that can be easily be merged (the thing about panic), and a refactoring PR that could be reviewed and merged separately. I'm not suggesting to change anything right now. I report here how I would have done it. Also, I can be simply wrong and the changes here are somehow needed to address the os.Exit issue.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really didn't want to refactor the wait groups & goroutines as I hate the resulting diff due to the tabbing. Unfortunately I honestly felt that it was necessary. 1: It was what allowed the panic calls to propagate up a reasonable stack (being recoverable by the calling user), and 2: it leaves the code in a much more simplified, and thus more readable/maintainable state. As was with the wait groups, it was literally just synchronous code made excessively complicated to allow being lazy with the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for confirming what I thought about the fact the code had no need for such asynchronous thing. You are right about the stack in the panic, it makes sense. So here, I would like to suggest you to split the first commit in two.
(They could be inverted of course) This way the PR diff will stay unchanged, but at least the history would be way clearer |
||
|
|
||
| } | ||
|
|
||
| // SetConfigFile explicitly defines the path, name and extension of the config file. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.