spf13 / viper

Go configuration with fangs
MIT License
27.14k stars 2.02k forks source link

dangling goroutine #839

Open 7d4b9 opened 4 years ago

7d4b9 commented 4 years ago

Considering the following snippets (end and begin of function WatchConfig)

So my Question is: Why making people believe that the design is standard with kind of "standard like/agnostic construct".

Lines initWG.Done() and eventWG.Wait() should be swapped to be ok but it is not possible without deadlock.

So to be more aligned with good readable goroutine

So here is what I propose to do:

// WatchConfig watches and notifies changes on the file configFile.
func WatchConfig() { v.WatchConfig() }
func (v *Viper) WatchConfig() {
        // 👉 GOROUTINE 0
    // Make sure not to run twice this routine
    v.CancelWatchConfig()

    // 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 {
        log.Printf("error: %v\n", err)
        return
    }

    configFile := filepath.Clean(filename)
    configDir, _ := filepath.Split(configFile)
    realConfigFile, _ := filepath.EvalSymlinks(filename)

    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        log.Fatal(err)
    }
    watcher.Add(configDir)

    // 👉cancellation and waiting point (may be a member of `Viper` object `v.cancelWatchConfig`)
    var watcherGroup sync.WaitGroup
    cancelWatchConfig  := func() {
        watcher.Close()
        watcherGroup.Wait()
    }
    // Process watcher events
    watcherGroup.Add(1)
    go func() {
                 // 👉 GOROUTINE 1
        defer watcherGroup.Done() 
        for event := range watcher.Events {
            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)
            const writeOrCreateMask = fsnotify.Write | fsnotify.Create
            if (filepath.Clean(event.Name) == configFile &&
                event.Op&writeOrCreateMask != 0) ||
                (currentConfigFile != "" && currentConfigFile != realConfigFile) {
                realConfigFile = currentConfigFile
                err := v.ReadInConfig()
                if err != nil {
                    log.Printf("error reading config file: %v\n", err)
                }
                if v.onConfigChange != nil {
                    v.onConfigChange(event)
                }
            } else if filepath.Clean(event.Name) == configFile &&
                event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
                return
            }
        }
    }()
    // Process watcher errors
    watcherGroup.Add(1)
    go func() {
                 // 👉 GOROUTINE 1 (Bis)
        defer watcherGroup.Done()
        for err := range watcher.Errors {
            log.Printf("watcher error: %v\n", err)
        }
    }()
}

This appear to be far away better as for now there is no more dangling and it is now POSSIBLE to cancel the whole watching with cancelWatchConfig

7d4b9 commented 4 years ago

837 Resolves This

7d4b9 commented 4 years ago

This also allows one to call WriteConfig while WatchConfig have been called already because now the WriteConfig can cancel and restart the watching of config file with no race conditions on file access (data corruption) This is resolving #742