Feat: Refactor WebSocket server to use actix-ws with channel-based clients#212
Merged
Conversation
- Replaced the deprecated actix-web-actors with actix-ws (0.3). - Removed the now-unused actix-broker dependency. - Promoted tokio (sync, macros, time) and futures-util from dev to runtime dependencies for the new async session task.
- Changed the Client alias from Recipient<ChatMessage> to a tokio mpsc UnboundedSender<String>, so the server pushes frames over a channel instead of an actor address. - Removed the ChatMessage message type and renamed ClientMetadata.recipient to tx. - Carried the channel sender in JoinRoom and made RelaySignalMessage hold a plain String payload.
- Removed `mod actor;` ahead of deleting the actor file. - Dropped ChatMessage from the public re-exports now that the type is gone.
- Removed actor.rs: WsChatSession is no longer an actor, and the remaining WsChatServer Actor impl moves into server.rs. - Dropped the dead actix-broker LeaveRoom subscription it carried (nothing ever published LeaveRoom to the broker).
- Removed the Handler<ChatMessage> impl now that sessions are not actors. - Send client notifications over the mpsc channel instead of try_send, build the relay payload as a String. - Logged the room/session-limit notification when the client has already disconnected.
- Relocated the WsChatServer Actor impl here from the deleted actor.rs. - Switched all room broadcasts and relays from Recipient/try_send to the per-client mpsc sender (ClientMetadata.tx). - Changed relay_message_to_user to take a String and logged failed room-list broadcasts.
- Replaced the WsChatSession actor (StreamHandler + WebsocketContext) with an async run() task that multiplexes inbound frames, the outbound mpsc channel, and the heartbeat via tokio::select!. - Reassembled fragmented frames with aggregate_continuations and started the heartbeat one interval out to match the previous run_interval timing. - Parsed client input into a typed UserCommand enum and routed all client replies through a deliver() helper that logs dropped frames. - Logged failed server sends (join/leave/list) instead of ignoring them.
- Replaced the actix-web-actors WsResponseBuilder with actix_ws::handle, wiring an mpsc channel and spawning the session run() task. - Supervised the spawned task so a panic is logged instead of dying silently. - Logged handshake failures at error level so they surface in Sentry.
- Replaced the DummyActor/Recipient fixture in test_join_leave_room with a plain mpsc channel. - Dropped the now-unused actix and ChatMessage imports.
- Drops `actix` from the dependency tree now that the WebSocket server is plain shared state rather than an Actix actor. - Only the actix-web framework crates remain (actix-web, actix-http, actix-rt, actix-ws, …); `actix` and `actix_derive` are gone.
- Deletes the old low-level `impl WsChatServer` module; its room helpers (add_client_to_room, broadcasts, relay, cleanup) move into the new consolidated `chat_server.rs`.
- Deletes the Actix message handlers. With the actor gone, these became plain WsChatServer methods (join_room, leave_room, list_rooms, cleanup_session, validate_and_relay_signal) and now live in chat_server.rs.
- Deletes the misnamed module. Its shared types move out: WsChatServer, ChatServerHandle, Client/Room/ClientMetadata to chat_server.rs, and WsChatSession to session.rs.
- Consolidates the WebSocket server: WsChatServer is now plain shared state (no Actix actor), reached through a cloneable ChatServerHandle (Arc<Mutex<WsChatServer>>) that hides locking and recovers from poisoning. - Merges the former server.rs helpers and handler.rs operations into a single impl, co-located with the types they act on. - Tightens visibility: only WsChatServer, ChatServerHandle, add_client_to_room, the rooms field, and cleanup_stale_sessions are public; the rest is private or pub(crate). Drops the dead take_room / remove_empty_rooms. - Logs the unknown-session/room paths in leave_room, list_rooms, cleanup_session.
- Replaces Addr<WsChatServer> with &ChatServerHandle; the helper methods (join_room, list_rooms, user_command, handle_text, …) are now synchronous since there is no actor mailbox to await. - Moves the WsChatSession struct into this module (next to its impl) and makes it and its fields pub(crate)/private.
- Replaces WsChatServer::from_registry() with an owned ChatServerHandle field (moved to the top of the struct); start_websocket/remove_client use it. - Routes every mutex lock through a lock_or_log helper that logs and bails on poisoning instead of panicking; is_code_expired now fails closed. - Tightens visibility: SessionData, the non-shared fields, and the in-crate methods become pub(crate)/private
- Replaces the actor's run_interval cleanup with a background task that calls chat_server.cleanup_stale_sessions() every CLEANUP_INTERVAL, now that the server is plain shared state.
- MAX_ROOMS_PER_SESSION, MAX_SESSIONS, MAX_WS_MESSAGES_PER_SEC are only used in-crate and aren't re-exported, so make them pub(crate).
- The #[get(...)] handlers are public via the pub use re-export in lib.rs but unreachable_pub mis-flags them through the macro expansion, so allow the lint for this module.
- Swaps the deleted handler/message/server modules for chat_server. - Updates re-exports: ChatServerHandle/WsChatServer from chat_server; drops the removed message types and the now-internal WsChatSession/ClientMetadata. - Enables #![warn(unreachable_pub)] to flag pub items that could be pub(crate)(enforced as an error in CI via -D warnings).
- Updates the server tech stack in the root and server READMEs to reflect actix-ws (async, non-actor connections) instead of the old actor-based WebSocket support.
- Makes chat_server's visibility scoped to within the crate to ensure encapsulation.
- Moves session cleanup logic into `SessionStore` for cleaner encapsulation. - Enhances maintainability by consolidating session management logic. - Reduces code duplication and increases modularity.
- Removed unnecessary random ID generation for sessions because the id will be assign once the user joined a room.
… room - MAX_CONTINUATION_SIZE: hard transport ceiling (2x MAX_SIGNAL_SIZE) so an oversize signaling message can be rejected gracefully by the handler before the transport tears the connection down - OUTBOUND_CHANNEL_CAPACITY: bound on the per-client outbound queue so a slow consumer sheds load instead of growing memory without limit - DEFAULT_ROOM: single source of truth for the auto-joined room name, replacing the duplicated "main" string literal
- Make Client a bounded mpsc::Sender; all server->client pushes use try_send so a slow consumer's frames are logged instead of growing memory unbounded - Replace users_share_room + relay_message_to_user with a single-pass relay_to_shared_room, halving the work done under the lock per signal - Funnel broadcast_room_list, broadcast_room_members, and send_join_message through one send_to_clients helper; drop send_join_message's unused id argument and its eager client eviction - Reference the DEFAULT_ROOM constant in leave_room instead of "main"
- Switch the outbound receiver to a bounded mpsc::Receiver and use try_send in deliver, matching the bounded Client channel - Join before leaving on /join: bail on the 0 rejection sentinel so a rejected join leaves the client in its current room instead of stranding it - Raise the transport ceiling to MAX_CONTINUATION_SIZE so oversize signals get a graceful error from the handler rather than a dropped connection - Drain and flush any queued frames after the loop, before close, so a break-path error notice actually reaches the client - Auto-join the DEFAULT_ROOM constant instead of the "main" literal - Set the heartbeat to MissedTickBehavior::Delay so ticks don't burst after a stall.
- Create the outbound channel with the bounded OUTBOUND_CHANNEL_CAPACITY - Return Uuid from get_or_create_session_uuid instead of a String, dropping the redundant re-parse and the unreachable "Server configuration error" branch in start_websocket - On a WebSocket handshake failure, call remove_client to undo the earlier count increment, so private sessions still expire instead of leaking a client
- Build the test client with a bounded mpsc::channel instead of unbounded_channel, matching the new bounded Client type
- Introduces a timeout mechanism to retry connections if not established on time. - Enhances reliability by addressing connections that may hang. - Updates connection warning delay for better UX.
- Extract stale-chunk reload logic to a separate utility for reusability. - Reloads on unhandled promise rejections due to background chunk load failures. - Enhances app reliability by automatically refreshing stale resources after a deploy.
- Remove specific version constraint for wget package. - Prevents potential issues from version conflicts during package updates.
- Updates package version for minor improvements.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes the Actix actor framework from the server's WebSocket layer.
WsChatServeris now plain shared state behind a cloneable handle, and each connection runs as anactix-wsasync task driven bytokio::select!(inbound frames, an outbound mpsc channel, and heartbeats). On top of the migration, this PR adds a round of correctness/resilience hardening on both the server and the web client.No wire-protocol changes — the client/server contract is the unchanged
WS_PREFIX_*text protocol.Server — actix-ws migration
actix,actix-broker,actix-web-actors; addactix-ws(0.4.0),tokio,futures-util.chat_server.rs—WsChatServerroom/session state (a plain struct) + a cloneableChatServerHandle(Arc<Mutex<…>>) that hides locking and recovers from poisoning.session.rs— per-connectionactix-wstask (WsChatSession::run).session_store.rs— in-memory registry; performs the handshake and spawns the task.#.Server — correctness & resilience
/joinno longer strands the client — join before leaving and bail on the0sentinel, so a room/session-limit rejection leaves the client where it was.DEFAULT_ROOMconstant, heartbeatMissedTickBehavior::Delay, andget_or_create_session_uuidreturningUuid.Web client
checking/never opens its data channel (a state that never fired afailedevent), feeding the existing bounded reconnect path; the "can't connect — refresh" banner now appears only after auto-recovery has had a shot.ErrorHandler.Docker
wgetin the web Dockerfile — Alpine keeps only the latest patch, so a pinned-rNversion breaks builds once a newer patch ships.