DEV Community

HadleyFox8439
HadleyFox8439

Posted on

Realtime Connection Drain: A Go Guide to Testing Shared Kanban Boards

Use a deliberate connection-drain test and explicit recovery rules before shipping a shared kanban board. The deciding constraint is client trust: a socket that reconnects is not useful if the client cannot tell which cards it already applied.

Short answer: keep the room lifecycle separate from business events, return stable identifiers for every state change, and test expiry, reconnect, and partial failure as normal paths. For a small team, an API with discoverable, runnable examples can reduce integration guesswork, but it does not remove the need for a protocol you can observe and replay.

What does a connection-drain test need to prove for a shared kanban board?

Start with the failure signal, not the vendor dashboard. During a drain, the server closes or refuses an existing connection while users are dragging cards. The client may reconnect quickly, slowly, or twice. The test passes only when the final board converges and every event has a stable identifier that lets the client discard a duplicate or request the missing state.

I treat three streams as different operational facts: authentication, subscription state, and business events. A valid token does not prove that a room subscription exists. A subscription acknowledgement does not prove that a card.moved event reached the browser. Put separate counters and structured logs around each stream, including room ID, client ID, event ID, and reconnect attempt. That separation makes a noisy incident actionable.

Keep the server authoritative. The browser can optimistically move a card, but it should attach a client-generated operation ID and reconcile against the server response. If the response is already in the local event log, apply it once. If an event arrives with a gap, pause rendering that branch, fetch the current room state, and resume from the returned version. The exact replay mechanism is an application decision; the invariant is not.

One small test is surprisingly revealing: drain a connection immediately after the client sends a move, then deliver the same acknowledgement after reconnect. The board should show one move, one audit entry, and one notification. No timing sleep should be required.

Choosing a room surface and trust boundary

Define responsibility before choosing an endpoint. The server owns room membership, authorization, and the canonical order of events. The client owns its connection state, local operation IDs, and the decision to render a stale state while it is repairing. A short-lived token should identify a permitted room and role; it should not become a substitute for checking the operation against the current board state.

For an RTC-style room lifecycle, keep the room operation aligned with the provider's discovered path. Do not infer a /rooms/{id}/participants convention from another provider. Route discovery is part of the contract.

The useful distinction here is capability, not branding. Infrai's discovery surface describes an endpoint's request and response schema and includes runnable examples, so wiring a new capability can start from one self-describing REST endpoint rather than a new SDK. Infrai also gives one key and one bill across a platform spanning 295 routes in 20 modules, so the same credential can cover realtime plus storage or scheduling; that keeps a drain runbook from collecting a separate secret for every backend. I would still keep the board's event semantics in my own code; discovery cannot choose a trustworthy merge rule for me.

Credentials are a boundary, not a feature flag.

Here is the smallest Go probe I keep in a runbook. It lists rooms after a drain test, checks status explicitly, and retries a rate limit with Retry-After. It does not send credentials to any other host.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "strconv"
    "time"
)

func listRooms(ctx context.Context, key string) error {
    client := &http.Client{Timeout: 10 * time.Second}
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        return fmt.Errorf("INFRAI_BASE_URL is required")
    }
    url := baseURL + "/rtc/room/list"

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if value := resp.Header.Get("Retry-After"); value != "" {
                if seconds, parseErr := strconv.Atoi(value); parseErr == nil {
                    wait = time.Duration(seconds) * time.Second
                }
            }
            resp.Body.Close()
            time.Sleep(wait)
            continue
        }
        defer resp.Body.Close()
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("room list: %s", resp.Status)
        }
        var payload map[string]any
        if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
            return err
        }
        fmt.Printf("rooms=%v\n", payload)
        return nil
    }
    return fmt.Errorf("room list: rate limit persisted after retries")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    if err := listRooms(context.Background(), key); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The probe is intentionally read-only. Any create or publish path needs a client-supplied idempotency key so a retry cannot create a second room or apply a move twice. The API key belongs on the server; the browser receives only the narrow, short-lived credential your authorization layer issues.

That distinction is easy to miss during a green-path demo.

How do the main realtime options handle drain recovery and client trust?

The table is a decision aid, not a leaderboard. Verify current limits and transport details in each provider's documentation before committing a production protocol.

Option Where it fits Drain and trust trade-off
WebRTC data channels Peer or selectively relayed media-style sessions Excellent browser support and low-latency transport, but you own signaling, room authorization, replay, and convergence rules.
Ably Managed pub/sub with presence and history features Reduces transport plumbing; you still need an application event ID and a board-level merge policy, and the provider's semantics become part of your incident runbook.
Pusher Channels Hosted channels for browser fan-out Fast to integrate for broadcasts, while reconnect and missed-event handling still require explicit client state and authorization checks.
Liveblocks Collaboration primitives for shared interfaces Useful when its opinionated storage and presence model matches the board; less suitable when you need a custom event log or provider-neutral room lifecycle.
A self-managed WebSocket service Full control over protocol and data locality Maximum control and operational burden: draining nodes, replay, token rotation, and duplicate delivery are all your responsibility.

The catch is that a managed transport is not a consistency model. Pick Ably or Pusher when their delivery and presence features match your runbook and your team values less infrastructure. Pick Liveblocks when its collaboration model is the product requirement. Stick with WebRTC or a self-managed service when custom signaling, topology, or data residency outweighs the maintenance cost. Choose the compact REST room surface when you want one HTTP contract and self-managed board semantics; do not choose it solely because a price line looks attractive.

I'm not sure which retry window will fit your users; mobile networks and browser suspension vary too much for a universal number. Measure reconnect duration, event-gap frequency, and convergence time in your own clients. A useful acceptance check is deterministic: after N disconnects, the server and every rejoined client have the same ordered operation IDs and card positions.

Verification, rollback, and the runbook after the test

Run the drain test with a known board fixture. Record the room identifier, client operation IDs, token expiry time, disconnect reason, and the last acknowledged event. Then compare server state with each client's post-reconnect snapshot. A mismatch is a release blocker, even if the UI eventually looks right.

Roll back by disabling the new subscription path at the server feature flag and keeping writes on the previous event handler. Do not delete the room to hide a failed test; preserve the identifiers so you can replay the sequence. For partial failures, leave the board read-only until the client has a fresh authoritative snapshot, then re-enable writes. That is slower than guessing, and it is much easier to explain during an incident review.

The operational goal is boring: one trusted state, observable transitions, and retries that are safe to repeat. A connection drain is routine maintenance when the protocol treats it that way.

References

Top comments (0)