Update infrastructure roadmap: Phase C uses Redis adapter not sticky sessions
19 room-targeted socket emit callsites need cross-instance fan-out — the Redis adapter handles them all automatically. Drops sticky sessions and the custom drain script in favour of compose update_config order:start-first. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
968aa72992
commit
164c2adfb7
1 changed files with 23 additions and 27 deletions
|
|
@ -109,32 +109,37 @@ Non-secret config (NODE_ENV, PGBOUNCER, APP_URL, etc.) stays inline in the compo
|
|||
|
||||
Requires Phase B (no `container_name`, healthcheck, pinned tag).
|
||||
|
||||
Run 2 web containers behind Traefik with sticky sessions. Rolling deploy takes one replica out of rotation, upgrades it, puts it back — other replica keeps serving.
|
||||
Run 2 web containers behind Traefik, with the **Redis Socket.IO adapter** handling cross-instance fan-out. Rolling deploy uses Docker Compose's `update_config` to bring up the new replica before stopping the old one.
|
||||
|
||||
**Why Redis (not sticky sessions + polling/LISTEN/NOTIFY):** there are 19 room-targeted `getSocketIO().to(draft-${seasonId}).emit()` callsites across routes, models, and the timer. With sticky sessions, two different browsers watching the same draft (phone + laptop) can land on different instances — every one of those 19 emits needs to reach both. Polling introduces unacceptable latency for live draft events (pause, force-pick, timer). LISTEN/NOTIFY would require instrumenting all 19 callsites. The `@socket.io/redis-adapter` intercepts all of them automatically with zero callsite changes, and lets us drop sticky sessions entirely (simpler deploy). Redis is a single ~5MB container — at this point it's clearly justified.
|
||||
|
||||
**Compose changes:**
|
||||
```yaml
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
|
||||
# On the brackt service:
|
||||
deploy:
|
||||
replicas: 2
|
||||
|
||||
labels:
|
||||
- "traefik.http.services.brackt.loadbalancer.sticky.cookie=true"
|
||||
- "traefik.http.services.brackt.loadbalancer.sticky.cookie.name=brackt_instance"
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first # new replica healthy before old one stops
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
# (remove sticky session labels — not needed with Redis adapter)
|
||||
```
|
||||
|
||||
Socket.IO clients must return to the same instance (sticky cookie). No Redis adapter needed at current scale — the second instance is purely for deploy rotation, not capacity.
|
||||
**App change** (`server/socket.ts`): install `@socket.io/redis-adapter` + `redis`, attach the adapter when `REDIS_URL` is set. Gate on env var so dev works without Redis.
|
||||
|
||||
**Rolling deploy script:**
|
||||
1. Scale down replica A (remove from Traefik pool)
|
||||
2. Wait for drain (or 5s hard cut — reconnect works)
|
||||
3. Pull new image on A, `docker compose up` A
|
||||
4. Healthcheck passes → A back in pool
|
||||
5. Repeat for B
|
||||
**Rolling deploy:** `docker compose up -d` with `order: start-first` handles it automatically — no custom drain script needed.
|
||||
|
||||
**Cross-instance broadcasts** (admin action on instance B needs to reach draft clients on instance A): use a pending-broadcasts table polled every 5s by each instance. With deadline-based timers the volume is tiny. Postgres `LISTEN/NOTIFY` is the upgrade path if polling proves too slow — requires a session-mode pgbouncer pool.
|
||||
**Files:** `.production-info/docker-compose-production.yml`, `server/socket.ts`, `package.json`.
|
||||
|
||||
**Files:** `.production-info/docker-compose-production.yml` (replicas + sticky labels), rolling deploy script.
|
||||
|
||||
**Verification:** rolling-deploy both instances during a staging draft; clients see at most one brief reconnect.
|
||||
**Verification:** start two instances locally, open two draft-room tabs, confirm pick events reach both. Rolling-deploy in production; clients see at most one brief reconnect.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -146,14 +151,6 @@ After Phase 1, the "tick" is an idle process that wakes only at pick deadlines.
|
|||
|
||||
---
|
||||
|
||||
### Phase E — Defer: Socket.IO Redis adapter
|
||||
|
||||
Only needed if a single draft's clients need to span multiple web instances (e.g., 50+ concurrent connections per draft, or geo-distributed). Not relevant at current scale.
|
||||
|
||||
If/when: add Redis, `@socket.io/redis-adapter`, drop sticky sessions. ~1-day change if Phases B–C are clean.
|
||||
|
||||
---
|
||||
|
||||
## Sequencing summary
|
||||
|
||||
| Phase | Status | Effort | Key win |
|
||||
|
|
@ -161,11 +158,10 @@ If/when: add Redis, `@socket.io/redis-adapter`, drop sticky sessions. ~1-day cha
|
|||
| 1 | ✅ Done | — | Deadline-based timer; eliminates 1s tick |
|
||||
| A | ✅ Done | — | External cron; automated standings sync + sim; /healthz |
|
||||
| B | 🔜 Next | ~1d | Compose delivered by CI; pinned tags; healthcheck; unblocks C |
|
||||
| C | Blocked on B | ~2–3d | Zero-downtime rolling deploys |
|
||||
| C | Blocked on B | ~2d | Zero-downtime rolling deploys via replicas + Redis adapter |
|
||||
| D | Optional | ~1wk | Deploy web without restarting timer process |
|
||||
| E | Deferred | ~1d | Cross-instance socket fan-out (not needed yet) |
|
||||
|
||||
**Total to zero-downtime deploys: ~3–4 days of focused work** (Phases B + C).
|
||||
**Total to zero-downtime deploys: ~3 days of focused work** (Phases B + C).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue