Skip to content

Commit b6be516

Browse files
rubysclaude
andcommitted
Support reload_config on resume hooks
When a Fly.io machine resumes from suspension and a resume hook specifies reload_config, the configuration is now properly reloaded. This triggers ready hooks (including prerender) to run after the resume hook completes. Changes: - Add configFile and reloadCallback fields to idle.Manager - Use ExecuteServerHooksWithReload for resume hooks - Add resumeReloadChan to ServerLifecycle for async reload - Update all test files with new NewManager signature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ce0fccc commit b6be516

7 files changed

Lines changed: 97 additions & 62 deletions

File tree

cmd/navigator/main.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,16 @@ func main() {
8383
// Create managers
8484
processManager := process.NewManager(cfg)
8585
appManager := process.NewAppManager(cfg)
86-
idleManager := idle.NewManager(cfg)
86+
87+
// Create reload channel for resume hook config reload
88+
resumeReloadChan := make(chan string, 1)
89+
idleManager := idle.NewManager(cfg, configFile, func(path string) {
90+
// Non-blocking send to avoid deadlock if channel is full
91+
select {
92+
case resumeReloadChan <- path:
93+
default:
94+
}
95+
})
8796

8897
// Load authentication if configured
8998
var basicAuth *auth.BasicAuth
@@ -116,12 +125,13 @@ func main() {
116125

117126
// Create and run server lifecycle
118127
lifecycle := &ServerLifecycle{
119-
configFile: configFile,
120-
cfg: cfg,
121-
appManager: appManager,
122-
processManager: processManager,
123-
basicAuth: basicAuth,
124-
idleManager: idleManager,
128+
configFile: configFile,
129+
cfg: cfg,
130+
appManager: appManager,
131+
processManager: processManager,
132+
basicAuth: basicAuth,
133+
idleManager: idleManager,
134+
resumeReloadChan: resumeReloadChan,
125135
}
126136

127137
if err := lifecycle.Run(); err != nil {
@@ -220,15 +230,16 @@ func printHelp() {
220230

221231
// ServerLifecycle manages the HTTP server lifecycle and signal handling
222232
type ServerLifecycle struct {
223-
configFile string
224-
cfg *config.Config
225-
appManager *process.AppManager
226-
processManager *process.Manager
227-
basicAuth *auth.BasicAuth
228-
idleManager *idle.Manager
229-
cableHandler *cable.Handler
230-
srv *http.Server
231-
reloadChan chan string // Channel for triggering config reload from CGI scripts
233+
configFile string
234+
cfg *config.Config
235+
appManager *process.AppManager
236+
processManager *process.Manager
237+
basicAuth *auth.BasicAuth
238+
idleManager *idle.Manager
239+
cableHandler *cable.Handler
240+
srv *http.Server
241+
reloadChan chan string // Channel for triggering config reload from CGI scripts
242+
resumeReloadChan chan string // Channel for triggering config reload from resume hooks
232243
}
233244

234245
// Run starts the server and handles signals until shutdown
@@ -311,6 +322,13 @@ func (l *ServerLifecycle) Run() error {
311322
}
312323
l.handleReload()
313324

325+
case configPath := <-l.resumeReloadChan:
326+
// Resume hook triggered reload
327+
if configPath != l.configFile {
328+
l.configFile = configPath
329+
}
330+
l.handleReload()
331+
314332
case sig := <-sigChan:
315333
switch sig {
316334
case syscall.SIGHUP:
@@ -342,7 +360,7 @@ func (l *ServerLifecycle) handleReload() {
342360
// Update configuration in all managers
343361
l.appManager.UpdateConfig(newConfig)
344362
l.processManager.UpdateManagedProcesses(newConfig)
345-
l.idleManager.UpdateConfig(newConfig)
363+
l.idleManager.UpdateConfig(newConfig, l.configFile)
346364

347365
// Update proxy settings
348366
proxy.SetTrustProxy(newConfig.Server.TrustProxy)

cmd/navigator/main_ready_hook_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ logging:
6363
// Create managers
6464
appManager := process.NewAppManager(cfg)
6565
processManager := process.NewManager(cfg)
66-
idleManager := idle.NewManager(cfg)
66+
idleManager := idle.NewManager(cfg, "", nil)
6767

6868
// Create lifecycle
6969
lifecycle := &ServerLifecycle{

cmd/navigator/main_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func TestHandleConfigReload(t *testing.T) {
263263
// Create real managers to avoid nil pointer issues
264264
appManager := process.NewAppManager(cfg)
265265
processManager := process.NewManager(cfg)
266-
idleManager := idle.NewManager(cfg)
266+
idleManager := idle.NewManager(cfg, "", nil)
267267

268268
// Create lifecycle with nonexistent config file
269269
lifecycle := &ServerLifecycle{
@@ -315,7 +315,7 @@ logging:
315315
// Create real managers to avoid nil pointer issues
316316
appManager := process.NewAppManager(cfg)
317317
processManager := process.NewManager(cfg)
318-
idleManager := idle.NewManager(cfg)
318+
idleManager := idle.NewManager(cfg, "", nil)
319319

320320
// Create lifecycle with valid config file
321321
lifecycle := &ServerLifecycle{
@@ -393,7 +393,7 @@ logging:
393393
t.Error("Expected non-nil app manager")
394394
}
395395

396-
idleManager := idle.NewManager(cfg)
396+
idleManager := idle.NewManager(cfg, "", nil)
397397
if idleManager == nil {
398398
t.Error("Expected non-nil idle manager")
399399
}
@@ -495,7 +495,7 @@ logging:
495495
// Create real managers to avoid nil pointer issues
496496
appManager := process.NewAppManager(cfg)
497497
processManager := process.NewManager(cfg)
498-
idleManager := idle.NewManager(cfg)
498+
idleManager := idle.NewManager(cfg, "", nil)
499499

500500
// Create lifecycle with valid config file
501501
lifecycle := &ServerLifecycle{
@@ -598,7 +598,7 @@ logging:
598598
// Create managers
599599
appManager := process.NewAppManager(cfg)
600600
processManager := process.NewManager(cfg)
601-
idleManager := idle.NewManager(cfg)
601+
idleManager := idle.NewManager(cfg, "", nil)
602602

603603
// Create lifecycle
604604
lifecycle := &ServerLifecycle{
@@ -690,7 +690,7 @@ logging:
690690
// Create real managers to avoid nil pointer issues
691691
appManager := process.NewAppManager(cfg)
692692
processManager := process.NewManager(cfg)
693-
idleManager := idle.NewManager(cfg)
693+
idleManager := idle.NewManager(cfg, "", nil)
694694

695695
// Create lifecycle with valid config file
696696
lifecycle := &ServerLifecycle{

internal/idle/manager.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@ type Manager struct {
2020
mutex sync.RWMutex
2121
timer *time.Timer
2222
config *config.Config
23-
idleActioned bool // Track if idle action was performed
24-
resuming bool // Track if resume hooks are currently running
25-
resumeCond *sync.Cond // Condition variable to wait for resume completion
26-
testMode bool // Prevents actual signal sending during tests
23+
configFile string // Current config file path for reload_config support
24+
reloadCallback func(configPath string) // Callback to trigger config reload
25+
idleActioned bool // Track if idle action was performed
26+
resuming bool // Track if resume hooks are currently running
27+
resumeCond *sync.Cond // Condition variable to wait for resume completion
28+
testMode bool // Prevents actual signal sending during tests
2729
}
2830

2931
// NewManager creates a new idle manager
30-
func NewManager(cfg *config.Config) *Manager {
32+
// The reloadCallback is called when a resume hook specifies reload_config and the config file was modified
33+
func NewManager(cfg *config.Config, configFile string, reloadCallback func(configPath string)) *Manager {
3134
m := &Manager{
32-
config: cfg,
33-
lastActivity: time.Now(),
35+
config: cfg,
36+
configFile: configFile,
37+
reloadCallback: reloadCallback,
38+
lastActivity: time.Now(),
3439
}
3540

3641
// Initialize condition variable
@@ -85,11 +90,22 @@ func (m *Manager) RequestStarted() {
8590
m.idleActioned = false
8691
m.resuming = true
8792

93+
// Capture values needed in goroutine
94+
configFile := m.configFile
95+
reloadCallback := m.reloadCallback
96+
8897
// Execute resume hooks asynchronously
8998
go func() {
9099
slog.Info("Executing server resume hooks")
91-
if err := process.ExecuteServerHooks(m.config.Hooks.Resume, "resume"); err != nil {
92-
slog.Error("Failed to execute resume hooks", "error", err)
100+
result := process.ExecuteServerHooksWithReload(m.config.Hooks.Resume, "resume", configFile)
101+
if result.Error != nil {
102+
slog.Error("Failed to execute resume hooks", "error", result.Error)
103+
} else if result.ReloadDecision.ShouldReload && reloadCallback != nil {
104+
// Resume hook triggered config reload
105+
slog.Info("Resume hook triggered config reload",
106+
"reason", result.ReloadDecision.Reason,
107+
"configFile", result.ReloadDecision.NewConfigFile)
108+
reloadCallback(result.ReloadDecision.NewConfigFile)
93109
}
94110

95111
m.mutex.Lock()
@@ -227,11 +243,12 @@ func (m *Manager) GetStats() (activeRequests int64, lastActivity time.Time) {
227243
}
228244

229245
// UpdateConfig updates the idle manager configuration after a reload
230-
func (m *Manager) UpdateConfig(newConfig *config.Config) {
246+
func (m *Manager) UpdateConfig(newConfig *config.Config, configFile string) {
231247
m.mutex.Lock()
232248
defer m.mutex.Unlock()
233249

234250
m.config = newConfig
251+
m.configFile = configFile
235252

236253
// Re-configure idle settings from new config
237254
if newConfig.Server.Idle.Action != "" && (newConfig.Server.Idle.Action == "suspend" || newConfig.Server.Idle.Action == "stop") {

0 commit comments

Comments
 (0)