- 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
9.4 KiB
Pekoe - Redis Pub/Sub Server
Project Overview
Pekoe is a Redis-compatible server implementing only the Pub/Sub functionality, written in Go using only the standard library (minimal external dependencies).
Purpose
A lightweight, focused Pub/Sub message broker that provides core Redis pub/sub capabilities without the complexity of full Redis functionality. Ideal for applications that need only pub/sub messaging.
Key Design Decisions
- Go stdlib only - Avoid external dependencies for simplicity and security
- Per-channel locking - Fine-grained concurrency control in the pubsub hub to maximize throughput
- RESP protocol - Full Redis-compatible wire protocol for client compatibility
- JSON config - Human-readable configuration with validation and sensible defaults
- Graceful shutdown - Signal handling for clean server termination (SIGINT, SIGTERM)
- Log re-opening - SIGUSR1 (Unix) or SIGHUP (Windows) for log rotation support
- Case-insensitive commands - Commands are case-insensitive per Redis specification
- INFO command - Full Redis-compatible INFO output for client compatibility
- Graceful shutdown timeout - Configurable timeout to prevent server hanging on misbehaving clients
Fundamental Rules
Do not apply changes without explicit user consent. When asked to analyze, review, or suggest any changes, present your proposal first and wait for confirmation before editing any files. Always ask "Should I implement this?" or "Should I apply these changes?" before modifying anything.
Keep codebase simple, small, maintainable and human-readable. Favour minimal, clear solutions over clever or abstract ones. Prefer clarity over cleverness. Prioritize explicitness. Minimize magic. Do not introduce patterns, layers, or indirection unless they directly solve a concrete problem. Every line of code must earn its place.
Create small focused modules and separate functionality over files. Do not make universal implementation; try to move functions to separate files and name files after function names.
Use a consistent structure. Subsequent searches will be faster and consume less context.
Avoid duplicate names. This is not strict rule. If codebase contain multiple files with the same name (e.g., index.ts), we have to read them all to discover the one exactly we need. This consumes time and context.
When stuck, ask clarification questions, or present a short plan, or both. Do not try to guess intent or make assumptions - ask.
Always make TODO plan before any job and follow it closely.
Report any found defects, issues, bugs. And keep track of them.
Comment the code. Add code comments to explain corner cases, complex logic, business rules, and design decisions. Focus on what problem the code is solving rather than how it is solving it. Write comments in easy to read and understand language, for humans to comprehend. Every public API must have a docstring.
Use domain names. Use naming conventions that reflect their purpose and domain concepts, not their technical implementation (e.g., OrderProcessor instead of OrderServiceFactory).
Test corner cases. Write comprehensive test suites that cover various scenarios and edge cases.
Use tags and labels. In comments, add tags or labels to files, classes, or functions to categorize them based on functionality, domain concepts, or design patterns.
Avoid obvious comments that do not add value.
Remove dead code. A half-finished refactoring can actually harm the contributions.
Update AGENTS.md and TODO.md automatically. Keep AGENTS.md dense, detailed and concise.
Never commit AGENTS.md, llms.txt, or TODO.md.
Architecture
pekoe/
├── cmd/pekoe/ # Entry point (main.go)
├── internal/
│ ├── config/ # Configuration loading and validation
│ │ └── configpath_*.go # Platform-specific default config paths
│ ├── 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, command routing
│ │ ├── 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/ # Init scripts for system service management
├── Makefile # Build automation
├── config_example.json # Example configuration
└── README.md # User documentation
Dependencies
- Go 1.24+
- Standard library only for production code
- Test dependencies (dev only):
github.com/redis/go-redis/v9- go-redis client for compatibility testsgithub.com/redis/rueidis- rueidis client for compatibility testsgithub.com/gomodule/redigo- redigo client for compatibility tests
Platform Support
Primary platform: Linux (amd64). Proven on Alpine Linux 3.16 x86_64.
Cross-compile targets: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, freebsd/amd64, windows/amd64.
Config file lookup:
- Linux (regular):
~/.pekoe.json→/etc/pekoe/config.json - Linux (root):
/etc/pekoe/config.json - macOS/BSD:
~/.pekoe.json→/usr/local/etc/pekoe/config.json - Windows:
--configflag required
Building: make (builds pekoe binary), make build-all (cross-compile), make install (to /usr/local/bin/).
Testing
- Go standard testing package
- Run all tests:
make test(with race detection) orgo test ./... - Run compatibility tests:
go test ./internal/compatibility/... - Run with verbose:
go test -v ./internal/compatibility/...
Test Coverage
Compatibility tests validate pekoe against multiple Redis client libraries:
| Library | Tests | Coverage |
|---|---|---|
| go-redis/v9 | 27 | Subscribe, publish, patterns, pubsub commands, unicode, binary |
| rueidis | 9 | Connectivity, publish, unicode messages |
| redigo | 19 | Raw protocol, subscribe, publish, patterns |
Test scenarios include:
- Basic SUBSCRIBE/PUBLISH/UNSUBSCRIBE
- Pattern PSUBSCRIBE/PUNSUBSCRIBE with wildcards
- Multiple channel/pattern subscriptions
- PUBSUB NUMSUB, CHANNELS, NUMPAT, SHARDCHANNELS, SHARDNUMSUB commands
- PING command (with and without message)
- QUIT command
- SELECT database switching (for client compatibility)
- INFO server information (full Redis-compatible output)
- CLIENT ID, SETNAME, GETNAME, LIST, INFO, KILL, PAUSE, UNPAUSE, REPLY commands
- Binary-safe messages
- Empty and long messages
- Unicode content
- Concurrent operations
- Special characters in channel names
- Mixed channel and pattern subscriptions
Commands Supported
- SUBSCRIBE - Subscribe to channels
- UNSUBSCRIBE - Unsubscribe from channels
- PSUBSCRIBE - Subscribe to patterns
- PUNSUBSCRIBE - Unsubscribe from patterns
- PUBLISH - Publish message to channel
- PUBSUB - Pub/Sub introspection (NUMSUB, CHANNELS, NUMPAT, SHARDCHANNELS, SHARDNUMSUB, HELP)
- PING - Health check
- QUIT - Disconnect
- SELECT - Switch database (for client compatibility, no-op in pekoe)
- INFO - Server information
- CLIENT - Connection introspection and control:
- CLIENT ID - Return unique connection ID
- CLIENT SETNAME/GETNAME - Connection naming
- CLIENT LIST - List connected clients
- CLIENT INFO - Client connection information
- CLIENT KILL - Close connections
- CLIENT PAUSE/UNPAUSE - Pause control
- CLIENT REPLY - Reply mode control (ON/OFF/SKIP)
- CLIENT TRACKING - Client-side caching (no-op for compatibility)
- CLIENT NO-EVICT/NO-TOUCH/CACHING - Compatibility stubs
Logging
Pekoe logs in a structured key=value format with timestamp and log level prefix:
Format: YYYY Mmm dd hh:mm:ss LEVEL pekoe[pid]: key=value ...
Example: 2024 Apr 2 14:30:15 INFO pekoe[12345]: event=startup addr=0.0.0.0:6379
Log levels:
debug- All messages including DEBUGinfo- INFO and above (default)warn- WARN, ERROR, CRITICALerror- ERROR, CRITICAL only
Signal handling:
- SIGINT/SIGTERM - Graceful shutdown
- SIGHUP - Reserved for future config reload
- SIGUSR1 (Unix) - Reopen log files for rotation
- SIGHUP (Windows) - Reopen log files for rotation
PID File Support
When pid_file is configured, pekoe will:
- Create the PID file on startup with the process ID
- Remove the PID file on graceful shutdown
- Log PID file events (created, removed, errors)
The PID file is optional - if pid_file is empty, no PID file is created.
Graceful Shutdown Timeout
When receiving shutdown signal (SIGINT/SIGTERM), pekoe waits for active connections to finish before exiting. The shutdown_timeout config option (default: 30 seconds) controls how long to wait:
- If all connections close within the timeout, shutdown completes normally
- If timeout expires, server exits forcefully (active connections are lost)
Set to 0 to wait forever (not recommended - server may hang on misbehaving clients).
Run ID
Each server instance generates a unique 40-character hexadecimal run ID on startup (like Redis). This ID is:
- Displayed in INFO output as
run_id - Useful for distinguishing between server restarts in logs
- Generated using 160 bits of cryptographic randomness