99 "os/signal"
1010 "reflect"
1111 "strings"
12+ "sync/atomic"
1213 "syscall"
1314 "time"
1415
@@ -153,6 +154,46 @@ type intervalFlagConfig struct {
153154 healthCheck string
154155}
155156
157+ // reloadState tracks the daemon's configuration reload state.
158+ //
159+ // The reloading flag is managed in two places:
160+ // - Set to true by handleSignals when SIGHUP is received
161+ // - Reset to false by the main reload loop after reload completes
162+ //
163+ // This ensures only one reload can be in progress at a time.
164+ // Additional SIGHUP signals received while reloading is true are dropped with a warning.
165+ //
166+ // The reloadChan is a buffered channel (size 1) that queues reload requests.
167+ // When handleSignals successfully sets the reloading flag, it sends to this channel.
168+ // The main loop receives from this channel and performs the actual reload operation.
169+ //
170+ // Both fields are unexported as this type is only used internally within the daemon command implementation.
171+ //
172+ // Use newReloadState to construct a properly initialized reloadState with its cleanup function.
173+ type reloadState struct {
174+ reloading atomic.Bool
175+ reloadChan chan struct {}
176+ }
177+
178+ // newReloadState creates a new reloadState with a buffered channel.
179+ // Returns the state and a cleanup function that should be deferred to properly close the reload channel.
180+ //
181+ // Example:
182+ //
183+ // state, cleanup := newReloadState()
184+ // defer cleanup()
185+ func newReloadState () (state * reloadState , cancelFunc func ()) {
186+ state = & reloadState {
187+ reloadChan : make (chan struct {}, 1 ),
188+ }
189+ cancelFunc = func () {
190+ close (state .reloadChan )
191+ }
192+
193+ // Named returns.
194+ return
195+ }
196+
156197func newDaemonCmd (baseCmd * cmd.BaseCmd , cfgLoader config.Loader , ctxLoader configcontext.Loader ) (* DaemonCmd , error ) {
157198 if cfgLoader == nil || reflect .ValueOf (cfgLoader ).IsNil () {
158199 return nil , fmt .Errorf ("config loader cannot be nil" )
@@ -388,8 +429,8 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
388429 defer signal .Stop (sigChan )
389430
390431 // Create reload channel for SIGHUP handling.
391- reloadChan := make ( chan struct {}, 1 )
392- defer close ( reloadChan ) // Ensure channel is closed on function exit
432+ state , cancelState := newReloadState ( )
433+ defer cancelState ()
393434
394435 runErr := make (chan error , 1 )
395436 go func () {
@@ -404,21 +445,22 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
404445 }
405446
406447 // Start signal handling in background.
407- go c .handleSignals (logger , sigChan , reloadChan , shutdownCancel )
448+ go c .handleSignals (logger , sigChan , state , shutdownCancel )
408449
409450 // Start the daemon's main loop which responds to reloads, shutdowns and startup errors.
410451 for {
411452 select {
412- case <- reloadChan :
413- logger .Info ("Reloading servers..." )
453+ case <- state .reloadChan :
414454 if err := c .reloadServers (shutdownCtx , d ); err != nil {
415- logger .Error ("Failed to reload servers, exiting to prevent inconsistent state" , "error" , err )
416- // Signal shutdown to exit cleanly.
417- shutdownCancel ()
418- return fmt .Errorf ("configuration reload failed with unrecoverable error: %w" , err )
455+ logger .Error (
456+ "Failed to reload servers, exiting to prevent inconsistent state" ,
457+ "error" , err ,
458+ )
459+ return fmt .Errorf ("configuration reload failed: %w" , err )
419460 }
420461
421- logger .Info ("Configuration reloaded successfully" )
462+ // Mark reloading as complete.
463+ state .reloading .Store (false )
422464 case <- shutdownCtx .Done ():
423465 logger .Info ("Shutting down daemon..." )
424466 err := <- runErr // Wait for cleanup and deferred logging
@@ -766,26 +808,32 @@ func (c *DaemonCmd) loadConfigCORS(cors *config.CORSConfigSection, logger hclog.
766808// handleSignals processes OS signals for daemon lifecycle management.
767809// This function is intended to be called in a dedicated goroutine.
768810//
769- // SIGHUP signals trigger configuration reloads via reloadChan.
770- // Termination signals (SIGTERM, SIGINT, os.Interrupt) trigger graceful shutdown via shutdownCancel.
771- // The function runs until a shutdown signal is received or sigChan is closed.
772- // Non-blocking sends to reloadChan prevent duplicate reload requests.
811+ // For SIGHUP signals:
812+ // 1. Attempts to set the shared reloading flag from false to true
813+ // 2. If successful, sends to reloadChan for main loop to process
814+ // 3. If flag already true, logs warning about duplicate reload requests and drops the signal
815+ // 4. The main loop is responsible for resetting the flag after a reload is complete
816+ //
817+ // This coordination ensures only one reload runs at a time while allowing
818+ // the actual reload work to happen in the main loop with proper context.
773819func (c * DaemonCmd ) handleSignals (
774820 logger hclog.Logger ,
775821 sigChan <- chan os.Signal ,
776- reloadChan chan <- struct {} ,
822+ state * reloadState ,
777823 shutdownCancel context.CancelFunc ,
778824) {
779825 for sig := range sigChan {
780826 switch sig {
781827 case syscall .SIGHUP :
782- logger .Info ("Received SIGHUP, triggering config reload" )
828+ if ! state .reloading .CompareAndSwap (false , true ) {
829+ logger .Warn ("SIGHUP: reload already in progress, skipping" )
830+ continue
831+ }
783832 select {
784- case reloadChan <- struct {}{}:
785- // Reload signal sent.
833+ case state . reloadChan <- struct {}{}:
834+ logger . Info ( "SIGHUP received, triggering reload" )
786835 default :
787- // Reload already pending, skip.
788- logger .Warn ("Config reload already in progress, skipping" )
836+ logger .Warn ("SIGHUP: reload channel full, skipping" )
789837 }
790838 case os .Interrupt , syscall .SIGTERM , syscall .SIGINT :
791839 logger .Info ("Received shutdown signal" , "signal" , sig )
0 commit comments