Short answer: test room capacity alerts as deterministic state transitions, then use one bounded deadline only to prove that the surrounding realtime pipeline delivers the expected transition. Never make a fixed sleep part of the assertion.
For a shared kanban board, the operationally useful definition of online is a server-accepted presence session. The alert should fire when the number of distinct members crosses the configured capacity, not whenever a browser sends another heartbeat. Reconnect and backfill are part of that definition — if a test skips them, it covers the demo path rather than the path that tends to wake someone up.
This is the decision rule: keep ordering, deduplication, membership, and threshold crossing in a pure reducer. Put clocks and transports outside it.
How should you test realtime room capacity alerts without flaky timing?
Start by naming the assertion precisely. A room with capacity 2 should produce one alert when its distinct online-member count moves from 1 to 2. Replaying the second join must produce no alert. A leave should re-arm the crossing, so a later join can produce a new alert. A sequence gap must stop reduction and request backfill; silently applying the newer event would create a state nobody can explain during an incident.
A sleep cannot prove any of those properties. time.Sleep(200 * time.Millisecond) mixes scheduling delay, local machine load, CI contention, transport delay, and application behavior into one guess. Raise it and the suite gets slower. Lower it and a healthy build occasionally fails. I've learned to treat a sleep in this kind of assertion as a missing synchronization point, because the test is waiting for elapsed time when it actually cares about an observed event or committed cursor.
Time isn't evidence.
Keep two layers. The reducer test calls the state transition directly and compares exact outputs. The pipeline test submits an input, waits on an acknowledgement or observable output, and carries a generous context deadline only as an escape hatch. The deadline failing is useful diagnostic evidence; it isn't the mechanism that makes the test pass.
Don't poll the member count every few milliseconds either. Polling is still timing dependence wearing a loop. If the production boundary does not expose a completion signal, add one at the boundary you own: an applied sequence number, an alert record, or a test-only observer attached to the same committed output.
Model membership before choosing a realtime transport
Presence becomes ambiguous as soon as one person opens two tabs. Counting connections can report two people where the board has one member; counting a bare user ID can take that member offline when one tab closes while another remains open. Track sessions under each member, and count the member while at least one accepted session remains. A leave must identify the session it closes.
Ordering needs equally explicit semantics. Give every room event a monotonically increasing sequence number from the authoritative writer. The reducer retains the last applied number. An event at or below that cursor is a replay and has no effect; an event above cursor + 1 is a gap, so reduction pauses until backfill supplies the missing range. This contract makes reconnect testable without pretending the network delivers exactly once.
The transport can then change without changing the test oracle. The W3C WebRTC specification defines data channels and their transport behavior, but a data channel does not decide what "online" means for a kanban room. WebSocket, server-sent delivery, and peer data channels all sit outside the membership reducer in this design. That's deliberate. The state machine owns business truth; transport code owns delivery and reconnection.
The catch is that exact, server-authoritative membership has a coordination cost. It is not suitable when the product intentionally accepts approximate presence across a very large audience and no action depends on an exact threshold. In that case, test an explicit error band and alert policy instead of smuggling exactness into assertions. For a workspace capacity control, stick with authoritative state when crossing the limit changes admission, billing, or operator action.
Build a reducer that makes replay and backfill boring
Here is the smallest useful Go model. It has no goroutine and no clock. The caller must feed each room's events in sequence, and a gap is a hard signal to fetch backfill rather than an invitation to guess.
Short on purpose.
package presence
import (
"errors"
"fmt"
)
var ErrSequenceGap = errors.New("presence sequence gap")
type Event struct {
Seq uint64
MemberID string
SessionID string
Kind string
}
type Alert struct {
Seq uint64
Online int
}
type Tracker struct {
capacity int
cursor uint64
sessions map[string]map[string]struct{}
atCapacity bool
}
func NewTracker(capacity int, cursor uint64, active map[string][]string) *Tracker {
t := &Tracker{
capacity: capacity,
cursor: cursor,
sessions: make(map[string]map[string]struct{}, len(active)),
}
for member, ids := range active {
t.sessions[member] = make(map[string]struct{}, len(ids))
for _, id := range ids {
t.sessions[member][id] = struct{}{}
}
}
t.atCapacity = len(t.sessions) >= capacity
return t
}
func (t *Tracker) Apply(e Event) (*Alert, error) {
if e.Seq <= t.cursor {
return nil, nil
}
if e.Seq != t.cursor+1 {
return nil, fmt.Errorf("%w: have %d, received %d", ErrSequenceGap, t.cursor, e.Seq)
}
switch e.Kind {
case "join":
if t.sessions[e.MemberID] == nil {
t.sessions[e.MemberID] = make(map[string]struct{})
}
t.sessions[e.MemberID][e.SessionID] = struct{}{}
case "leave":
delete(t.sessions[e.MemberID], e.SessionID)
if len(t.sessions[e.MemberID]) == 0 {
delete(t.sessions, e.MemberID)
}
default:
return nil, fmt.Errorf("unknown presence event kind %q", e.Kind)
}
t.cursor = e.Seq
wasAtCapacity := t.atCapacity
t.atCapacity = len(t.sessions) >= t.capacity
if !wasAtCapacity && t.atCapacity {
return &Alert{Seq: e.Seq, Online: len(t.sessions)}, nil
}
return nil, nil
}
Notice the order of operations. An unknown event does not advance the cursor. A duplicate does not mutate sessions. A second tab for the same member changes session state but not the distinct-member count. Those are small details until reconnect races with a tab close; then they are the whole postmortem.
The corresponding test does not wait. It restores a snapshot at sequence 40, applies backfilled events, and asserts outputs synchronously.
No sleeping. Ever.
package presence
import (
"errors"
"testing"
)
func TestCapacityCrossingAfterReconnect(t *testing.T) {
tracker := NewTracker(2, 40, map[string][]string{
"ana": {"tab-a"},
})
join := Event{Seq: 41, MemberID: "bo", SessionID: "tab-b", Kind: "join"}
alert, err := tracker.Apply(join)
if err != nil {
t.Fatal(err)
}
if alert == nil || alert.Seq != 41 || alert.Online != 2 {
t.Fatalf("unexpected alert: %#v", alert)
}
replay, err := tracker.Apply(join)
if err != nil || replay != nil {
t.Fatalf("replay changed output: alert=%#v err=%v", replay, err)
}
_, err = tracker.Apply(Event{
Seq: 43, MemberID: "cy", SessionID: "tab-c", Kind: "join",
})
if !errors.Is(err, ErrSequenceGap) {
t.Fatalf("expected sequence gap, got %v", err)
}
}
I don't assert that "no duplicate arrived within 50 ms." Absence over a guessed window is weak evidence. The replay assertion above is stronger: for the same cursor and event, the reducer cannot emit a second alert. At the integration layer, the alert sink should also enforce a stable idempotency key such as room ID plus crossing sequence, because a worker can retry after its acknowledgement is lost.
Verify deployment signals and keep rollback mechanical
Before rollout, exercise the reducer with generated event sequences: joins from multiple tabs, leaves in either tab order, exact replays, reconnect snapshots, and gaps at every position. Preserve invariants rather than asserting incidental implementation details. The online count cannot be negative; a replay cannot change it; a gap cannot advance the cursor; and one below-to-at-capacity transition emits one alert. Race-enabled Go tests are useful around the adapter, while the reducer itself should stay synchronous.
In staging, disconnect a client after it has received an event but before it records the cursor, reconnect with the older cursor, and confirm that replay is harmless. Then disconnect before delivery, reconnect, backfill the missing event, and confirm the same final membership. These are two distinct failure paths. Combining them into "reconnect works" hides the part that matters.
Production telemetry should expose the applied cursor by room, sequence-gap count, backfill count and age, active members, threshold crossings, and duplicate alerts rejected by the sink. I'm not sure what backfill-age threshold is right for every board; room activity and the operator's response target should determine it. Pick the threshold from observed healthy behavior, document it in the runbook, and alert on sustained deviation rather than a single slow sample.
Roll out behind a per-room switch. Keep the previous alert evaluator available while the new reducer observes traffic without sending notifications, compare their resulting state at stable cursors, and enable emission gradually. Rollback should disable new emissions without deleting the cursor or deduplication records. If those records vanish, turning the system back on can replay old crossings as fresh alerts — exactly the kind of duplicate delivery an otherwise clean rollback can create.
A final operational check is intentionally plain: can an engineer start from the stored snapshot and event log, reproduce the room's current membership, and explain why each alert did or did not fire? If yes, the test suite is checking a replayable contract. If no, another timing tweak won't fix the design.
Top comments (0)