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:
eleksir
2026-04-03 01:02:03 +03:00
parent 9e30243533
commit 92cd652b0d
5 changed files with 266 additions and 10 deletions

View File

@@ -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.

View File

@@ -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) {