Files
dsutanto 1c7b78b580
CI / lint (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / build (push) Canceled after 0s
ai-dev: graceful shutdown, tests, stdlib context, lint/CI, error logging
- 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
2026-09-03 12:52:21 +07:00

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 |