From a826216d1b23dd5fa55fa8858e27b295b1c36b53 Mon Sep 17 00:00:00 2001 From: PS Narayanan Date: Tue, 20 May 2025 10:51:41 +0530 Subject: [PATCH] Add fsnotify-based config hot reload --- go.mod | 5 +-- pkg/config/config.go | 84 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index c53f168..3e14935 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/gorilla/mux v1.8.0 github.com/lib/pq v1.10.9 github.com/oschwald/geoip2-golang v1.9.0 - github.com/slack-go/slack v0.12.2 - gorm.io/driver/postgres v1.5.2 + github.com/slack-go/slack v0.12.2 + github.com/fsnotify/fsnotify v1.6.0 + gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.4 ) diff --git a/pkg/config/config.go b/pkg/config/config.go index 82820fa..56b5543 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,6 +5,10 @@ import ( "io" "log" "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" ) // PluginConfig represents the configuration for a single plugin @@ -37,6 +41,69 @@ type Config struct { Endpoints []Endpoint `json:"endpoints"` } +var ( + current *Config + mu sync.RWMutex +) + +func loadConfigFromFile(path string) (*Config, error) { + configJsonFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer configJsonFile.Close() + + byteData, err := io.ReadAll(configJsonFile) + if err != nil { + return nil, err + } + + var cfg Config + if err := json.Unmarshal(byteData, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func watchConfigFile(path string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Error creating watcher: %v", err) + return + } + defer watcher.Close() + + dir := filepath.Dir(path) + if err := watcher.Add(dir); err != nil { + log.Printf("Error watching config directory: %v", err) + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&(fsnotify.Write|fsnotify.Create) != 0 && filepath.Clean(event.Name) == filepath.Clean(path) { + log.Printf("Configuration file changed. Reloading\n") + if cfg, err := loadConfigFromFile(path); err != nil { + log.Printf("Failed to reload configuration: %v", err) + } else { + mu.Lock() + *current = *cfg + mu.Unlock() + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("Watcher error: %v", err) + } + } +} + // Init initializes the configuration from a file. // The config file path can be set dynamically using environment variables. // The default is assumed to be `config.json` in the same directory. @@ -47,18 +114,17 @@ func Init() *Config { } log.Printf("Reading configuration file %s\n", configFilePath) - configJsonFile, err := os.Open(configFilePath) - if err != nil { - panic(err) - } - defer configJsonFile.Close() - byteData, err := io.ReadAll(configJsonFile) + cfg, err := loadConfigFromFile(configFilePath) if err != nil { panic(err) } - var config Config - json.Unmarshal(byteData, &config) + mu.Lock() + current = cfg + mu.Unlock() + + go watchConfigFile(configFilePath) + log.Printf("Configuration file loaded.\n") - return &config + return current }