Files
scrabble-game/gateway/internal/connectsrv/activeusers.go
T
Ilia Denisov 8700fbfae1
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 19s
CI / deploy (pull_request) Failing after 1s
Stage 16: deploy infra & test contour
- backend + gateway multi-stage distroless Dockerfiles; the gateway embeds and
  serves the SPA at / and /telegram/ via go:embed (committed dist placeholder,
  real build baked in by the image's node stage)
- deploy/docker-compose.yml: backend + gateway + Postgres + Telegram connector
  (VPN sidecar) + OTel Collector + Prometheus (15d) + Tempo (72h) + Grafana,
  fronted by a caddy owning a single /_gm Basic-Auth (admin console + Grafana
  subpath); inter-service on a private network, only caddy on the edge network
- new metrics: backend accounts_created_total{kind} (robots excluded) and an
  in-memory gateway active_users{window=24h,7d} gauge
- CI: single .gitea/workflows/ci.yaml (unit/integration/ui + a gated test-contour
  deploy) on the new feature/* -> development -> master branch model; the old
  go-unit/integration/ui-test workflows are folded in; the connector-scoped
  compose is retired (superseded by deploy/)
- docs: ARCHITECTURE §11/§12/§13, root + gateway READMEs, CLAUDE.md branching,
  PLAN.md (stage 16 done + refinements + Stage 17 forward-notes)
2026-06-05 11:42:26 +02:00

64 lines
1.6 KiB
Go

package connectsrv
import (
"sync"
"time"
)
// activeUsers tracks distinct authenticated accounts by last-action time, backing
// the in-memory active_users gauge. It is single-process by design (the gateway is
// single-instance in the MVP, docs/ARCHITECTURE.md §10): the distinct count is
// correct for one process, resets on restart, and is a live operational gauge, not
// a billing figure. Memory is bounded by the number of distinct accounts active
// within the longest window; stale entries are pruned on observation.
type activeUsers struct {
mu sync.Mutex
lastSeen map[string]time.Time
now func() time.Time
}
// newActiveUsers returns an empty tracker using the wall clock.
func newActiveUsers() *activeUsers {
return &activeUsers{lastSeen: make(map[string]time.Time), now: time.Now}
}
// seen records that account uid performed an authenticated action now.
func (a *activeUsers) seen(uid string) {
if uid == "" {
return
}
a.mu.Lock()
a.lastSeen[uid] = a.now()
a.mu.Unlock()
}
// counts returns, for each window, the number of distinct accounts last seen
// within it, pruning entries older than the longest window in the same pass.
func (a *activeUsers) counts(windows []time.Duration) []int {
a.mu.Lock()
defer a.mu.Unlock()
now := a.now()
var longest time.Duration
for _, w := range windows {
if w > longest {
longest = w
}
}
res := make([]int, len(windows))
for uid, ts := range a.lastSeen {
age := now.Sub(ts)
if age > longest {
delete(a.lastSeen, uid)
continue
}
for i, w := range windows {
if age <= w {
res[i]++
}
}
}
return res
}