Bugfixes, add INFO command, graceful shutdown. elimitate dead code
- Remove unsed handleClientKill, handleClientKill - Fix misleading error message on rate limit - Eliminated unused channel-to-pattern index that was causing unnecessary work - Fixed O(n²) duplicate check in PublishToPattern - Limit total pattern subscriptions to 10,000 - Limit channel subscriptions per client to 10,000 - Added INFO command - Added graceful shutdown timeout - Validate that log directory is writable during config load - Make SELECT properly handle whitespace around DB index like Redis - Return proper error instead of empty array for unsupported commands on sharded pubsub stubs - More accurate error messages throughout - Updated README.md
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -23,21 +24,35 @@ import (
|
||||
|
||||
const MaxDatabases = 16 // Redis default maximum databases (0-15)
|
||||
|
||||
// generateRunID generates a 40-character hexadecimal run ID like Redis.
|
||||
// Redis uses SHA1 of random data for 160 bits of randomness.
|
||||
func generateRunID() string {
|
||||
b := make([]byte, 20)
|
||||
_, _ = rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// Sentinel errors for connection handling
|
||||
var (
|
||||
ErrQuit = errors.New("client quit")
|
||||
)
|
||||
|
||||
// Version is set at build time via -ldflags
|
||||
var Version = "unknown"
|
||||
|
||||
// Server is the main Pekoe server instance.
|
||||
// It manages the TCP listener, connection lifecycle, and delegates to the pubsub hub.
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
hub pubsubHub
|
||||
ln net.Listener
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
active atomic.Int64 // atomic counter for active connections
|
||||
cfg *config.Config
|
||||
hub pubsubHub
|
||||
ln net.Listener
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
active atomic.Int64 // atomic counter for active connections
|
||||
version string // server version for INFO command
|
||||
startTime time.Time // server start time for uptime calculation
|
||||
runID string // random run ID
|
||||
}
|
||||
|
||||
// PubSubHub defines the interface our server expects from the pubsub implementation.
|
||||
@@ -65,10 +80,13 @@ type pubsubHub interface {
|
||||
func New(cfg *config.Config, hub pubsubHub) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
hub: hub,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
cfg: cfg,
|
||||
hub: hub,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
version: Version,
|
||||
startTime: time.Now(),
|
||||
runID: generateRunID(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +123,11 @@ func (s *Server) Run() error {
|
||||
// createPidFile creates the PID file with the current process ID
|
||||
func (s *Server) createPidFile() error {
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
// Remove existing pid file first to handle stale files from previous runs
|
||||
// This also fixes permission denied errors when file already exists
|
||||
if err := os.Remove(s.cfg.PidFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("failed to remove existing pid file: %w", err)
|
||||
}
|
||||
//nolint: gosec
|
||||
return os.WriteFile(s.cfg.PidFile, []byte(pid), 0644)
|
||||
}
|
||||
@@ -280,6 +303,8 @@ func (s *Server) processCommand(client *pubsub.Client, cmd *Command, writer *Con
|
||||
return s.handleClient(client, cmd.Args[1:], writer)
|
||||
case "select":
|
||||
return s.handleSelect(client, cmd.Args[1:], writer)
|
||||
case "info":
|
||||
return s.handleInfo(writer)
|
||||
default:
|
||||
// Check if client is in pubsub mode - only pubsub commands allowed
|
||||
if client != nil && (client.SubsCount() > 0 || client.PatternSubsCount() > 0) {
|
||||
@@ -315,8 +340,27 @@ func (s *Server) waitForSignal() {
|
||||
logger.Error("event=listener_close_error error=%v", err)
|
||||
}
|
||||
|
||||
// Wait for all active connections to finish
|
||||
s.wg.Wait()
|
||||
// Wait for all active connections to finish with timeout
|
||||
shutdownTimeout := time.Duration(s.cfg.ShutdownTimeout) * time.Second
|
||||
if shutdownTimeout > 0 {
|
||||
// Create a channel to wait on wg.Wait() with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// All connections finished normally
|
||||
case <-time.After(shutdownTimeout):
|
||||
// Timeout reached - force cleanup
|
||||
logger.Warn("event=shutdown_timeout timeout=%ds active_connections=%d", s.cfg.ShutdownTimeout, s.active.Load())
|
||||
}
|
||||
} else {
|
||||
// No timeout - wait forever (original behavior)
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
// Remove PID file
|
||||
s.removePidFile()
|
||||
|
||||
Reference in New Issue
Block a user