llama-swap.go: remove debounce, replace fmt.Printlns (#731)

small fixes to clean up the main(): 

- remove the debounced config reload 
- replace fmt.Println with a proxy.LogMonitor for consistency
This commit is contained in:
Benson Wong
2026-05-02 16:28:53 -07:00
committed by GitHub
parent c79114d40a
commit 11b7913287
+74 -72
View File
@@ -4,12 +4,12 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "strings"
"sync"
"syscall" "syscall"
"time" "time"
@@ -34,24 +34,40 @@ func main() {
keyFile := flag.String("tls-key-file", "", "TLS key file") keyFile := flag.String("tls-key-file", "", "TLS key file")
showVersion := flag.Bool("version", false, "show version of build") showVersion := flag.Bool("version", false, "show version of build")
watchConfig := flag.Bool("watch-config", false, "Automatically reload config file on change") watchConfig := flag.Bool("watch-config", false, "Automatically reload config file on change")
mainLogger := proxy.NewLogMonitor()
flag.Parse() // Parse the command-line flags flag.Parse() // Parse the command-line flags
if *showVersion { if *showVersion {
fmt.Printf("version: %s (%s), built at %s\n", version, commit, date) fmt.Printf("version: %s (%s), built at %s", version, commit, date)
os.Exit(0) os.Exit(0)
} }
conf, err := config.LoadConfig(*configPath) conf, err := config.LoadConfig(*configPath)
if err != nil { if err != nil {
fmt.Printf("Error loading config: %v\n", err) mainLogger.Errorf("Error loading config: %", err)
os.Exit(1) os.Exit(1)
} }
if len(conf.Profiles) > 0 { if len(conf.Profiles) > 0 {
fmt.Println("WARNING: Profile functionality has been removed in favor of Groups. See the README for more information.") mainLogger.Warn("Profile functionality has been removed in favor of Groups. See the README for more information.")
} }
switch strings.ToLower(strings.TrimSpace(conf.LogLevel)) {
case "debug":
mainLogger.SetLogLevel(proxy.LevelDebug)
case "info":
mainLogger.SetLogLevel(proxy.LevelInfo)
case "warn":
mainLogger.SetLogLevel(proxy.LevelWarn)
case "error":
mainLogger.SetLogLevel(proxy.LevelError)
default:
mainLogger.SetLogLevel(proxy.LevelInfo)
}
mainLogger.Debugf("PID: %d", os.Getpid())
if mode := os.Getenv("GIN_MODE"); mode != "" { if mode := os.Getenv("GIN_MODE"); mode != "" {
gin.SetMode(mode) gin.SetMode(mode)
} else { } else {
@@ -78,15 +94,7 @@ func main() {
// Setup channels for server management // Setup channels for server management
exitChan := make(chan struct{}) exitChan := make(chan struct{})
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
// Reload signals (SIGHUP on POSIX, none on Windows — Windows does not
// deliver SIGHUP). Always wired up so `kill -HUP` works regardless of
// --watch-config.
reloadChan := make(chan os.Signal, 1)
if runtime.GOOS != "windows" {
signal.Notify(reloadChan, syscall.SIGHUP)
}
// Context that bounds the lifetime of background watcher goroutines. // Context that bounds the lifetime of background watcher goroutines.
watcherCtx, watcherCancel := context.WithCancel(context.Background()) watcherCtx, watcherCancel := context.WithCancel(context.Background())
@@ -97,20 +105,36 @@ func main() {
} }
// Support for watching config and reloading when it changes // Support for watching config and reloading when it changes
reloading := false
var reloadMutex sync.Mutex
reloadProxyManager := func() { reloadProxyManager := func() {
reloadMutex.Lock()
if reloading {
reloadMutex.Unlock()
return
}
reloading = true
reloadMutex.Unlock()
defer func() {
reloadMutex.Lock()
reloading = false
reloadMutex.Unlock()
}()
mainLogger.Info("Reloading Configuration")
if currentPM, ok := srv.Handler.(*proxy.ProxyManager); ok { if currentPM, ok := srv.Handler.(*proxy.ProxyManager); ok {
conf, err = config.LoadConfig(*configPath) conf, err = config.LoadConfig(*configPath)
if err != nil { if err != nil {
fmt.Printf("Warning, unable to reload configuration: %v\n", err) mainLogger.Warnf("Unable to reload configuration: %v", err)
return return
} }
fmt.Println("Configuration Changed") mainLogger.Debug("Configuration Changed")
currentPM.Shutdown() currentPM.Shutdown()
newPM := proxy.New(conf) newPM := proxy.New(conf)
newPM.SetVersion(date, commit, version) newPM.SetVersion(date, commit, version)
srv.Handler = newPM srv.Handler = newPM
fmt.Println("Configuration Reloaded") mainLogger.Debug("Configuration Reloaded")
// wait a few seconds and tell any UI to reload // wait a few seconds and tell any UI to reload
time.AfterFunc(3*time.Second, func() { time.AfterFunc(3*time.Second, func() {
@@ -121,7 +145,7 @@ func main() {
} else { } else {
conf, err = config.LoadConfig(*configPath) conf, err = config.LoadConfig(*configPath)
if err != nil { if err != nil {
fmt.Printf("Error, unable to load configuration: %v\n", err) mainLogger.Errorf("Unable to load configuration: %v", err)
os.Exit(1) os.Exit(1)
} }
newPM := proxy.New(conf) newPM := proxy.New(conf)
@@ -132,94 +156,72 @@ func main() {
// load the initial proxy manager // load the initial proxy manager
reloadProxyManager() reloadProxyManager()
debouncedReload := debounce(time.Second, reloadProxyManager)
// Listen for ConfigFileChangedEvent unconditionally so SIGHUP and the
// poll-based watcher both feed the same debounced reload pipeline. The
// UI also listens for the matching ReloadingStateEnd emitted from
// reloadProxyManager.
defer event.On(func(e proxy.ConfigFileChangedEvent) {
if e.ReloadingState == proxy.ReloadingStateStart {
debouncedReload()
}
})()
// SIGHUP (or platform-equivalent) → reload. Back-to-back signals collapse
// to one reload via the debounce window, which is the desired behavior.
go func() {
for range reloadChan {
fmt.Println("Received reload signal, reloading configuration")
event.Emit(proxy.ConfigFileChangedEvent{
ReloadingState: proxy.ReloadingStateStart,
})
}
}()
if *watchConfig { if *watchConfig {
go func() { go func() {
absConfigPath, err := filepath.Abs(*configPath) absConfigPath, err := filepath.Abs(*configPath)
if err != nil { if err != nil {
fmt.Printf("Error getting absolute path for watching config file: %v\n", err) mainLogger.Errorf("watch-config unable to determine absolute path for watching config file: %v", err)
return return
} }
fmt.Println("Watching configuration for changes (poll-based, 2s interval)") mainLogger.Info("Watching configuration for changes (poll-based, 2s interval)")
(&configwatcher.Watcher{ (&configwatcher.Watcher{
Path: absConfigPath, Path: absConfigPath,
Interval: configwatcher.DefaultInterval, Interval: configwatcher.DefaultInterval,
OnChange: func() { OnChange: func() {
event.Emit(proxy.ConfigFileChangedEvent{ reloadProxyManager()
ReloadingState: proxy.ReloadingStateStart,
})
}, },
}).Run(watcherCtx) }).Run(watcherCtx)
}() }()
} }
// shutdown on signal // Signal handling
go func() { go func() {
sig := <-sigChan for {
fmt.Printf("Received signal %v, shutting down...\n", sig) sig := <-sigChan
watcherCancel() switch sig {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) case syscall.SIGHUP:
defer cancel() mainLogger.Debug("Received SIGHUP")
reloadProxyManager()
case syscall.SIGINT, syscall.SIGTERM:
mainLogger.Debugf("Received signal %v, shutting down...", sig)
watcherCancel()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if pm, ok := srv.Handler.(*proxy.ProxyManager); ok { if pm, ok := srv.Handler.(*proxy.ProxyManager); ok {
pm.Shutdown() pm.Shutdown()
} else { } else {
fmt.Println("srv.Handler is not of type *proxy.ProxyManager") mainLogger.Error("srv.Handler is not of type *proxy.ProxyManager")
} }
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
fmt.Printf("Server shutdown error: %v\n", err) mainLogger.Errorf("Server shutdown: %v", err)
}
close(exitChan)
return
default:
// do nothing on other signals
}
} }
close(exitChan)
}() }()
// Start server // Start server
go func() { go func() {
var err error var err error
if useTLS { if useTLS {
fmt.Printf("llama-swap listening with TLS on https://%s\n", *listenStr) mainLogger.Infof("llama-swap listening with TLS on https://%s", *listenStr)
err = srv.ListenAndServeTLS(*certFile, *keyFile) err = srv.ListenAndServeTLS(*certFile, *keyFile)
} else { } else {
fmt.Printf("llama-swap listening on http://%s\n", *listenStr) mainLogger.Infof("llama-swap listening on http://%s", *listenStr)
err = srv.ListenAndServe() err = srv.ListenAndServe()
} }
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
log.Fatalf("Fatal server error: %v\n", err) mainLogger.Errorf("Fatal server error: %v", err)
os.Exit(1)
} }
}() }()
// Wait for exit signal // Wait for exit signal
<-exitChan <-exitChan
} }
func debounce(interval time.Duration, f func()) func() {
var timer *time.Timer
return func() {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(interval, f)
}
}