8.0 KiB
8.0 KiB
Technical Design: WebSocket Chat
Architecture Overview
This feature integrates the existing pkg/realtime package into the chat-api service to provide WebSocket chat functionality with Redis pub/sub for horizontal scaling.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client 1 │ │ Client 2 │ │ Client N │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ WebSocket │ WebSocket │ WebSocket
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ chat-api Pod 1 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ realtime.Handler │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ LocalHub │ │ │
│ │ │ - connections map[id]Connection │ │ │
│ │ │ - rooms map[room]map[id]struct{} │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ RedisBroadcaster │ │
│ │ - Publish() → Redis channel │ │
│ │ - Run() ← Redis PSubscribe │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────┘
│
┌────────────┴────────────┐
│ Redis Pub/Sub │
│ realtime:global │
│ realtime:room:{name} │
└────────────┬────────────┘
│
┌──────────────────────────┴───────────────────────────┐
│ chat-api Pod 2 │
│ (same structure) │
└──────────────────────────────────────────────────────┘
Component Design
1. Configuration Extension
Extend internal/config/config.go to include Redis configuration:
type Config struct {
// ... existing fields ...
// Redis
RedisURL string
}
func Load() *Config {
return &Config{
// ... existing ...
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
}
}
2. Service Bootstrap (main.go)
Wire up the realtime components in the service entry point:
func main() {
cfg := config.Load()
ctx := context.Background()
// Create hub (local connection registry)
hub := realtime.NewHub(logger)
go hub.Run(ctx)
// Create Redis broadcaster (for multi-pod)
var broadcaster realtime.Broadcaster
if cfg.RedisURL != "" {
redisClient := redis.NewClient(parseRedisURL(cfg.RedisURL))
broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger)
go broadcaster.Run(ctx)
}
// Register routes with realtime handler
api.RegisterRoutes(application, exampleService, hub, broadcaster)
}
3. Route Registration
Add WebSocket routes in internal/api/routes.go:
func RegisterRoutes(
application *app.App,
exampleService *service.ExampleService,
hub realtime.Hub,
broadcaster realtime.Broadcaster,
) {
// ... existing routes ...
// WebSocket handler
wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{
Broadcaster: broadcaster,
OnConnect: func(conn realtime.Connection) {
logger.Info("client connected", "id", conn.ID())
},
OnDisconnect: func(conn realtime.Connection) {
logger.Info("client disconnected", "id", conn.ID())
},
OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message {
// Set message type if not specified
if msg.Type == "" {
msg.Type = realtime.MessageTypeChat
}
return msg
},
AuthRequired: cfg.AuthEnabled,
})
// Mount WebSocket routes
application.Mount("/api/chat-api/ws", wsHandler.Routes())
}
4. Message Flow
-
Client → Server (Incoming)
- Client sends JSON message via WebSocket
WSClient.readPump()decodes messageOnMessagecallback transforms/filtersRedisBroadcaster.Publish()sends to Redis channel
-
Redis → All Pods (Distribution)
- Redis delivers to all subscribed pods
RedisBroadcaster.Run()receives via PSubscribe- Filters out messages from same pod (echo prevention)
- Forwards to
LocalHub.Broadcast()
-
Server → Clients (Outgoing)
LocalHub.doBroadcast()iterates room connectionsWSClient.Send()queues messageWSClient.writePump()writes to WebSocket
5. Redis Channel Naming
| Pattern | Usage |
|---|---|
realtime:global |
Messages without room (broadcast to all) |
realtime:room:{name} |
Room-specific messages |
6. Graceful Shutdown
The existing pkg/realtime package handles:
- Context cancellation triggers hub shutdown
- Hub closes all connections
- WSClient sends close frame
- Redis broadcaster closes subscription
File Changes
| File | Change |
|---|---|
services/chat-api/internal/config/config.go |
Add RedisURL field |
services/chat-api/cmd/server/main.go |
Wire hub, broadcaster, Redis client |
services/chat-api/internal/api/routes.go |
Mount WebSocket handler |
services/chat-api/internal/api/spec.go |
Add WebSocket endpoint documentation |
Testing Strategy
Unit Tests
- Config loading with Redis URL
- Message transformation in OnMessage callback
Integration Tests
- WebSocket connection upgrade
- Message send/receive flow
- Room-based isolation
- Multi-client broadcast
Manual Testing
# Terminal 1: Start server
./scripts/dev.sh
# Terminal 2: Connect with wscat
wscat -c ws://localhost:8001/api/chat-api/ws
# Terminal 3: Another client
wscat -c ws://localhost:8001/api/chat-api/ws
# Send message from Terminal 2, verify received in Terminal 3
{"type":"chat","data":{"content":"Hello!"}}
Security Considerations
- Authentication: Optional auth via
AUTH_ENABLEDconfig - Origin Check: WebSocket upgrader validates Origin header (configurable)
- Message Size: 64KB limit prevents memory exhaustion
- Buffer Limits: 256-message buffer drops excess (prevents slow-client backpressure)
Rollout Plan
- Deploy with
REDIS_URLset to enable cross-pod messaging - Without
REDIS_URL, operates in single-pod mode (local broadcast only) - No database migrations required
- Feature is additive (no breaking changes)