[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# Viper WatchConfig and Hot Reload
|
||||
|
||||
## Basic setup
|
||||
|
||||
```go
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
log.Printf("config changed: %s (op: %s)", e.Name, e.Op)
|
||||
// re-read affected values and apply them
|
||||
})
|
||||
```
|
||||
|
||||
`WatchConfig` starts a background goroutine that watches the config file using fsnotify. Call it after `ReadInConfig`.
|
||||
|
||||
## The atomic-rename trap
|
||||
|
||||
Most editors (vim, neovim, many CI tools) write config files by creating a new file and then renaming it over the old one. This replaces the inode that fsnotify is watching — the watch may not fire, or may fire with `Op: RENAME` instead of `Op: WRITE`, or may fire twice.
|
||||
|
||||
**Test hot reload with direct file writes, not editor saves:**
|
||||
|
||||
```go
|
||||
// reliable in tests:
|
||||
os.WriteFile("config.yaml", newContent, 0644)
|
||||
|
||||
// unreliable for testing:
|
||||
// opening vim and :w — may trigger RENAME instead of WRITE
|
||||
```
|
||||
|
||||
In production, this is less of an issue if your config management tool (Kubernetes ConfigMap volume mount, Consul Template, etc.) is aware of inode behavior.
|
||||
|
||||
## Race-safe reload pattern
|
||||
|
||||
Config reload happens in a background goroutine. Any shared state updated in `OnConfigChange` must be synchronized:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
MaxConn int `mapstructure:"max_conn"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
var newCfg Config
|
||||
if err := viper.Unmarshal(&newCfg); err != nil {
|
||||
log.Printf("error reloading config: %v", err)
|
||||
return // keep old config on error
|
||||
}
|
||||
|
||||
cfg.mu.Lock()
|
||||
cfg.LogLevel = newCfg.LogLevel
|
||||
cfg.MaxConn = newCfg.MaxConn
|
||||
cfg.mu.Unlock()
|
||||
|
||||
log.Printf("config reloaded: log_level=%s", newCfg.LogLevel)
|
||||
})
|
||||
```
|
||||
|
||||
Reads use `appCfg.mu.RLock()`. Never read directly from viper in hot paths during reload — the window between `OnConfigChange` firing and viper updating its internal state is non-deterministic.
|
||||
|
||||
## Debouncing rapid changes
|
||||
|
||||
Some filesystems fire multiple events per save. Debounce to avoid reloading multiple times:
|
||||
|
||||
```go
|
||||
var reloadTimer *time.Timer
|
||||
var reloadMu sync.Mutex
|
||||
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
reloadMu.Lock()
|
||||
defer reloadMu.Unlock()
|
||||
if reloadTimer != nil {
|
||||
reloadTimer.Stop()
|
||||
}
|
||||
reloadTimer = time.AfterFunc(100*time.Millisecond, func() {
|
||||
applyNewConfig()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Validating config before applying
|
||||
|
||||
Always validate reloaded config before applying it — an invalid config mid-reload should keep the previous working config:
|
||||
|
||||
```go
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
var candidate Config
|
||||
if err := viper.Unmarshal(&candidate); err != nil {
|
||||
log.Printf("reload: invalid config, keeping previous: %v", err)
|
||||
return
|
||||
}
|
||||
if err := validate(candidate); err != nil {
|
||||
log.Printf("reload: validation failed, keeping previous: %v", err)
|
||||
return
|
||||
}
|
||||
applyConfig(candidate)
|
||||
})
|
||||
```
|
||||
|
||||
## Stopping the watcher
|
||||
|
||||
There is no documented way to stop `WatchConfig` once started. Design your application so that the watcher's lifetime matches the process lifetime. For testing, create a new `viper.New()` instance per test — the watcher is per-instance and is garbage-collected with the instance.
|
||||
Reference in New Issue
Block a user