Skip to content

Commit d4f844e

Browse files
committed
feat: config hot-reload
1 parent 3cc09c7 commit d4f844e

File tree

2 files changed

+80
-21
lines changed

2 files changed

+80
-21
lines changed

llama-swap.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"flag"
55
"fmt"
66
"log"
7+
"net/http"
78
"os"
89
"os/signal"
910
"path/filepath"
11+
"sync"
1012
"syscall"
1113
"time"
1214

@@ -15,9 +17,14 @@ import (
1517
"github.com/mostlygeek/llama-swap/proxy"
1618
)
1719

18-
var version string = "0"
19-
var commit string = "abcd1234"
20-
var date = "unknown"
20+
var (
21+
version string = "0"
22+
commit string = "abcd1234"
23+
date string = "unknown"
24+
25+
// Global mutex for server operations
26+
serverMux sync.Mutex
27+
)
2128

2229
func main() {
2330
// Define a command-line flag for the port
@@ -58,20 +65,51 @@ func main() {
5865
}
5966
}
6067

68+
// Channel to signal when we want to exit
69+
exitChan := make(chan struct{})
70+
71+
// Set up signal handling
6172
sigChan := make(chan os.Signal, 1)
6273
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
74+
75+
// Handle shutdown signals
6376
go func() {
64-
<-sigChan
65-
fmt.Println("Shutting down llama-swap")
77+
sig := <-sigChan
78+
fmt.Printf("Received signal %v, shutting down...\n", sig)
6679
proxyManager.Shutdown()
67-
os.Exit(0)
80+
close(exitChan)
6881
}()
6982

83+
// Start server in main thread
7084
fmt.Println("llama-swap listening on " + *listenStr)
71-
if err := proxyManager.Run(*listenStr); err != nil {
72-
fmt.Printf("Server error: %v\n", err)
73-
os.Exit(1)
74-
}
85+
go func() {
86+
for {
87+
serverMux.Lock()
88+
runChan := make(chan error)
89+
90+
// Start server in separate goroutine
91+
go func() {
92+
runChan <- proxyManager.Run(*listenStr)
93+
}()
94+
95+
// Wait for server to complete or config reload
96+
err = <-runChan
97+
98+
if err != nil && err != http.ErrServerClosed {
99+
fmt.Printf("Fatal server error: %v\n", err)
100+
serverMux.Unlock()
101+
close(exitChan)
102+
return
103+
}
104+
105+
serverMux.Unlock()
106+
// If we get here, it's because of a normal shutdown (config reload)
107+
// Just continue the loop to let the new server start
108+
}
109+
}()
110+
111+
// Wait for exit signal
112+
<-exitChan
75113
}
76114

77115
// watchConfigFile monitors the configuration file for changes and recreates the ProxyManager with the new config.
@@ -91,15 +129,13 @@ func watchConfigFile(configPath string, pm **proxy.ProxyManager, listenStr *stri
91129

92130
log.Printf("Watching config file for changes: %s", configPath)
93131

94-
// Debounce timer
95132
var debounceTimer *time.Timer
96-
debounceDuration := 2 * time.Second // Adjust as needed
133+
debounceDuration := 2 * time.Second
97134

98135
for {
99136
select {
100137
case event, ok := <-watcher.Events:
101138
if !ok {
102-
log.Println("File watcher channel closed.")
103139
return
104140
}
105141
// We only care about writes to the specific config file
@@ -119,18 +155,20 @@ func watchConfigFile(configPath string, pm **proxy.ProxyManager, listenStr *stri
119155
}
120156

121157
// Stop and cleanup old ProxyManager
158+
serverMux.Lock()
122159
oldPM := *pm
123-
log.Println("Shutting down old ProxyManager instance before reload...")
124160
oldPM.Shutdown()
125161

126162
// Create new ProxyManager with new config
127163
newPM := proxy.New(newConfig)
128164
*pm = newPM
165+
serverMux.Unlock()
129166

130-
// Start serving with new ProxyManager
167+
// Start serving with new ProxyManager (outside the lock)
131168
go func() {
132-
if err := newPM.Run(*listenStr); err != nil {
133-
log.Printf("Server error after config reload: %v", err)
169+
// Try to start the server
170+
if err := newPM.Run(*listenStr); err != nil && err != http.ErrServerClosed {
171+
log.Printf("Error starting server after config reload: %v", err)
134172
}
135173
}()
136174

proxy/proxymanager.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proxy
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -29,6 +30,7 @@ type ProxyManager struct {
2930
config *Config
3031
currentProcesses map[string]*Process
3132
ginEngine *gin.Engine
33+
server *http.Server
3234

3335
// logging
3436
proxyLogger *LogMonitor
@@ -189,7 +191,17 @@ func New(config *Config) *ProxyManager {
189191
}
190192

191193
func (pm *ProxyManager) Run(addr ...string) error {
192-
return pm.ginEngine.Run(addr...)
194+
address := ":8080"
195+
if len(addr) > 0 {
196+
address = addr[0]
197+
}
198+
199+
pm.server = &http.Server{
200+
Addr: address,
201+
Handler: pm.ginEngine,
202+
}
203+
204+
return pm.server.ListenAndServe()
193205
}
194206

195207
func (pm *ProxyManager) HandlerFunc(w http.ResponseWriter, r *http.Request) {
@@ -229,13 +241,22 @@ func (pm *ProxyManager) stopProcesses() {
229241
pm.currentProcesses = make(map[string]*Process)
230242
}
231243

232-
// Shutdown is called to shutdown all upstream processes
233-
// when llama-swap is shutting down.
244+
// Shutdown gracefully shuts down the server and all upstream processes
234245
func (pm *ProxyManager) Shutdown() {
235246
pm.Lock()
236247
defer pm.Unlock()
237248

238-
// shutdown process in parallel
249+
// First, shutdown the HTTP server gracefully
250+
if pm.server != nil {
251+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
252+
defer cancel()
253+
254+
if err := pm.server.Shutdown(ctx); err != nil {
255+
pm.proxyLogger.Errorf("HTTP server Shutdown: %v", err)
256+
}
257+
}
258+
259+
// Then shutdown all processes in parallel
239260
var wg sync.WaitGroup
240261
for _, process := range pm.currentProcesses {
241262
wg.Add(1)

0 commit comments

Comments
 (0)