mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Tim Menninger <tmenninger@everpuredata.com>
To: Trond Myklebust <trondmy@kernel.org>, Anna Schumaker <anna@kernel.org>
Cc: Chuck Lever <cel@kernel.org>, Jeff Layton <jlayton@kernel.org>,
	NeilBrown <neil@brown.name>,
	Olga Kornievskaia <okorniev@redhat.com>,
	Dai Ngo <Dai.Ngo@oracle.com>, Tom Talpey <tom@talpey.com>,
	linux-nfs@vger.kernel.org, linux-kernel@vger.kernel.org,
	Eric Badger <ebadger@everpuredata.com>,
	Jon Curley <jcurley@everpuredata.com>
Subject: [PATCH 2/2] SUNRPC: let cancelled RPC/RDMA requests drain before release
Date: Thu,  5 Nov 2026 03:07:09 +0000	[thread overview]
Message-ID: <20261105030710.2652306-3-tmenninger@everpuredata.com> (raw)
In-Reply-To: <20261105030710.2652306-1-tmenninger@everpuredata.com>

During pNFS/flexfiles recovery, cancellation can release a posted
RPC/RDMA request before the server has finished accessing its Read or
Write chunks. xprt_rdma_free() can then invalidate advertised rkeys
while the peer still uses them. Resulting QP errors disrupt other RPCs
and can sustain a cancellation/reconnect loop after the data server
recovers. This was observed on Linux 6.8.0, 6.18.34, and 7.3-rc4.

Allow transports to defer exit of cancelled asynchronous tasks while
their requests can still complete normally. RPC/RDMA opts in while
registered MRs remain on the same live connection. Keep these requests
available for reply-driven cleanup, preserving the cancellation error
while wake callbacks process their transport status.

Use the existing request timeout and transport timer. After normal
no-reply timeout handling, finish cancellation instead of re-entering
draining. Suppress a requested conditional disconnect when the reply
completed before release. Synchronous and signalled tasks skip the
optional wait; remaining reclamation uses the existing release path.

Signed-off-by: Tim Menninger <tmenninger@everpuredata.com>
---
 include/linux/sunrpc/xprt.h     |   3 +
 include/trace/events/sunrpc.h   |   1 +
 net/sunrpc/sched.c              |  21 +++++-
 net/sunrpc/xprt.c               | 129 +++++++++++++++++++++++++++++++-
 net/sunrpc/xprtrdma/transport.c |  33 ++++++++
 5 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/include/linux/sunrpc/xprt.h b/include/linux/sunrpc/xprt.h
