[PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd

Shardul Bankar <[email protected]> Sun, 26 Jul 2026 11:25:39 +0530
Newsgroups dev.linux.lists.mptcp
Message-ID <20260726-mptcp_penalise_send-v1-1-84485e0e995b@mpiricsoftware.com>
Issue #345: a poorly-performing but usable subflow (high latency, loss,
bufferbloat) can soak up connection resources and cause head-of-line
blocking of the aggregate stream. Give the default packet scheduler a way
to send less than such a subflow's full congestion window.

Once a subflow has been picked for transmission, flag it for penalisation
when:
- its smoothed delivery rate (avg_pacing_rate) is below half that of the
  fastest path, keying on rate, not RTT, so a slow-but-high-throughput
  path is left alone;
- the fastest path is cwnd-limited (saturated), so shifting load off the
  slow path is worthwhile;
- the subflow is in TCP_CA_Open, so its cwnd is not already being reduced
  by loss recovery;
- it has not been penalised in the last RTT.

The reduction halves tcp_snd_cwnd (floor 2) and ssthresh if cwnd
is past it. It is applied in the push path under the subflow socket lock,
which protects snd_cwnd (the scheduler runs under the msk lock). The
congestion control grows the window back, ACK-clocked; that regrowth is
the built-in probe, so no explicit MPTCP-side probing is needed.

Co-developed-by: Matthieu Baerts (NGI0) <[email protected]>
Signed-off-by: Shardul Bankar <[email protected]>
---
 net/mptcp/protocol.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---
 net/mptcp/protocol.h |  2 ++
 2 files changed, 67 insertions(+), 3 deletions(-)

diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 954e20bb27de..d31bcb9ad894 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -1556,6 +1556,44 @@ bool mptcp_subflow_active(struct mptcp_subflow_context *subflow)
 #define SSK_MODE_BACKUP	1
 #define SSK_MODE_MAX	2
 
+/* Penalise a subflow whose delivery (pacing) rate is below the fraction
+ * 1 / MPTCP_PENALISE_RATE_RATIO of the fastest path's rate. Keying on rate,
+ * not RTT, throttles only a path whose throughput contribution is small
+ * relative to the head-of-line cost it imposes, and leaves a merely
+ * higher-latency but high-throughput path alone.
+ */
+#define MPTCP_PENALISE_RATE_RATIO	2
+
+/* Rate-limit the penalty to at most once per subflow RTT, so the congestion
+ * control can grow the window back between reductions.
+ */
+static bool mptcp_penalise_throttle_ok(struct mptcp_subflow_context *subflow)
+{
+	struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
+	u32 rtt = usecs_to_jiffies(tcp_sk(ssk)->srtt_us >> 3);
+
+	return tcp_jiffies32 - subflow->last_penalise >= max_t(u32, rtt, 1);
+}
+
+/* Halve the congestion window (and ssthresh, if cwnd is past it) of a subflow
+ * the scheduler flagged. Runs in the push path under the subflow socket lock,
+ * which protects snd_cwnd. The congestion control grows the window back,
+ * ACK-clocked, and that regrowth is the built-in probe, so no explicit probing
+ * is needed.
+ */
+static void mptcp_penalise_cwnd(struct sock *ssk)
+{
+	struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk);
+	struct tcp_sock *tp = tcp_sk(ssk);
+	u32 cwnd = tcp_snd_cwnd(tp);
+
+	subflow->penalise = false;
+	subflow->last_penalise = tcp_jiffies32;
+	tcp_snd_cwnd_set(tp, max_t(u32, cwnd >> 1, 2));
+	if (cwnd >= tp->snd_ssthresh)
+		tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh >> 1, 2);
+}
+
 /* implement the mptcp packet scheduler;
  * returns the subflow that will transmit the next DSS
  * additionally updates the rtx timeout
@@ -1565,9 +1603,9 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 	struct subflow_send_info send_info[SSK_MODE_MAX];
 	struct mptcp_subflow_context *subflow;
 	struct sock *sk = (struct sock *)msk;
-	u32 pace, burst, wmem;
+	u32 pace, burst, wmem, max_pace = 0;
 	int i, nr_active = 0;
-	struct sock *ssk;
+	struct sock *ssk, *fastest = NULL;
 	u64 linger_time;
 	long tout = 0;
 
@@ -1596,6 +1634,14 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 				continue;
 		}
 
+		/* track the fastest path by delivery rate; the penalty below
+		 * throttles paths that are slow relative to it.
+		 */
+		if (pace > max_pace) {
+			max_pace = pace;
+			fastest = ssk;
+		}
+
 		linger_time = div_u64((u64)READ_ONCE(ssk->sk_wmem_queued) << 32, pace);
 		if (linger_time < send_info[backup].linger_time) {
 			send_info[backup].ssk = ssk;
@@ -1623,12 +1669,25 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 	if (!ssk || !sk_stream_memory_free(ssk))
 		return NULL;
 
+	/* Flag the chosen subflow for cwnd halving (applied in the push path)
+	 * when its delivery rate is a small fraction of the fastest path's and
+	 * that fast path is saturated (cwnd-limited), so moving load off the
+	 * slow path is worthwhile. Only penalise a path in TCP_CA_Open, one
+	 * whose cwnd is not already being shrunk by loss recovery, and at most
+	 * once per RTT.
+	 */
+	subflow = mptcp_subflow_ctx(ssk);
+	subflow->penalise = fastest && ssk != fastest &&
+			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
+			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
+			    tcp_is_cwnd_limited(fastest) &&
+			    mptcp_penalise_throttle_ok(subflow);
+
 	burst = min(MPTCP_SEND_BURST_SIZE, mptcp_wnd_end(msk) - msk->snd_nxt);
 	wmem = READ_ONCE(ssk->sk_wmem_queued);
 	if (!burst)
 		return ssk;
 
-	subflow = mptcp_subflow_ctx(ssk);
 	subflow->avg_pacing_rate = div_u64((u64)subflow->avg_pacing_rate * wmem +
 					   READ_ONCE(ssk->sk_pacing_rate) * burst,
 					   burst + wmem);
@@ -1685,6 +1744,9 @@ static int __subflow_push_pending(struct sock *sk, struct sock *ssk,
 	struct mptcp_data_frag *dfrag;
 	int len, copied = 0, err = 0;
 
+	if (mptcp_subflow_ctx(ssk)->penalise)
+		mptcp_penalise_cwnd(ssk);
+
 	while ((dfrag = mptcp_send_head(sk))) {
 		info->sent = dfrag->already_sent;
 		info->limit = dfrag->data_len;
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index da40c6f3705f..2bf801292563 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -587,6 +587,7 @@ struct mptcp_subflow_context {
 		__unused : 8;
 	bool	data_avail;
 	bool	scheduled;
+	bool	penalise;	    /* scheduler flagged this subflow for cwnd halving */
 	bool	pm_listener;	    /* a listener managed by the kernel PM? */
 	bool	fully_established;  /* path validated */
 	u32	lent_mem_frag;
@@ -606,6 +607,7 @@ struct mptcp_subflow_context {
 	u8	stale_count;
 
 	u32	subflow_id;
+	u32	last_penalise;	    /* tcp_jiffies32 of the last cwnd penalty */
 
 	long	delegated_status;
 	unsigned long	fail_tout;

-- 
2.34.1