Files
Alarm/src/realtime/latency_test.go
T
Admin 6e6a1e9c8a [feat](alarm): add a durable per-device delivery ledger
pg_notify and the socket fan-out are not durable, so an offline phone used to miss a trigger forever and no-auto-stop would leave it ringing at nobody. Write one ledger row per enabled subscription in the trigger transaction, replay what a subscription still owes on every socket connect, and clear a row only on an explicit ack (live socket ack or ackAlarm); alarmStatus is a read-only check-in that answers 409 ERR_ALARM_TRIGGERED_OFFLINE with the missed frame. Ack is idempotent, and a destroyed alert cascades its debt away so a client is never told it missed an alarm that no longer exists.
2026-10-19 02:26:46 +01:00

77 lines
2.6 KiB
Go

// Copyright (C) 2026 Dcntrlized
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package realtime
import (
"sort"
"testing"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
// TestBroadcastLatency measures the fan-out path on real sockets: from the
// instant Broadcast is called to the instant BOTH subscribers have decoded the
// frame. It excludes the HTTP action and the database insert that precede a
// trigger in production, so it is the floor for the end-to-end number.
//
// It logs p50/p95/max and only fails on a gross regression (p95 > 500ms) to
// stay useful without being flaky on a loaded machine.
func TestBroadcastLatency(t *testing.T) {
alertID := uuid.New()
_, wsURL, hub := startTestServer(t, alertID)
first := dial(t, wsURL)
second := dial(t, wsURL)
waitClients(t, hub, alertID, 2)
const iterations = 100
samples := make([]time.Duration, 0, iterations)
for i := 0; i < iterations; i++ {
eventID := uuid.NewString()
frame := AlarmFrame{
Type: "alarm",
AlertID: alertID.String(),
EventID: eventID,
Action: "trigger",
Message: "latency probe",
ServerTime: time.Now().UTC().Format(time.RFC3339Nano),
}
start := time.Now()
if delivered := hub.Broadcast(alertID, frame); delivered != 2 {
t.Fatalf("iteration %d: Broadcast delivered to %d clients, want 2", i, delivered)
}
for _, conn := range []*websocket.Conn{first, second} {
got := readAlarm(t, conn)
if got.EventID != eventID {
t.Fatalf("iteration %d: event id = %q, want %q", i, got.EventID, eventID)
}
}
samples = append(samples, time.Since(start))
}
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
p50 := samples[len(samples)*50/100]
p95 := samples[len(samples)*95/100]
max := samples[len(samples)-1]
t.Logf("broadcast -> 2 clients decoded: n=%d p50=%v p95=%v max=%v",
len(samples), p50, p95, max)
if p95 > 500*time.Millisecond {
t.Fatalf("p95 latency %v exceeds the 500ms budget", p95)
}
}