Skip to content

Commit 2194f64

Browse files
committed
feat: config hot-reload
1 parent ac7bf0e commit 2194f64

File tree

3 files changed

+73
-22
lines changed

3 files changed

+73
-22
lines changed

llama-swap.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ func main() {
7878
for {
7979
select {
8080
case newManager := <-reloadChan:
81-
// Stop old manager processes and swap handler
81+
log.Println("Config change detected, waiting for in-flight requests to complete...")
82+
// Stop old manager processes gracefully (this waits for in-flight requests)
83+
currentManager.StopProcesses()
84+
// Now do a full shutdown to clear the process map
8285
currentManager.Shutdown()
8386
currentManager = newManager
8487
srv.Handler = newManager

proxy/logMonitor_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,39 @@ func TestLogMonitor(t *testing.T) {
2121
client2Messages := make([]byte, 0)
2222

2323
var wg sync.WaitGroup
24-
wg.Add(1)
24+
wg.Add(2) // One for each client
2525

26+
// Write messages first
27+
logMonitor.Write([]byte("1"))
28+
logMonitor.Write([]byte("2"))
29+
logMonitor.Write([]byte("3"))
30+
31+
// Start goroutines to collect messages
2632
go func() {
2733
defer wg.Done()
28-
for {
34+
messageCount := 0
35+
for messageCount < 3 {
2936
select {
3037
case data := <-client1:
3138
client1Messages = append(client1Messages, data...)
39+
messageCount++
40+
}
41+
}
42+
}()
43+
44+
go func() {
45+
defer wg.Done()
46+
messageCount := 0
47+
for messageCount < 3 {
48+
select {
3249
case data := <-client2:
3350
client2Messages = append(client2Messages, data...)
34-
default:
35-
return
51+
messageCount++
3652
}
3753
}
3854
}()
3955

40-
logMonitor.Write([]byte("1"))
41-
logMonitor.Write([]byte("2"))
42-
logMonitor.Write([]byte("3"))
43-
44-
// Wait for the goroutine to finish
56+
// Wait for both goroutines to finish
4557
wg.Wait()
4658

4759
// Check the buffer

proxy/proxymanager_test.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,8 @@ func TestProxyManager_Shutdown(t *testing.T) {
296296
assert.Equal(t, StateFailed, process.CurrentState(), "process should be in failed state")
297297
})
298298

299-
// Test Case 2: Clean shutdown of running processes
300-
t.Run("clean shutdown of processes", func(t *testing.T) {
299+
// Test Case 2: Clean shutdown waits for in-flight requests
300+
t.Run("clean shutdown waits for requests", func(t *testing.T) {
301301
// Create configuration with valid model
302302
config := &Config{
303303
HealthCheckTimeout: 15,
@@ -309,18 +309,54 @@ func TestProxyManager_Shutdown(t *testing.T) {
309309

310310
proxy := New(config)
311311

312-
// Start a model
313-
reqBody := `{"model":"model1"}`
314-
req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBufferString(reqBody))
315-
w := httptest.NewRecorder()
316-
proxy.ServeHTTP(w, req)
312+
// Start a model and keep track of request completion
313+
requestDone := make(chan bool)
314+
requestStarted := make(chan bool)
317315

318-
// Verify model started successfully
319-
assert.Equal(t, http.StatusOK, w.Code)
320-
assert.Len(t, proxy.currentProcesses, 1, "process should be running")
316+
// Start long-running request in goroutine
317+
go func() {
318+
reqBody := fmt.Sprintf(`{"model":"model1"}`)
319+
req := httptest.NewRequest("POST", "/v1/chat/completions?wait=3000ms", bytes.NewBufferString(reqBody))
320+
w := httptest.NewRecorder()
321+
322+
// Signal that request is about to start
323+
requestStarted <- true
321324

322-
// Shutdown
323-
proxy.Shutdown()
325+
proxy.ServeHTTP(w, req)
326+
327+
// Verify request completed successfully
328+
assert.Equal(t, http.StatusOK, w.Code)
329+
requestDone <- true
330+
}()
331+
332+
// Wait for request to start
333+
<-requestStarted
334+
335+
// Start shutdown in goroutine
336+
shutdownComplete := make(chan bool)
337+
go func() {
338+
proxy.StopProcesses()
339+
shutdownComplete <- true
340+
}()
341+
342+
// Verify shutdown waits for request
343+
select {
344+
case <-shutdownComplete:
345+
t.Error("Shutdown completed before request finished")
346+
case <-time.After(1 * time.Second):
347+
// Expected: shutdown is still waiting after 1 second
348+
}
349+
350+
// Wait for request to complete
351+
<-requestDone
352+
353+
// Now shutdown should complete quickly
354+
select {
355+
case <-shutdownComplete:
356+
// Expected: shutdown completes after request is done
357+
case <-time.After(1 * time.Second):
358+
t.Error("Shutdown did not complete after request finished")
359+
}
324360

325361
// Verify cleanup
326362
assert.Len(t, proxy.currentProcesses, 0, "no processes should remain after shutdown")

0 commit comments

Comments
 (0)