From 914f6aa83346bb83d2b8168a2c9858dd16e89bc7 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 7 Feb 2026 13:42:55 -0600 Subject: [PATCH 1/5] mess around with being able to reload router --- cmd/showbridge/main.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/showbridge/main.go b/cmd/showbridge/main.go index 1ddd4c1..b88039f 100644 --- a/cmd/showbridge/main.go +++ b/cmd/showbridge/main.go @@ -9,6 +9,7 @@ import ( "os/signal" "slices" "sync" + "syscall" "github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go/internal/config" @@ -23,7 +24,8 @@ import ( ) var ( - version = "dev" + version = "dev" + sigHangup = make(chan os.Signal, 1) ) func main() { @@ -71,6 +73,8 @@ func main() { } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + + signal.Notify(sigHangup, syscall.SIGHUP) defer cancel() err := cmd.Run(ctx, os.Args) @@ -180,6 +184,40 @@ func run(ctx context.Context, c *cli.Command) error { router.Start(context.Background()) }) + go func() { + for { + select { + case <-sigHangup: + commandLogger.Info("received SIGHUP, reloading configuration") + newConfig, err := readConfig(configPath) + if err != nil { + commandLogger.Error("error reading config file", "error", err) + continue + } + newRouter, moduleErrors, routeErrors := showbridge.NewRouter(newConfig, tracer) + + for _, moduleError := range moduleErrors { + commandLogger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) + } + + for _, routeError := range routeErrors { + commandLogger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) + } + + if moduleErrors == nil && routeErrors == nil { + router.Stop() + routerRunner.Wait() + router = newRouter + routerRunner.Go(func() { + router.Start(context.Background()) + }) + } + case <-ctx.Done(): + return + } + } + }() + <-ctx.Done() commandLogger.Debug("shutting down router") router.Stop() From bd89df3da2625d2e40561f4777b7987d4c92070b Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 7 Feb 2026 14:15:24 -0600 Subject: [PATCH 2/5] create app struct --- cmd/showbridge/main.go | 128 +++++++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/cmd/showbridge/main.go b/cmd/showbridge/main.go index b88039f..e2f1f09 100644 --- a/cmd/showbridge/main.go +++ b/cmd/showbridge/main.go @@ -84,6 +84,15 @@ func main() { } +type showbridgeApp struct { + ctx context.Context + configPath string + logger *slog.Logger + router *showbridge.Router + routerRunner *sync.WaitGroup + tracer trace.Tracer +} + func readConfig(configPath string) (config.Config, error) { cfg := config.Config{} @@ -104,12 +113,7 @@ func readConfig(configPath string) (config.Config, error) { func run(ctx context.Context, c *cli.Command) error { configPath := c.String("config") if configPath == "" { - return errors.New("config value cannot be empty") - } - - config, err := readConfig(configPath) - if err != nil { - return err + return errors.New("config path cannot be empty") } logLevel := slog.LevelInfo @@ -150,8 +154,6 @@ func run(ctx context.Context, c *cli.Command) error { slog.SetDefault(slog.New(logHandler)) - commandLogger := slog.Default().With("component", "cmd") - var tracer trace.Tracer if c.Bool("trace") { exporter, err := otlptracehttp.New(ctx) @@ -168,64 +170,80 @@ func run(ctx context.Context, c *cli.Command) error { tracer = otel.Tracer("showbridge") } - router, moduleErrors, routeErrors := showbridge.NewRouter(config, tracer) - - for _, moduleError := range moduleErrors { - commandLogger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) + showbridgeApp := &showbridgeApp{ + ctx: ctx, + configPath: configPath, + logger: slog.Default().With("component", "cmd"), + routerRunner: &sync.WaitGroup{}, + tracer: tracer, } - for _, routeError := range routeErrors { - commandLogger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) + router, err := showbridgeApp.getNewRouter() + if err != nil { + return fmt.Errorf("failed to initialize router: %w", err) } + showbridgeApp.router = router - routerRunner := sync.WaitGroup{} - - routerRunner.Go(func() { + showbridgeApp.routerRunner.Go(func() { router.Start(context.Background()) }) - go func() { - for { - select { - case <-sigHangup: - commandLogger.Info("received SIGHUP, reloading configuration") - newConfig, err := readConfig(configPath) - if err != nil { - commandLogger.Error("error reading config file", "error", err) - continue - } - newRouter, moduleErrors, routeErrors := showbridge.NewRouter(newConfig, tracer) + go showbridgeApp.handleHangup() - for _, moduleError := range moduleErrors { - commandLogger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) - } - - for _, routeError := range routeErrors { - commandLogger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) - } - - if moduleErrors == nil && routeErrors == nil { - router.Stop() - routerRunner.Wait() - router = newRouter - routerRunner.Go(func() { - router.Start(context.Background()) - }) - } - case <-ctx.Done(): - return - } - } - }() - - <-ctx.Done() - commandLogger.Debug("shutting down router") - router.Stop() - commandLogger.Debug("waiting for router to exit") - routerRunner.Wait() + <-showbridgeApp.ctx.Done() + showbridgeApp.logger.Debug("shutting down router") + showbridgeApp.router.Stop() + showbridgeApp.logger.Debug("waiting for router to exit") + showbridgeApp.routerRunner.Wait() return nil } +func (app *showbridgeApp) handleHangup() { + for { + select { + case <-sigHangup: + app.logger.Info("received SIGHUP, reloading configuration") + newRouter, err := app.getNewRouter() + if err != nil { + app.logger.Error("failed to reload configuration", "error", err) + continue + } + app.router.Stop() + app.routerRunner.Wait() + app.router = newRouter + app.routerRunner.Go(func() { + app.router.Start(context.Background()) + }) + app.logger.Info("configuration reloaded successfully") + case <-app.ctx.Done(): + return + } + } +} + +func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) { + config, err := readConfig(app.configPath) + if err != nil { + return nil, err + } + + router, moduleErrors, routeErrors := showbridge.NewRouter(config, app.tracer) + + for _, moduleError := range moduleErrors { + app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) + } + + for _, routeError := range routeErrors { + app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) + } + + if moduleErrors != nil || routeErrors != nil { + return nil, fmt.Errorf("errors initializing modules or routes") + } + + return router, nil +} + func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { r, err := resource.Merge( resource.Default(), From 5e957d7d03cf2e8de1cd51e48c9c61be1a194042 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 7 Feb 2026 16:28:06 -0600 Subject: [PATCH 3/5] expose running config from router --- router.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/router.go b/router.go index 108a985..bc1052e 100644 --- a/router.go +++ b/router.go @@ -24,6 +24,7 @@ type Router struct { moduleWait sync.WaitGroup logger *slog.Logger tracer trace.Tracer + runningConfig config.Config } func (r *Router) addModule(moduleDecl config.ModuleConfig) error { @@ -107,6 +108,7 @@ func NewRouter(config config.Config, tracer trace.Tracer) (*Router, []module.Mod RouteInstances: []route.Route{}, logger: slog.Default().With("component", "router"), tracer: tracer, + runningConfig: config, } router.logger.Debug("creating") @@ -250,3 +252,7 @@ func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload return nil } + +func (r *Router) RunningConfig() config.Config { + return r.runningConfig +} From 7fa3b6d33deaa1179c27d68c2fdf380b65747434 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 7 Feb 2026 16:29:49 -0600 Subject: [PATCH 4/5] TODO --- cmd/showbridge/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/showbridge/main.go b/cmd/showbridge/main.go index e2f1f09..4a664ea 100644 --- a/cmd/showbridge/main.go +++ b/cmd/showbridge/main.go @@ -222,6 +222,7 @@ func (app *showbridgeApp) handleHangup() { } func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) { + // TODO(jwetzell): what should happen when the config file is unchanged? config, err := readConfig(app.configPath) if err != nil { return nil, err From 725e8ad67043df01edba338c15457233c74003c5 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 7 Feb 2026 17:07:18 -0600 Subject: [PATCH 5/5] add mutex for router operations --- cmd/showbridge/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/showbridge/main.go b/cmd/showbridge/main.go index 4a664ea..aa1cb81 100644 --- a/cmd/showbridge/main.go +++ b/cmd/showbridge/main.go @@ -91,6 +91,7 @@ type showbridgeApp struct { router *showbridge.Router routerRunner *sync.WaitGroup tracer trace.Tracer + routerMutex sync.Mutex } func readConfig(configPath string) (config.Config, error) { @@ -182,11 +183,13 @@ func run(ctx context.Context, c *cli.Command) error { if err != nil { return fmt.Errorf("failed to initialize router: %w", err) } + showbridgeApp.routerMutex.Lock() showbridgeApp.router = router showbridgeApp.routerRunner.Go(func() { router.Start(context.Background()) }) + showbridgeApp.routerMutex.Unlock() go showbridgeApp.handleHangup() @@ -208,6 +211,7 @@ func (app *showbridgeApp) handleHangup() { app.logger.Error("failed to reload configuration", "error", err) continue } + app.routerMutex.Lock() app.router.Stop() app.routerRunner.Wait() app.router = newRouter @@ -215,6 +219,7 @@ func (app *showbridgeApp) handleHangup() { app.router.Start(context.Background()) }) app.logger.Info("configuration reloaded successfully") + app.routerMutex.Unlock() case <-app.ctx.Done(): return }