You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a developer building the relay's routing layer, I want a thread-safe Registry type that maps server-ids to a single binary WS connection (1:1) and to a list of phone WS connections (1:N), with race-tested operations for register / unregister / lookup / list, so that the WS upgrade tickets and the frame-forwarding loop have a single canonical place to coordinate connections.
Context
Foundational data structure for the relay's routing core. Every later ticket (/v1/server upgrade #4, /v1/client upgrade #5, frame forwarding #6, heartbeat #7, grace period #8, health endpoint #10) reads or writes through this registry. Filed second (after #1 envelope) because everything stacks on it.
The registry holds opaque connection handles — interfaces with the methods routing needs (Send([]byte) error, ConnID() string, Close()), not concrete WS types. This keeps the registry testable with mock connections and lets the WS upgrade tickets choose the WebSocket library without rebuilding the registry.
Registry struct — internal sync.RWMutex + two maps: binaries map[string]Conn and phones map[string][]Conn.
NewRegistry() *Registry.
(r *Registry) ClaimServer(serverID string, conn Conn) error — registers binary; errors with sentinel ErrServerIDConflict if already claimed.
(r *Registry) ReleaseServer(serverID string) (released bool) — removes binary entry; returns false if no binary held the slot.
(r *Registry) RegisterPhone(serverID string, conn Conn) error — appends phone to the slice; errors with sentinel ErrNoServer if no binary holds the server-id.
(r *Registry) UnregisterPhone(serverID string, connID string) — removes phone by ConnID. No-op if not present.
Tests in internal/relay/registry_test.go covering:
ClaimServer succeeds when slot empty; second claim returns ErrServerIDConflict; ReleaseServer + reclaim succeeds.
RegisterPhone succeeds only when binary holds the server-id; ErrNoServer when not.
UnregisterPhone removes the right phone (by ConnID) and leaves siblings untouched.
PhonesFor returns a snapshot — modifying the returned slice must NOT affect the registry's internal state.
Counts() returns expected values across the full lifecycle.
Race tests (go test -race -count=20) hammering ClaimServer / ReleaseServer / RegisterPhone / UnregisterPhone / BinaryFor concurrently from many goroutines without data races.
Doc comments on every exported symbol explaining contract + concurrency guarantees.
Technical Notes
Stdlib only (sync, errors).
Sentinel errors (var ErrServerIDConflict = errors.New(...)) so callers can errors.Is.
Tests live in package relay (per established convention in envelope_test.go) so they can reach unexported helpers if needed.
User Story
As a developer building the relay's routing layer, I want a thread-safe
Registrytype that maps server-ids to a single binary WS connection (1:1) and to a list of phone WS connections (1:N), with race-tested operations for register / unregister / lookup / list, so that the WS upgrade tickets and the frame-forwarding loop have a single canonical place to coordinate connections.Context
Foundational data structure for the relay's routing core. Every later ticket (
/v1/serverupgrade #4,/v1/clientupgrade #5, frame forwarding #6, heartbeat #7, grace period #8, health endpoint #10) reads or writes through this registry. Filed second (after #1 envelope) because everything stacks on it.The registry holds opaque connection handles — interfaces with the methods routing needs (
Send([]byte) error,ConnID() string,Close()), not concrete WS types. This keeps the registry testable with mock connections and lets the WS upgrade tickets choose the WebSocket library without rebuilding the registry.Acceptance Criteria
internal/relay/registry.goexporting:Conninterface —ConnID() string,Send([]byte) error,Close(). Real implementations land later (relay: WS upgrade for /v1/server — accept binary connection, validate headers, claim server-id #4 binary, relay: WS upgrade for /v1/client — accept phone connection, look up server-id, register #5 phone); tests use mocks.Registrystruct — internalsync.RWMutex+ two maps:binaries map[string]Connandphones map[string][]Conn.NewRegistry() *Registry.(r *Registry) ClaimServer(serverID string, conn Conn) error— registers binary; errors with sentinelErrServerIDConflictif already claimed.(r *Registry) ReleaseServer(serverID string) (released bool)— removes binary entry; returns false if no binary held the slot.(r *Registry) RegisterPhone(serverID string, conn Conn) error— appends phone to the slice; errors with sentinelErrNoServerif no binary holds the server-id.(r *Registry) UnregisterPhone(serverID string, connID string)— removes phone by ConnID. No-op if not present.(r *Registry) BinaryFor(serverID string) (Conn, bool)— lookup.(r *Registry) PhonesFor(serverID string) []Conn— returns a snapshot slice (caller can safely iterate without holding the lock).(r *Registry) Counts() (binaries, phones int)— for the health endpoint (relay: /healthz returns version + connected-binary count + connected-phone count (JSON) #10).internal/relay/registry_test.gocovering:ErrServerIDConflict; ReleaseServer + reclaim succeeds.ErrNoServerwhen not.go test -race -count=20) hammering ClaimServer / ReleaseServer / RegisterPhone / UnregisterPhone / BinaryFor concurrently from many goroutines without data races.Technical Notes
sync,errors).var ErrServerIDConflict = errors.New(...)) so callers canerrors.Is.package relay(per established convention inenvelope_test.go) so they can reach unexported helpers if needed.Size Estimate
S — single new file, ~80 LOC + ~120 LOC tests with race coverage. 2 new exported types (
Conn,Registry); the rest are methods and sentinel errors.