- T1: SIGTERM/SIGINT → client.Disconnect(250) → os.Exit(0) - T2: 11 tests with miniredis mock (handleMessage, getEnv, parseInt, randomString) - T3: golang.org/x/net/context → stdlib context, math/rand → math/rand/v2 - T4: .golangci.yml + .github/workflows/ci.yml - T5: Redis SETEX error log includes key and memberID - Dockerfile: add git for go mod tidy
2.7 KiB
2.7 KiB
SPEC.md — MQTT Presence Redis Go
§G Goal
MQTT presence bridge: subscribe MQTT topic, parse memberID, store presence JSON in Redis with TTL. Port of Django original for Backone platform.
§C Constraints
- Single binary, single file (
mqtt_presence_redis_go.go) - Go 1.24.3
- All config via env vars, no config files
- Redis 6+ required (SETEX)
- MQTT 3.1.1 via paho client
- Docker deployment (multi-stage: golang:1.24-alpine → alpine:3.20)
- No graceful shutdown — blocks forever (
select {}) - No tests, no CI, no linter
§I Interfaces
| id | type | detail |
|---|---|---|
| I.mqtt | input | Subscribe MQTT_TOPIC_PRESENCE (default presence), QoS 0, KeepAlive 30s, CleanSession true, AutoReconnect true (paho defaults) |
| I.redis | output | SETEX presence:<memberID> with JSON {mqtt, ts}, TTL MQTT_REDIS_SETEX seconds |
| I.env | config | MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASS, MQTT_TOPIC_PRESENCE, MQTT_REDIS_HOST, MQTT_REDIS_PORT, MQTT_REDIS_DB, MQTT_REDIS_PREFIX, MQTT_REDIS_SETEX, MQTT_REDIS_PASSWORD |
§V Invariants
| id | invariant |
|---|---|
| V1 | memberID extracted as first ;-delimited segment of MQTT payload |
| V2 | memberID truncated to 50 chars max |
| V3 | Redis key format: <MQTT_REDIS_PREFIX>:<memberID> (default presence:<memberID>) |
| V4 | Redis value: JSON {mqtt: <raw_payload>, ts: <unix_timestamp>} |
| V5 | Redis TTL: MQTT_REDIS_SETEX seconds (default 86400 = 24h) |
| V6 | MQTT client ID: mqtt_presence_redis_go_<random10> — random per restart |
| V7 | Startup fails fast (log.Fatalf) if Redis ping or MQTT connect fails |
| V8 | On MQTT connect/reconnect: auto-subscribe to presence topic |
| V9 | Empty payload → memberID = "" → Redis key presence: → write still occurs (current behavior, not early-return) |
| V10 | Each MQTT message OVERWRITES prior Redis value for same memberID — no append, no merge |
| V11 | MQTT paho defaults apply: KeepAlive=30s, CleanSession=true, AutoReconnect=true |
| V12 | Malformed env var (non-numeric MQTT_REDIS_DB/MQTT_REDIS_SETEX) → parseInt returns 0 silently |
§T Tasks
| id | status | desc | cites |
|---|---|---|---|
| T1 | x | Add graceful shutdown (SIGTERM → disconnect MQTT → exit) | V8 |
| T2 | x | Add tests for handleMessage parsing logic | V1,V2,V3,V4,V9,V10 |
| T3 | x | Replace golang.org/x/net/context with stdlib context |
— |
| T4 | x | Add golangci-lint config and CI pipeline | — |
| T5 | x | Add error metrics/logging for Redis SETEX failures | — |
§B Bugs
| id | date | cause | fix |
| B1 | 2026-09-03 | V9 stated "empty payload → no write" but strings.Split("", ";") returns [""] (len 1) — guard never fires. Empty payload writes to Redis. | V9 corrected |