e4.0.0 Refactored, file watcher and improved builder, doc update
This commit is contained in:
355
doc/reconfiguration.md
Normal file
355
doc/reconfiguration.md
Normal file
@ -0,0 +1,355 @@
|
||||
# Live Reconfiguration
|
||||
|
||||
The config package supports automatic configuration reloading when files change, enabling zero-downtime reconfiguration.
|
||||
|
||||
## Basic File Watching
|
||||
|
||||
### Enable Auto-Update
|
||||
|
||||
```go
|
||||
cfg, _ := config.NewBuilder().
|
||||
WithDefaults(&Config{}).
|
||||
WithFile("config.toml").
|
||||
Build()
|
||||
|
||||
// Enable automatic reloading
|
||||
cfg.AutoUpdate()
|
||||
|
||||
// Your application continues running
|
||||
// Config reloads automatically when file changes
|
||||
|
||||
// Stop watching when done
|
||||
defer cfg.StopAutoUpdate()
|
||||
```
|
||||
|
||||
### Watch for Changes
|
||||
|
||||
```go
|
||||
// Get notified of configuration changes
|
||||
changes := cfg.Watch()
|
||||
|
||||
go func() {
|
||||
for path := range changes {
|
||||
log.Printf("Configuration changed: %s", path)
|
||||
|
||||
// React to specific changes
|
||||
switch path {
|
||||
case "server.port":
|
||||
// Restart server with new port
|
||||
restartServer()
|
||||
case "log.level":
|
||||
// Update log level
|
||||
updateLogLevel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
## Watch Options
|
||||
|
||||
### Custom Watch Configuration
|
||||
|
||||
```go
|
||||
opts := config.WatchOptions{
|
||||
PollInterval: 500 * time.Millisecond, // Check every 500ms
|
||||
Debounce: 200 * time.Millisecond, // Wait 200ms after changes
|
||||
MaxWatchers: 50, // Limit concurrent watchers
|
||||
ReloadTimeout: 10 * time.Second, // Timeout for reload
|
||||
VerifyPermissions: true, // Security check
|
||||
}
|
||||
|
||||
cfg.AutoUpdateWithOptions(opts)
|
||||
```
|
||||
|
||||
### Watch Without Auto-Update
|
||||
|
||||
```go
|
||||
// Just watch, don't auto-reload
|
||||
changes := cfg.WatchWithOptions(config.WatchOptions{
|
||||
PollInterval: time.Second,
|
||||
})
|
||||
|
||||
// Manually reload when desired
|
||||
go func() {
|
||||
for range changes {
|
||||
if shouldReload() {
|
||||
cfg.LoadFile("config.toml")
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
### Value Changes
|
||||
|
||||
The watcher detects and notifies about:
|
||||
- New values added
|
||||
- Existing values modified
|
||||
- Values removed
|
||||
- Type changes
|
||||
|
||||
```go
|
||||
changes := cfg.Watch()
|
||||
|
||||
for path := range changes {
|
||||
newVal, exists := cfg.Get(path)
|
||||
if !exists {
|
||||
log.Printf("Removed: %s", path)
|
||||
continue
|
||||
}
|
||||
|
||||
sources := cfg.GetSources(path)
|
||||
fileVal, hasFile := sources[config.SourceFile]
|
||||
|
||||
log.Printf("Changed: %s = %v (from file: %v)",
|
||||
path, newVal, hasFile)
|
||||
}
|
||||
```
|
||||
|
||||
### Special Notifications
|
||||
|
||||
```go
|
||||
changes := cfg.Watch()
|
||||
|
||||
for notification := range changes {
|
||||
switch notification {
|
||||
case "file_deleted":
|
||||
log.Warn("Config file was deleted")
|
||||
|
||||
case "permissions_changed":
|
||||
log.Error("Config file permissions changed - potential security issue")
|
||||
|
||||
case "reload_timeout":
|
||||
log.Error("Config reload timed out")
|
||||
|
||||
default:
|
||||
if strings.HasPrefix(notification, "reload_error:") {
|
||||
log.Error("Reload error:", notification)
|
||||
} else {
|
||||
// Normal path change
|
||||
handleConfigChange(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debouncing
|
||||
|
||||
Rapid file changes are automatically debounced:
|
||||
|
||||
```go
|
||||
// Multiple rapid saves to config.toml
|
||||
// Only triggers one reload after debounce period
|
||||
|
||||
opts := config.WatchOptions{
|
||||
PollInterval: 100 * time.Millisecond,
|
||||
Debounce: 500 * time.Millisecond, // Wait 500ms
|
||||
}
|
||||
|
||||
cfg.AutoUpdateWithOptions(opts)
|
||||
```
|
||||
|
||||
## Permission Monitoring
|
||||
|
||||
```go
|
||||
opts := config.WatchOptions{
|
||||
VerifyPermissions: true, // Enabled by default
|
||||
}
|
||||
|
||||
cfg.AutoUpdateWithOptions(opts)
|
||||
|
||||
// Detects if file becomes world-writable
|
||||
changes := cfg.Watch()
|
||||
for change := range changes {
|
||||
if change == "permissions_changed" {
|
||||
// File permissions changed
|
||||
// Possible security breach
|
||||
alert("Config file permissions modified!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern: Reconfiguration
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
listener net.Listener
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *Server) watchConfig() {
|
||||
changes := s.cfg.Watch()
|
||||
|
||||
for path := range changes {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "server."):
|
||||
s.scheduleRestart()
|
||||
|
||||
case path == "log.level":
|
||||
s.updateLogLevel()
|
||||
|
||||
case strings.HasPrefix(path, "feature."):
|
||||
s.reloadFeatures()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) scheduleRestart() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Graceful restart logic
|
||||
log.Info("Scheduling server restart for config changes")
|
||||
// ... drain connections, restart listener ...
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern: Feature Flags
|
||||
|
||||
```go
|
||||
type FeatureFlags struct {
|
||||
cfg *config.Config
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (ff *FeatureFlags) Watch() {
|
||||
changes := ff.cfg.Watch()
|
||||
|
||||
for path := range changes {
|
||||
if strings.HasPrefix(path, "features.") {
|
||||
feature := strings.TrimPrefix(path, "features.")
|
||||
enabled, _ := ff.cfg.Get(path)
|
||||
|
||||
log.Printf("Feature %s: %v", feature, enabled)
|
||||
ff.notifyFeatureChange(feature, enabled.(bool))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *FeatureFlags) IsEnabled(feature string) bool {
|
||||
ff.mu.RLock()
|
||||
defer ff.mu.RUnlock()
|
||||
|
||||
val, exists := ff.cfg.Get("features." + feature)
|
||||
return exists && val.(bool)
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern: Multi-Stage Reload
|
||||
|
||||
```go
|
||||
func watchConfigWithValidation(cfg *config.Config) {
|
||||
changes := cfg.Watch()
|
||||
|
||||
for range changes {
|
||||
// Stage 1: Snapshot current config
|
||||
backup := cfg.Clone()
|
||||
|
||||
// Stage 2: Validate new configuration
|
||||
if err := validateNewConfig(cfg); err != nil {
|
||||
log.Error("Invalid configuration:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Stage 3: Apply changes
|
||||
if err := applyConfigChanges(cfg, backup); err != nil {
|
||||
log.Error("Failed to apply changes:", err)
|
||||
// Could restore from backup here
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Configuration successfully reloaded")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Watch Status
|
||||
|
||||
```go
|
||||
// Check if watching is active
|
||||
if cfg.IsWatching() {
|
||||
log.Printf("Auto-update is enabled")
|
||||
log.Printf("Active watchers: %d", cfg.WatcherCount())
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
|
||||
```go
|
||||
// Limit watchers to prevent resource exhaustion
|
||||
opts := config.WatchOptions{
|
||||
MaxWatchers: 10, // Max 10 concurrent watch channels
|
||||
}
|
||||
|
||||
// Watchers beyond limit receive closed channels
|
||||
cfg.AutoUpdateWithOptions(opts)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Stop Watching**: Use `defer cfg.StopAutoUpdate()` to clean up
|
||||
2. **Handle All Notifications**: Check for special error notifications
|
||||
3. **Validate After Reload**: Ensure new config is valid before applying
|
||||
4. **Use Debouncing**: Prevent reload storms from rapid edits
|
||||
5. **Monitor Permissions**: Enable permission verification for security
|
||||
6. **Graceful Updates**: Plan how your app handles config changes
|
||||
7. **Log Changes**: Audit configuration modifications
|
||||
|
||||
## Limitations
|
||||
|
||||
- File watching uses polling (not inotify/kqueue)
|
||||
- No support for watching multiple files
|
||||
- Changes only detected for registered paths
|
||||
- Reloads entire file (no partial updates)
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Changes Not Detected
|
||||
|
||||
```go
|
||||
// Ensure path is registered before watching
|
||||
cfg.Register("new.value", "default")
|
||||
|
||||
// Now changes to new.value will be detected
|
||||
```
|
||||
|
||||
### Rapid Reloads
|
||||
|
||||
```go
|
||||
// Increase debounce to prevent rapid reloads
|
||||
opts := config.WatchOptions{
|
||||
Debounce: 2 * time.Second, // Wait 2s after changes stop
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Leaks
|
||||
|
||||
```go
|
||||
// Always stop watching to prevent goroutine leaks
|
||||
watcher := cfg.Watch()
|
||||
|
||||
// Use context for cancellation
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case change := <-watcher:
|
||||
handleChange(change)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [File Configuration](file.md) - File format and loading
|
||||
- [Access Patterns](access.md) - Reacting to changed values
|
||||
- [Builder Pattern](builder.md) - Setting up watching with builder
|
||||
Reference in New Issue
Block a user