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:
@@ -5,7 +5,6 @@ package pubsub
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -16,14 +15,20 @@ type Hub struct {
|
||||
subs map[string]map[*Client]struct{}
|
||||
patterns map[string]*patternEntry
|
||||
|
||||
// Pattern index for fast channel-to-pattern matching
|
||||
// Maps channel name to patterns that would match it
|
||||
patternIndex map[string][]string
|
||||
|
||||
// Client registry for CLIENT LIST/INFO/KILL commands
|
||||
clients map[uint64]*Client
|
||||
|
||||
// Rate limiting
|
||||
patternCount int // Total active pattern subscriptions
|
||||
channelCount int // Total active channel subscriptions
|
||||
}
|
||||
|
||||
// MaxPatternSubscriptions limits pattern subscriptions to prevent DoS
|
||||
const MaxPatternSubscriptions = 10000
|
||||
|
||||
// MaxChannelSubscriptions limits channel subscriptions per client to prevent DoS
|
||||
const MaxChannelSubscriptions = 10000
|
||||
|
||||
// patternEntry holds compiled regex and subscribers for a pattern.
|
||||
type patternEntry struct {
|
||||
regex *regexp.Regexp
|
||||
@@ -35,15 +40,19 @@ type patternCacheType struct {
|
||||
sync.Map
|
||||
}
|
||||
|
||||
// Delete removes a pattern from the cache
|
||||
func (pc *patternCacheType) Delete(pattern string) {
|
||||
pc.Map.Delete(pattern)
|
||||
}
|
||||
|
||||
var patternCache = patternCacheType{}
|
||||
|
||||
// NewHub creates a new pubsub hub instance ready for use.
|
||||
func NewHub() *Hub {
|
||||
return &Hub{
|
||||
subs: make(map[string]map[*Client]struct{}),
|
||||
patterns: make(map[string]*patternEntry),
|
||||
patternIndex: make(map[string][]string),
|
||||
clients: make(map[uint64]*Client),
|
||||
subs: make(map[string]map[*Client]struct{}),
|
||||
patterns: make(map[string]*patternEntry),
|
||||
clients: make(map[uint64]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +106,15 @@ func (h *Hub) Subscribe(client *Client, channel string) bool {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Check per-client subscription limit
|
||||
if client.SubsCount() >= MaxChannelSubscriptions {
|
||||
return false
|
||||
}
|
||||
|
||||
clients, exists := h.subs[channel]
|
||||
if !exists {
|
||||
clients = make(map[*Client]struct{})
|
||||
h.subs[channel] = clients
|
||||
|
||||
// New channel - add to pattern index
|
||||
h.addChannelToPatternIndex(channel)
|
||||
}
|
||||
|
||||
if _, already := clients[client]; already {
|
||||
@@ -111,19 +122,11 @@ func (h *Hub) Subscribe(client *Client, channel string) bool {
|
||||
}
|
||||
|
||||
clients[client] = struct{}{}
|
||||
h.channelCount++
|
||||
client.AddSubscription(channel)
|
||||
return true
|
||||
}
|
||||
|
||||
// addChannelToPatternIndex adds a channel to the pattern index for all matching patterns.
|
||||
func (h *Hub) addChannelToPatternIndex(channel string) {
|
||||
for pattern, entry := range h.patterns {
|
||||
if entry.regex.MatchString(channel) {
|
||||
h.patternIndex[channel] = append(h.patternIndex[channel], pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe removes a client from a channel.
|
||||
func (h *Hub) Unsubscribe(client *Client, channel string) bool {
|
||||
if channel == "" {
|
||||
@@ -149,6 +152,7 @@ func (h *Hub) Unsubscribe(client *Client, channel string) bool {
|
||||
delete(h.subs, channel)
|
||||
}
|
||||
|
||||
h.channelCount--
|
||||
client.RemoveSubscription(channel)
|
||||
return true
|
||||
}
|
||||
@@ -199,25 +203,11 @@ func (h *Hub) Publish(channel string, message []byte) int {
|
||||
func (h *Hub) PublishToPattern(channel string, message []byte) int {
|
||||
h.mu.RLock()
|
||||
|
||||
// First check the pattern index for patterns that have been indexed
|
||||
// Copy the slice to avoid aliasing issues when appending
|
||||
matchedPatterns := slices.Clone(h.patternIndex[channel])
|
||||
|
||||
// Also check all patterns to see if any match this channel
|
||||
// This ensures we don't miss patterns when publishing to channels with no direct subscribers
|
||||
// Collect unique patterns that match the channel using a map for O(1) deduplication
|
||||
matchedPatterns := make(map[string]bool)
|
||||
for pattern, entry := range h.patterns {
|
||||
if entry.regex.MatchString(channel) {
|
||||
// Check if already in matchedPatterns to avoid duplicates
|
||||
found := false
|
||||
for _, p := range matchedPatterns {
|
||||
if p == pattern {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
matchedPatterns = append(matchedPatterns, pattern)
|
||||
}
|
||||
matchedPatterns[pattern] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +222,7 @@ func (h *Hub) PublishToPattern(channel string, message []byte) int {
|
||||
}
|
||||
var matches []match
|
||||
|
||||
for _, pattern := range matchedPatterns {
|
||||
for pattern := range matchedPatterns {
|
||||
if entry, ok := h.patterns[pattern]; ok {
|
||||
for client := range entry.clients {
|
||||
matches = append(matches, match{client: client, pattern: pattern})
|
||||
@@ -276,15 +266,14 @@ func (h *Hub) PatternSubscribe(client *Client, pattern string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
entry.clients[client] = struct{}{}
|
||||
|
||||
// Add pattern to index for all existing channels
|
||||
for ch := range h.subs {
|
||||
if regex.MatchString(ch) {
|
||||
h.patternIndex[ch] = append(h.patternIndex[ch], pattern)
|
||||
}
|
||||
// Check pattern subscription limit to prevent DoS
|
||||
if h.patternCount >= MaxPatternSubscriptions {
|
||||
return false, fmt.Errorf("too many pattern subscriptions")
|
||||
}
|
||||
|
||||
entry.clients[client] = struct{}{}
|
||||
h.patternCount++
|
||||
|
||||
client.AddPatternSubscription(pattern)
|
||||
return true, nil
|
||||
}
|
||||
@@ -346,31 +335,15 @@ func (h *Hub) PatternUnsubscribe(client *Client, pattern string) bool {
|
||||
|
||||
if len(entry.clients) == 0 {
|
||||
delete(h.patterns, pattern)
|
||||
// Remove pattern from index
|
||||
h.removePatternFromIndex(pattern)
|
||||
h.patternCount--
|
||||
// Remove pattern from cache
|
||||
patternCache.Delete(pattern)
|
||||
}
|
||||
|
||||
client.RemovePatternSubscription(pattern)
|
||||
return true
|
||||
}
|
||||
|
||||
// removePatternFromIndex removes a pattern from all channel indexes.
|
||||
func (h *Hub) removePatternFromIndex(pattern string) {
|
||||
for ch, patterns := range h.patternIndex {
|
||||
newPatterns := make([]string, 0, len(patterns)-1)
|
||||
for _, p := range patterns {
|
||||
if p != pattern {
|
||||
newPatterns = append(newPatterns, p)
|
||||
}
|
||||
}
|
||||
if len(newPatterns) == 0 {
|
||||
delete(h.patternIndex, ch)
|
||||
} else {
|
||||
h.patternIndex[ch] = newPatterns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PatternCount returns the total number of pattern subscriptions.
|
||||
func (h *Hub) PatternCount() int {
|
||||
h.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user