- 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
57 lines
2.7 KiB
Markdown
57 lines
2.7 KiB
Markdown
# 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 |
|