34730930e72781ad72d1505d8aaac1ed96cab8b8
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0de2baa72d |
feat(channels): opt-in session persistence — v0.6 chunk 3
ChannelSession<P> (src/channels/session.rs): a channel actor that outlives its transport, buffering outbound broadcasts as the same Arc<Broadcast<P>>s the relay path carries (zero re-allocation) until rejoin, buffer cap, or TTL. Registered per pattern via PrefixRouter::channel_session::<S>(pattern, factory) / channel_session_default::<C>(pattern); one registry gen_server per registration, monomorphic over S::Key (no type erasure). Machinery: deploy() computes the key on the conn actor and casts the join handshake to the registry; the registry maps key -> (pid, control sender), forwards reattaches, spawns-and-monitors fresh actors, and prunes on Down (pid-guarded against replaced entries). The session actor selects [inbound, bus, ctl] attached and select_timeout([ctl, bus], ttl-remaining) detached; detach triggers are the closed inbound arm and ws send failure (the failed broadcast is the buffer's first entry — nothing lost). Drain re-encodes under the new join generation. As-landed decisions (each in module docs): - ChannelFactory gained a #[doc(hidden)] deploy() seam (default = ephemeral linked actor, the chunk-1 behavior verbatim); JoinHandshake and ChannelInbox are opaque pub structs. Channel construction moved from run_channel into deploy — same linked blast radius. - Every attach calls ch.join() again (rejoin acks need a payload and channels get to re-auth); session channels must treat join as re-entrant. - A rejected rejoin ENDS the session (no zombie sessions held open for unauthorized clients); next join is a cold start. - A second transport EVICTS the first (best-effort phx_close); a session is single-transport. - Explicit leave ends the session, not just the attachment. - TTL per detach episode; subscribe once, on first successful join; cap.max(1) semantics (cap 0 = first buffered message tears down, but an empty buffer still waits out the TTL). - Key: Clone beyond the ratified Eq+Hash+Send — the registry keeps a pid -> key reverse index for Down pruning. - The registry spawns eagerly at registration (channel_session is in-runtime-only, same law as ChannelHub::new): a lazy OnceLock would block std-sync under green threads on first-join races. - Shutdown composes WITHOUT links: conns die -> router Arcs drop -> registry inbox closes -> registry exits -> ctl senders drop -> detached sessions wake on the closed ctl arm, terminate, exit. shutdown_with_detached_session_terminates is the proof (ttl 30s vs 5s budget — only the chain can reap it). Tests: 6 session unit tests (buffer-and-drain ordering across reconnect, TTL expiry, cap teardown, leave, eviction incl. stale conn-entry self-prune, rejected rejoin) + the shutdown integration test. Shared toy codec + conn harness extracted to channels/testkit.rs (mod.rs tests now import it; no behavior change). Hammer: 35x lifecycle subset (+channels +session) + 3x full (phoenix) + 1x full (phoenix+smarm-trace) + featureless + channels-only, all green; dbg-grep clean. Suite: 90 unit + 46 integration + 2 doc under --features phoenix. |
||
|
|
0531390613 |
feat(channels): core protocol machinery — v0.6 chunk 1
Feature 'channels' (zero new deps): wire-neutral ChannelFrame/FrameRef + Encode/Decode codec traits (one method each, whole-envelope — the codec owns the wire format end to end); Channel trait; ChannelFactory + TopicRouter + shipped PrefixRouter (exact map + head:* prefix map); ChannelSocket (reply/push/broadcast/broadcast_from/broadcast_to); ChannelHub (router + PubSub<Broadcast<P>>, in-runtime + non-static per the v0.5 pattern, hub.upgrade(conn) entry, server-side hub.broadcast). Topology as ratified: one channel actor per (socket, topic), linked to the conn actor, select(&[&inbound, &bus_rx]) with inbound at index 0; subscription pinned to the channel actor pid. Subscribe-before-ok-reply makes the join ack first-class (the v0.5 on_open race, answered structurally). Cleanup is the sender-drop chain (conn handler drops -> inbound closes -> closed arm wakes -> terminate); bus arm dropped from the select set once observed closed (closed arms stay ready forever). Heartbeat answered conn-side on topic 'phoenix'; rejoin replaces the old generation with a phx_close to the old join_ref; lazy prune of dead channel entries on first failed forward (the pubsub discipline). As-landed deviations from the ratified sketch (module docs, veto by diff): join takes &mut self (trait-object callable; construction moved to ChannelFactory which still gets the full raw topic); reply(status, payload) with the ref threaded invisibly (the sketch listed both); broadcast gained the event parameter (unencodable without it); route() returns Arc not Box. Outbound payloads encode from borrows (FrameRef) so bus fan-out never clones P; payload-less control frames (phx_close, error/heartbeat replies) encode as the codec's empty payload rather than conjuring a P. 11 runtime-backed unit tests with a toy pipe codec (core provably JSON-free). Suites: 78 w/ feature, 67 featureless, both green. |