Skip to content

Replace response and multi_packet with ActiveOutput enum#427

Merged
leynos merged 8 commits intomainfrom
terragon/refactor-connection-output-state-c1pwtb
Feb 1, 2026
Merged

Replace response and multi_packet with ActiveOutput enum#427
leynos merged 8 commits intomainfrom
terragon/refactor-connection-output-state-c1pwtb

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jan 26, 2026

Summary

  • Replaces separate response stream and multi-packet state with a single ActiveOutput enum for the connection's output source, centralizing mutual exclusion at the type level.
  • Enforces mutual exclusion via ActiveOutput and explicit API guards, reducing runtime bugs and clarifying lifecycle.
  • Updates polling, signaling, and correlation handling to source from ActiveOutput.

Changes

Core Functionality

  • Introduced ActiveOutput<F, E> with variants None, Response(FrameStream<F, E>), MultiPacket(MultiPacketContext).
  • Replaced ConnectionActor.response and ConnectionActor.multi_packet with active_output: ActiveOutput<F, E>.
  • Added ActiveOutput helper methods:
    • is_response, is_multi_packet, multi_packet_mut, clear_response
  • New API surfaces (returning Result with ConnectionStateError on illegal transitions):
    • set_response(stream: Option<FrameStream<F, E>>) -> Result<(), ConnectionStateError>
    • set_multi_packet(channel: Option<mpsc::Receiver>) -> Result<(), ConnectionStateError>
    • set_multi_packet_with_correlation(channel: Option<mpsc::Receiver>, correlation_id: Option) -> Result<(), ConnectionStateError>
  • Added ConnectionStateError enum with variants MultiPacketActive and ResponseActive; Display and Error impls included.
  • Initialization derives active_output from the provided response and channel.
  • In the event loop and lifecycle, all state transitions route through ActiveOutput.
  • When ending a multi-packet sequence, the code closes the channel, emits the terminator frame, and clears active_output; correlation handling reads from ActiveOutput's multi-packet context when active.
  • Removed legacy runtime guards in favor of type-level guarantees where possible.
  • Updated correlation handling to read from active_output.multi_packet_mut() when a multi-packet context is active.

API Support / Test Surface

  • Test harness updated to query actor.active_output.is_multi_packet() instead of the old multi_packet state.
  • Tests updated to handle the new Result-returning API (e.g., .expect(...) on API calls that may fail).
  • Updated end-of-stream and shutdown paths to operate through ActiveOutput for consistency.

Misc

  • Added internal EventAvailability concept to simplify polling logic based on which sources are active.
  • All internal state transitions now go through ActiveOutput to preserve mutual exclusivity.

Why

  • Safer, clearer state management with explicit mutual exclusivity between output sources.
  • Reduces runtime branches and potential state corruption by moving state control into a single enum-driven path.

Migration notes

  • The APIs set_response, set_multi_packet, and set_multi_packet_with_correlation now return Result and may error if an incompatible source is already active. Call sites should handle these errors (e.g., via .expect or proper matching).

Risk & Mitigations

  • API surface changes require updates to call sites; tests and test harness have been updated accordingly.
  • Ensure edge cases around channel closing, terminator emission, and correlation handling are exercised by tests.

How to test

  • Build and run tests: cargo test
  • Verify behavioral invariants:
    • Setting a new response clears any active multi-packet output and vice versa.
    • Polling selects the correct source depending on active_output.
    • Shutdown and end-of-stream paths terminate cleanly and emit/consume correlation IDs as expected.

Notes for reviewers

  • ActiveOutput enforces mutual exclusivity at the type level, reducing runtime state checks.
  • All existing multi-packet lifecycle logic has been adapted to work through ActiveOutput; please verify edge cases around channel closing and terminator emission.

📎 Task: https://www.terragonlabs.com/task/14d3e409-e284-48da-9a13-ff56268fd638

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 26, 2026

Reviewer's Guide

Refactors ConnectionActor to manage its output source via a single ActiveOutput enum, enforcing mutual exclusivity between streaming responses and multi-packet channels, and updates event polling, multi-packet lifecycle handling, shutdown, and tests to use this new abstraction.

Sequence diagram for next_event polling using ActiveOutput

sequenceDiagram
participant ConnActor
participant Shutdown
participant HighQueue
participant LowQueue
participant MultiPacketChannel
participant ResponseStream

ConnActor->>ConnActor: compute_availability(state)
ConnActor->>ConnActor: extract multi_channel,response_stream from active_output
alt shutdown_active
  ConnActor->>Shutdown: await_shutdown()
  Shutdown-->>ConnActor: triggered
  ConnActor-->>ConnActor: Event_Shutdown
else response_active
  ConnActor->>ResponseStream: poll_response()
  ResponseStream-->>ConnActor: PollResult
  ConnActor-->>ConnActor: Event_Response
else multi_packet_active
  ConnActor->>MultiPacketChannel: poll_queue()
  MultiPacketChannel-->>ConnActor: PollResult
  ConnActor-->>ConnActor: Event_MultiPacket
else queues_only
  ConnActor->>HighQueue: poll_queue()
  HighQueue-->>ConnActor: PollResult
  ConnActor->>LowQueue: poll_queue()
  LowQueue-->>ConnActor: PollResult
  ConnActor-->>ConnActor: Event_High_Low_Idle
end
Loading

Class diagram for ConnectionActor output refactor using ActiveOutput enum

classDiagram
class ConnectionActor~F,E~ {
  - Option<mpsc::Receiver<F>> high_rx
  - Option<mpsc::Receiver<F>> low_rx
  - ActiveOutput<F,E> active_output
  - CancellationToken shutdown
  + set_response(stream: Option<FrameStream<F,E>>)
  + set_multi_packet(channel: Option<mpsc::Receiver<F>>)
  + set_multi_packet_with_correlation(channel: Option<mpsc::Receiver<F>>, correlation_id: Option<u64>)
  - apply_multi_packet_correlation(frame: &mut F)
  + run(out: &mut Vec<F>) Result<(),E>
  - compute_availability(state: &ActorState) EventAvailability
  - next_event(state: &ActorState) Event<F,E>
  - end_multi_packet(reason: MultiPacketTerminationReason, state: &mut ActorState, out: &mut Vec<F>)
  - shutdown_queues(state: &mut ActorState)
  - handle_queue_frame(kind: QueueKind, frame: F, ctx: DrainContext<F>, state: &mut ActorState)
}

class ActiveOutput~F,E~ {
  + None
  + Response(FrameStream<F,E>)
  + MultiPacket(MultiPacketContext<F>)
  + is_response() bool
  + is_multi_packet() bool
  + multi_packet_mut() Option<&mut MultiPacketContext<F>>
  + clear_response()
}

class MultiPacketContext~F~ {
  - Option<mpsc::Receiver<F>> channel
  - MultiPacketStamp stamp
  + new() MultiPacketContext<F>
  + install(channel: Option<mpsc::Receiver<F>>, stamp: MultiPacketStamp)
  + channel_mut() Option<&mut mpsc::Receiver<F>>
  + stamp() MultiPacketStamp
  + correlation_id() Option<u64>
}

class EventAvailability {
  + bool high
  + bool low
  + bool multi_packet
  + bool response
}

ConnectionActor "1" o-- "1" ActiveOutput
ConnectionActor "1" o-- "1" EventAvailability
ActiveOutput "1" o-- "1" MultiPacketContext
MultiPacketContext "1" o-- "1" MultiPacketStamp
Loading

Flow diagram for ActiveOutput state transitions in ConnectionActor

flowchart LR
  none[None]
  resp[Response]
  multi[MultiPacket]

  none -- set_response_Some --> resp
  none -- set_multi_packet_Some --> multi

  resp -- set_response_None --> none
  resp -- shutdown_queues --> none
  resp -- protocol_error --> none

  multi -- end_multi_packet --> none
  multi -- shutdown_queues --> none
  multi -- set_multi_packet_None --> none

  none -. initialize_with_response_Some .-> resp
  none -. initialize_with_response_None .-> none
Loading

File-Level Changes

Change Details Files
Replace separate response and multi-packet fields with ActiveOutput enum and helpers.
  • Introduce ActiveOutput<F, E> enum with None, Response(FrameStream<F, E>), and MultiPacket(MultiPacketContext) variants.
  • Replace ConnectionActor.response and ConnectionActor.multi_packet with a single active_output field and initialize it from the optional response stream.
  • Add helper methods on ActiveOutput (is_response, is_multi_packet, multi_packet_mut, clear_response) to query and manipulate output state.
src/connection.rs
Drive event polling and availability through ActiveOutput.
  • Add EventAvailability struct and compute_availability method to centralize flags for high, low, multi-packet, and response readiness.
  • Update next_event to derive the active multi-packet channel or response stream from active_output and use availability flags in tokio::select!.
  • Construct ActorState using active_output.is_response() and active_output.is_multi_packet() instead of separate response/multi_packet checks, removing the old runtime invariant assertion.
src/connection.rs
Rework multi-packet correlation, termination, and shutdown logic to operate via ActiveOutput.
  • Update apply_multi_packet_correlation, emit queue handling, and try_multi_packet_poll to access the multi-packet context via active_output.multi_packet_mut().
  • Refactor handle_multi_packet_termination to close the channel, emit the terminator frame with correct correlation ID, and then clear active_output to None.
  • Adjust shutdown logic to extract correlation IDs from the active multi-packet context, close the channel, mark state closed, and signal hooks based on the ActiveOutput variant, while clearing active_output.
  • Replace direct response field clearing with active_output.clear_response() in error and end-of-stream paths to avoid double-closing and ensure consistent command-end signaling.
src/connection.rs
Simplify MultiPacketContext and remove legacy helpers now handled by ActiveOutput.
  • Remove MultiPacketContext::clear and take_channel, keeping channel_mut, stamp, and correlation_id for use inside ActiveOutput’s MultiPacket variant.
  • Have ConnectionActor::set_multi_packet and set_multi_packet_with_correlation construct a fresh MultiPacketContext, install the receiver and stamp, and store it in active_output, with debug assertions guarding against conflicting response state.
src/connection.rs
Update test harness to use ActiveOutput for multi-packet availability.
  • Change ActorHarness::has_multi_queue to query actor.active_output.is_multi_packet() instead of the removed multi_packet.is_active().
src/connection/test_support.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#430 Refactor ConnectionActor to replace separate response: Option<FrameStream<F, E>> and multi_packet: MultiPacketContext<F> fields with a single enum (e.g., ActiveOutput<F, E>) that encodes the mutually-exclusive output state, and update all related methods and state management to use this enum.
#430 Simplify and centralize output-handling logic (including tokio::select! / event polling and multi-packet lifecycle management) to rely on the new enum for availability and source selection, thereby removing the previous runtime invariant checks that ensured at most one of response or multi-packet was active.

Possibly linked issues

  • #(unassigned): The PR introduces ActiveOutput and updates ConnectionActor exactly as requested to encode mutual exclusion in types.
  • Implement connection actor with prioritised write loop #129: PR replaces separate response/multi_packet flags with an ActiveOutput enum, simplifying ConnectionActor state and transitions as requested.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 26, 2026

Walkthrough

Introduce an ActiveOutput abstraction to make response streams and multi-packet channels mutually exclusive; make set_response/set_multi_packet/set_multi_packet_with_correlation fallible (return Result<..., ConnectionStateError>); update draining, framing, shutdown, multi-packet internals, tests and harness to use and propagate the new API and state model.

Changes

Cohort / File(s) Summary
Core connection model
src/connection/mod.rs
Replace response/multi_packet fields with active_output; add ConnectionStateError; make set_response/set_multi_packet/set_multi_packet_with_correlation return Result; integrate availability computation into polling and shutdown.
ActiveOutput & helpers
src/connection/output.rs
Add ActiveOutput enum, EventAvailability, ShutdownResult, MultiPacketCloseResult; implement state inspection/mutation helpers (is_response/is_multi_packet/multi_packet_mut/clear_response/shutdown/close_multi_packet/clear).
Draining & framing
src/connection/drain.rs, src/connection/frame.rs
Add DrainContext and QueueKind; switch stamping checks and channel access to use active_output.multi_packet_mut() via DrainContext; guard frame stamping and channel use on the active context presence.
Multi-packet internals
src/connection/multi_packet.rs
Remove module-private helpers clear, take_channel, and is_active; rely on ActiveOutput for channel lifecycle and stamping context management.
Shutdown & response handling
src/connection/shutdown.rs, src/connection/response.rs
Replace direct response/multi-packet mutations with ActiveOutput-driven shutdown/close/clear flows; use shutdown result to decide on_command_end invocation and logging.
Connection support types
src/connection/channels.rs, src/connection/counter.rs
Add ConnectionChannels bundle and ActiveConnection RAII counter with atomic global tracking and metrics hooks.
Test harness & imports
src/connection/test_support.rs
Make harness APIs fallible: set_multi_queue now returns Result<(), ConnectionStateError>; import drain::{DrainContext, QueueKind}; update has_multi_queue to inspect active_output.
Tests / fixtures
tests/..., tests/fixtures/...
Convert ignored Results from set_multi_packet*/set_multi_queue calls into explicit error handling: propagate with ?, map errors with .map_err(...) ?, or assert with .expect(...); add rstest parameterisation in harness tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Test as Test/Client
participant Actor as ConnectionActor
participant Active as ActiveOutput
participant MP as MultiPacketContext
participant Resp as FrameStream
Test->>Actor: call set_multi_packet(Some(rx))
Actor->>Active: inspect current active output
alt Active is Response
Active-->>Actor: indicate ResponseActive
Actor-->>Test: return Err(ConnectionStateError::ResponseActive)
else Active is None
Actor->>Active: install MultiPacketContext(MP) with channel rx
Active-->>Actor: Ok(())
Actor-->>Test: return Ok(())
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • codescene-delta-analysis

