Skip to content

Commit 6327c67

Browse files
Add tests and docs for per-connection memory budgets (8.3.6) (#489)
* test(memory-budgets): add comprehensive tests for per-connection memory budgets Add new async unit tests for apply_memory_pressure covering Abort, Pause, and Continue actions. Introduce two new BDD suites: budget_cleanup and budget_transitions, with six scenarios validating cleanup semantics, budget reclamation, soft-limit pacing escalation, recovery after assembly completion, and tightest dimension enforcement. Create fixtures, steps, and scenario bindings to support the new BDD tests. Update docs/roadmap.md to mark 8.3.6 as done and add implementation decisions to ADR 0002. This change significantly improves test coverage for the three-tier per-connection memory budget protection system, enabling better verification of budget enforcement, back-pressure, cleanup, and pressure-level transitions without modifying production code. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * test(budget): harden connection abort assertions for budget-related errors Refined the `assert_connection_aborted` method in both `budget_transitions.rs` and `memory_budget_hard_cap.rs` tests to specifically verify that the error kind is `InvalidData`, which is the expected error kind produced by budget enforcement, instead of accepting any server error. Updated documentation accordingly to reflect this stricter error validation. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * refactor(tests): simplify error handling and adjust payload checks in budget tests Refactor budget transition and memory budget hard cap tests: - Simplified the handling of send_result by flattening nested matches into combinators - Updated assert_connection_aborted to drain payloads after server task finishes to catch payloads during teardown - Documented that send errors in send_first_frames_for_range are intentionally ignored due to expected abort behavior These changes improve code clarity and correctness in test fixtures. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * refactor(tests/fixtures): refactor MemoryBudgetHardCapWorld server join handling - Added SERVER_JOIN_TIMEOUT constant for server join timeout - Extracted join_server() method to unify server task joining and error handling - Replaced inline server join handling in assert_connection_aborted with join_server() - Introduced helper parse function for cleaner HardCapConfig parsing - Moved with_runtime() impl block for better code organization - Removed redundant error handling branch for join errors This improves test reliability by timing out server joins and simplifies error handling. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> --------- Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
1 parent e5f08b6 commit 6327c67

16 files changed

+1714
-76
lines changed

docs/adr-002-streaming-requests-and-shared-message-assembly.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,28 @@ Precedence is:
373373
- Explicit budgets always take precedence over derived defaults (ADR-002
374374
precedence rule: explicit > global caps > derived defaults).
375375

376+
#### Implementation decisions (2026-03-01)
377+
378+
- Roadmap item `8.3.6` adds comprehensive test coverage for the three-tier
379+
per-connection memory budget protection model (items `8.3.1` through `8.3.5`).
380+
- Three `#[tokio::test]` unit tests validate the `apply_memory_pressure()`
381+
async function for each `MemoryPressureAction` variant (`Abort` returns
382+
`InvalidData`, `Pause` invokes the purge closure, `Continue` is a no-op).
383+
- A `budget_cleanup` BDD suite (3 scenarios) validates end-to-end cleanup
384+
semantics: timeout purge reclaims budget headroom for subsequent frames,
385+
completed assemblies reclaim budget for subsequent frames, and connection
386+
close frees all partial assemblies via RAII drop of `MessageAssemblyState`.
387+
- A `budget_transitions` BDD suite (3 scenarios) validates pressure
388+
transitions: soft-limit pacing escalates to connection termination after
389+
repeated per-frame rejections, assembly completion recovers the connection
390+
from soft-limit pressure, and the tightest aggregate dimension
391+
(`min(bytes_per_connection, bytes_in_flight)`) controls enforcement when the
392+
two dimensions differ.
393+
- Cleanup relies entirely on Rust's RAII semantics. When `process_stream`
394+
returns, its local `message_assembly: Option<MessageAssemblyState>` drops,
395+
which drops the internal `HashMap<MessageKey, PartialAssembly>`, freeing all
396+
partial body and metadata buffers. No custom `Drop` implementation is needed.
397+
376398
#### Budget enforcement
377399

378400
- Budgets MUST cover: bytes buffered per message, bytes buffered per

docs/execplans/8-3-6-tests-for-per-connection-memory-budgets.md

Lines changed: 604 additions & 0 deletions
Large diffs are not rendered by default.

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ and standardized per-connection memory budgets.
299299
`InvalidData`) behaviour.
300300
- [x] 8.3.5. Define derived defaults based on `buffer_capacity` when budgets
301301
are not set explicitly.
302-
- [ ] 8.3.6. Write tests for budget enforcement, back-pressure, and cleanup
302+
- [x] 8.3.6. Write tests for budget enforcement, back-pressure, and cleanup
303303
semantics.
304304

305305
### 8.4. Transport helper

