6.2 KiB
6.2 KiB
Technical Design: WebSocket Chat with Redis Pub/Sub
Architecture
The implementation leverages the existing pkg/realtime package which provides:
LocalHub: In-memory connection and room managementRedisBroadcaster: Cross-pod message distribution via Redis pub/subHandler: HTTP handler for WebSocket upgrade and lifecycle
┌─────────────────────────────────────────────────────────────────┐
│ chat-api Service │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ WebSocket │────▶│ LocalHub │────▶│ Redis Broadcaster│ │
│ │ Handler │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └────────┬────────┘ │
│ │ ▲ │ │
│ │ │ ▼ │
│ ▼ │ ┌──────────────┐ │
│ ┌──────────────┐ │ │ Redis │ │
│ │ Clients │ └──────────────│ Pub/Sub │ │
│ │ (WebSocket) │ └──────────────┘ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Components
1. Configuration (internal/config/config.go)
Add Redis URL configuration:
type Config struct {
// ... existing fields
RedisURL string
}
func Load() *Config {
return &Config{
// ... existing
RedisURL: os.Getenv("REDIS_URL"),
}
}
2. Main Entry Point (cmd/server/main.go)
Initialize realtime components with context for graceful shutdown:
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create hub and start event loop
hub := realtime.NewHub(logger)
go hub.Run(ctx)
// Create Redis broadcaster (if configured)
var broadcaster realtime.Broadcaster
if cfg.RedisURL != "" {
redisClient := redis.NewClient(&redis.Options{...})
broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger)
go broadcaster.Run(ctx)
}
// Pass to route registration
api.RegisterRoutes(application, exampleService, hub, broadcaster)
}
3. Route Registration (internal/api/routes.go)
Mount WebSocket handler:
func RegisterRoutes(app *app.App, exampleService *service.ExampleService,
hub realtime.Hub, broadcaster realtime.Broadcaster) {
wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{
Broadcaster: broadcaster,
AuthRequired: cfg.AuthEnabled,
})
app.Route("/api/chat-api", func(r app.Router) {
// ... existing routes
// WebSocket routes
r.Mount("/ws", wsHandler.Routes())
})
}
4. WebSocket Handler (pkg/realtime/handler.go)
The existing handler already provides:
Routes()returning Chi router withGET /andGET /{room}HandleWebSocket()for upgrade and lifecycleGetStats()for connection statistics
Add stats endpoint in routes.go:
r.Get("/ws/stats", func(w http.ResponseWriter, r *http.Request) {
stats := wsHandler.GetStats()
httpresponse.OK(w, r, stats)
})
Message Flow
Outbound (Client → Server → Redis → All Pods)
- Client sends JSON message via WebSocket
WSClient.readPump()decodes messageHandler.makeMessageHandler()processes message- If broadcaster configured:
Broadcaster.Publish()to Redis - Redis distributes to all subscribed pods
- Each pod's
RedisBroadcaster.Run()receives message Hub.Broadcast()delivers to local connections
Inbound (Redis → Server → Clients)
RedisBroadcaster.Run()subscribes to Redis channels- Receives message, skips if from same pod (echo prevention)
- Calls
Hub.Broadcast()with message LocalHub.doBroadcast()delivers to room or all connections- Each
Connection.Send()queues to client's send buffer WSClient.writePump()writes to WebSocket
Redis Channel Structure
realtime:global- Messages without room targetingrealtime:room:{room}- Messages for specific room
Configuration
Environment variables:
| Variable | Description | Default |
|---|---|---|
REDIS_URL |
Redis connection URL | (empty = local-only mode) |
AUTH_ENABLED |
Require authentication for WebSocket | false |
Graceful Shutdown
- Server receives SIGTERM/SIGINT
- Context cancelled
Hub.Run()exits, closes all connectionsRedisBroadcaster.Run()closes Redis subscription- Server shutdown completes
Files to Modify
services/chat-api/internal/config/config.go- Add RedisURLservices/chat-api/cmd/server/main.go- Initialize hub/broadcasterservices/chat-api/internal/api/routes.go- Mount WebSocket handlerservices/chat-api/.env.example- Add REDIS_URL
Files to Create
services/chat-api/internal/api/handlers/ws.go- Stats handler wrapperservices/chat-api/internal/api/handlers/ws_test.go- WebSocket tests
Dependencies
Already available in pkg/go.mod:
github.com/gorilla/websocket v1.5.3github.com/redis/go-redis/v9 v9.7.0