Skip to content

Commit 4eb0668

Browse files
authored
Add atomic flag coordination for SIGHUP reload handling (#189)
* Add reloadState type with atomic flag to coordinate between signal handler and main loop * Prevent duplicate SIGHUP signals from triggering overlapping reloads * Reset reload flag after completion to allow subsequent reloads * Add comprehensive test suite covering concurrent signals and edge cases * Update signal handler to use atomic CompareAndSwap for thread safety
1 parent 4444354 commit 4eb0668

File tree

2 files changed

+427
-60
lines changed

2 files changed

+427
-60
lines changed

cmd/daemon.go

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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+
156197
func 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.
773819
func (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

Comments
 (0)