Add stub database selection, and fix differences between pekoe and c-redis
- PUBSUB CHANNELS glob pattern matching - Implement SELECT + max DB validation (0-15) (default db max is 16), for perl clients - CLIENT KILL ID (single/range) + MAXAGE - CLIENT INFO events + resp fields - PUBSUB SHARDCHANNELS/SHARDNUMSUB stubs
This commit is contained in:
@@ -5,7 +5,9 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eleksir/pekoe/internal/glob"
|
||||
"github.com/eleksir/pekoe/internal/logger"
|
||||
"github.com/eleksir/pekoe/internal/protocol"
|
||||
"github.com/eleksir/pekoe/internal/pubsub"
|
||||
@@ -132,12 +134,24 @@ func (s *Server) handlePubsub(args [][]byte, writer *ConnWriter) error {
|
||||
pattern = string(args[1])
|
||||
}
|
||||
channels := s.channelList(pattern)
|
||||
result := make([]protocol.Value, len(channels))
|
||||
for i, ch := range channels {
|
||||
result[i] = protocol.Bulk(ch)
|
||||
result := make([]protocol.Value, 0, len(channels))
|
||||
for _, ch := range channels {
|
||||
if pattern == "" || glob.Match(pattern, ch) {
|
||||
result = append(result, protocol.Bulk(ch))
|
||||
}
|
||||
}
|
||||
writer.WriteArray(result)
|
||||
|
||||
case "shardchannels":
|
||||
// PUBSUB SHARDCHANNELS [pattern] - List active sharded channels
|
||||
// Pekoe doesn't support sharded pubsub, return empty list
|
||||
writer.WriteArray(nil)
|
||||
|
||||
case "shardnumsub":
|
||||
// PUBSUB SHARDNUMSUB [shardchannel ...] - Get subscriber count for sharded channels
|
||||
// Pekoe doesn't support sharded pubsub, return empty array
|
||||
writer.WriteArray(nil)
|
||||
|
||||
case "numpat":
|
||||
// PUBSUB NUMPAT - Count of unique pattern subscriptions
|
||||
writer.WriteInt(int64(s.hub.PatternCount()))
|
||||
@@ -147,7 +161,9 @@ func (s *Server) handlePubsub(args [][]byte, writer *ConnWriter) error {
|
||||
helpText := []string{
|
||||
"PUBSUB <subcommand> [<argument> [<argument> ...]]>",
|
||||
" PUBSUB CHANNELS [pattern] - List active channels (optionally filtered by pattern)",
|
||||
" PUBSUB SHARDCHANNELS [pattern] - List active sharded channels (not supported)",
|
||||
" PUBSUB NUMSUB [channel ...] - Get subscriber count for channels",
|
||||
" PUBSUB SHARDNUMSUB [shardchannel ...] - Get subscriber count for sharded channels (not supported)",
|
||||
" PUBSUB NUMPAT - Get the number of pattern subscriptions",
|
||||
" PUBSUB HELP - Show this help message",
|
||||
}
|
||||
@@ -349,11 +365,15 @@ func (s *Server) buildClientList(args [][]byte) string {
|
||||
func (s *Server) handleClientKill(currentClient *pubsub.Client, args [][]byte) (int, error) {
|
||||
clients := s.hub.GetAllClients()
|
||||
killed := 0
|
||||
var notFoundErr error
|
||||
|
||||
// Parse arguments
|
||||
var killID uint64
|
||||
var killIDMin uint64
|
||||
var killIDMax uint64
|
||||
var killAddr string
|
||||
var skipMe = true
|
||||
var maxAgeMs int64
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := strings.ToUpper(string(args[i]))
|
||||
@@ -361,6 +381,12 @@ func (s *Server) handleClientKill(currentClient *pubsub.Client, args [][]byte) (
|
||||
case "ID":
|
||||
if i+2 < len(args) {
|
||||
// ID min max format - kill all IDs in range
|
||||
if _, err := fmt.Sscanf(string(args[i+1]), "%d", &killIDMin); err != nil {
|
||||
return 0, fmt.Errorf("invalid client ID: %s", args[i+1])
|
||||
}
|
||||
if _, err := fmt.Sscanf(string(args[i+2]), "%d", &killIDMax); err != nil {
|
||||
return 0, fmt.Errorf("invalid client ID: %s", args[i+2])
|
||||
}
|
||||
i += 2
|
||||
} else if i+1 < len(args) {
|
||||
// Single ID
|
||||
@@ -374,14 +400,35 @@ func (s *Server) handleClientKill(currentClient *pubsub.Client, args [][]byte) (
|
||||
killAddr = string(args[i+1])
|
||||
i++
|
||||
}
|
||||
case "LADDR":
|
||||
// LADDR not fully supported, just skip
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
}
|
||||
case "SKIPME":
|
||||
if i+1 < len(args) {
|
||||
skipMe = strings.ToUpper(string(args[i+1])) == "YES"
|
||||
i++
|
||||
}
|
||||
case "MAXAGE":
|
||||
if i+1 < len(args) {
|
||||
if _, err := fmt.Sscanf(string(args[i+1]), "%d", &maxAgeMs); err != nil {
|
||||
return 0, fmt.Errorf("invalid maxage: %s", args[i+1])
|
||||
}
|
||||
if maxAgeMs <= 0 {
|
||||
return 0, fmt.Errorf("maxage must be greater than 0")
|
||||
}
|
||||
i++
|
||||
}
|
||||
case "TYPE":
|
||||
// TYPE filter handled separately in handleClient
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, c := range clients {
|
||||
// Skip current client if requested
|
||||
if skipMe && c.ID() == currentClient.ID() {
|
||||
@@ -390,11 +437,29 @@ func (s *Server) handleClientKill(currentClient *pubsub.Client, args [][]byte) (
|
||||
|
||||
shouldKill := false
|
||||
|
||||
if killID > 0 && c.ID() == killID {
|
||||
shouldKill = true
|
||||
// Check ID (single or range)
|
||||
if killID > 0 {
|
||||
shouldKill = c.ID() == killID
|
||||
} else if killIDMin > 0 || killIDMax > 0 {
|
||||
id := c.ID()
|
||||
if killIDMin > 0 && id < killIDMin {
|
||||
shouldKill = false
|
||||
} else if killIDMax > 0 && id > killIDMax {
|
||||
shouldKill = false
|
||||
} else {
|
||||
shouldKill = true
|
||||
}
|
||||
}
|
||||
if killAddr != "" && c.RemoteAddr() == killAddr {
|
||||
shouldKill = true
|
||||
|
||||
// Check ADDR
|
||||
if !shouldKill && killAddr != "" {
|
||||
shouldKill = c.RemoteAddr() == killAddr
|
||||
}
|
||||
|
||||
// Check MAXAGE
|
||||
if shouldKill && maxAgeMs > 0 {
|
||||
age := now.Sub(c.CreatedAt()).Milliseconds()
|
||||
shouldKill = age >= maxAgeMs
|
||||
}
|
||||
|
||||
if shouldKill {
|
||||
@@ -406,7 +471,39 @@ func (s *Server) handleClientKill(currentClient *pubsub.Client, args [][]byte) (
|
||||
}
|
||||
}
|
||||
|
||||
return killed, nil
|
||||
// For single ID/ADDR that wasn't found, Redis returns error
|
||||
if killed == 0 && (killID > 0 || killAddr != "") {
|
||||
return 0, fmt.Errorf("No such client")
|
||||
}
|
||||
|
||||
return killed, notFoundErr
|
||||
}
|
||||
|
||||
// handleSelect processes SELECT command.
|
||||
// SELECT db - Switch to a different database (0-15).
|
||||
// For pekoe (pubsub-only), this only tracks the selected DB for client compatibility.
|
||||
// Redis default max DB is 16 (0-15), configurable via max-databases in config.
|
||||
func (s *Server) handleSelect(client *pubsub.Client, args [][]byte, writer *ConnWriter) error {
|
||||
if len(args) != 1 {
|
||||
writer.WriteError("ERR wrong number of arguments for 'select' command")
|
||||
return nil
|
||||
}
|
||||
|
||||
dbIndexStr := string(args[0])
|
||||
var dbIndex int
|
||||
if _, err := fmt.Sscanf(dbIndexStr, "%d", &dbIndex); err != nil {
|
||||
writer.WriteError("ERR invalid DB index")
|
||||
return nil
|
||||
}
|
||||
|
||||
if dbIndex < 0 || dbIndex >= MaxDatabases {
|
||||
writer.WriteError("ERR DB index is out of range")
|
||||
return nil
|
||||
}
|
||||
|
||||
client.SetSelectedDB(dbIndex)
|
||||
writer.WriteStatus("OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePsubscribe processes PSUBSCRIBE command.
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"github.com/eleksir/pekoe/internal/pubsub"
|
||||
)
|
||||
|
||||
const MaxDatabases = 16 // Redis default maximum databases (0-15)
|
||||
|
||||
// Sentinel errors for connection handling
|
||||
var (
|
||||
ErrQuit = errors.New("client quit")
|
||||
@@ -276,6 +278,8 @@ func (s *Server) processCommand(client *pubsub.Client, cmd *Command, writer *Con
|
||||
return s.handleQuit(client, writer)
|
||||
case "client":
|
||||
return s.handleClient(client, cmd.Args[1:], writer)
|
||||
case "select":
|
||||
return s.handleSelect(client, cmd.Args[1:], writer)
|
||||
default:
|
||||
// Check if client is in pubsub mode - only pubsub commands allowed
|
||||
if client != nil && (client.SubsCount() > 0 || client.PatternSubsCount() > 0) {
|
||||
|
||||
Reference in New Issue
Block a user