src/app/frame_handling/backpressure_tests.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,49 @@ fn resolve_derived_budgets_change_with_frame_budget() -> TestResult {
336336
}
337337
Ok(())
338338
}
339+
340+
// ---------- apply_memory_pressure async tests ----------
341+
342+
#[tokio::test]
343+
async fn apply_memory_pressure_abort_returns_invalid_data() {
344+
use super::backpressure::{MemoryPressureAction, apply_memory_pressure};
345+
346+
let result = apply_memory_pressure(MemoryPressureAction::Abort, || {}).await;
347+
let err = result.expect_err("Abort should return an error");
348+
assert_eq!(
349+
err.kind(),
350+
io::ErrorKind::InvalidData,
351+
"expected InvalidData error kind, got {:?}: {err}",
352+
err.kind()
353+
);
354+
}
355+
356+
#[tokio::test]
357+
async fn apply_memory_pressure_pause_invokes_purge_closure() {
358+
use super::backpressure::{MemoryPressureAction, apply_memory_pressure};
359+
360+
tokio::time::pause();
361+
362+
let mut purge_called = false;
363+
let result = apply_memory_pressure(
364+
MemoryPressureAction::Pause(Duration::from_millis(1)),
365+
|| purge_called = true,
366+
)
367+
.await;
368+
result.expect("Pause should return Ok");
369+
assert!(
370+
purge_called,
371+
"expected purge closure to be called during Pause action"
372+
);
373+
}
374+
375+
#[tokio::test]
376+
async fn apply_memory_pressure_continue_is_noop() {
377+
use super::backpressure::{MemoryPressureAction, apply_memory_pressure};
378+
379+
let result = apply_memory_pressure(MemoryPressureAction::Continue, || {
380+
panic!("purge closure should not be called for Continue action");
381+
})
382+
.await;
383+
result.expect("Continue should return Ok");
384+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@budget_cleanup
2+
Feature: Budget cleanup and reclamation semantics
3+
Memory budgets are reclaimed when partial assemblies time out or
4+
complete, allowing new frames to arrive within the freed headroom.
5+
When a connection closes, all partial assemblies are freed via RAII.
6+
7+
Scenario: Timeout purge reclaims budget for subsequent frames
8+
Given a cleanup app configured as 200/2048/10/10
9+
When a cleanup first frame for key 1 with body "aaaaaaaa" arrives
10+
And cleanup virtual time advances by 201 milliseconds
11+
And a cleanup first frame for key 2 with body "bbbbbbbb" arrives
12+
And a cleanup final continuation for key 2 sequence 1 with body "cc" arrives
13+
Then cleanup payload "bbbbbbbbcc" is eventually received
14+
And no cleanup connection error is recorded
15+
16+
Scenario: Completed assembly reclaims budget for subsequent frames
17+
Given a cleanup app configured as 200/2048/10/10
18+
When a cleanup first frame for key 3 with body "cccccccc" arrives
19+
And a cleanup final continuation for key 3 sequence 1 with body "d" arrives
20+
Then cleanup payload "ccccccccd" is eventually received
21+
When a cleanup first frame for key 4 with body "eeeeeeee" arrives
22+
And a cleanup final continuation for key 4 sequence 1 with body "f" arrives
23+
Then cleanup payload "eeeeeeeef" is eventually received
24+
And no cleanup connection error is recorded
25+
26+
Scenario: Connection close frees all partial assemblies
27+
Given a cleanup app configured as 200/2048/100/100
28+
When a cleanup first frame for key 5 with body "aaa" arrives
29+
And a cleanup first frame for key 6 with body "bbb" arrives
30+
And the cleanup client disconnects
31+
Then no cleanup connection error is recorded
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@budget_transitions
2+
Feature: Budget pressure transitions and dimension interactions
3+
Memory budget protection tiers interact correctly under changing
4+
pressure levels: soft-limit pacing at 80 per cent, hard-cap abort
5+
at 100 per cent. The tightest aggregate dimension controls
6+
enforcement when per-connection and in-flight limits differ.
7+
8+
Scenario: Soft pressure escalates to connection termination
9+
Given a transition app configured as 200/2048/10/10
10+
When transition first frames for keys 1 to 15 each with body "aa" arrive
11+
Then the transition connection terminates with an error
12+
13+
Scenario: Recovery from soft limit after assembly completion
14+
Given a transition app configured as 200/2048/10/10
15+
When a transition first frame for key 20 with body "aaaaaaaa" arrives
16+
And a transition final continuation for key 20 sequence 1 with body "b" arrives
17+
Then transition payload "aaaaaaaab" is eventually received
18+
When a transition first frame for key 21 with body "cc" arrives
19+
And a transition final continuation for key 21 sequence 1 with body "d" arrives
20+
Then transition payload "ccd" is eventually received
21+
And no transition connection error is recorded
22+
23+
Scenario: Tightest aggregate dimension controls enforcement
24+
Given a transition app configured as 200/2048/20/10
25+
When transition first frames for keys 30 to 44 each with body "aa" arrive
26+
Then the transition connection terminates with an error

0 commit comments

Comments
 (0)