veza/veza-backend-api/internal/config/watcher.go
2025-12-03 20:29:37 +01:00

136 lines
3.3 KiB
Go

package config
import (
"fmt"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"go.uber.org/zap"
)
// ConfigWatcher surveille les fichiers de configuration pour changements (T0040)
type ConfigWatcher struct {
watcher *fsnotify.Watcher
reloader *ConfigReloader
logger *zap.Logger
stopChan chan struct{}
stopOnce sync.Once // Ensures stopChan is closed only once
wg sync.WaitGroup
debounce time.Duration
}
// NewConfigWatcher crée un nouveau watcher de configuration (T0040)
func NewConfigWatcher(reloader *ConfigReloader, logger *zap.Logger) (*ConfigWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("failed to create watcher: %w", err)
}
return &ConfigWatcher{
watcher: watcher,
reloader: reloader,
logger: logger,
stopChan: make(chan struct{}),
stopOnce: sync.Once{}, // Initialize sync.Once
debounce: 500 * time.Millisecond,
}, nil
}
// Watch surveille les fichiers .env pour changements (T0040)
func (w *ConfigWatcher) Watch(envFiles []string) error {
// Ajouter les fichiers à surveiller
for _, file := range envFiles {
// Résoudre le chemin absolu pour éviter les problèmes de chemins relatifs
absPath, err := filepath.Abs(file)
if err != nil {
w.logger.Warn("Failed to resolve absolute path", zap.String("file", file), zap.Error(err))
absPath = file
}
if err := w.watcher.Add(absPath); err != nil {
w.logger.Warn("Failed to watch file", zap.String("file", absPath), zap.Error(err))
continue
}
w.logger.Info("Watching config file", zap.String("file", absPath))
}
w.wg.Add(1)
go w.watchLoop()
return nil
}
// watchLoop boucle principale de surveillance avec debouncing (T0040)
func (w *ConfigWatcher) watchLoop() {
defer w.wg.Done()
var debounceTimer *time.Timer
for {
select {
case event, ok := <-w.watcher.Events:
if !ok {
return
}
// Ignorer les opérations autres que Write et Create
if event.Op&fsnotify.Write == 0 && event.Op&fsnotify.Create == 0 {
continue
}
w.logger.Debug("Config file changed", zap.String("file", event.Name), zap.String("op", event.Op.String()))
// Arrêter le timer précédent si existant
if debounceTimer != nil {
debounceTimer.Stop()
}
// Démarrer un nouveau timer de debounce
debounceTimer = time.NewTimer(w.debounce)
// Goroutine pour attendre le debounce et relancer
go func(fileName string) {
<-debounceTimer.C
w.logger.Info("Config file changed, reloading", zap.String("file", fileName))
if err := w.reloader.ReloadAll(); err != nil {
w.logger.Error("Failed to reload config", zap.Error(err))
} else {
w.logger.Info("Config reloaded successfully")
}
}(event.Name)
case err, ok := <-w.watcher.Errors:
if !ok {
return
}
w.logger.Error("Watcher error", zap.Error(err))
case <-w.stopChan:
// Arrêter le timer si actif
if debounceTimer != nil {
debounceTimer.Stop()
}
return
}
}
}
// Stop arrête la surveillance proprement (T0040)
func (w *ConfigWatcher) Stop() error {
w.stopOnce.Do(func() {
close(w.stopChan)
})
err := w.watcher.Close()
w.wg.Wait()
return err
}
// GetWatchedFiles retourne la liste des fichiers surveillés (T0040)
func (w *ConfigWatcher) GetWatchedFiles() []string {
if w.watcher == nil {
return []string{}
}
return w.watcher.WatchList()
}