* [PATCH] fd/ceph/mds_client: add per-request lock for completion
@ 2026-07-08 20:42 Max Kellermann
0 siblings, 0 replies; only message in thread
From: Max Kellermann @ 2026-07-08 20:42 UTC (permalink / raw)
To: idryomov, amarkuze, ceph-devel, linux-kernel; +Cc: Max Kellermann
ceph_mdsc_wait_request() took `ceph_mds_client.mutex` only to resolve
a request's terminal state after the wait returned: either a reply had
arrived and handle_reply() had set `r_reply`/`CEPH_MDS_R_GOT_RESULT`,
or there was an error and the waiter had to abort the request by
setting `r_err`/`CEPH_MDS_R_ABORTED` so a late reply is discarded. It
never touched any `ceph_mds_client` fields; it used the big mutex
purely as the serialization point against the matching section at the
tail of handle_reply(), and (indirectly) against __do_request() and
handle_forward().
`ceph_mds_client.mutex` is a coarse lock, also covering the request
tree, tid counters, the session table and the mdsmap. Making the
waiter take it means request completion contends with request
submission (ceph_mdsc_submit_request() holds it across
__register_request() and __do_request()) even though the two only
share a handful of per-request fields.
This patch adds a per-request spinlock that guards a request's
terminal state: `r_err`, `r_reply`, `CEPH_MDS_R_GOT_RESULT` and
`CEPH_MDS_R_ABORTED`.
Lock ordering is `ceph_mds_client.mutex`, `r_fill_mutex`,
`r_completion_lock`; the spinlock is always innermost and no sleeping
lock is taken while it is held.
The two BUG_ON()s in handle_forward()'s resend branch are removed.
They asserted that a request being resent had neither errored nor
completed, an invariant that held only because the client mutex
serialized the resend against the waiter's abort. Now that the waiter
aborts under `r_completion_lock` without the client mutex, a forward
can race an abort; this is harmless because __do_request() re-checks
the terminal state under `r_completion_lock` and unregisters the
request instead of resending it.
This weakens one invariant: previously, the abort path held
`ceph_mds_client.mutex`, so it was mutually exclusive with
__do_request() and a request could never be sent once
`CEPH_MDS_R_ABORTED` was set. With the abort now protected only by
`r_completion_lock`, the bit can be set concurrently with a
resend-path __do_request() (kick_requests(), __wake_requests(),
handle_forward()) after that function's one-time entry check, so an
aborted request may still be sent or queued. This is harmless:
`ABORTED` is re-checked at every consumer: handle_reply() discards the
reply, ceph_fill_trace() skips the dcache work that depends on locks
held by the (now returned) caller, and the next __do_request()
unregisters it. This matches the existing property that an
interrupted request may still execute on the MDS.
Signed-off-by: Max Kellermann <max.kellermann@ionos.com>
---
fs/ceph/locks.c | 21 ++++++----
fs/ceph/mds_client.c | 99 ++++++++++++++++++++++++++++++++++----------
fs/ceph/mds_client.h | 10 +++++
3 files changed, 102 insertions(+), 28 deletions(-)
diff --git a/fs/ceph/locks.c b/fs/ceph/locks.c
index 677221bd64e0..35cadcd7cb2a 100644
--- a/fs/ceph/locks.c
+++ b/fs/ceph/locks.c
@@ -177,25 +177,32 @@ static int ceph_lock_wait_for_completion(struct ceph_mds_client *mdsc,
doutc(cl, "request %llu was interrupted\n", req->r_tid);
+ /*
+ * mdsc->mutex guards the r_session check below. The terminal-state
+ * arbitration against a concurrent reply/forward handler is done under
+ * r_completion_lock (the same lock ceph_mdsc_wait_request() uses),
+ * since those handlers no longer serialize on mdsc->mutex.
+ * r_fill_mutex additionally fences ceph_fill_trace() /
+ * ceph_readdir_prepopulate(), which consult CEPH_MDS_R_ABORTED while
+ * relying on locks (dir mutex) held by our caller.
+ * Lock ordering: mdsc->mutex -> r_fill_mutex -> r_completion_lock.
+ */
mutex_lock(&mdsc->mutex);
+ mutex_lock(&req->r_fill_mutex);
+ spin_lock(&req->r_completion_lock);
if (test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags)) {
err = 0;
} else {
- /*
- * ensure we aren't running concurrently with
- * ceph_fill_trace or ceph_readdir_prepopulate, which
- * rely on locks (dir mutex) held by our caller.
- */
- mutex_lock(&req->r_fill_mutex);
req->r_err = err;
set_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
- mutex_unlock(&req->r_fill_mutex);
if (!req->r_session) {
// haven't sent the request
err = 0;
}
}
+ spin_unlock(&req->r_completion_lock);
+ mutex_unlock(&req->r_fill_mutex);
mutex_unlock(&mdsc->mutex);
if (!err)
return 0;
diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
index 853bf698b356..824d271e55c9 100644
--- a/fs/ceph/mds_client.c
+++ b/fs/ceph/mds_client.c
@@ -2722,6 +2722,7 @@ ceph_mdsc_create_request(struct ceph_mds_client *mdsc, int op, int mode)
return ERR_PTR(-ENOMEM);
mutex_init(&req->r_fill_mutex);
+ spin_lock_init(&req->r_completion_lock);
req->r_mdsc = mdsc;
req->r_started = jiffies;
req->r_start_latency = ktime_get();
@@ -3528,7 +3529,9 @@ static int __prepare_send_request(struct ceph_mds_session *session,
}
msg = create_request_message(session, req, drop_cap_releases);
if (IS_ERR(msg)) {
+ spin_lock(&req->r_completion_lock);
req->r_err = PTR_ERR(msg);
+ spin_unlock(&req->r_completion_lock);
return PTR_ERR(msg);
}
req->r_request = msg;
@@ -3586,9 +3589,34 @@ static void __do_request(struct ceph_mds_client *mdsc,
int mds = -1;
int err = 0;
bool random;
+ bool done, aborted;
- if (req->r_err || test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags)) {
- if (test_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags))
+ /*
+ * The request may already have reached its terminal state,
+ * either because a reply/forward arrived or because
+ * ceph_mdsc_wait_request() aborted it. Sample that state
+ * under r_completion_lock so we agree with those paths, which
+ * no longer all serialize on mdsc->mutex.
+ *
+ * This is only a point-in-time check. Since the abort path
+ * does not own mdsc->mutex, CEPH_MDS_R_ABORTED may be set
+ * concurrently after this snapshot while we go on to choose a
+ * MDS and send the request, so a request can be sent (or
+ * queued) after it has been aborted. That is harmless:
+ * ABORTED is re-checked at every consumer: handle_reply()
+ * discards the reply, ceph_fill_trace() skips the work that
+ * relies on locks held by the (now returned) caller, and the
+ * next __do_request() sees the flag and unregisters the
+ * request. ABORTED is an advisory "stop waiting" flag, not a
+ * send barrier.
+ */
+ spin_lock(&req->r_completion_lock);
+ done = req->r_err || test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags);
+ aborted = test_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
+ spin_unlock(&req->r_completion_lock);
+
+ if (done) {
+ if (aborted)
__unregister_request(mdsc, req);
return;
}
@@ -3742,7 +3770,9 @@ static void __do_request(struct ceph_mds_client *mdsc,
TASK_KILLABLE);
if (err) {
mutex_lock(&req->r_fill_mutex);
+ spin_lock(&req->r_completion_lock);
set_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
+ spin_unlock(&req->r_completion_lock);
mutex_unlock(&req->r_fill_mutex);
goto out_session;
}
@@ -3783,7 +3813,9 @@ static void __do_request(struct ceph_mds_client *mdsc,
finish:
if (err) {
doutc(cl, "early error %d\n", err);
+ spin_lock(&req->r_completion_lock);
req->r_err = err;
+ spin_unlock(&req->r_completion_lock);
complete_request(mdsc, req);
__unregister_request(mdsc, req);
}
@@ -3910,6 +3942,8 @@ int ceph_mdsc_wait_request(struct ceph_mds_client *mdsc,
ceph_mds_request_wait_callback_t wait_func)
{
struct ceph_client *cl = mdsc->fsc->client;
+ bool aborted = false;
+ bool got_result;
int err;
/* wait */
@@ -3928,32 +3962,47 @@ int ceph_mdsc_wait_request(struct ceph_mds_client *mdsc,
err = timeleft; /* killed */
}
doutc(cl, "do_request waited, got %d\n", err);
- mutex_lock(&mdsc->mutex);
-
- /* only abort if we didn't race with a real reply */
- if (test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags)) {
- err = le32_to_cpu(req->r_reply_info.head->result);
- } else if (err < 0) {
- doutc(cl, "aborted request %lld with %d\n", req->r_tid, err);
+ if (err < 0) {
/*
- * ensure we aren't running concurrently with
- * ceph_fill_trace or ceph_readdir_prepopulate, which
- * rely on locks (dir mutex) held by our caller.
+ * Timed out or killed. A reply may be getting published
+ * concurrently, so serialize against handle_reply() on
+ * r_completion_lock before deciding to abort. r_fill_mutex
+ * additionally fences ceph_fill_trace()/ceph_readdir_prepopulate(),
+ * which consult CEPH_MDS_R_ABORTED while relying on locks (dir
+ * mutex) held by our caller.
*/
mutex_lock(&req->r_fill_mutex);
- req->r_err = err;
- set_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
+ spin_lock(&req->r_completion_lock);
+ got_result = test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags);
+ if (!got_result) {
+ req->r_err = err;
+ set_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
+ aborted = true;
+ }
+ spin_unlock(&req->r_completion_lock);
mutex_unlock(&req->r_fill_mutex);
+ } else {
+ /*
+ * The wait completed, so complete_all() has run and the terminal
+ * state stored by handle_reply()/__do_request() before it is
+ * visible without locking.
+ */
+ got_result = test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags);
+ }
+ if (got_result)
+ err = le32_to_cpu(req->r_reply_info.head->result);
+ else if (!aborted)
+ err = req->r_err;
+
+ if (aborted) {
+ doutc(cl, "aborted request %lld with %d\n", req->r_tid, err);
if (req->r_parent &&
(req->r_op & CEPH_MDS_OP_WRITE))
ceph_invalidate_dir_request(req);
- } else {
- err = req->r_err;
}
- mutex_unlock(&mdsc->mutex);
return err;
}
@@ -4186,7 +4235,7 @@ static void handle_reply(struct ceph_mds_session *session, struct ceph_msg *msg)
ceph_unreserve_caps(mdsc, &req->r_caps_reservation);
}
out_err:
- mutex_lock(&mdsc->mutex);
+ spin_lock(&req->r_completion_lock);
if (!test_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags)) {
if (err) {
req->r_err = err;
@@ -4197,7 +4246,7 @@ static void handle_reply(struct ceph_mds_session *session, struct ceph_msg *msg)
} else {
doutc(cl, "reply arrived after request %lld was aborted\n", tid);
}
- mutex_unlock(&mdsc->mutex);
+ spin_unlock(&req->r_completion_lock);
mutex_unlock(&session->s_mutex);
@@ -4259,8 +4308,10 @@ static void handle_forward(struct ceph_mds_client *mdsc,
* 8 bits.
*/
mutex_lock(&req->r_fill_mutex);
+ spin_lock(&req->r_completion_lock);
req->r_err = -EMULTIHOP;
set_bit(CEPH_MDS_R_ABORTED, &req->r_req_flags);
+ spin_unlock(&req->r_completion_lock);
mutex_unlock(&req->r_fill_mutex);
aborted = true;
pr_warn_ratelimited_client(cl, "forward tid %llu seq overflow\n",
@@ -4268,8 +4319,14 @@ static void handle_forward(struct ceph_mds_client *mdsc,
} else {
/* resend. forward race not possible; mds would drop */
doutc(cl, "forward tid %llu to mds%d (we resend)\n", tid, next_mds);
- BUG_ON(req->r_err);
- BUG_ON(test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags));
+ /*
+ * The request had not reached its terminal state when sampled
+ * above. It may now race an abort from
+ * ceph_mdsc_wait_request(), which no longer serializes on
+ * mdsc->mutex; __do_request() re-checks the terminal state
+ * under r_completion_lock and unregisters instead of resending
+ * if that happened, so this is safe.
+ */
req->r_attempts = 0;
req->r_num_fwd = fwd_seq;
req->r_resend_mds = next_mds;
diff --git a/fs/ceph/mds_client.h b/fs/ceph/mds_client.h
index 731d6ad04956..449b5e28b2de 100644
--- a/fs/ceph/mds_client.h
+++ b/fs/ceph/mds_client.h
@@ -394,6 +394,16 @@ struct ceph_mds_request {
struct ceph_msg *r_reply;
struct ceph_mds_reply_info_parsed r_reply_info;
int r_err;
+
+ /*
+ * Serializes the request's terminal state (r_err, r_reply,
+ * CEPH_MDS_R_GOT_RESULT and CEPH_MDS_R_ABORTED) between the reply and
+ * forward handlers, __do_request() and ceph_mdsc_wait_request().
+ *
+ * Lock ordering: mdsc->mutex -> r_fill_mutex -> r_completion_lock.
+ */
+ spinlock_t r_completion_lock;
+
u32 r_readdir_offset;
struct page *r_locked_page;
--
2.47.3
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-07-08 20:42 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-08 20:42 [PATCH] fd/ceph/mds_client: add per-request lock for completion Max Kellermann
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox