Skip to content

Implement hard-cap memory-budget protection (8.3.4)#483

Merged
leynos merged 4 commits intomainfrom
implement-hard-cap-per-connection-memory-e53x8n
Feb 26, 2026
Merged

Implement hard-cap memory-budget protection (8.3.4)#483
leynos merged 4 commits intomainfrom
implement-hard-cap-per-connection-memory-e53x8n

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Feb 26, 2026

Summary

  • Introduces hard-cap (8.3.4) protection for inbound memory budgets and consolidates policy into evaluate_memory_pressure(), returning actions to take (Continue, Pause, Abort).
  • Wires hard-cap checks into the inbound read loop to terminate on breach, while preserving soft-limit pacing and per-frame budget enforcement.
  • Adds unit tests and behavioural tests for hard-cap scenarios, plus ADR/exec-plan updates and docs alignment.

Changes

  • Core policy and helpers
    • Added MemoryPressureAction enum and evaluate_memory_pressure() in src/app/frame_handling/backpressure.rs.
    • Implemented has_hard_cap_been_breached() and integrated evaluate_memory_pressure() with existing soft-limit logic.
    • Updated frame_handling/mod.rs to publicly expose MemoryPressureAction and evaluate_memory_pressure for inbound handling.
  • Inbound read loop integration
    • Updated src/app/inbound_handler.rs to call evaluate_memory_pressure() at the top of the read loop.
    • Behavior:
      • Abort: immediately return an InvalidData error (terminates connection).
      • Pause: sleep for soft-limit duration and purge expired state.
      • Continue: proceed with normal processing.
  • Tests
    • Unit tests for hard-cap and combined policy added/updated in src/app/frame_handling/backpressure_tests.rs.
    • Added comprehensive behavioural tests for hard-cap memory budgets:
      • tests/features/memory_budget_hard_cap.feature
      • tests/fixtures/memory_budget_hard_cap.rs
      • tests/steps/memory_budget_hard_cap_steps.rs
      • tests/scenarios/memory_budget_hard_cap_scenarios.rs
  • Documentation & planning
    • ADR updates: docs/adr-002-streaming-requests-and-shared-message-assembly.md updated with hard-cap decisions.
    • Exec plan: new docs/execplans/8-3-4-hard-cap-behaviour.md describing plan and criteria (new).
    • Documentation alignment: updated docs/generic-message-fragmentation-and-re-assembly-design.md, docs/users-guide.md, docs/roadmap.md to reflect hard-cap behavior and status.
    • Roadmap: item 8.3.4 updated to show done after gates pass.

Rationale

  • The hard cap acts as a defence-in-depth safety net to terminate a connection when the total buffered inbound bytes exceed the aggregate cap.
  • Centralising memory-pressure logic in evaluate_memory_pressure() keeps inbound_handler.rs concise and maintains the 400-line file limit.
  • Tests (unit and BDD) validate breach paths and normal operation under budgeted scenarios.

Testing plan

  • Unit tests: cargo test --lib frame_handling
  • Behavioural tests: cargo test --test bdd --all-features memory_budget_hard_cap
  • Validation steps align with the ExecPlan gates and ADR updates.

Public API impact

  • No public API surface changes. New internal helpers and enums are encapsulated under crate visibility (pub(crate) / pub(super)) and re-exported for internal use.

Artefacts

  • Modified
    • src/app/frame_handling/backpressure.rs
    • src/app/frame_handling/backpressure_tests.rs
    • src/app/frame_handling/mod.rs
    • src/app/inbound_handler.rs
  • Added
    • docs/execplans/8-3-4-hard-cap-behaviour.md
    • tests/features/memory_budget_hard_cap.feature
    • tests/fixtures/memory_budget_hard_cap.rs
    • tests/steps/memory_budget_hard_cap_steps.rs
    • tests/scenarios/memory_budget_hard_cap_scenarios.rs
  • Updated
    • docs/adr-002-streaming-requests-and-shared-message-assembly.md
    • docs/generic-message-fragmentation-and-re-assembly-design.md
    • docs/roadmap.md
    • docs/users-guide.md
    • tests/fixtures/mod.rs, tests/steps/mod.rs, tests/scenarios/mod.rs

Validation notes

  • Ensure the hard-cap path returns InvalidData from process_stream and does not increment the existing DeserFailureTracker.
  • Validate that soft-cap pauses still occur at ~80% of the aggregate cap and that normal processing resumes after the pause.
  • Verify BDD scenarios observe either connection termination or normal operation depending on budget configuration.

If reviewers have questions about edge cases (e.g., interaction with partial assemblies on hard cap), please reference the ADR update and the exec-plan for staging rationale.

📎 Task: https://www.devboxer.com/task/daf2c9a4-6b90-435a-b4fa-3ac698c8e8d1

Implement hard-cap connection abort for inbound memory budgets (roadmap item 8.3.4) as a defence-in-depth safety net. When total buffered bytes strictly exceed the aggregate cap (min of bytes_per_connection and bytes_in_flight), the inbound read loop immediately aborts the connection with InvalidData, bypassing the DeserFailureTracker. A combined evaluate_memory_pressure() function is added that returns MemoryPressureAction (Continue, Pause, Abort) to integrate both soft-limit and hard-cap checks, keeping inbound_handler.rs within the 400-line limit.

Add comprehensive unit tests and rstest-bdd behavioural tests validating abort behaviour and connection health within budget. Update relevant documentation, including the ADR, user guide, roadmap, and design notes, to reflect the three-tier memory budget protection model: per-frame enforcement, soft-limit read pacing, and hard-cap abort.

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

sourcery-ai Bot commented Feb 26, 2026

Reviewer's Guide

Implements a unified inbound memory-pressure policy with a new hard-cap connection abort path, refactors the inbound read loop to consult this policy, and adds unit/BDD tests and documentation updates to describe the three-tier memory-budget model and mark roadmap item 8.3.4 complete.

Class diagram for backpressure policy and inbound handler integration

classDiagram
    class MemoryBudgets {
        +bytes_per_message: BudgetBytes
        +bytes_per_connection: BudgetBytes
        +bytes_in_flight: BudgetBytes
    }

    class MessageAssemblyState {
        +total_buffered_bytes() usize
        +buffered_count() usize
    }

    class MemoryPressureAction {
        <<enum>>
        +Continue
        +Pause
        +Abort
    }

    class BackpressureModule {
        +evaluate_memory_pressure(state: Option~&MessageAssemblyState~, budgets: Option~MemoryBudgets~) MemoryPressureAction
        +has_hard_cap_been_breached(state: Option~&MessageAssemblyState~, budgets: Option~MemoryBudgets~) bool
        +should_pause_inbound_reads(state: Option~&MessageAssemblyState~, budgets: Option~MemoryBudgets~) bool
        -SOFT_LIMIT_NUMERATOR: u128
        -SOFT_LIMIT_DENOMINATOR: u128
        -SOFT_LIMIT_PAUSE_DURATION: Duration
        -active_aggregate_limit_bytes(budgets: MemoryBudgets) usize
    }

    class InboundHandler {
        +process_stream(stream: TStream, memory_budgets: Option~MemoryBudgets~) io::Result
        -read_timeout_ms: u64
        -message_assembly: Option~MessageAssemblyState~
        -pipeline: Pipeline
    }

    class FrameHandlingMod {
        +evaluate_memory_pressure(state: Option~&MessageAssemblyState~, budgets: Option~MemoryBudgets~) MemoryPressureAction
        +decode_envelope(...)
        +reassemble_if_needed(...)
        +forward_response(...)
    }

    BackpressureModule ..> MemoryPressureAction : returns
    BackpressureModule ..> MessageAssemblyState : reads_buffer_stats
    BackpressureModule ..> MemoryBudgets : reads_limits

    FrameHandlingMod ..> BackpressureModule : re_exports

    InboundHandler ..> FrameHandlingMod : uses_evaluate_memory_pressure
    InboundHandler ..> MessageAssemblyState : maintains
    InboundHandler ..> MemoryBudgets : configured_by_app
    InboundHandler ..> BackpressureModule : indirect_policy_dependency
Loading

Flow diagram for three-tier inbound memory protection model

flowchart TD
    A[Start inbound frame processing] --> B{Per_frame_budget_enforcement}
    B -->|Frame exceeds per_message or aggregate budgets| C[Reject frame and free offending assembly]
    C --> D[Surface InvalidData via DeserFailureTracker]
    B -->|Frame within budgets| E[Accept frame and update MessageAssemblyState]

    E --> F{Evaluate_memory_pressure}
    F -->|Hard cap breached
buffered_bytes > aggregate_limit| G[Abort connection with InvalidData]
    G --> H[Drop MessageAssemblyState and free all partial assemblies]

    F -->|Soft limit reached
buffered_bytes >= 80% of aggregate_limit| I[Pause reads for 5 ms]
    I --> J[Purge expired assemblies]
    J --> K[Continue processing next frame]

    F -->|Below soft limit
normal pressure| K
    K --> L[Read next frame from socket]
    L --> B
Loading

File-Level Changes

Change Details Files
Add unified memory-pressure policy including hard-cap detection and expose it for inbound handling.
  • Introduce MemoryPressureAction enum (Continue/Pause/Abort) and evaluate_memory_pressure() to combine hard-cap and soft-limit checks.
  • Implement has_hard_cap_been_breached() using total_buffered_bytes() against the active aggregate limit and make should_pause_inbound_reads() internal to the module.
  • Extend backpressure unit tests with custom budget helper, hard-cap threshold and precedence tests, and behaviour tests for evaluate_memory_pressure().
  • Update frame_handling module exports to re-export MemoryPressureAction and evaluate_memory_pressure instead of the soft-limit helpers.
src/app/frame_handling/backpressure.rs
src/app/frame_handling/backpressure_tests.rs
src/app/frame_handling/mod.rs
Integrate hard-cap handling into the inbound read loop and adjust behaviour on memory pressure.
  • Replace direct soft-limit check in process_stream with a match on evaluate_memory_pressure(), keeping the file within the line-count constraint.
  • On Abort, log a warning and return an InvalidData io::Error to terminate the connection without incrementing DeserFailureTracker.
  • On Pause, sleep for the provided duration, then purge expired assemblies; on Continue, fall through to normal frame reading.
src/app/inbound_handler.rs
Add BDD coverage and fixtures to validate hard-cap abort and healthy operation under memory budgets.
  • Introduce HardCapConfig parser and MemoryBudgetHardCapWorld fixture that configures WireframeApp with budgets, drives a duplex client/server, and inspects connection and payload outcomes.
  • Add rstest-bdd step definitions and scenarios for hard-cap apps, including sending ranges of first frames and final continuations, and asserting connection abort or payload delivery.
  • Register new hard-cap modules in the BDD fixtures, steps, and scenarios mod files.
tests/features/memory_budget_hard_cap.feature
tests/fixtures/memory_budget_hard_cap.rs
tests/steps/memory_budget_hard_cap_steps.rs
tests/scenarios/memory_budget_hard_cap_scenarios.rs
tests/fixtures/mod.rs
tests/steps/mod.rs
tests/scenarios/mod.rs
Document the three-tier inbound memory protection model and record hard-cap implementation decisions, marking the roadmap item complete.
  • Update user guide and fragmentation design docs to describe per-frame enforcement, soft-limit pacing, and hard-cap abort semantics including thresholds and InvalidData behaviour.
  • Extend ADR-002 with 8.3.4 implementation decisions covering where the hard-cap check runs, comparison semantics, interaction with DeserFailureTracker, and use of active_aggregate_limit_bytes().
  • Add an execution plan document for 8.3.4 capturing constraints, risks, decisions, tests, and gates, and mark roadmap item 8.3.4 as done.
docs/users-guide.md
docs/generic-message-fragmentation-and-re-assembly-design.md
docs/adr-002-streaming-requests-and-shared-message-assembly.md
docs/execplans/8-3-4-hard-cap-behaviour.md
docs/roadmap.md

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 Feb 26, 2026

Walkthrough

Introduces hard-cap memory pressure evaluation alongside soft-limit back-pressure for inbound message handling. Adds MemoryPressureAction enum and evaluate_memory_pressure() function to centralise memory-budget policy decisions, enabling three-tier protection: per-frame enforcement, soft-limit pacing, and hard-cap connection abort on budget overage.

Changes

Cohort / File(s) Summary
Documentation & Design
docs/adr-002-streaming-requests-and-shared-message-assembly.md, docs/execplans/8-3-4-hard-cap-behaviour.md, docs/generic-message-fragmentation-and-re-assembly-design.md, docs/users-guide.md, docs/roadmap.md
Updated design documentation to describe three-tier memory protection model (per-frame enforcement, soft-limit pacing, hard-cap abort). Expanded budget-enforcement messaging and clarified abort semantics. Marked roadmap item 8.3.4 as complete.
Core Backpressure Logic
src/app/frame_handling/backpressure.rs, src/app/frame_handling/backpressure_tests.rs
Added MemoryPressureAction enum and evaluate_memory_pressure() function to centralise hard-cap and soft-limit checks. Added has_hard_cap_been_breached() helper. Modified should_pause_inbound_reads() visibility to pub(super). Replaced soft-limit direct exposure with encapsulated evaluation returning specific actions (Abort, Pause, Continue).
Inbound Handler Integration
src/app/inbound_handler.rs
Refactored memory-pressure handling from simple pause to conditional action evaluation. Abort now terminates connection with InvalidData on hard-cap breach; Pause applies timed delay with expiry purging; Continue proceeds normally.
Module Exports
src/app/frame_handling/mod.rs
Updated public re-export list: replaced should_pause_inbound_reads and soft_limit_pause_duration with MemoryPressureAction and evaluate_memory_pressure.
Behavioral Test Infrastructure
tests/features/memory_budget_hard_cap.feature, tests/fixtures/memory_budget_hard_cap.rs, tests/scenarios/memory_budget_hard_cap_scenarios.rs, tests/steps/memory_budget_hard_cap_steps.rs, tests/fixtures/mod.rs, tests/scenarios/mod.rs, tests/steps/mod.rs
Introduced comprehensive BDD test suite for hard-cap scenarios. Added HardCapConfig parser, MemoryBudgetHardCapWorld fixture with app startup, frame transmission, and assertion helpers. Wired Cucumber-style given/when/then steps. Verified connection termination on budget violation and payload assembly within limits.

Sequence Diagram

sequenceDiagram
    participant Inbound as Inbound Read Loop
    participant Eval as evaluate_memory<br/>_pressure()
    participant Check as has_hard_cap_<br/>been_breached()
    participant Pause as should_pause_<br/>inbound_reads()
    participant Handler as Inbound Handler<br/>Error Path

    Inbound->>Eval: evaluate_memory_pressure(state, budgets)
    Eval->>Check: Check hard cap exceeded?
    
    alt Hard Cap Breached
        Check-->>Eval: true
        Eval-->>Inbound: MemoryPressureAction::Abort
        Inbound->>Handler: Return InvalidData IoError
        Handler->>Handler: Terminate connection
    else Hard Cap OK
        Check-->>Eval: false
        Eval->>Pause: Check soft-limit threshold
        alt At/Above Soft Limit
            Pause-->>Eval: true
            Eval-->>Inbound: MemoryPressureAction::Pause(duration)
            Inbound->>Inbound: Sleep for duration
            Inbound->>Inbound: Purge expired assemblies
        else Below Soft Limit
            Pause-->>Eval: false
            Eval-->>Inbound: MemoryPressureAction::Continue
            Inbound->>Inbound: Proceed with read
        end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Suggested Labels

codex

Poem

A triple shield guards memory's flow, ✨
Per-frame first, then soft-cap's slow,
Hard cap halts when bounds o'erflow—
Three tiers deep, threats don't take toll! 🛡️

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title 'Implement hard-cap memory-budget protection (8.3.4)' directly and clearly summarises the main change—adding hard-cap protection for inbound memory budgets as part of roadmap item 8.3.4.
Description check ✅ Passed The description is comprehensive, well-structured, and directly related to the changeset. It covers the summary, core changes, test additions, documentation updates, rationale, and validation notes—all aligned with the actual modifications in the PR.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-hard-cap-per-connection-memory-e53x8n

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 Feb 26, 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/app/frame_handling/backpressure_tests.rs

Comment on file

fn hard_cap_uses_smallest_aggregate_budget_dimension() -> TestResult {
    let per_message =
        NonZeroUsize::new(1024).ok_or_else(|| io::Error::other("1024 is non-zero"))?;
    let per_connection =
        NonZeroUsize::new(200).ok_or_else(|| io::Error::other("200 is non-zero"))?;
    let in_flight = NonZeroUsize::new(100).ok_or_else(|| io::Error::other("100 is non-zero"))?;
    let budgets = MemoryBudgets::new(
        BudgetBytes::new(per_message),
        BudgetBytes::new(per_connection),
        BudgetBytes::new(in_flight),
    );
    let state = state_with_buffered_bytes(101)?;
    if !has_hard_cap_been_breached(Some(&state), Some(budgets)) {
        return Err(io::Error::other("expected breach at smallest aggregate dimension").into());
    }
    Ok(())
}

❌ New issue: Code Duplication
The module contains 6 functions with similar structure: evaluate_returns_abort_above_hard_cap,evaluate_returns_continue_below_soft_limit,evaluate_returns_continue_when_no_budgets_configured,evaluate_returns_pause_at_soft_limit and 2 more functions

@coderabbitai

This comment was marked as resolved.

…ure tests

- Introduce `custom_budgets` helper to reduce test duplication
- Simplify tests by replacing repeated budget setup with helper
- Add `ExpectedAction` enum for expected results in parameterized test
- Consolidate multiple evaluate_memory_pressure tests into one parameterized test
- Add `action_matches` function to compare action variants ignoring Duration payload
- Increase test clarity and maintainability through restructuring

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos marked this pull request as ready for review February 26, 2026 14:34
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 5 issues

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

## Individual Comments