Poem

Two streams once vied for single route,
ActiveOutput settles their dispute.
Errors now surface where states collide,
Tests lift their flags and no longer hide. 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the ActiveOutput refactor, API changes, motivation, and testing strategy. It directly addresses the substantial changes across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title directly and accurately summarises the primary structural change: replacing two separate fields (response and multi_packet) with a single ActiveOutput enum.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/refactor-connection-output-state-c1pwtb

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 30, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/connection.rs

Comment on lines +548 to +557

        let multi_available = self.active_output.is_multi_packet() && !state.is_shutting_down();
        let resp_available = self.active_output.is_response() && !state.is_shutting_down();

        // Extract either the multi-packet channel or response stream since they're
        // mutually exclusive - only one can be active at a time.
        let (multi_channel, response_stream) = match &mut self.active_output {
            ActiveOutput::MultiPacket(ctx) => (ctx.channel_mut(), None),
            ActiveOutput::Response(stream) => (None, Some(stream)),
            ActiveOutput::None => (None, None),
        };

❌ New issue: Complex Method
ConnectionActor.next_event has a cyclomatic complexity of 9, threshold = 9

@coderabbitai

This comment was marked as resolved.

@leynos leynos marked this pull request as ready for review January 30, 2026 12:28
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/connection.rs:276` </location>
<code_context>
+    clippy::struct_excessive_bools,
+    reason = "Availability flags are a natural fit for booleans; no state machine needed"
+)]
+#[derive(Clone, Copy)]
+struct EventAvailability {
+    high: bool,
</code_context>

<issue_to_address>
**issue (complexity):** Consider pushing shutdown/closure logic into `ActiveOutput` and inlining the availability booleans in `next_event` to keep invariants while simplifying control flow and types.

You can keep the type-level invariant while reducing the new surface area and branching. Two focused suggestions:

---

### 1. Move shutdown / closure behavior into `ActiveOutput`

Right now `start_shutdown` needs to know internal details of `ActiveOutput`/`MultiPacketContext` and manually do:

- correlation extraction
- `mem::replace`
- channel closure
- `on_command_end`

You can encapsulate this in `ActiveOutput` with a higher-level API and keep `start_shutdown` linear:

```rust
impl<F, E> ActiveOutput<F, E> {
    fn shutdown(
        &mut self,
        reason: MultiPacketTerminationReason,
        hooks: &mut ProtocolHooks<F, E>,
        ctx: &mut ConnectionContext,
        state: &mut ActorState,
        log: impl Fn(MultiPacketTerminationReason, Option<u64>),
    ) {
        match self {
            ActiveOutput::MultiPacket(ctx_mp) => {
                let correlation = ctx_mp.correlation_id();
                log(reason, correlation);

                if let Some(rx) = ctx_mp.channel_mut() {
                    rx.close();
                    state.mark_closed();
                }

                if let Some(frame) = hooks.stream_end_frame(ctx) {
                    // let ConnectionActor handle emitting; or call a callback
                    // depending on where you want this to live
                    // emit_multi_packet_frame(frame, out);
                }

                hooks.on_command_end(ctx);
                *self = ActiveOutput::None;
            }
            ActiveOutput::Response(_) => {
                state.mark_closed();
                *self = ActiveOutput::None;
            }
            ActiveOutput::None => {}
        }
    }
}
```

Then `ConnectionActor::start_shutdown` becomes simpler and doesn’t need `mem::replace` or to know about correlation IDs:

```rust
fn start_shutdown(&mut self, state: &mut ActorState) {
    if let Some(rx) = &mut self.high_rx {
        rx.close();
    }
    if let Some(rx) = &mut self.low_rx {
        rx.close();
    }

    self.active_output.shutdown(
        MultiPacketTerminationReason::Shutdown,
        &mut self.hooks,
        &mut self.ctx,
        state,
        |reason, correlation| self.log_multi_packet_closure(reason, correlation),
    );
}
```

You can apply the same pattern to `handle_multi_packet_closed`, e.g. `ActiveOutput::handle_multi_packet_closed(...)`, so the actor body stops pattern-matching on internal enum variants in multiple places.

---

### 2. Inline `EventAvailability` into `next_event`

`EventAvailability` and `compute_availability` only wrap four simple booleans and force a jump to understand what’s polled. You can keep the new `ActiveOutput` but remove this indirection:

```rust
async fn next_event(&mut self, state: &ActorState) -> Event<F, E> {
    let high_available = self.high_rx.is_some();
    let low_available = self.low_rx.is_some();
    let multi_available = self.active_output.is_multi_packet() && !state.is_shutting_down();
    let resp_available = self.active_output.is_response() && !state.is_shutting_down();

    // Extract either the multi-packet channel or response stream since they're
    // mutually exclusive.
    let (multi_channel, response_stream) = match &mut self.active_output {
        ActiveOutput::MultiPacket(ctx) => (ctx.channel_mut(), None),
        ActiveOutput::Response(stream) => (None, Some(stream)),
        ActiveOutput::None => (None, None),
    };

    tokio::select! {
        biased;
        () = Self::await_shutdown(self.shutdown.clone()), if state.is_active() => Event::Shutdown,
        res = Self::poll_queue(self.high_rx.as_mut()), if high_available => Event::High(res),
        res = Self::poll_queue(self.low_rx.as_mut()), if low_available => Event::Low(res),
        res = Self::poll_queue(multi_channel), if multi_available => Event::MultiPacket(res),
        res = Self::poll_response(response_stream), if resp_available => Event::Response(res),
        else => Event::Idle,
    }
}
```

This preserves the `ActiveOutput` invariant and the borrow-checker workaround but removes the extra type and method, making availability logic local and easier to follow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/connection.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection.rs`:
- Around line 387-398: Replace the debug-only checks that allow conflicting
active_output in release builds: in set_response and the two other setter
methods that assign self.active_output (the other setters in this file that
mirror set_response's active_output assignment), change the debug_assert! that
checks !self.active_output.is_multi_packet() to assert! with the same error
message so the code panics in all builds when attempting to set a
response/error/stream while a multi-packet channel is active; keep the existing
concatenated message text and only change debug_assert! to assert!.

Comment thread src/connection.rs Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 30, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/connection.rs

Comment on lines +615 to +625

        let multi_available = self.active_output.is_multi_packet() && !state.is_shutting_down();
        let response_available = self.active_output.is_response() && !state.is_shutting_down();

        // Extract either the multi-packet channel or response stream since they're
        // mutually exclusive - only one can be active at a time. This must be
        // inlined to satisfy the borrow checker's disjoint field analysis.
        let (multi_channel, response_stream) = match &mut self.active_output {
            ActiveOutput::MultiPacket(ctx) => (ctx.channel_mut(), None),
            ActiveOutput::Response(stream) => (None, Some(stream)),
            ActiveOutput::None => (None, None),
        };

❌ New issue: Complex Method
ConnectionActor.next_event has a cyclomatic complexity of 9, threshold = 9

@coderabbitai

This comment was marked as resolved.

@leynos leynos force-pushed the terragon/refactor-connection-output-state-c1pwtb branch from 0dc5e17 to 1f4af30 Compare January 30, 2026 15:09
Consolidate the separate response stream and multi_packet channel fields
into a single ActiveOutput enum that enforces mutual exclusion at compile
time rather than via runtime debug_assert! checks. This change includes:

- ActiveOutput enum with None, Response, and MultiPacket variants
- ConnectionStateError enum for propagated errors from setter methods
- set_response and set_multi_packet now return Result<(), ConnectionStateError>
- ShutdownResult and MultiPacketCloseResult for encapsulated shutdown logic
- EventAvailability struct to compute source availability for next_event
- Removed unused methods from MultiPacketContext (clear, take_channel, is_active)

All call sites updated to handle the new Result returns appropriately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos force-pushed the terragon/refactor-connection-output-state-c1pwtb branch from 1f4af30 to 26bb362 Compare January 30, 2026 18:46
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection/mod.rs`:
- Line 19: Replace the manual Display/Error implementation for the
ConnectionStateError enum by deriving thiserror::Error and annotating each
variant with #[error("...")] messages; specifically remove the fmt import and
the impl blocks for fmt::Display and std::error::Error, add use thiserror::Error
to imports, change #[derive(Clone, Copy, Debug, PartialEq, Eq)] to
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] on ConnectionStateError, and
add #[error("cannot set response while a multi-packet channel is active")] to
MultiPacketActive and #[error("cannot set multi-packet channel while a response
stream is active")] to ResponseActive so the variant messages are co-located
with the enum.

Comment thread src/connection/mod.rs Outdated
Replace manual Display and Error implementations with thiserror derive
macro annotations, co-locating error messages with their variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection/mod.rs`:
- Around line 114-228: Extract the ActiveOutput, ShutdownResult,
MultiPacketCloseResult, and EventAvailability types into a new submodule (e.g.,
an output submodule) and re-export them from the parent connection module; move
the enum/struct/impl blocks as-is into the new file, adjust their visibility
(pub or pub(crate)) to match how they're used elsewhere, add mod output; pub use
output::{ActiveOutput, ShutdownResult, MultiPacketCloseResult,
EventAvailability}; in the original file, and update any local references to
rely on the re-exports; ensure the impl methods (is_response, is_multi_packet,
multi_packet_mut, clear_response, shutdown, close_multi_packet, clear) remain
intact and compile.

Comment thread src/connection/mod.rs Outdated
Move ActiveOutput, ShutdownResult, MultiPacketCloseResult, and
EventAvailability into a dedicated output submodule to reduce the
size of the main connection module.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract types to dedicated submodules for better organization:

- DrainContext and QueueKind moved to drain.rs
- ActiveConnection counter types moved to new counter.rs
- ConnectionChannels moved to new channels.rs

All types are re-exported as needed to maintain the public API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/connection/test_support.rs (1)

235-265: 🧹 Nitpick | 🔵 Trivial

Consolidate the multi-queue tests using rstest fixtures and parameterised cases.

Collapse the three tests into a single parameterised case and share harness setup via a fixture to eliminate duplication.

♻️ Proposed refactor
 #[cfg(test)]
 mod tests {
+    use rstest::{fixture, rstest};
     use tokio::sync::mpsc;

     use super::*;

-    #[test]
-    fn has_multi_queue_false_by_default() {
-        let harness = ActorHarness::new().expect("build ActorHarness");
-        assert!(
-            !harness.has_multi_queue(),
-            "multi-packet queue should start inactive"
-        );
-    }
-
-    #[test]
-    fn has_multi_queue_true_after_install() {
-        let mut harness = ActorHarness::new().expect("build ActorHarness");
-        let (_tx, rx) = mpsc::channel(1);
-        harness.set_multi_queue(Some(rx)).expect("set_multi_queue");
-        assert!(
-            harness.has_multi_queue(),
-            "multi-packet queue should be active after install"
-        );
-    }
-
-    #[test]
-    fn has_multi_queue_false_after_clear() {
-        let mut harness = ActorHarness::new().expect("build ActorHarness");
-        let (_tx, rx) = mpsc::channel(1);
-        harness.set_multi_queue(Some(rx)).expect("set_multi_queue");
-        assert!(
-            harness.has_multi_queue(),
-            "multi-packet queue should be active after install"
-        );
-        harness.set_multi_queue(None).expect("clear multi_queue");
-        assert!(
-            !harness.has_multi_queue(),
-            "multi-packet queue should be inactive after clear"
-        );
-    }
+    #[fixture]
+    fn harness() -> ActorHarness {
+        ActorHarness::new().expect("build ActorHarness")
+    }
+
+    #[rstest]
+    #[case::default(false, false, false)]
+    #[case::install(true, false, true)]
+    #[case::clear(true, true, false)]
+    fn has_multi_queue_states(
+        #[case] install: bool,
+        #[case] clear: bool,
+        #[case] expected: bool,
+        mut harness: ActorHarness,
+    ) {
+        if install {
+            let (_tx, rx) = mpsc::channel(1);
+            harness.set_multi_queue(Some(rx)).expect("set_multi_queue");
+        }
+        if clear {
+            harness.set_multi_queue(None).expect("clear multi_queue");
+        }
+        assert_eq!(
+            harness.has_multi_queue(),
+            expected,
+            "multi-packet queue state mismatch"
+        );
+    }
 }

Per coding guidelines, utilise rstest fixtures for shared setup in Rust tests and replace duplicated tests with #[rstest(...)] parameterised cases.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leynos, yes! The file has been successfully decomposed and now meets the guideline. ✓

Verification results:

  • Current line count: 397 lines (3 lines under the 400-line limit)
  • Target: ≤400 lines
  • Status: ✓ Resolved

The additional extraction work since the last check has brought the file into compliance:

  • ActiveOutput, ShutdownResult, MultiPacketCloseResult, and EventAvailability → output.rs (120 lines)
  • ConnectionChannels → channels.rs
  • Additional refinements reduced the file from 451 → 397 lines

The decomposition maintains logical organisation while satisfying the file size constraint.

Refactored tests in src/connection/test_support.rs to use rstest fixtures and parameterized test cases. Consolidated three separate tests of the multi-queue state (default, after install, after clear) into a single parametrized test to improve readability and reduce duplication.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection/test_support.rs`:
- Around line 234-239: The file's formatting around the test fixture (the
harness() function that returns ActorHarness and calls ActorHarness::new()) does
not match rustfmt; run the workspace formatter (e.g., make fmt or cargo fmt
--workspace) or run cargo fmt on src/connection/test_support.rs to apply the
expected formatting changes, then re-commit the updated file so cargo fmt --all
-- --check passes in CI.

Comment thread src/connection/test_support.rs
Added a comment to the harness() fixture in test_support.rs that explains it provides an ActorHarness for parameterised multi-queue state tests.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection/test_support.rs`:
- Around line 229-233: Add a module-level doc comment above the `mod tests`
block using a `//!` comment that briefly explains the purpose of the test module
(e.g., that it contains unit/integration helpers and test cases for connection
test support, fixtures like `fixture`/`rstest`, and use of `tokio::sync::mpsc`).
Place the `//!` comment immediately before `mod tests` so the module-level
documentation requirement is satisfied and clearly describes what the test
utilities and test cases inside `mod tests` are for.

Comment thread src/connection/test_support.rs
Added documentation comment for the tests module describing that it contains unit tests for the `ActorHarness` fixture using parameterised `rstest` cases.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Refactor connection output state to ActiveOutput enum Refactor connection output state to ActiveOutput enum with guards Feb 1, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/connection/test_support.rs`:
- Around line 238-266: Change the harness fixture to be fallible (return
Result<ActorHarness, Box<dyn std::error::Error>> or
anyhow::Result<ActorHarness>) and construct it with ActorHarness::new()? (i.e.
return ActorHarness::new().map_err(Into::into) so the error is propagated
instead of .expect()); change the test function has_multi_queue_states to return
Result<(), Box<dyn std::error::Error>> (or anyhow::Result) and replace the
.expect() calls on harness.set_multi_queue(...) with ? to propagate errors, and
end the test with Ok(()) after the assert_eq!; keep references to harness,
ActorHarness::new, has_multi_queue_states, and set_multi_queue to locate the
changes.

Comment thread src/connection/test_support.rs
Changed the test support harness function and tests to return Result types
with proper error propagation instead of panicking. This improves error
handling and test reliability by using standard Result and ? operator
instead of expect calls.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Refactor connection output state to ActiveOutput enum with guards Replace response and multi_packet with ActiveOutput enum Feb 1, 2026
@leynos leynos merged commit 61c27d1 into main Feb 1, 2026
6 checks passed
@leynos leynos deleted the terragon/refactor-connection-output-state-c1pwtb branch February 1, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor ConnectionActor to use single enum for mutually-exclusive response/multi-packet state

1 participant