Files
pekoe/README.md
eleksir 488ce1e4d1 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
2026-04-03 10:01:29 +03:00

669 lines
18 KiB
Markdown

# Pekoe - Redis Pub/Sub Server
Pekoe is a lightweight, Redis-compatible Pub/Sub message broker written in Go using only the standard library. It provides core Redis pub/sub functionality without the complexity of a full Redis implementation.
## What is Pub/Sub?
Pub/Sub (Publish/Subscribe) is a messaging pattern where:
- **Publishers** send messages to channels
- **Subscribers** receive messages from channels they subscribed to
- The publisher doesn't know who (if anyone) receives the message
```
Publisher Channel Subscribers
| | |
|--- "hello" to "news" --->| |
| |--- "hello" -------->| Client A
| |--- "hello" -------->| Client B
| |--- "hello" -------->| Client C
```
## Quick Start
### Build
```bash
# Build the binary
make
# Or manually
go build -o pekoe ./cmd/pekoe
```
### Run
```bash
# Start the server (config is auto-detected, see Configuration section)
pekoe
# Or specify a config file explicitly
pekoe --config /path/to/config.json
# Show version
pekoe --version
```
### Connect with Redis Client
Use any Redis client to connect. Here are some examples:
**Using redis-cli:**
```bash
redis-cli -p 6379
> SUBSCRIBE news
Reading messages... (press Ctrl-C to exit)
1) "subscribe"
2) "news"
3) (integer) 1
# In another terminal, publish a message:
redis-cli -p 6379 PUBLISH news "Hello World"
```
**Using Python (redis-py):**
```python
import redis
r = redis.Redis(port=6379)
# Subscribe to a channel
p = r.pubsub()
p.subscribe('news')
# In another client, publish
r.publish('news', 'Hello World')
# Receive the message
print(p.get_message()) # {'type': 'message', 'channel': 'news', 'data': 'Hello World'}
```
**Using Go (go-redis):**
```go
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// Publisher
pub := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
pub.Publish(ctx, "news", "Hello from Go!")
// Subscriber
sub := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
ps := sub.Subscribe(ctx, "news")
defer ps.Close()
ch := ps.Channel()
msg := <-ch
fmt.Printf("Received: %s\n", msg.Payload)
}
```
## Configuration
Copy `config_example.json` to your config location and edit:
```json
{
"bind": "0.0.0.0",
"port": 6379,
"max_clients": 10000,
"read_timeout": 0,
"write_timeout": 0,
"max_message_bytes": 536870912,
"log_level": "info"
}
```
### Config File Lookup
When no `--config` flag is given, pekoe searches for a config file automatically:
**Linux:**
- Regular user: `~/.pekoe.json`, then `/etc/pekoe/config.json`
- Root user: `/etc/pekoe/config.json` only
**macOS and BSD:**
- `~/.pekoe.json`, then `/usr/local/etc/pekoe/config.json`
**Windows:**
- `--config` flag is required, no default lookup
On Linux, macOS, and BSD, if `--config` is given the specified file is used directly.
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `bind` | string | `"0.0.0.0"` | IP address to bind to |
| `port` | int | `6379` | Port number (1-65535) |
| `max_clients` | int | `10000` | Maximum concurrent connections |
| `read_timeout` | int | `0` | Read timeout in seconds (0 = no timeout) |
| `write_timeout` | int | `0` | Write timeout in seconds (0 = no timeout) |
| `max_message_bytes` | int | `536870912` | Max message size in bytes (512MB) |
| `log_level` | string | `"info"` | Logging level: debug, info, warn, error |
| `log_file` | string | `""` | Log file path (empty = stdout only) |
| `pid_file` | string | `""` | PID file path (empty = no PID file) |
| `shutdown_timeout` | int | `30` | Graceful shutdown timeout in seconds (0 = wait forever) |
If you don't provide a config file, sensible defaults are used.
## Supported Commands
### SUBSCRIBE
Subscribe to one or more channels. The server sends a confirmation message for each channel.
```bash
# Subscribe to a single channel
SUBSCRIBE news
# Subscribe to multiple channels
SUBSCRIBE news sports weather
```
**Response:**
```
1) "subscribe" # message type
2) "news" # channel name
3) (integer) 1 # total subscriptions
```
### UNSUBSCRIBE
Unsubscribe from channels. If no channel is specified, unsubscribe from all.
```bash
# Unsubscribe from specific channel
UNSUBSCRIBE news
# Unsubscribe from all channels
UNSUBSCRIBE
# Unsubscribe from specific channels
UNSUBSCRIBE news sports
```
### PSUBSCRIBE
Subscribe to channels matching a pattern. Supports wildcards:
- `*` matches any sequence of characters
- `?` matches any single character
```bash
# Subscribe to all channels starting with "news"
PSUBSCRIBE news*
# Subscribe to multiple patterns
PSUBSCRIBE news.* sports.*
```
**Response:**
```
1) "psubscribe"
2) "news.*"
3) (integer) 1
```
### PUNSUBSCRIBE
Unsubscribe from patterns. If no pattern is specified, unsubscribe from all.
```bash
# Unsubscribe from all patterns
PUNSUBSCRIBE
# Unsubscribe from specific pattern
PUNSUBSCRIBE news.*
```
### PUBLISH
Publish a message to a channel. Returns the number of subscribers (both regular and pattern) who received it.
```bash
PUBLISH news "Hello World"
# Returns: (integer) 5 (5 subscribers received the message)
```
### PING
Check server connectivity. Returns PONG or the optional message.
```bash
PING
# Returns: +PONG
PING hello
# Returns: +hello
```
### SELECT
Switch to a different database (0-15). In Pekoe this is a no-op since there's no persistent data, but it maintains client compatibility.
```bash
SELECT 5
# Returns: +OK
```
### INFO
Returns comprehensive server information in Redis-compatible format.
```bash
INFO
# Returns: # Server\r\nredis_version:...\r\n...
```
Returns sections: Server, Clients, Memory, Persistence, Stats, Replication, CPU, Keyspace.
### PUBSUB
Introspection commands for monitoring:
**NUMSUB** - Get subscriber count for channels:
```bash
PUBSUB NUMSUB news sports
# Returns: 1) "news" 2) (integer) 3 3) "sports" 4) (integer) 5
```
**CHANNELS** - List all active channels, optionally filtered by pattern:
```bash
PUBSUB CHANNELS
# Returns: 1) "news" 2) "sports" 3) "weather"
PUBSUB CHANNELS news*
# Returns: 1) "news" 2) "news.sports"
```
**NUMPAT** - Count of pattern subscriptions:
```bash
PUBSUB NUMPAT
# Returns: (integer) 3
```
**HELP** - Get help on pubsub commands:
```bash
PUBSUB HELP
```
### CLIENT
CLIENT command provides connection introspection and control:
```bash
CLIENT ID # Returns unique connection ID
CLIENT SETNAME x # Set connection name (max 255 chars, no spaces)
CLIENT GETNAME # Get connection name (or nil)
CLIENT LIST # List all connected clients
CLIENT INFO # Get information about this connection
CLIENT KILL ID id # Close connection by ID
CLIENT PAUSE 1000 # Pause client (no-op for compatibility)
CLIENT UNPAUSE # Unpause client (no-op for compatibility)
CLIENT REPLY OFF # Disable replies (ON|OFF|SKIP)
CLIENT TRACKING ON # Client-side caching (no-op for compatibility)
CLIENT NO-EVICT ON # No-evict mode (no-op for compatibility)
CLIENT NO-TOUCH ON # No-touch mode (no-op for compatibility)
CLIENT CACHING YES # Caching mode (no-op for compatibility)
```
**CLIENT INFO format:**
```
id=<id> addr=<addr> laddr=<local> fd=-1 age=<seconds> idle=<seconds> flags=<flags> db=0 sub=<count> psub=<count> multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 tot-mem=0 cmd=client user=default redir=-1
```
## Client Library Compatibility
Pekoe has been tested with multiple Redis client libraries:
| Library | Language | Status |
|---------|----------|--------|
| [go-redis/v9](https://github.com/redis/go-redis) | Go | Fully compatible |
| [rueidis](https://github.com/redis/rueidis) | Go | Compatible (use `DisableCache: true`) |
| [redigo](https://github.com/gomodule/redigo) | Go | Fully compatible |
### Go-redis Example
```go
import "github.com/redis/go-redis/v9"
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
ps := client.Subscribe(ctx, "channel")
```
### Rueidis Example
```go
import "github.com/redis/rueidis"
client, _ := rueidis.NewClient(rueidis.ClientOption{
InitAddress: []string{"localhost:6379"},
DisableCache: true, // Required for pekoe
})
```
### Redigo Example
```go
import "github.com/gomodule/redigo/redis"
conn, _ := redis.Dial("tcp", "localhost:6379")
conn.Send("SUBSCRIBE", "channel")
```
## Architecture
```
pekoe/
├── cmd/pekoe/ # Entry point
├── internal/ # Internal packages
│ ├── config/ # Configuration loading and validation
│ ├── logger/ # Timestamp-based logging with level filtering
│ ├── protocol/ # RESP protocol parsing and serialization
│ ├── pubsub/ # Pub/Sub core (Hub, Client)
│ ├── server/ # TCP server, connection handling, command routing
│ │ ├── server.go # Server lifecycle, connection handling
│ │ ├── handlers.go # Command handlers (subscribe, publish, client, etc.)
│ │ ├── conn.go # RESP connection reader/writer
│ │ └── signals*.go # Platform-specific signal handling
│ └── compatibility/ # Client library compatibility tests
├── contrib/ # Contribution files (init scripts, etc.)
├── Makefile # Build automation
├── config.json # Default configuration
└── README.md # This file
```
### Key Components
**Hub (pubsub/hub.go)**
- Maintains channel-to-subscribers mapping
- Maintains pattern subscriptions
- Handles message broadcasting to both regular and pattern subscribers
**Client (pubsub/client.go)**
- Represents a connected subscriber
- Tracks which channels/patterns they're subscribed to
- Sends messages to the client
**Server (server/server.go, server/handlers.go)**
- Listens on TCP socket
- Parses Redis protocol commands
- Routes commands to appropriate handlers in handlers.go
- Enforces pubsub mode (only pubsub commands allowed when subscribed)
- Supports CLIENT command for compatibility
**Protocol (protocol/protocol.go)**
- Parses RESP (Redis Serialization Protocol)
- Serializes responses back to clients
**Logger (logger/logger.go)**
- Structured logging with timestamp, level prefix, and key=value fields
- Level filtering (debug, info, warn, error)
- Configurable output (stdout or file) via config file
## Testing
```bash
# Run all tests with race detection
make test
# Or directly with go
go test -v -race ./...
# Run compatibility tests only
go test -v ./internal/compatibility/...
# Run specific test
go test -v -run TestGoRedisBasicSubscribe ./internal/compatibility/...
```
### Compatibility Tests
The compatibility test suite validates pekoe against multiple Redis client libraries:
- **go-redis tests**: 27 tests covering basic subscribe, publish, patterns, pubsub commands
- **rueidis tests**: 9 tests covering connectivity, publish, unicode messages
- **redigo tests**: 19 tests covering raw protocol interactions
Test coverage includes:
- Basic SUBSCRIBE/PUBLISH/UNSUBSCRIBE
- Pattern PSUBSCRIBE/PUNSUBSCRIBE with wildcards
- Multiple channel/pattern subscriptions
- PUBSUB NUMSUB, CHANNELS, NUMPAT, SHARDCHANNELS, SHARDNUMSUB, HELP commands
- PING command (with and without message)
- SELECT database switching (for client compatibility)
- INFO server information
- CLIENT ID, SETNAME, GETNAME, LIST, INFO, KILL, PAUSE, REPLY commands
- Binary-safe messages
- Empty and long messages
- Unicode content
- Concurrent operations
- Special characters in channel names
- Mixed channel and pattern subscriptions
## Logging
Pekoe logs in a structured key=value format with timestamp and log level prefix.
**Log format:**
```
YYYY Mmm dd hh:mm:ss LEVEL pekoe[pid]: key=value key=value ...
```
**Example:**
```
2024 Apr 2 14:30:15 INFO pekoe[12345]: event=startup addr=0.0.0.0:6379 max_clients=10000
2024 Apr 2 14:30:20 DEBUG pekoe[12345]: client=192.168.1.100:54321 command=subscribe args=1
2024 Apr 2 14:30:20 DEBUG pekoe[12345]: client=192.168.1.100:54321 event=subscribe channel=news
2024 Apr 2 14:30:25 ERROR pekoe[12345]: client=192.168.1.100:54321 error=unknown_command command=invalidcmd
```
**Log levels:**
| Level | Description |
|-------|-------------|
| `debug` | All messages including DEBUG |
| `info` | INFO and above (default) |
| `warn` | WARN, ERROR, CRITICAL |
| `error` | ERROR, CRITICAL only |
**Events logged:**
| Event | Level | Description |
|-------|-------|-------------|
| `event=startup` | INFO | Server started |
| `event=shutdown` | INFO | Shutdown signal received |
| `event=shutdown_complete` | INFO | Server stopped |
| `event=subscribe` | DEBUG | Client subscribed to channel |
| `event=unsubscribe` | DEBUG | Client unsubscribed from channel |
| `event=psubscribe` | DEBUG | Client subscribed to pattern |
| `event=punsubscribe` | DEBUG | Client unsubscribed from pattern |
| `event=publish` | DEBUG | Message published |
| `event=quit` | INFO | Client sent QUIT |
| `event=log_reopened` | INFO | Log file reopened (after SIGUSR1/SIGHUP) |
| `event=sighup_ignored` | INFO | SIGHUP received (reload not implemented) |
| `error=unknown_command` | ERROR | Unknown command received |
| `error=invalid_in_pubsub_mode` | ERROR | Invalid command in pubsub mode |
| `error=protocol` | ERROR | Protocol error |
| `error=processing` | ERROR | Error processing command |
| `error=flush` | ERROR | Error flushing response |
| `error=log_reopen_error` | ERROR | Failed to reopen log file |
| `event=max_clients_reached` | WARN | Connection rejected (max clients) |
| `event=accept_temporary` | WARN | Accept error (transient, retrying) |
| `event=accept_error` | ERROR | Accept error (fatal) |
| `event=listener_close_error` | ERROR | Error closing listener on shutdown |
### Log Rotation
Pekoe supports log re-opening via signals for log rotation. Configure a PID file to enable easy signal sending.
**Configuration example:**
```json
{
"log_file": "/var/log/pekoe.log",
"pid_file": "/var/run/pekoe.pid"
}
```
**Events logged:**
- `event=pid_file_created` - PID file created on startup
- `event=pid_file_removed` - PID file removed on shutdown
- `event=pid_file_error` - Error creating PID file (non-fatal)
**Unix/Linux:**
```bash
# Move the current log file
mv /var/log/pekoe.log /var/log/pekoe.log.1
# Send SIGUSR1 to reopen the log file
kill -USR1 $(cat /var/run/pekoe.pid)
```
**Windows:**
```powershell
# On Windows, SIGHUP is used for log rotation since SIGUSR1 is not available
# Send SIGHUP to reopen the log file
# Note: Signal handling on Windows requires appropriate tools
```
**Example logrotate configuration:**
```
/var/log/pekoe.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
kill -USR1 $(cat /var/run/pekoe.pid 2>/dev/null) 2>/dev/null || true
endscript
}
```
## Why Pekoe?
1. **Minimal dependencies** - Uses only Go standard library
2. **Lightweight** - Only implements pub/sub, no extra features
3. **Redis compatible** - Use existing Redis clients
4. **High performance** - Fine-grained locking for concurrent operations
5. **Pattern subscriptions** - Support for PSUBSCRIBE/PUNSUBSCRIBE
6. **Easy to understand** - Clean, well-documented code
7. **Tested** - Comprehensive compatibility tests with multiple client libraries
## Use Cases
- **Real-time notifications** - Push updates to connected clients
- **Chat systems** - Message distribution between users
- **Live updates** - Stock prices, sports scores, news feeds
- **Microservices** - Service-to-service messaging
- **Load balancing** - Distribute work across workers
## Error Handling
Pekoe handles errors gracefully:
- **Protocol errors** - Returns ERR message, closes connection
- **Client disconnection** - Automatically cleans up subscriptions
- **Full channels** - Returns 0 for publish to empty channels
- **Invalid commands** - Returns unknown command error
- **Invalid patterns** - Returns error for malformed patterns
- **Case-insensitive commands** - Commands are case-insensitive per Redis spec
## Platform Support
Primary platform is **Linux (amd64)**. Pekoe is proven to work on **Alpine Linux 3.16 x86_64**.
Cross-compilation targets (via `make build-all`):
- linux/amd64, linux/arm64
- darwin/amd64, darwin/arm64
- freebsd/amd64
- windows/amd64
## Building
```bash
# Clone the repository
git clone https://github.com/eleksir/pekoe.git
cd pekoe
# Build the binary
make
# Run tests with race detection
make test
# Run linters
make lint
# Cross-compile for all platforms
make build-all
# Install to /usr/local/bin (requires root)
make install
```
### Makefile Targets
| Target | Description |
|--------|-------------|
| `make` | Clean and build (default) |
| `make build` | Build the binary |
| `make test` | Run tests with race detection |
| `make clean` | Remove built binaries |
| `make build-all` | Cross-compile for all platforms |
| `make lint` | Run linters (`go vet`, `golangci-lint` if available) |
| `make install` | Build and install to `/usr/local/bin/` |
| `make help` | Show available targets |
### Installation
```bash
# Install to /usr/local/bin (requires root)
make install
# Or manually
cp pekoe /usr/local/bin/
# Create config directory and copy default config
mkdir -p /etc/pekoe
cp config_example.json /etc/pekoe/config.json
```
### Run
```bash
# Start with auto-detected config
./pekoe
# Start with explicit config file
./pekoe --config /path/to/config.json
```
### Alpine Linux Init Script
For Alpine Linux, copy the init script from `contrib/pekoe.initd`:
```bash
cp contrib/pekoe.initd /etc/init.d/pekoe
chmod +x /etc/init.d/pekoe
rc-update add pekoe default
```
## Limitations
- No persistence (messages are not stored)
- No clustering or replication
- No authentication
These are intentional design choices to keep Pekoe simple and focused on pub/sub messaging.