### Comment 1
<location path="src/app/frame_handling/backpressure_tests.rs" line_range="195-204" />
<code_context>
+
+/// Compare pressure actions by variant, ignoring the `Duration` payload in
+/// `Pause`.
+fn action_matches(actual: &MemoryPressureAction, expected: &MemoryPressureAction) -> bool {
+    matches!(
+        (actual, expected),
+        (
+            MemoryPressureAction::Continue,
+            MemoryPressureAction::Continue
+        ) | (
+            MemoryPressureAction::Pause(_),
+            MemoryPressureAction::Pause(_)
+        ) | (MemoryPressureAction::Abort, MemoryPressureAction::Abort)
+    )
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a targeted test that asserts the soft-limit pause duration is the expected value, not just the variant

Because `action_matches` ignores the `Duration` in `MemoryPressureAction::Pause`, the current parameterized test wouldn’t catch an accidental change to `SOFT_LIMIT_PAUSE_DURATION`. To cover this, add a small unit test that exercises `evaluate_memory_pressure` in a soft-limit scenario, matches on `MemoryPressureAction::Pause(d)`, and asserts `d == Duration::from_millis(5)` alongside the existing test.

Suggested implementation:

```rust
use super::{
    MemoryPressureAction,
    backpressure::{has_hard_cap_been_breached, should_pause_inbound_reads},
    evaluate_memory_pressure,
};

#[test]
fn soft_limit_pause_uses_expected_duration() -> Result<(), Box<dyn Error>> {
    // Arrange a scenario where the soft limit is breached but the hard cap is not.
    //
    // This should cause `evaluate_memory_pressure` to return a `Pause`
    // with the configured soft-limit pause duration (currently 5ms).
    let (state, budgets) = soft_limit_only_state_and_budgets()?;

    let action = evaluate_memory_pressure(&state, &budgets)?;

    match action {
        MemoryPressureAction::Pause(d) => {
            assert_eq!(d, Duration::from_millis(5));
        }
        other => {
            panic!("expected MemoryPressureAction::Pause(_), got {other:?}");
        }
    }

    Ok(())
}

```

To wire this test up to your existing code, you will need to:

1. Replace `soft_limit_only_state_and_budgets()` with concrete setup that produces a “soft limit breached, hard cap not breached” scenario. The easiest way is usually to:
   - Reuse the same fixtures/builders used by your existing “combined evaluate_memory_pressure” tests, or
   - Introduce a small helper returning `(state, budgets)` that configures usage just above the soft limit and below the hard cap.
2. Adjust the call to `evaluate_memory_pressure(&state, &budgets)?` if its signature differs (e.g., argument order, additional parameters, or if it does not return a `Result`).
3. If `evaluate_memory_pressure` does not use `Result`, remove the `Result` return type and `?` operators from the test accordingly.
4. Ensure that `MemoryPressureAction` implements `Debug` (or adapt the panic message) if `{other:?}` is not currently valid.
These adjustments should align the new test with the conventions and helpers already present in `backpressure_tests.rs`.
</issue_to_address>

### Comment 2
<location path="tests/fixtures/memory_budget_hard_cap.rs" line_range="24-33" />
<code_context>
+/// We cannot pass `MemoryPressureAction` directly in `#[case]` attributes
+/// because the `Pause` variant contains a `Duration`, so we use a simple tag
+/// that the test body maps to the real variant for comparison.
+#[derive(Clone, Copy, Debug)]
+enum ExpectedAction {
+    Continue,
</code_context>
<issue_to_address>
**suggestion (testing):** Add unit-style tests for `HardCapConfig::from_str` to cover invalid/edge-case parsing

Since `HardCapConfig` is the only way to configure this fixture, its failure paths should be covered directly rather than relying solely on BDD scenarios. Please add focused tests for `HardCapConfig::from_str` covering:

- A valid four-segment input.
- Each missing segment variant (e.g. `"/64/100/100"`, `"10//100/100"`).
- An extra segment (e.g. `"10/64/100/100/extra"`).
- Non-numeric segment values.
- Zero budgets/timeouts, asserting they either return `Err` or produce a config that fails early in `start_app`.

This will make regressions in the parsing logic much easier to detect and localize.
</issue_to_address>

### Comment 3
<location path="tests/fixtures/memory_budget_hard_cap.rs" line_range="244-253" />
<code_context>
+    /// Assert that the connection has terminated with an error.
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting that no payloads are delivered in abort scenarios to verify in-flight messages are not surfaced

Right now the BDD step and `assert_connection_aborted` only check that an error occurs, not that no payloads were delivered. Please also assert that `observed_payloads` is empty at the time of abort (either in this helper or a new one) so a scenario like “Connection terminates after repeated budget violations” verifies that the hard-cap path drops in-flight data instead of delivering it.
</issue_to_address>

### Comment 4
<location path="src/app/inbound_handler.rs" line_range="281" />
<code_context>

         loop {
-            if frame_handling::should_pause_inbound_reads(
+            match frame_handling::evaluate_memory_pressure(
                 message_assembly.as_ref(),
                 self.memory_budgets,
</code_context>
<issue_to_address>
**issue (review_instructions):** This `match` introduces a 3-way branch and should be encapsulated in a predicate/helper function to comply with the conditional-branching guideline.

The review guideline asks to move conditionals with more than two branches into a predicate function. This `match` over `MemoryPressureAction` has three arms (`Abort`, `Pause`, `Continue`) and is now inlined in the loop body.

Consider extracting this branching into a small helper (e.g. a function that takes the action and performs the appropriate side-effect, or a function that maps the action to a simpler boolean/enum used here) so that the loop body does not directly contain a >2-branch conditional.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Move conditionals with >2 branches into a predicate function.

</details>
</issue_to_address>

### Comment 5
<location path="docs/execplans/8-3-4-hard-cap-behaviour.md" line_range="263" />
<code_context>
+
+### BDD test pattern
+
+The project uses a 4-file pattern for BDD tests with `rstest-bdd` v0.5.0:
+
+1. **Feature file** (`tests/features/<name>.feature`): Gherkin scenarios.
</code_context>
<issue_to_address>
**suggestion (review_instructions):** Define “BDD” (behaviour-driven development) at its first use in this document.

To follow the “define uncommon acronyms on first use” guidance, expand this sentence to something like:

"The project uses a 4-file pattern for behaviour-driven development (BDD) tests with `rstest-bdd` v0.5.0:"

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</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/app/frame_handling/backpressure_tests.rs
Comment thread tests/fixtures/memory_budget_hard_cap.rs
Comment thread tests/fixtures/memory_budget_hard_cap.rs Outdated
Comment thread src/app/inbound_handler.rs Outdated
Comment thread docs/execplans/8-3-4-hard-cap-behaviour.md
@coderabbitai coderabbitai Bot added the codex label Feb 26, 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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/execplans/8-3-4-hard-cap-behaviour.md`:
- Around line 89-91: Replace the British "-ising" spelling in the sentence "stop
and present options with trade-offs before finalising behaviour" with the Oxford
English Dictionary preferred "-izing" form: change "finalising behaviour" to
"finalizing behaviour" (ensure the surrounding sentence remains unchanged) to
comply with the en-GB-oxendict -ize spelling guideline in
docs/execplans/8-3-4-hard-cap-behaviour.md; verify there are no other instances
of "-ising" in that file and update them to "-izing" where appropriate.

In `@src/app/frame_handling/backpressure_tests.rs`:
- Around line 229-251: The parameterized test evaluate_memory_pressure_behaviour
contains a duplicate case: remove the redundant
#[case::abort_takes_priority_over_pause(101, true, ExpectedAction::Abort)]
(which is identical to #[case::above_hard_cap(101, true,
ExpectedAction::Abort)]), or replace it with a distinct scenario (e.g.,
different buffered_bytes or use_budgets flag) so each #[case::...] in the
evaluate_memory_pressure_behaviour test is unique; update any references to the
removed case name if used elsewhere.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfe299b and a6fa80c.

📒 Files selected for processing (16)
  • docs/adr-002-streaming-requests-and-shared-message-assembly.md
  • docs/execplans/8-3-4-hard-cap-behaviour.md
  • docs/generic-message-fragmentation-and-re-assembly-design.md
  • docs/roadmap.md
  • docs/users-guide.md
  • src/app/frame_handling/backpressure.rs
  • src/app/frame_handling/backpressure_tests.rs
  • src/app/frame_handling/mod.rs
  • src/app/inbound_handler.rs
  • tests/features/memory_budget_hard_cap.feature
  • tests/fixtures/memory_budget_hard_cap.rs
  • tests/fixtures/mod.rs
  • tests/scenarios/memory_budget_hard_cap_scenarios.rs
  • tests/scenarios/mod.rs
  • tests/steps/memory_budget_hard_cap_steps.rs
  • tests/steps/mod.rs

Comment thread docs/execplans/8-3-4-hard-cap-behaviour.md
Comment thread src/app/frame_handling/backpressure_tests.rs
…cement

- Introduce `apply_memory_pressure` async function to act on memory pressure actions:
  Abort logs a warning and returns an error to terminate the connection.
  Pause logs at debug level, awaits pause duration, then purges expired assemblies.
  Continue is a no-op.
- Replace inline memory pressure handling in inbound_handler with `apply_memory_pressure` usage.
- Add tests for pause duration and hard-cap behavior.
- Expand memory budget hard cap fixture tests to cover error cases.
- Update documentation for hard-cap behavior with clarifications and spelling fixes.
- Refactor exports to include `apply_memory_pressure`.

This change enforces a per-connection memory hard cap by aborting connections that exceed it and applies backpressure via pausing when soft limits are reached.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement hard-cap abort for inbound memory budgets (8.3.4) Implement three-tier memory-budget protection (8.3.4) Feb 26, 2026
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 26, 2026

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/app/frame_handling/backpressure_tests.rs" line_range="195-204" />
<code_context>
+
+/// Compare pressure actions by variant, ignoring the `Duration` payload in
+/// `Pause`.
+fn action_matches(actual: &MemoryPressureAction, expected: &MemoryPressureAction) -> bool {
+    matches!(
+        (actual, expected),
+        (
+            MemoryPressureAction::Continue,
+            MemoryPressureAction::Continue
+        ) | (
+            MemoryPressureAction::Pause(_),
+            MemoryPressureAction::Pause(_)
+        ) | (MemoryPressureAction::Abort, MemoryPressureAction::Abort)
+    )
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a targeted test that asserts the soft-limit pause duration is the expected value, not just the variant

Because `action_matches` ignores the `Duration` in `MemoryPressureAction::Pause`, the current parameterized test wouldn’t catch an accidental change to `SOFT_LIMIT_PAUSE_DURATION`. To cover this, add a small unit test that exercises `evaluate_memory_pressure` in a soft-limit scenario, matches on `MemoryPressureAction::Pause(d)`, and asserts `d == Duration::from_millis(5)` alongside the existing test.

Suggested implementation:

```rust
use super::{
    MemoryPressureAction,
    backpressure::{has_hard_cap_been_breached, should_pause_inbound_reads},
    evaluate_memory_pressure,
};

#[test]
fn soft_limit_pause_uses_expected_duration() -> Result<(), Box<dyn Error>> {
    // Arrange a scenario where the soft limit is breached but the hard cap is not.
    //
    // This should cause `evaluate_memory_pressure` to return a `Pause`
    // with the configured soft-limit pause duration (currently 5ms).
    let (state, budgets) = soft_limit_only_state_and_budgets()?;

    let action = evaluate_memory_pressure(&state, &budgets)?;

    match action {
        MemoryPressureAction::Pause(d) => {
            assert_eq!(d, Duration::from_millis(5));
        }
        other => {
            panic!("expected MemoryPressureAction::Pause(_), got {other:?}");
        }
    }

    Ok(())
}

```

To wire this test up to your existing code, you will need to:

1. Replace `soft_limit_only_state_and_budgets()` with concrete setup that produces a “soft limit breached, hard cap not breached” scenario. The easiest way is usually to:
   - Reuse the same fixtures/builders used by your existing “combined evaluate_memory_pressure” tests, or
   - Introduce a small helper returning `(state, budgets)` that configures usage just above the soft limit and below the hard cap.
2. Adjust the call to `evaluate_memory_pressure(&state, &budgets)?` if its signature differs (e.g., argument order, additional parameters, or if it does not return a `Result`).
3. If `evaluate_memory_pressure` does not use `Result`, remove the `Result` return type and `?` operators from the test accordingly.
4. Ensure that `MemoryPressureAction` implements `Debug` (or adapt the panic message) if `{other:?}` is not currently valid.
These adjustments should align the new test with the conventions and helpers already present in `backpressure_tests.rs`.
</issue_to_address>

### Comment 2
<location path="tests/fixtures/memory_budget_hard_cap.rs" line_range="24-33" />
<code_context>
+/// We cannot pass `MemoryPressureAction` directly in `#[case]` attributes
+/// because the `Pause` variant contains a `Duration`, so we use a simple tag
+/// that the test body maps to the real variant for comparison.
+#[derive(Clone, Copy, Debug)]
+enum ExpectedAction {
+    Continue,
</code_context>
<issue_to_address>
**suggestion (testing):** Add unit-style tests for `HardCapConfig::from_str` to cover invalid/edge-case parsing

Since `HardCapConfig` is the only way to configure this fixture, its failure paths should be covered directly rather than relying solely on BDD scenarios. Please add focused tests for `HardCapConfig::from_str` covering:

- A valid four-segment input.
- Each missing segment variant (e.g. `"/64/100/100"`, `"10//100/100"`).
- An extra segment (e.g. `"10/64/100/100/extra"`).
- Non-numeric segment values.
- Zero budgets/timeouts, asserting they either return `Err` or produce a config that fails early in `start_app`.

This will make regressions in the parsing logic much easier to detect and localize.
</issue_to_address>

### Comment 3
<location path="tests/fixtures/memory_budget_hard_cap.rs" line_range="244-253" />
<code_context>
+    /// Assert that the connection has terminated with an error.
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting that no payloads are delivered in abort scenarios to verify in-flight messages are not surfaced

Right now the BDD step and `assert_connection_aborted` only check that an error occurs, not that no payloads were delivered. Please also assert that `observed_payloads` is empty at the time of abort (either in this helper or a new one) so a scenario like “Connection terminates after repeated budget violations” verifies that the hard-cap path drops in-flight data instead of delivering it.
</issue_to_address>

### Comment 4
<location path="src/app/inbound_handler.rs" line_range="281" />
<code_context>

         loop {
-            if frame_handling::should_pause_inbound_reads(
+            match frame_handling::evaluate_memory_pressure(
                 message_assembly.as_ref(),
                 self.memory_budgets,
</code_context>
<issue_to_address>
**issue (review_instructions):** This `match` introduces a 3-way branch and should be encapsulated in a predicate/helper function to comply with the conditional-branching guideline.

The review guideline asks to move conditionals with more than two branches into a predicate function. This `match` over `MemoryPressureAction` has three arms (`Abort`, `Pause`, `Continue`) and is now inlined in the loop body.

Consider extracting this branching into a small helper (e.g. a function that takes the action and performs the appropriate side-effect, or a function that maps the action to a simpler boolean/enum used here) so that the loop body does not directly contain a >2-branch conditional.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Move conditionals with >2 branches into a predicate function.

</details>
</issue_to_address>

### Comment 5
<location path="docs/execplans/8-3-4-hard-cap-behaviour.md" line_range="263" />
<code_context>
+
+### BDD test pattern
+
+The project uses a 4-file pattern for BDD tests with `rstest-bdd` v0.5.0:
+
+1. **Feature file** (`tests/features/<name>.feature`): Gherkin scenarios.
</code_context>
<issue_to_address>
**suggestion (review_instructions):** Define “BDD” (behaviour-driven development) at its first use in this document.

To follow the “define uncommon acronyms on first use” guidance, expand this sentence to something like:

"The project uses a 4-file pattern for behaviour-driven development (BDD) tests with `rstest-bdd` v0.5.0:"

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 26, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check Comment 1: src/app/frame_handling/backpressure_tests.rs - look for test for soft-limit pause duration
echo "=== COMMENT 1: Checking for soft-limit pause duration test ==="
rg -n "soft_limit_pause.*duration|pause_uses_expected_duration" src/app/frame_handling/backpressure_tests.rs
echo ""

# Check Comment 2: tests/fixtures/memory_budget_hard_cap.rs - look for HardCapConfig::from_str tests
echo "=== COMMENT 2: Checking for HardCapConfig::from_str unit tests ==="
rg -n "HardCapConfig.*from_str|#\[test\]" tests/fixtures/memory_budget_hard_cap.rs -A 2
echo ""

# Check Comment 3: tests/fixtures/memory_budget_hard_cap.rs - check assert_connection_aborted implementation
echo "=== COMMENT 3: Checking assert_connection_aborted for payload assertions ==="
sed -n '244,280p' tests/fixtures/memory_budget_hard_cap.rs
echo ""

# Check Comment 4: src/app/inbound_handler.rs - check if 3-way match is extracted
echo "=== COMMENT 4: Checking inbound_handler.rs for match extraction ==="
sed -n '275,310p' src/app/inbound_handler.rs
echo ""

# Check Comment 5: docs/execplans/8-3-4-hard-cap-behaviour.md - check if BDD is defined
echo "=== COMMENT 5: Checking if BDD is defined on first use ==="
sed -n '260,270p' docs/execplans/8-3-4-hard-cap-behaviour.md
echo ""

Length of output: 4491


🏁 Script executed:

#!/bin/bash

# Get more details on Comment 1: the full test implementation
echo "=== COMMENT 1 DETAILS: Full test implementation ==="
sed -n '253,270p' src/app/frame_handling/backpressure_tests.rs
echo ""

# Get more details on Comment 2: look for the test module with all from_str tests
echo "=== COMMENT 2 DETAILS: Full test module for HardCapConfig ==="
rg -n "#\[cfg\(test\)\]" tests/fixtures/memory_budget_hard_cap.rs -A 150
echo ""

# Get details on Comment 4: look for apply_memory_pressure function
echo "=== COMMENT 4 DETAILS: apply_memory_pressure implementation ==="
ast-grep --pattern 'async fn apply_memory_pressure($$$) {
  $$$
}'
echo ""
rg -n "pub.*fn apply_memory_pressure" src/app/frame_handling/
echo ""

# Double check comment 5 - search for "behaviour-driven development" or expanded BDD
echo "=== COMMENT 5 DETAILS: Check if BDD expanded anywhere ==="
rg -n "behaviour-driven development|behavior-driven development" docs/execplans/8-3-4-hard-cap-behaviour.md
echo ""

Length of output: 2758


🏁 Script executed:

#!/bin/bash

# Get the full apply_memory_pressure implementation
echo "=== COMMENT 4: Full apply_memory_pressure implementation ==="
sed -n '65,95p' src/app/frame_handling/backpressure.rs

Length of output: 1196


@leynos: I've checked the current state of the code for all five review comments. Here's the status:

✅ Resolved (4 of 5)

Comment 1: Soft-limit pause duration test

RESOLVED — Found evaluate_pause_uses_expected_duration test that:

  • Creates a soft-limit scenario (80 buffered bytes)
  • Matches on MemoryPressureAction::Pause(duration)
  • Asserts duration == Duration::from_millis(5)

Comment 2: HardCapConfig::from_str unit tests

RESOLVED — Comprehensive test module hard_cap_config_tests added covering:

  • Valid four-segment input
  • Parameterised invalid_inputs_are_rejected with cases for missing segments, extra segments, non-numeric values, and negative values
  • Zero timeout acceptance
  • Zero budget parsing (with comment explaining NonZeroUsize enforcement happens later in start_app)

Comment 3: Assert no payloads delivered in abort scenarios

RESOLVEDassert_connection_aborted now:

  • Drains ready payloads
  • Checks observed_payloads.is_empty()
  • Returns error if payloads were delivered before abort
  • Doc comment explicitly states "and that no payloads were delivered before the abort"

Comment 4: Extract 3-way match to helper function

RESOLVED — New apply_memory_pressure function added that encapsulates the branching:

  • Takes MemoryPressureAction and a purge callback
  • Handles Abort (returns InvalidData), Pause (sleeps + purges), and Continue
  • The inbound handler now calls this helper instead of inline matching

❌ Not Resolved (1 of 5)

Comment 5: Define "BDD" acronym on first use

NOT RESOLVED — Line 263 in docs/execplans/8-3-4-hard-cap-behaviour.md still reads:

"The project uses a 4-file pattern for BDD tests with rstest-bdd v0.5.0:"

The acronym is not expanded at this location. While "behaviour-driven development" appears at line 114, it's in a different context (risk section) rather than at the first use of the acronym.


Summary: Four of the five comments have been fully addressed. Only Comment 5 (defining the BDD acronym) remains outstanding.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement three-tier memory-budget protection (8.3.4) Implement hard-cap memory-budget protection (8.3.4) Feb 26, 2026
@leynos leynos merged commit 7ae19cf into main Feb 26, 2026
6 checks passed
@leynos leynos deleted the implement-hard-cap-per-connection-memory-e53x8n branch February 26, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant