fix!: typed read-loop exit — routine server recycles are not errors; Disconnected carries the reason - #956
Conversation
…ectReason in Disconnected read_messages_loop returned Err even for a clean server-initiated stream recycle (the normal WhatsApp reconnect path, classified as info! by its own body), so every severity consumer had to re-derive "was this actually a problem?" — and the span's err(Debug) capture (ERROR by default in tracing-attributes) promoted each routine recycle to an error-tracker issue. - read_messages_loop now returns Result<ReadLoopExit, ReadLoopError>: clean recycle is Ok(ServerRecycle(reason)), Err is reserved for genuine failures (startup, dirty transport error, lost event channel). err(Debug) stays at ERROR and is now correct by construction. - connect_graph's err() drops to warn: run() already classifies its failures (debug! for a transient handshake retry, error! otherwise) — the default ERROR double-reported the real case and promoted every transient retry to an issue. - BREAKING: events::Disconnected now carries the DisconnectReason (Serialize, snake_case), so consumers can tell a routine recycle (reason.is_clean_shutdown()) from a real failure without parsing logs. DisconnectReason is re-exported from transport alongside TransportEvent. - wa.conn.read_loop / wa.conn.connect spans now tag lid/pn via record_identity_on_span, same as wa.iq / wa.send.message / wa.conn.run.
|
Warning Review limit reached
Next review available in: 17 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughI want to be direct: this changes how disconnects get classified and reported, and now we know why a connection dropped, not just that it dropped. ChangesDisconnect Reason Propagation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Look, I need this to be bulletproof before it ships — every disconnect needs a reason attached, no excuses, and the tracing noise needs to actually go away. That's it. Ship it when it's right. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client/lifecycle.rs`:
- Around line 343-385: The graceful-exit branch in the disconnect handling logic
leaves `intentional_reconnect` set, which can incorrectly suppress later
`Disconnected` events. Update the `match loop_result` handling in
`src/client/lifecycle.rs` so the `ReadLoopExit::Expected` path also clears or
consumes `intentional_reconnect` just like the `ServerRecycle` and `Err` paths
do, ensuring stale reconnect state does not leak into the next connection cycle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: cfb09b94-d07c-46db-bd0f-0d3f5cae00f8
📒 Files selected for processing (5)
src/client/lifecycle.rssrc/client/node_io.rssrc/transport.rswacore/src/net.rswacore/src/types/events.rs
There was a problem hiding this comment.
2 issues found across 5 files
Confidence score: 3/5
- In
src/client/lifecycle.rs,reconnect()can leaveintentional_reconnectset when the read loop exits withReadLoopExit::Expected, which can cause a later real disconnect to be misclassified and skip aDisconnectedevent; that risks missed client state transitions after a genuine drop — clear/reset the flag on expected exit paths before merging. - In
src/client/node_io.rs, theReadLoopExit/ReadLoopErrordoc comments include PR-history context instead of project-preferred rationale-focused docs, which is low runtime risk but may make future maintenance harder — trim comments to the enduring "why" perAGENTS.mdbefore or shortly after merge.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
📦 Binary size report
.text per crate
Top movers (cargo-bloat attribution)
Baseline: |
… docs reconnect() sets the flag then tears the loop down via the shutdown signal — the Expected path — where nothing consumed it (pre-existing: the old Ok(()) arms didn't either, and the short-circuited `||` skipped the swap whenever expected_disconnect was set). The stale flag misclassified the next genuine disconnect as intentional, swallowing its Disconnected event. Swap exactly once, before the match, so every exit consumes it. Also trims the ReadLoopExit/ReadLoopError docs to the enduring rationale per AGENTS.md.
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Requires human review: This PR restructures the read loop's error handling and adds a typed return enum, changing the Disconnected event from a unit struct to one with a reason field. These are core logic changes with moderate blast radius; they require a human reviewer to validate correctness and backward compatibility.
Re-trigger cubic
Problem
A routine server-initiated stream recycle (clean EOF — the normal WhatsApp reconnect path) surfaced as an ERROR in error trackers:
read_messages_loopreturnedErreven for the case its own body classifies asinfo!, and#[instrument(err(Debug))]defaults to ERROR regardless of that classification. TheDisconnectedevent also carried no reason, so consumers couldn't tell a recycle from a real failure without parsing logs.Changes
read_messages_loopreturnsResult<ReadLoopExit, ReadLoopError>: clean recycle isOk(ServerRecycle(reason));Erris reserved for genuine failures (startup, dirty transport error, lost event channel). The span'serr(Debug)stays at ERROR and is now correct by construction — the routine/anomalous distinction lives once, in the type, instead of being re-derived per consumer (logs, tracing, error trackers, embedders without tracing).connect_graph'serr()drops towarn:run()already classifies its failures (debug!for a transient handshake retry,error!otherwise) — the default ERROR double-reported the real case and promoted every transient retry to an issue.events::Disconnectednow carriesreason: DisconnectReason(Serialize, snake_case).DisconnectReasonis re-exported fromtransportalongsideTransportEvent.wa.conn.read_loop/wa.conn.connectspans taglid/pnviarecord_identity_on_span, same aswa.iq/wa.send.message/wa.conn.run(tracing: tag wa.iq / wa.send.message / wa.conn.run spans with account identity #951).Test plan
cargo check --workspace --all-targetscargo clippy -p whatsapp-rust --features tracing --all-targetscargo test -p whatsapp-rust --features tracing --lib(879) /cargo test -p wacore --lib(1039)