index ad8ecf8ccfb48..8b5b99ee34207 100644
--- a/include/linux/sunrpc/xprt.h
+++ b/include/linux/sunrpc/xprt.h
@@ -177,6 +177,7 @@ struct rpc_xprt_ops {
 	void		(*wait_for_reply_request)(struct rpc_task *task);
 	void		(*timer)(struct rpc_xprt *xprt, struct rpc_task *task);
 	void		(*release_request)(struct rpc_task *task);
+	bool		(*release_needs_drain)(struct rpc_rqst *req);
 	void		(*close)(struct rpc_xprt *xprt);
 	void		(*destroy)(struct rpc_xprt *xprt);
 	void		(*set_connect_timeout)(struct rpc_xprt *xprt,
@@ -395,6 +396,8 @@ bool			xprt_prepare_transmit(struct rpc_task *task);
 void			xprt_request_enqueue_transmit(struct rpc_task *task);
 int			xprt_request_enqueue_receive(struct rpc_task *task);
 void			xprt_request_wait_receive(struct rpc_task *task);
+bool			xprt_request_release_needs_wait(struct rpc_task *task);
+void			xprt_request_wait_for_drain(struct rpc_task *task);
 void			xprt_request_dequeue_xprt(struct rpc_task *task);
 bool			xprt_request_need_retransmit(struct rpc_task *task);
 void			xprt_transmit(struct rpc_task *task);
diff --git a/include/trace/events/sunrpc.h b/include/trace/events/sunrpc.h
index ff855197880de..263e8f6520ee3 100644
--- a/include/trace/events/sunrpc.h
+++ b/include/trace/events/sunrpc.h
@@ -410,6 +410,7 @@ DEFINE_RPC_RUNNING_EVENT(sync_wake);
 DEFINE_RPC_RUNNING_EVENT(complete);
 DEFINE_RPC_RUNNING_EVENT(timeout);
 DEFINE_RPC_RUNNING_EVENT(signalled);
+DEFINE_RPC_RUNNING_EVENT(exit_wait);
 DEFINE_RPC_RUNNING_EVENT(end);
 DEFINE_RPC_RUNNING_EVENT(call_done);
 
diff --git a/net/sunrpc/sched.c b/net/sunrpc/sched.c
index 81890327d81a9..f1806f530fab7 100644
--- a/net/sunrpc/sched.c
+++ b/net/sunrpc/sched.c
@@ -932,6 +932,8 @@ static void __rpc_execute(struct rpc_task *task)
 	int task_is_async = RPC_IS_ASYNC(task);
 	int status = 0;
 	unsigned long pflags = current->flags;
+	bool task_draining;
+	s64 error_state;
 
 	WARN_ON_ONCE(RPC_IS_QUEUED(task));
 	if (RPC_IS_QUEUED(task))
@@ -939,6 +941,7 @@ static void __rpc_execute(struct rpc_task *task)
 
 	for (;;) {
 		void (*do_action)(struct rpc_task *);
+		task_draining = false;
 
 		/*
 		 * Perform the next FSM step or a pending callback.
@@ -949,8 +952,13 @@ static void __rpc_execute(struct rpc_task *task)
 		/* Tasks with an RPC error status should exit */
 		if (do_action && do_action != rpc_exit_task &&
 		    (status = rpc_task_rpc_status(task)) != 0) {
-			task->tk_status = status;
-			do_action = rpc_exit_task;
+			if (xprt_request_release_needs_wait(task)) {
+				task_draining = true;
+				do_action = xprt_request_wait_for_drain;
+			} else {
+				task->tk_status = status;
+				do_action = rpc_exit_task;
+			}
 		}
 		/* Callbacks override all actions */
 		if (task->tk_callback) {
@@ -989,8 +997,13 @@ static void __rpc_execute(struct rpc_task *task)
 			spin_unlock(&queue->lock);
 			continue;
 		}
-		/* Wake up any task that has an exit status */
-		if (rpc_task_rpc_status(task) != 0) {
+		/* Wake up any task that has an exit status. A task that has
+		 * deliberately gone to sleep to drain its request is exempted,
+		 * unless the task was signalled (i.e. urgently terminated).
+		 */
+		error_state = rpc_task_read_error_state(task);
+		if ((error_state & RPC_TASK_ERR_STATE_SIGNALLED) ||
+		    ((s32)error_state != 0 && !task_draining)) {
 			rpc_wake_up_task_queue_locked(queue, task);
 			spin_unlock(&queue->lock);
 			continue;
diff --git a/net/sunrpc/xprt.c b/net/sunrpc/xprt.c
index 66eca8e4690b6..cf24cb52667da 100644
--- a/net/sunrpc/xprt.c
+++ b/net/sunrpc/xprt.c
@@ -1237,6 +1237,7 @@ static void xprt_timer(struct rpc_task *task)
 {
 	struct rpc_rqst *req = task->tk_rqstp;
 	struct rpc_xprt *xprt = req->rq_xprt;
+	int status;
 
 	if (task->tk_status != -ETIMEDOUT)
 		return;
@@ -1245,6 +1246,13 @@ static void xprt_timer(struct rpc_task *task)
 	if (!req->rq_reply_bytes_recvd) {
 		if (xprt->ops->timer)
 			xprt->ops->timer(xprt, task);
+
+		/* An already-cancelled task should not drain again */
+		if (xprt->ops->release_needs_drain) {
+			status = rpc_task_rpc_status(task);
+			if (status)
+				rpc_exit(task, status);
+		}
 	} else
 		task->tk_status = 0;
 }
@@ -1528,6 +1536,114 @@ xprt_request_dequeue_xprt(struct rpc_task *task)
 	}
 }
 
+/**
+ * xprt_request_release_needs_wait - check whether request release must wait
+ * @task: RPC task being terminated
+ *
+ * Returns true if the request is pinned or its transport resources must
+ * drain before release. Removes pending transmissions when safe to do so.
+ *
+ * Context: Called from RPC task execution context. Acquires and releases
+ *          xprt->queue_lock. The transport's ->release_needs_drain() must not
+ *          sleep.
+ */
+bool xprt_request_release_needs_wait(struct rpc_task *task)
+{
+	struct rpc_rqst *req = task->tk_rqstp;
+	struct rpc_xprt *xprt;
+	bool defer = false;
+
+	if (!RPC_IS_ASYNC(task) || RPC_SIGNALLED(task))
+		return false;
+
+	if (!req)
+		return false;
+	xprt = req->rq_xprt;
+	if (!xprt->ops->release_needs_drain)
+		return false;
+
+	/* Has not been transmitted yet, no need to defer */
+	if (!test_bit(RPC_TASK_NEED_RECV, &task->tk_runstate))
+		return false;
+
+	spin_lock(&xprt->queue_lock);
+	if (xprt_is_pinned_rqst(req))
+		defer = true;
+	else if (test_bit(RPC_TASK_NEED_XMIT, &task->tk_runstate))
+		xprt_request_dequeue_transmit_locked(task);
+	else
+		defer = xprt->ops->release_needs_drain(req);
+	spin_unlock(&xprt->queue_lock);
+
+	return defer;
+}
+
+/**
+ * xprt_request_wait_for_drain - wait until a failed request can be released
+ * @task: RPC task whose request may still be in flight
+ *
+ * Like xprt_request_wait_receive(), but for a task that has already
+ * been cancelled. The task keeps its request on the receive queue and
+ * sleeps on the pending queue until the reply arrives or the connection
+ * is lost. The wait is bounded by the request's retransmit timeout, at
+ * which point the transport's timer callback runs exactly as it would
+ * for an uncancelled request.
+ *
+ * Context: Called from RPC task execution context. May sleep while waiting
+ *          for a pinned request, with xprt->queue_lock released. Holds
+ *          queue_lock when checking request state and queuing the task.
+ */
+void xprt_request_wait_for_drain(struct rpc_task *task)
+{
+	struct rpc_rqst *req = task->tk_rqstp;
+	struct rpc_xprt *xprt = req->rq_xprt;
+
+	trace_rpc_task_exit_wait(task, task->tk_action);
+
+	/*
+	 * Drop any send ownership handed to this task before waiting.
+	 */
+	xprt_release_write(xprt, task);
+
+	/*
+	 * Signals stop optional reply draining, but do not interrupt an
+	 * in-progress wait for request pins to be released.
+	 */
+	spin_lock(&xprt->queue_lock);
+	while (xprt_is_pinned_rqst(req) && !RPC_SIGNALLED(task)) {
+		set_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
+		spin_unlock(&xprt->queue_lock);
+		xprt_wait_on_pinned_rqst(req);
+		spin_lock(&xprt->queue_lock);
+		clear_bit(RPC_TASK_MSG_PIN_WAIT, &task->tk_runstate);
+	}
+
+	if (xprt_is_pinned_rqst(req) || RPC_SIGNALLED(task))
+		goto out_unlock;
+
+	/*
+	 * The pinned operation may have completed or failed while we waited.
+	 * Recheck whether the request needs to drain or can be dequeued.
+	 */
+	if (test_bit(RPC_TASK_NEED_XMIT, &task->tk_runstate)) {
+		xprt_request_dequeue_transmit_locked(task);
+	} else if (test_bit(RPC_TASK_NEED_RECV, &task->tk_runstate) &&
+		   xprt->ops->release_needs_drain(req) &&
+		   !RPC_SIGNALLED(task)) {
+		rpc_sleep_on_timeout(&xprt->pending, task, xprt_timer,
+				     xprt_request_timeout(req));
+
+		if (RPC_SIGNALLED(task))
+			rpc_wake_up_queued_task(&xprt->pending, task);
+		else if (xprt_request_retransmit_after_disconnect(task))
+			rpc_wake_up_queued_task_set_status(&xprt->pending, task,
+							   -ENOTCONN);
+	}
+
+out_unlock:
+	spin_unlock(&xprt->queue_lock);
+}
+
 /**
  * xprt_request_prepare - prepare an encoded request for transport
  * @req: pointer to rpc_rqst
@@ -1609,11 +1725,13 @@ xprt_request_transmit(struct rpc_rqst *req, struct rpc_task *snd_task)
 	unsigned int connect_cookie;
 	int is_retrans = RPC_WAS_SENT(task);
 	int status;
+	s64 error_state;
 
 	if (test_bit(XPRT_CLOSE_WAIT, &xprt->state))
 		return -ENOTCONN;
 
 	if (!req->rq_bytes_sent) {
+		error_state = rpc_task_read_error_state(task);
 		if (xprt_request_data_received(task)) {
 			status = 0;
 			goto out_dequeue;
@@ -1623,10 +1741,14 @@ xprt_request_transmit(struct rpc_rqst *req, struct rpc_task *snd_task)
 			status = -EBADMSG;
 			goto out_dequeue;
 		}
-		if (RPC_SIGNALLED(task)) {
+		if (error_state & RPC_TASK_ERR_STATE_SIGNALLED) {
 			status = -ERESTARTSYS;
 			goto out_dequeue;
 		}
+		if (xprt->ops->release_needs_drain && (s32)error_state != 0) {
+			status = -ECANCELED;
+			goto out_dequeue;
+		}
 	}
 
 	/*
@@ -1695,7 +1817,7 @@ xprt_transmit(struct rpc_task *task)
 		xprt_pin_rqst(next);
 		spin_unlock(&xprt->queue_lock);
 		status = xprt_request_transmit(next, task);
-		if (status == -EBADMSG && next != req)
+		if ((status == -EBADMSG || status == -ECANCELED) && next != req)
 			status = 0;
 		spin_lock(&xprt->queue_lock);
 		xprt_unpin_rqst(next);
@@ -2061,7 +2183,8 @@ void xprt_release(struct rpc_task *task)
 	req->rq_disconnect_on_release = false;
 	spin_unlock(&xprt->queue_lock);
 
-	if (unlikely(disconnect && req->rq_ntrans > 0))
+	if (unlikely(disconnect && req->rq_ntrans > 0 &&
+		     !(xprt->ops->release_needs_drain && req->rq_reply_bytes_recvd)))
 		xprt_conditional_disconnect(xprt, req->rq_connect_cookie);
 
 	spin_lock(&xprt->transport_lock);
diff --git a/net/sunrpc/xprtrdma/transport.c b/net/sunrpc/xprtrdma/transport.c
index e38637c2ec787..ed521d9aa8953 100644
--- a/net/sunrpc/xprtrdma/transport.c
+++ b/net/sunrpc/xprtrdma/transport.c
@@ -434,6 +434,35 @@ xprt_rdma_timer(struct rpc_xprt *xprt, struct rpc_task *task)
 	xprt_force_disconnect(xprt);
 }
 
+/**
+ * xprt_rdma_release_needs_drain - decide whether a cancelled request must drain
+ * @rqst: transmitted RPC request whose task has been cancelled
+ *
+ * A Call posted on the current connection may have advertised chunks that the
+ * server is still reading or writing. Invalidating those MRs before the Reply
+ * arrives provokes a remote access error that tears down the QP. Keep a
+ * cancelled request available for reply-driven MR cleanup while its connection
+ * remains live. A false result leaves any remaining reclamation to the normal
+ * release path.
+ *
+ * Context: Called with xprt->queue_lock held and @rqst unpinned.
+ *          Must not sleep.
+ *
+ * Returns true if @rqst still has registered memory on the current connection.
+ */
+static bool
+xprt_rdma_release_needs_drain(struct rpc_rqst *rqst)
+{
+	struct rpc_xprt *xprt = rqst->rq_xprt;
+	struct rpcrdma_req *req = rpcr_to_rdmar(rqst);
+
+	if (!xprt_connected(xprt) ||
+	    rqst->rq_connect_cookie != READ_ONCE(xprt->connect_cookie))
+		return false;
+
+	return !list_empty(&req->rl_registered);
+}
+
 /**
  * xprt_rdma_set_connect_timeout - set timeouts for establishing a connection
  * @xprt: controlling transport instance
@@ -655,6 +684,9 @@ xprt_rdma_free(struct rpc_task *task)
 	struct rpc_rqst *rqst = task->tk_rqstp;
 	struct rpcrdma_req *req = rpcr_to_rdmar(rqst);
 
+	/* Graceful draining can be skipped or end before all MRs are reclaimed.
+	 * Complete any remaining cleanup through the normal release path.
+	 */
 	if (unlikely(!list_empty(&req->rl_registered))) {
 		trace_xprtrdma_mrs_zap(task);
 		frwr_unmap_sync(rpcx_to_rdmax(rqst->rq_xprt), req);
@@ -795,6 +827,7 @@ static const struct rpc_xprt_ops xprt_rdma_procs = {
 	.release_request	= xprt_release_rqst_cong,       /* ditto */
 	.wait_for_reply_request	= xprt_wait_for_reply_request_def, /* ditto */
 	.timer			= xprt_rdma_timer,
+	.release_needs_drain	= xprt_rdma_release_needs_drain,
 	.rpcbind		= rpcb_getport_async,	/* sunrpc/rpcb_clnt.c */
 	.set_port		= xprt_rdma_set_port,
 	.connect		= xprt_rdma_connect,
-- 
2.53.0


  parent reply	other threads:[~2026-11-05  3:07 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05  3:07 [PATCH 0/2] " Tim Menninger
2026-11-05  3:07 ` [PATCH 1/2] SUNRPC: maintain an rpc_task signalled flag Tim Menninger
2026-11-05  3:07 ` Tim Menninger [this message]
2026-11-05  4:44 ` [PATCH 0/2] SUNRPC: let cancelled RPC/RDMA requests drain before release Trond Myklebust
2026-11-05 14:15 ` Chuck Lever

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261105030710.2652306-3-tmenninger@everpuredata.com \
    --to=tmenninger@everpuredata.com \
    --cc=Dai.Ngo@oracle.com \
    --cc=anna@kernel.org \
    --cc=cel@kernel.org \
    --cc=ebadger@everpuredata.com \
    --cc=jcurley@everpuredata.com \
    --cc=jlayton@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neil@brown.name \
    --cc=okorniev@redhat.com \
    --cc=tom@talpey.com \
    --cc=trondmy@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®