From 0e093017cbc4484cb7473e929a7ea60f569dd870 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Jun 2025 00:17:41 +0100 Subject: [PATCH 1/4] Add fairness design for push queues --- docs/asynchronous-outbound-messaging-design.md | 18 ++++++++++++++++++ .../asynchronous-outbound-messaging-roadmap.md | 3 +++ docs/multi-layered-testing-strategy.md | 4 +++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/asynchronous-outbound-messaging-design.md b/docs/asynchronous-outbound-messaging-design.md index 2e558797..05156721 100644 --- a/docs/asynchronous-outbound-messaging-design.md +++ b/docs/asynchronous-outbound-messaging-design.md @@ -133,6 +133,24 @@ loop { } ``` +#### 3.2.1 Fairness for low-priority frames + +Continuous bursts of urgent messages can prevent the low-priority queue from +ever being drained. To mitigate this without removing the deterministic bias, +each `ConnectionActor` tracks how many high-priority frames have been processed +in a row. After a configurable threshold (`max_high_before_low`), the actor +checks `low_priority_push_rx.try_recv()` and, if a frame is present, processes +it and resets the counter. + +An optional time slice (for example 100 µs) can also be configured. When the +elapsed time handling high-priority frames exceeds this slice and the low queue +is not empty, the actor yields to a low-priority frame. Application builders +expose `with_fairness(usize)` to set the threshold, defaulting to 16. Setting it +to zero preserves the original strict ordering. + +This fairness mechanism ensures low-priority traffic continues to progress even +under sustained high-priority load. + ### 3.3 Connection Actor Overview ```mermaid diff --git a/docs/asynchronous-outbound-messaging-roadmap.md b/docs/asynchronous-outbound-messaging-roadmap.md index 6cb4ece0..aeaee2b9 100644 --- a/docs/asynchronous-outbound-messaging-roadmap.md +++ b/docs/asynchronous-outbound-messaging-roadmap.md @@ -13,6 +13,8 @@ design documents. - [x] **Connection actor** with a biased `select!` loop that polls for shutdown, high/low queues and response streams as described in [Design §3.2][design-write-loop]. +- [ ] **Fairness counter** to yield to the low-priority queue after a burst of + high-priority frames ([Design §3.2.1][design-fairness]). - [ ] **Internal protocol hooks** `before_send` and `on_command_end` invoked from the actor ([Design §4.3][design-hooks]). @@ -54,6 +56,7 @@ design documents. [design-dlq]: asynchronous-outbound-messaging-design.md#52-optional-dead-letter-queue-dlq-for-critical-messages [design-errors]: asynchronous-outbound-messaging-design.md#5-error-handling--resilience +[design-fairness]: asynchronous-outbound-messaging-design.md#321-fairness-for-low-priority-frames [design-hooks]: asynchronous-outbound-messaging-design.md#43-configuration-via-the-wireframeprotocol-trait [design-push-handle]: asynchronous-outbound-messaging-design.md#41-the-pushhandle [design-queues]: asynchronous-outbound-messaging-design.md#31-prioritised-message-queues diff --git a/docs/multi-layered-testing-strategy.md b/docs/multi-layered-testing-strategy.md index 0c1d66cf..372119e9 100644 --- a/docs/multi-layered-testing-strategy.md +++ b/docs/multi-layered-testing-strategy.md @@ -413,7 +413,9 @@ inputs. pushes, low-priority pushes, and multi-frame `Response::Stream`s for a single connection. The test asserts that the final output stream respects the strict priority order (`shutdown > high > low > stream`) and that no frames are ever -lost or reordered within their own channel. +lost or reordered within their own channel. When the fairness counter is +configured, sequences containing relentless high-priority pushes must still +observe periodic low-priority frames. **Measurable Objective:** The test suite must pass **1,000,000 generated test cases**, verifying frame ordering and completeness on every run. From d5675c824c6b19bb976089bc1fc0e7485e974ae3 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Jun 2025 01:56:37 +0100 Subject: [PATCH 2/4] Add fairness flow diagram --- .../asynchronous-outbound-messaging-design.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/asynchronous-outbound-messaging-design.md b/docs/asynchronous-outbound-messaging-design.md index 05156721..8925ed04 100644 --- a/docs/asynchronous-outbound-messaging-design.md +++ b/docs/asynchronous-outbound-messaging-design.md @@ -151,6 +151,27 @@ to zero preserves the original strict ordering. This fairness mechanism ensures low-priority traffic continues to progress even under sustained high-priority load. +The flow diagram below summarises the fairness logic. + +```mermaid +flowchart TD + A[Start select! loop] --> B{High-priority frame available?} + B -- Yes --> C[Process high-priority frame] + C --> D[Increment high_priority_counter] + D --> E{high_priority_counter >= max_high_before_low?} + E -- Yes --> F{Low-priority frame available?} + F -- Yes --> G[Process low-priority frame] + G --> H[Reset high_priority_counter] + F -- No --> I[Continue] + E -- No --> I + B -- No --> J{Low-priority frame available?} + J -- Yes --> K[Process low-priority frame] + J -- No --> I + I --> A + H --> A + K --> A +``` + ### 3.3 Connection Actor Overview ```mermaid From b1c30f135df2c165130d74465917df00b7812ae8 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Jun 2025 12:30:12 +0100 Subject: [PATCH 3/4] Document fairness counter --- docs/asynchronous-outbound-messaging-design.md | 17 +++++++++++++---- docs/multi-layered-testing-strategy.md | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/asynchronous-outbound-messaging-design.md b/docs/asynchronous-outbound-messaging-design.md index 8925ed04..d7e1234e 100644 --- a/docs/asynchronous-outbound-messaging-design.md +++ b/docs/asynchronous-outbound-messaging-design.md @@ -143,16 +143,25 @@ checks `low_priority_push_rx.try_recv()` and, if a frame is present, processes it and resets the counter. An optional time slice (for example 100 µs) can also be configured. When the -elapsed time handling high-priority frames exceeds this slice and the low queue -is not empty, the actor yields to a low-priority frame. Application builders -expose `with_fairness(usize)` to set the threshold, defaulting to 16. Setting it -to zero preserves the original strict ordering. +elapsed time spent handling high-priority frames exceeds this slice, and the low +queue is not empty, the actor yields to a low-priority frame. Application +builders expose `with_fairness(usize)` to set the threshold, defaulting to 16. +An additional `with_fairness_time_slice(Duration)` method allows tuning of the +time-based yield. Setting the counter to zero preserves the original strict +ordering. This fairness mechanism ensures low-priority traffic continues to progress even under sustained high-priority load. + + The flow diagram below summarises the fairness logic. +The diagram shows how the actor yields to the low-priority queue +after N high-priority frames. + + + ```mermaid flowchart TD A[Start select! loop] --> B{High-priority frame available?} diff --git a/docs/multi-layered-testing-strategy.md b/docs/multi-layered-testing-strategy.md index 372119e9..a02e7d08 100644 --- a/docs/multi-layered-testing-strategy.md +++ b/docs/multi-layered-testing-strategy.md @@ -414,7 +414,7 @@ pushes, low-priority pushes, and multi-frame `Response::Stream`s for a single connection. The test asserts that the final output stream respects the strict priority order (`shutdown > high > low > stream`) and that no frames are ever lost or reordered within their own channel. When the fairness counter is -configured, sequences containing relentless high-priority pushes must still +configured, sequences containing continuous high-priority pushes must still observe periodic low-priority frames. **Measurable Objective:** The test suite must pass **1,000,000 generated test From 4bd5a025c3f3c0a97e40450f3b00123f7670920e Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Jun 2025 19:21:31 +0100 Subject: [PATCH 4/4] Consolidate fairness config --- docs/asynchronous-outbound-messaging-design.md | 8 ++++---- docs/asynchronous-outbound-messaging-roadmap.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/asynchronous-outbound-messaging-design.md b/docs/asynchronous-outbound-messaging-design.md index d7e1234e..d83a84c3 100644 --- a/docs/asynchronous-outbound-messaging-design.md +++ b/docs/asynchronous-outbound-messaging-design.md @@ -145,10 +145,10 @@ it and resets the counter. An optional time slice (for example 100 µs) can also be configured. When the elapsed time spent handling high-priority frames exceeds this slice, and the low queue is not empty, the actor yields to a low-priority frame. Application -builders expose `with_fairness(usize)` to set the threshold, defaulting to 16. -An additional `with_fairness_time_slice(Duration)` method allows tuning of the -time-based yield. Setting the counter to zero preserves the original strict -ordering. +builders expose `with_fairness(FairnessConfig)` where `FairnessConfig` groups +the counter threshold and an optional `time_slice`. The counter defaults to 16 +while `time_slice` is disabled. Setting the counter to zero preserves the +original strict ordering. This fairness mechanism ensures low-priority traffic continues to progress even under sustained high-priority load. diff --git a/docs/asynchronous-outbound-messaging-roadmap.md b/docs/asynchronous-outbound-messaging-roadmap.md index aeaee2b9..4ac84a72 100644 --- a/docs/asynchronous-outbound-messaging-roadmap.md +++ b/docs/asynchronous-outbound-messaging-roadmap.md @@ -13,7 +13,7 @@ design documents. - [x] **Connection actor** with a biased `select!` loop that polls for shutdown, high/low queues and response streams as described in [Design §3.2][design-write-loop]. -- [ ] **Fairness counter** to yield to the low-priority queue after a burst of +- [ ] **Fairness counter** to yield to the low-priority queue after bursts of high-priority frames ([Design §3.2.1][design-fairness]). - [ ] **Internal protocol hooks** `before_send` and `on_command_end` invoked from the actor ([Design §4.3][design-hooks]).