- Fix metrics: change MessagesTotal, ConnectionsTotal, DisconnectionsTotal from Gauge to Counter with proper _total naming convention - Fix broadcast write-error handling: failed clients now get properly removed with accurate metrics updates - Add graceful shutdown: SIGINT/SIGTERM handling with 10s timeout, CloseGoingAway frame sent to clients before disconnect - Add integration tests: 11 tests using real WebSocket connections covering connect, broadcast, disconnect, concurrency, and shutdown - Fix example client port: changed from 8000 to 8443 to match config - Rewrite README.md to reflect current features and usage - Add AGENTS.md and .agents/summary/ documentation for AI assistants
6.6 KiB
6.6 KiB
AGENTS.md — AI Assistant Guide for websocket-relay
This file provides context for AI coding assistants working on this project. It focuses on information not found in README.md and is optimized for quick comprehension.
Project Identity
websocket-relay is a minimal Go WebSocket relay server that broadcasts every incoming message to all connected clients (hub-and-spoke / fan-out pattern). It supports TLS, Prometheus metrics, and graceful shutdown.
Directory Structure
websocket-relay/
├── main.go # Entry point: config loading, signal handling, graceful shutdown
├── internal/
│ ├── config/config.go # YAML config loader (server port, TLS, metrics)
│ ├── hub/hub.go # Core logic: WebSocket hub, connection mgmt, broadcast
│ └── metrics/metrics.go # Prometheus counter/gauge definitions
├── example/index.html # Browser P2P chat demo client
├── config.yaml # Runtime config (edit for local dev)
├── config.example.yaml # Reference config with TLS enabled
├── Makefile # build, test, release, run, deps, clean
├── .gitea/workflows/
│ ├── ci.yml # Push/PR → test + lint
│ └── release.yml # Tag v* → cross-compile + Gitea release
└── .agents/summary/ # Generated documentation (see index.md)
Architecture at a Glance
graph LR
C1[Client] -->|ws| HUB[Hub goroutine]
C2[Client] -->|ws| HUB
HUB -->|broadcast| C1
HUB -->|broadcast| C2
HUB --> MET[Prometheus :9090]
- Single Hub goroutine runs a
selectloop on 4 channels:register,unregister,broadcast,stop - Per-client reader goroutine reads messages and pushes to
broadcastchannel - No write goroutine — writes happen inline during broadcast (under RLock)
- Thread safety via
sync.RWMutexon the clients map - Graceful shutdown via SIGINT/SIGTERM → HTTP server shutdown → Hub shutdown → clean exit
Coding Patterns
Package Layout
- All internal packages live under
internal/(Go internal package convention — cannot be imported externally) - Flat package structure — each package has one primary
.gofile - Tests use
_test.gosuffix in the same package (white-box testing)
Naming Conventions
- Exported functions/types:
PascalCase(e.g.,New,Run,HandleWebSocket,Shutdown) - Config struct uses nested anonymous structs with
yamltags - Metrics use package-level
varblock withpromautofor auto-registration
Error Handling
- Fatal errors at startup →
log.Fatal() - WebSocket upgrade errors → logged and returned (no panic)
- Write errors during broadcast → client removed with proper metrics update
- Config load errors → fatal (server won't start without valid config)
- Shutdown errors → logged but not fatal (best-effort cleanup)
Concurrency Pattern
- CSP via channels (not mutexes for coordination)
- The Hub
selectloop is the single coordination point - RWMutex used additionally for broadcast iteration safety
stopchannel (closed on shutdown) signals the Hub to terminate
Graceful Shutdown Pattern
main()listens for SIGINT/SIGTERM viaos/signal- On signal: stops accepting new HTTP connections → shuts down Hub → exits
- 10-second timeout ensures the process doesn't hang indefinitely
- Clients receive a WebSocket Close frame (
CloseGoingAway) before disconnection
How to Write & Run Tests
Running Tests
make test # Runs: go test -v ./...
go test ./internal/hub/ # Single package
go test -run TestNew ./internal/hub/ # Single test
Test Conventions
- Test files:
*_test.goin same package - Use standard
testing.T— no test framework - Table-driven tests not yet adopted (tests are simple)
- Temp files for config tests (
os.CreateTemp) - Hub tests start
go h.Run()and usedefer h.Shutdown()for cleanup - Integration tests use
httptest.Server+ real WebSocket dials
Adding New Tests
// File: internal/<pkg>/<pkg>_test.go
package <pkg>
import "testing"
func TestFeature(t *testing.T) {
// Setup
h := New()
go h.Run()
defer h.Shutdown()
// Assert
if h.ClientCount() != 0 {
t.Errorf("expected 0, got %d", h.ClientCount())
}
}
Missing Test Coverage
internal/metrics— no tests (metrics are auto-registered, mostly testing Prometheus library)- No benchmarks
Configuration
Config is loaded from YAML (default: config.yaml, override with --config-file flag):
server:
port: 8443
tls:
enabled: false # Set true + provide cert/key for wss://
cert_file: cert.pem
key_file: key.pem
metrics:
enabled: true
port: 9090
Build & Deployment
make build # → build/websocket-relay
make release # → build/websocket-relay-linux-amd64, build/websocket-relay-darwin-arm64
make run # → go run .
make deps # → go mod tidy
make clean # → rm build artifacts
Release Process
- Tag with
v*prefix (e.g.,git tag v1.2.0) - Push tag → Gitea Actions builds linux/amd64 + darwin/arm64
- Binaries uploaded as Gitea Release assets
Known Issues & Technical Debt
| Issue | Severity | Location |
|---|---|---|
No message size limits (ReadLimit) |
Security | internal/hub/hub.go |
| No connection count limits | Security | internal/hub/hub.go |
gorilla/websocket is archived |
Debt | go.mod |
Adding Features — Quick Guide
Adding a new config field
- Add field to
Configstruct ininternal/config/config.gowithyamltag - Add to
config.yamlandconfig.example.yaml - Use in
main.goviacfg.YourSection.YourField
Adding a new metric
- Add
vartointernal/metrics/metrics.gousingpromauto.NewGauge/Counter/Histogram - Call
metrics.YourMetric.Inc()(or.Set(),.Observe()) where needed
Adding a new HTTP endpoint
- Add handler method to Hub or create new handler
- Register in
main.go:mux.HandleFunc("/path", handler)
Adding a new internal package
- Create
internal/<name>/<name>.go - Import as
websocket-relay/internal/<name>
Detailed Documentation
For deeper analysis, see .agents/summary/index.md which provides a complete knowledge base with:
- Architecture diagrams and concurrency model
- Component-level documentation with all structs/methods
- Complete interface specifications
- Data model definitions and state machines
- Workflow sequence diagrams
- Dependency analysis and security notes
- Prioritized improvement recommendations