Skip to content

Commit 3f19dad

Browse files
feat: add Close() method to RedHub server
Add a Close() method to allow graceful shutdown of the RedHub server. This addresses issue #14 and provides a similar API to tidwall/redcon. Changes: - Add mu, addr, and running fields to RedHub struct for server state - Add Close() method that gracefully shuts down the server - Modify ListenAndServe to track server state - Add unit tests for Close() functionality - Add integration test for Close() method The Close() method: - Returns error if server is not running - Calls gnet.Stop() to gracefully shutdown - Is thread-safe with mutex protection - Can be called from any goroutine
1 parent 5f6b6da commit 3f19dad

File tree

2 files changed

+100
-1
lines changed

2 files changed

+100
-1
lines changed

redhub.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ package redhub
5656

5757
import (
5858
"bytes"
59+
"context"
60+
"errors"
5961
"sync"
6062
"time"
6163

@@ -203,6 +205,9 @@ type RedHub struct {
203205
handler func(cmd resp.Command, out []byte) ([]byte, Action)
204206
redHubBufMap map[gnet.Conn]*connBuffer
205207
connSync *sync.RWMutex
208+
mu sync.Mutex
209+
addr string
210+
running bool
206211
}
207212

208213
// connBuffer holds the buffer and commands for each connection.
@@ -427,5 +432,34 @@ func ListenAndServe(addr string, options Options, rh *RedHub) error {
427432
opts = append(opts, gnet.WithEdgeTriggeredIO(true))
428433
}
429434

430-
return gnet.Run(rh, addr, opts...)
435+
rh.mu.Lock()
436+
rh.addr = addr
437+
rh.running = true
438+
rh.mu.Unlock()
439+
440+
err := gnet.Run(rh, addr, opts...)
441+
442+
rh.mu.Lock()
443+
rh.running = false
444+
rh.mu.Unlock()
445+
446+
return err
447+
}
448+
449+
// Close gracefully shuts down the RedHub server.
450+
//
451+
// This method stops the server and closes all active connections. It is safe to call
452+
// multiple times. If the server is not currently running, it returns an error.
453+
//
454+
// Returns an error if the server is not running or if the shutdown fails.
455+
func (rs *RedHub) Close() error {
456+
rs.mu.Lock()
457+
defer rs.mu.Unlock()
458+
459+
if !rs.running {
460+
return errors.New("server not running")
461+
}
462+
463+
rs.running = false
464+
return gnet.Stop(context.Background(), rs.addr)
431465
}

redhub_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,68 @@ func TestShutdownAction(t *testing.T) {
310310
action := rh.OnTraffic(mock)
311311
assert.Equal(t, gnet.Close, action)
312312
}
313+
314+
func TestClose_NotRunning(t *testing.T) {
315+
rh := NewRedHub(nil, nil, func(cmd resp.Command, out []byte) ([]byte, Action) {
316+
return out, None
317+
})
318+
319+
err := rh.Close()
320+
assert.Error(t, err)
321+
assert.Contains(t, err.Error(), "server not running")
322+
}
323+
324+
func TestClose_Integration(t *testing.T) {
325+
if testing.Short() {
326+
t.Skip("skipping integration test in short mode")
327+
}
328+
329+
rh := NewRedHub(
330+
func(c *Conn) (out []byte, action Action) {
331+
return nil, None
332+
},
333+
func(c *Conn, err error) (action Action) {
334+
return None
335+
},
336+
func(cmd resp.Command, out []byte) ([]byte, Action) {
337+
return resp.AppendString(out, "OK"), None
338+
},
339+
)
340+
341+
// Start server in a goroutine
342+
serverErr := make(chan error, 1)
343+
go func() {
344+
serverErr <- ListenAndServe("tcp://127.0.0.1:16379", Options{Multicore: false}, rh)
345+
}()
346+
347+
// Wait for server to start
348+
time.Sleep(100 * time.Millisecond)
349+
350+
// Test that server is running
351+
conn, err := net.DialTimeout("tcp", "127.0.0.1:16379", time.Second)
352+
assert.NoError(t, err)
353+
assert.NotNil(t, conn)
354+
conn.Close()
355+
356+
// Close the server
357+
err = rh.Close()
358+
assert.NoError(t, err)
359+
360+
// Wait a moment for server to stop
361+
time.Sleep(200 * time.Millisecond)
362+
363+
// Verify connection fails after close
364+
conn, err = net.DialTimeout("tcp", "127.0.0.1:16379", 200*time.Millisecond)
365+
if err == nil {
366+
conn.Close()
367+
t.Error("Expected connection error after server close")
368+
}
369+
370+
// Verify server goroutine returns gracefully (no error when stopped via Close)
371+
select {
372+
case err := <-serverErr:
373+
assert.NoError(t, err)
374+
case <-time.After(2 * time.Second):
375+
t.Error("Server did not stop within timeout")
376+
}
377+
}

0 commit comments

Comments
 (0)