* [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply
@ 2026-10-23 2:10 Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 1/4] nfsd: share the grant slot record between delegations and layouts Daejun Park via B4 Relay
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-23 2:10 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey
Cc: Christoph Hellwig, Benjamin Coddington, Trond Myklebust,
Anna Schumaker, linux-nfs, linux-kernel, Daejun Park
With [3], a client that writes to a file through the metadata server
while it does I/O through a layout of the same file gets
NFS4ERR_RECALLCONFLICT for LAYOUTGET after LAYOUTGET. One Linux block
layout client writing 1 MiB blocks with O_DIRECT through its layout and
512 bytes at the middle of a page, which it sends through the metadata
server, to the same file did 51, 54 and 12 of its 1 MiB writes in 30
seconds in three runs with patches 1 to 3. Patch 4 lets such a LAYOUTGET
succeed with an immediate recall instead when the lease break comes from
the same client (240, 249 and 244). That recall can overtake the
LAYOUTGET reply: a client that does not know the layout stateid yet
answers NFS4ERR_NOMATCHING_LAYOUT, nfsd drops the layout, and the Linux
client loses the writes it then sends through it, which read back as
zeroes although fsync() succeeded. Without patches 2 and 3, patch 4 lost
75 of 600, 79 of 589 and 87 of 600 blocks. The same race exists today:
on nfsd-testing, a recall overtook a LAYOUTGET reply in one of three 30
second runs of the test below without its delay, and that block read
back as zeroes. A block layout client can take long to process a reply,
as pnfs_layout_process() may need a GETDEVICEINFO and a blkmapd upcall
for a new device while it holds the slot of the LAYOUTGET. Patch 2 has
the details.
Patch 1 lets layouts share the record of the slot that carried a grant
with the delegation code [1]. Patch 2 resends the recall once, as the
delegation code does, when the client answers NFS4ERR_NOMATCHING_LAYOUT
before the slots of the LAYOUTGETs have retired. Patch 3 names those
LAYOUTGETs as referring calls (RFC 8881 sections 2.10.6.3 and
12.5.5.2.1), so that a client that honors them holds the recall back;
the Linux client does with [2]. The resend comes first because the
referring call alone does not help the Linux client without [2] once it
needs more than half a second: it then answers NFS4ERR_NOMATCHING_LAYOUT
anyway, and the half second it waits makes each such recall longer.
A pNFS client writes 1 MiB blocks with O_DIRECT to a new file for 60
seconds next to an NFSv4.0 client writing to another range of it, with a
test-only delay of one second in the pNFS client before
pnfs_layout_process(). Blocks that read back as zeroes / blocks written
through a layout, and the NFSv4.0 client, two runs each:
nfsd-testing 13/57, 14/58 600 in 14.6, 15.0 s
+ patches 1 and 2 0/30, 0/41 600 in 21.2, 20.9 s
+ patches 1 to 3 0/20, 0/28 600 in 35.8, 25.2 s
+ patches 1 to 4 0/23, 0/24 62, 9 in 60 s
+ patches 1 to 4, [2] on client 0/22, 0/24 227, 15 in 60 s
The NFSv4.0 client stops after 60 seconds. Every block lost was one
whose reply the recall overtook. Patch 4 did not act in this test
(nfsd_layout_break_pending did not fire), as the lease breaks come from
the other client; rows 3 and 4 differ in how many recalls met an
unprocessed reply: 8 and 4 against 23 and 24, which in row 4 is every
LAYOUTGET. The cost is on the writer that breaks the lease: a recall
that meets a LAYOUTGET reply the client has not processed within half a
second lasts at least two and a half seconds, and WRITEs through the
metadata server get NFS4ERR_DELAY meanwhile. The NFSv4.0 client needs
about 15 seconds plus, for each such recall, nothing on nfsd-testing,
two seconds with patches 1 and 2, and two and a half from patch 3 on;
such recalls numbered 13 and 14, 3 and 3, 8 and 4, and 23 and 24 in the
rows above without [2]. Each one also keeps the NFSv4.0 client breaking
the lease for longer, so the count feeds on itself and varies between
runs of the same code; two runs do not rank the stages. With
fs.lease-break-time at two seconds or less, such a recall outlasts the
lease break and the client is fenced.
With a delay of four seconds, longer than the resend covers, a client
without [2] loses every block whose reply the recall overtakes: 8 of 15
in each of two runs with patches 1 and 2, where the NFSv4.0 client was
done after 31.5 and 31.2 seconds and broke no lease after that, and 15
of 15 in each of two runs with patches 1 to 3 and with patches 1 to 4,
where it was still writing after 60 seconds. With [2], none (11 and 7
blocks).
QEMU guests (nvmet-tcp target, nfsd on XFS, NFSv4.2 block layout clients
and an NFSv4.0 client), nfsd-testing 764ffa45f3f2 with KASAN and
lockdep; no KASAN or lockdep report from nfsd or the clients. SCSI and
flexfiles layouts were not tested. Without the delay, no recall overtook
a reply with the series in three runs of 30 seconds. The runs of patch 4
around the deadline of a lease break are from v1, on nfsd-testing
b4e06bef3884; patch 4 is the same code in v2. One boot of the metadata
server VM, on an image of v1, reported "BUG: KASAN: wild-memory-access
in do_raw_spin_lock" 0.5 seconds in, during identify_cpu() and before
any NFS traffic, as one boot did in the v1 tests. pynfs BLOCK3 fails
with NFS4ERR_BADXDR with and without the series; local recall tests (not
posted) pass with the series, and two of them fail on nfsd-testing.
The series applies on nfsd-testing, which has [1] and [3]. Patch 2 needs
patches of [3] that it names, patch 3 should not go before v7.1, and
patch 4 needs patches 2 and 3.
[1] https://lore.kernel.org/r/20260802-nfsd-deleg-destroy-badhandle-v1-0-323aa7196055@kernel.org
[2] https://lore.kernel.org/r/20261020-nfs-referring-call-delay-v1-1-0be8f0608f7c@samsung.com
[3] https://lore.kernel.org/r/20261015-nfsd-layout-recall-v3-0-ba3269f8ca22@samsung.com
---
Changes in v2:
- 2/4: keep a record of each LAYOUTGET whose slot has not retired, up
to the latest eight, instead of the last one only (Sashiko). A
LAYOUTGET that cannot allocate its record fails with NFS4ERR_DELAY
before any blocks are allocated.
- 3/4: name every recorded LAYOUTGET as a referring call (Sashiko), and
make room for eight in NFS4_enc_cb_layout_sz, which had room for one.
In a pynfs test (not posted) with two LAYOUTGETs on two slots, v1
named the second only and v2 names both, in each of five runs.
- 3/4: nfsd41_cb_referring_call() and nfsd41_cb_referring_grant() report
an allocation failure. A recall that could not name a LAYOUTGET does
not trust NFS4ERR_NOMATCHING_LAYOUT; it is sent again every two
seconds until the client answers otherwise or the recall ends as the
polls do (Sashiko). With a test-only switch that fails the allocation
and the four second delay, v1 lost 8 of 15 blocks in each of two runs
and v2 none. A client that answers NFS4ERR_NOMATCHING_LAYOUT to such a
recall is fenced as one that does not answer, after fs.lease-break-time
or two lease periods, and every LAYOUTGET for the file, its own
included, fails until then. The end of the polls and the fencing move
into helpers.
- 2/4, 4/4: the answer to the resend is still trusted when the slot of
the LAYOUTGET has not retired (Sashiko on 2/4 and 4/4). Once the
recall names the LAYOUTGET, waiting for it is the client's part: RFC
8881 section 12.5.5.2.1 says the client MUST wait for the referenced
calls before it processes the recall, and the Linux client does with
[2]. Waiting for the slot to retire instead waits for the client's
next request on that slot. An idle Linux client sends only its lease
renewal, two thirds of a lease period after its last request (60
seconds by default) and on its lowest free slot, and does not use a
higher slot again until it is busy. Until then every LAYOUTGET for the
file fails, and a recall that comes from a lease break has the client
fenced after fs.lease-break-time (45 seconds by default) although it
has no layout. An earlier version that polled until the slot retired,
behind an idle reader that answered NFS4ERR_NOMATCHING_LAYOUT 1112
times, ended the write 23 seconds after the reader's first read,
against 13 seconds when resending once, on that older base; the writer
starts 8 seconds after the reader, and the poll ended only because the
reader read again 20 seconds after its first read. On nfsd-testing the
write ends 8.8 seconds after the first read without the series and
11.8 with patches 1 and 2: the resend costs the writer 3 seconds, also
with [2], as nfsd cannot tell a client that waits for referring calls
from one that does not. The comment in nfsd4_cb_layout_done() now says
what is left at risk.
- Rebased on nfsd-testing 764ffa45f3f2 and tested there.
- Link to v1: https://lore.kernel.org/r/20261021-nfsd-layout-refcall-v1-0-797096b92377@samsung.com
---
Daejun Park (4):
nfsd: share the grant slot record between delegations and layouts
nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT
nfsd: send a referring call with CB_LAYOUTRECALL
nfsd: do not fail a LAYOUTGET on a lease break by the same client
fs/nfsd/nfs4callback.c | 13 +-
fs/nfsd/nfs4layouts.c | 325 ++++++++++++++++++++++++++++++++++++++++---------
fs/nfsd/nfs4proc.c | 12 ++
fs/nfsd/nfs4state.c | 118 ++++++++++++------
fs/nfsd/pnfs.h | 3 +
fs/nfsd/state.h | 65 ++++++++--
fs/nfsd/trace.h | 2 +
fs/nfsd/xdr4cb.h | 6 +-
8 files changed, 439 insertions(+), 105 deletions(-)
---
base-commit: 764ffa45f3f2189cd86ec6d46d248fc0440507b6
change-id: 20261021-nfsd-layout-refcall-60b4f9d6944d
Best regards,
--
Daejun Park <daejun7.park@samsung.com>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/4] nfsd: share the grant slot record between delegations and layouts
2026-10-23 2:10 [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply Daejun Park via B4 Relay
@ 2026-10-23 2:10 ` Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 2/4] nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT Daejun Park via B4 Relay
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-23 2:10 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey
Cc: Christoph Hellwig, Benjamin Coddington, Trond Myklebust,
Anna Schumaker, linux-nfs, linux-kernel, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
The delegation code remembers the forward-channel slot that carried the
request that granted a delegation (dl_recall_grant), tests whether the
client has retired that slot since (nfsd4_recall_grant_slot_retired()),
and names the request as a referring call in the CB_SEQUENCE of a
recall. The next two patches need the same for layouts.
Make the record a type of its own, struct nfsd4_grant_slot, and move the
code that fills it in, the slot test and the code that adds the
referring call into nfsd41_record_grant_slot(),
nfsd41_grant_slot_retired() and nfsd41_cb_referring_grant(), named for
the session machinery they depend on. The flag that was part of the
delegation's record stays with the delegation, as
dl_recall_retired_at_send.
No change in behavior.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/nfsd/nfs4state.c | 115 ++++++++++++++++++++++++++++++++++++----------------
fs/nfsd/state.h | 24 +++++++----
2 files changed, 96 insertions(+), 43 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index f2b52ba901..735eaa4b7f 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1291,7 +1291,7 @@ __alloc_init_deleg(struct nfs4_client *clp, struct nfs4_file *fp,
dp->dl_recalled = false;
dp->dl_recall_rejected = false;
dp->dl_recall_grant.valid = false;
- dp->dl_recall_grant.retired_at_send = false;
+ dp->dl_recall_retired_at_send = false;
get_nfs4_file(fp);
dp->dl_stid.sc_file = fp;
nfsd4_init_cb(&dp->dl_recall, dp->dl_stid.sc_client,
@@ -6251,37 +6251,49 @@ bool nfsd_wait_for_delegreturn(struct svc_rqst *rqstp, struct inode *inode)
* gen_sessionid() composes a sessionid from the client's clientid and a
* sequence counter, so the sequence alone identifies the granting session.
*/
-static void nfsd4_recall_grant_sessionid(const struct nfs4_delegation *dp,
- struct nfsd4_sessionid *sid)
+static void nfsd41_grant_slot_sessionid(struct nfs4_client *clp,
+ const struct nfsd4_grant_slot *grant,
+ struct nfsd4_sessionid *sid)
{
- sid->clientid = dp->dl_stid.sc_client->cl_clientid;
- sid->sequence = dp->dl_recall_grant.sessionid_seq;
+ sid->clientid = clp->cl_clientid;
+ sid->sequence = grant->sessionid_seq;
sid->reserved = 0;
}
-static bool nfsd4_recall_grant_slot_retired(struct nfs4_delegation *dp)
+/**
+ * nfsd41_grant_slot_retired - has the client seen the reply to a grant?
+ * @clp: client that sent the granting request
+ * @grant: forward-channel slot that carried the request
+ *
+ * Context: Takes nn->client_lock.
+ *
+ * Return: true if the client has gone on to use the slot for a later
+ * request, or has given the slot up, so that it must have received the
+ * reply. False if that cannot be shown, and if @grant is not valid.
+ */
+bool nfsd41_grant_slot_retired(struct nfs4_client *clp,
+ const struct nfsd4_grant_slot *grant)
{
- struct nfs4_client *clp = dp->dl_stid.sc_client;
struct nfsd_net *nn = net_generic(clp->net, nfsd_net_id);
struct nfsd4_session *ses;
struct nfsd4_sessionid sid;
bool retired = false;
void *entry;
- if (!dp->dl_recall_grant.valid)
+ if (!grant->valid)
return false;
- nfsd4_recall_grant_sessionid(dp, &sid);
+ nfsd41_grant_slot_sessionid(clp, grant, &sid);
/*
- * A missing session does not prove the client saw the grant: a
+ * A missing session does not prove the client saw the reply: a
* DESTROY_SESSION unhashes its own session before the reply to
* that compound is encoded.
*/
spin_lock(&nn->client_lock);
ses = __find_in_sessionid_hashtbl((struct nfs4_sessionid *)&sid,
clp->net);
- entry = ses ? xa_load(&ses->se_slots, dp->dl_recall_grant.slotid) : NULL;
+ entry = ses ? xa_load(&ses->se_slots, grant->slotid) : NULL;
if (xa_is_value(entry)) {
/*
* A slot is freed only once the client has acknowledged
@@ -6294,18 +6306,49 @@ static bool nfsd4_recall_grant_slot_retired(struct nfs4_delegation *dp)
/*
* A reactivated slot was freed and rebuilt, so the same
- * acknowledgment applies. The seqid test errs toward
- * revoking: a rebuilt slot restarting at seqid 1 matches
- * an old grant.
+ * acknowledgment applies. The seqid test errs toward "not
+ * retired", the side on which the callers do not take the
+ * client's answer at once: a rebuilt slot restarting at
+ * seqid 1 matches an old grant.
*/
retired = (slot->sl_flags & NFSD4_SLOT_REUSED) ||
((slot->sl_flags & NFSD4_SLOT_INITIALIZED) &&
- slot->sl_seqid != dp->dl_recall_grant.seqid);
+ slot->sl_seqid != grant->seqid);
}
spin_unlock(&nn->client_lock);
return retired;
}
+/**
+ * nfsd41_cb_referring_grant - name a granting request as referring call
+ * @cb: callback that refers to state the request created
+ * @clp: client that sent the request
+ * @grant: forward-channel slot that carried the request
+ *
+ * Does nothing if @grant is not valid.
+ *
+ * Context: May sleep. Caller serializes access to @cb, as for
+ * nfsd41_cb_referring_call().
+ */
+void nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
+ struct nfs4_client *clp,
+ const struct nfsd4_grant_slot *grant)
+{
+ struct nfsd4_sessionid sid;
+
+ if (!grant->valid)
+ return;
+ nfsd41_grant_slot_sessionid(clp, grant, &sid);
+ nfsd41_cb_referring_call(cb, (struct nfs4_sessionid *)&sid,
+ grant->slotid, grant->seqid);
+}
+
+static bool nfsd4_recall_grant_slot_retired(struct nfs4_delegation *dp)
+{
+ return nfsd41_grant_slot_retired(dp->dl_stid.sc_client,
+ &dp->dl_recall_grant);
+}
+
/*
* ->prepare does not run on every send: nfsd4_run_cb_work() skips it
* on a requeue, and a retry via rpc_restart_call_prepare() re-enters
@@ -6315,9 +6358,9 @@ static bool nfsd4_recall_grant_slot_retired(struct nfs4_delegation *dp)
*/
static void nfsd4_refresh_recall_grant(struct nfs4_delegation *dp)
{
- dp->dl_recall_grant.retired_at_send =
+ dp->dl_recall_retired_at_send =
nfsd4_recall_grant_slot_retired(dp);
- if (dp->dl_recall_grant.retired_at_send)
+ if (dp->dl_recall_retired_at_send)
nfsd41_cb_destroy_referring_call_list(&dp->dl_recall);
}
@@ -6345,15 +6388,9 @@ static bool nfsd4_cb_recall_prepare(struct nfsd4_callback *cb)
nfsd4_refresh_recall_grant(dp);
- if (dp->dl_recall_grant.valid && !dp->dl_recall_grant.retired_at_send) {
- struct nfsd4_sessionid sid;
-
- nfsd4_recall_grant_sessionid(dp, &sid);
- nfsd41_cb_referring_call(&dp->dl_recall,
- (struct nfs4_sessionid *)&sid,
- dp->dl_recall_grant.slotid,
- dp->dl_recall_grant.seqid);
- }
+ if (!dp->dl_recall_retired_at_send)
+ nfsd41_cb_referring_grant(&dp->dl_recall, dp->dl_stid.sc_client,
+ &dp->dl_recall_grant);
return true;
}
@@ -6405,7 +6442,7 @@ static int nfsd4_cb_recall_done(struct nfsd4_callback *cb,
* the grant. Trust the rejection only if the slot had
* retired when this recall was sent.
*/
- if (dp->dl_recall_grant.retired_at_send) {
+ if (dp->dl_recall_retired_at_send) {
nfsd4_deleg_recall_rejected(dp);
return 1;
}
@@ -7014,19 +7051,25 @@ static bool nfsd4_want_read_deleg(const struct nfsd4_open *open)
OPEN4_SHARE_ACCESS_WANT_READ_DELEG;
}
-static void
-nfs4_delegation_record_grant_slot(struct nfs4_delegation *dp,
- const struct nfsd4_compound_state *cstate)
+/**
+ * nfsd41_record_grant_slot - remember the slot that carries a request
+ * @grant: record to fill in
+ * @cstate: compound state of the request
+ *
+ * Leaves @grant alone for a request without a session (NFSv4.0).
+ */
+void nfsd41_record_grant_slot(struct nfsd4_grant_slot *grant,
+ const struct nfsd4_compound_state *cstate)
{
const struct nfsd4_sessionid *sid;
if (!cstate->session)
return;
sid = (struct nfsd4_sessionid *)cstate->session->se_sessionid.data;
- dp->dl_recall_grant.sessionid_seq = sid->sequence;
- dp->dl_recall_grant.slotid = cstate->slot->sl_index;
- dp->dl_recall_grant.seqid = cstate->slot->sl_seqid;
- dp->dl_recall_grant.valid = true;
+ grant->sessionid_seq = sid->sequence;
+ grant->slotid = cstate->slot->sl_index;
+ grant->seqid = cstate->slot->sl_seqid;
+ grant->valid = true;
}
static struct nfs4_delegation *
@@ -7135,7 +7178,7 @@ nfs4_set_delegation(struct nfsd4_open *open,
* visible to lease breakers. A conflicting open can drive
* CB_RECALL to completion from that point on.
*/
- nfs4_delegation_record_grant_slot(dp, cstate);
+ nfsd41_record_grant_slot(&dp->dl_recall_grant, cstate);
if (stp->st_stid.sc_export)
dp->dl_stid.sc_export = exp_get(stp->st_stid.sc_export);
@@ -10771,7 +10814,7 @@ nfsd_get_dir_deleg(struct nfsd4_compound_state *cstate,
dp = alloc_init_dir_deleg(clp, fp);
if (!dp)
goto out_delegees;
- nfs4_delegation_record_grant_slot(dp, cstate);
+ nfsd41_record_grant_slot(&dp->dl_recall_grant, cstate);
if (cstate->current_fh.fh_export)
dp->dl_stid.sc_export =
exp_get(cstate->current_fh.fh_export);
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 5f43668486..2c00b334c3 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -276,6 +276,14 @@ struct nfsd4_cb_notify {
struct nfsd4_callback ncn_cb; // notify4 callback
};
+/* Forward-channel slot that carried a request which created state */
+struct nfsd4_grant_slot {
+ u32 sessionid_seq;
+ u32 slotid;
+ u32 seqid;
+ bool valid;
+};
+
/*
* Represents a delegation stateid. The nfs4_client holds references to these
* and they are put when it is being destroyed or when the delegation is
@@ -315,13 +323,8 @@ struct nfs4_delegation {
bool dl_setattr;
/* Forward-channel slot that carried the granting request */
- struct {
- u32 sessionid_seq;
- u32 slotid;
- u32 seqid;
- bool valid;
- bool retired_at_send;
- } dl_recall_grant;
+ struct nfsd4_grant_slot dl_recall_grant;
+ bool dl_recall_retired_at_send;
union {
/* for CB_GETATTR */
@@ -949,6 +952,13 @@ extern void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
struct nfs4_sessionid *sessionid,
u32 slotid, u32 seqno);
extern void nfsd41_cb_destroy_referring_call_list(struct nfsd4_callback *cb);
+extern void nfsd41_record_grant_slot(struct nfsd4_grant_slot *grant,
+ const struct nfsd4_compound_state *cstate);
+extern bool nfsd41_grant_slot_retired(struct nfs4_client *clp,
+ const struct nfsd4_grant_slot *grant);
+extern void nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
+ struct nfs4_client *clp,
+ const struct nfsd4_grant_slot *grant);
extern void nfsd4_init_cb(struct nfsd4_callback *cb, struct nfs4_client *clp,
const struct nfsd4_callback_ops *ops, enum nfsd4_cb_op op);
extern bool nfsd4_run_cb(struct nfsd4_callback *cb);
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 2/4] nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT
2026-10-23 2:10 [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 1/4] nfsd: share the grant slot record between delegations and layouts Daejun Park via B4 Relay
@ 2026-10-23 2:10 ` Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 3/4] nfsd: send a referring call with CB_LAYOUTRECALL Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 4/4] nfsd: do not fail a LAYOUTGET on a lease break by the same client Daejun Park via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-23 2:10 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey
Cc: Christoph Hellwig, Benjamin Coddington, Trond Myklebust,
Anna Schumaker, linux-nfs, linux-kernel, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
When the lease of a layout is broken right after nfsd has handed the
layout out, the CB_LAYOUTRECALL can reach the client before the reply to
the LAYOUTGET. A client that does not know the layout stateid yet
answers NFS4ERR_NOMATCHING_LAYOUT, nfsd4_cb_layout_done() takes that as
the end of the recall and drops the layouts, and the client then gets
the reply and writes through a layout that the server no longer has.
The Linux client loses those writes. Its next LAYOUTGET with that layout
stateid gets NFS4ERR_BAD_STATEID, and it calls
pnfs_mark_layout_stateid_invalid(), which clears NFS_INO_LAYOUTCOMMIT
and the layout segments; the block layout driver drops the extents it
has written through the layout with the layout header. No LAYOUTCOMMIT
is sent for them, they stay unwritten extents on the server, they read
back as zeroes, and fsync() on the client succeeds.
Since "nfsd: fail a LAYOUTGET in progress when the layout lease is
broken", a lease break that meets the LAYOUTGET before it has added its
layout fails it, so the recall can only overtake the reply to a
LAYOUTGET that has added its layout. In three traced runs of the test
below without a delay on the client, a recall overtook a reply once, and
the client lost that block; with a test-only delay of one second it lost
every block whose reply the recall overtook.
The delegation code has the same problem with NFS4ERR_BAD_STATEID [1].
It trusts the answer if the slot that carried the grant had retired when
the recall was sent, and otherwise sends the recall once more after two
seconds. Do the same for layouts. The layouts of a layout stateid can
come from several LAYOUTGETs on several slots, so the layout stateid
keeps a record of each LAYOUTGET that added a layout or merged into
one. nfsd4_layoutget() allocates the record before
->proc_layoutget() allocates any blocks, and fails with NFS4ERR_DELAY
if it cannot, so that no layout goes out without one. Once the layout
has been added, nfsd4_record_layout_grant() puts the record on the list
while ls_mutex, which nfsd4_cb_layout_prepare() takes as well, is still
held. Records whose slots have retired are dropped there and in
->prepare, which notes whether any is left. When the client answers
NFS4ERR_NOMATCHING_LAYOUT while layouts are left, a record was left at
the send and the client has not answered NFS4_OK to this recall, ->done
sends the recall once more after two seconds. The Linux client frees
the slot of a LAYOUTGET only after pnfs_layout_process() has taken the
layout in.
Only the first LAYOUTGET of a layout stateid can meet a client that
does not know it, as every later one presents the layout stateid. A
client that has sent a later one tells a reply that arrives late by its
seqid (RFC 8881 section 12.5.5.2.1.2). So the list keeps the latest
NFSD4_LAYOUT_MAX_GRANTS (eight) records and drops the oldest one past
that. Records of a destroyed session, which never show as retired, leave
the same way.
The resend repeats the request with its seqid, as the polls do, although
RFC 8881 section 12.5.3 has a new seqid for each request: a client that
did not take the request in expects that seqid next, whether or not it
has processed the LAYOUTGET reply since, and the Linux client answers a
seqid that skips one with NFS4ERR_DELAY unless the recall names
referring calls that are complete, which this patch does not send.
A client that took the request in answers the resend with
NFS4ERR_OLD_STATEID: when the Linux client has the layout stateid but no
layout segment that the recall matches, initiate_file_draining() takes
in the seqid of the recall before it answers NFS4ERR_NOMATCHING_LAYOUT.
"nfsd: keep polling a layout recall answered with NFS4ERR_OLD_STATEID"
has the polls carry a new seqid then, which the client answers afresh. A
LAYOUTRETURN during the delay that returns the last layout does not stop
the resend, but the answer to it then ends the recall without fencing
the client, as "nfsd: do not fence a client for a layout recall with no
layouts left" has it. The resend is traced as nfsd_layout_recall_resend.
The answer to the resend is trusted, where the delegation code trusts it
only if the slot has retired by then and otherwise leaves the delegation
to the laundromat, which revokes it. The counterpart for a layout is to
poll until the slot retires, which an idle Linux client does only with
its lease renewal, two thirds of a lease period after its last request,
and only on its lowest free slot. Every LAYOUTGET for the file fails
meanwhile, and a recall that comes from a lease break has the client
fenced after fs.lease-break-time, also when it really has no layout.
The resend covers a client that takes the reply in within the two
seconds before it; a client that takes longer still loses the data. The
next patch lets a client that honors referring calls hold the recall
back instead.
The resend costs the writer that broke the lease. A pNFS block layout
client writes 1 MiB blocks with O_DIRECT to a new file for 60 seconds
while an NFSv4.0 client writes to another range of it, so that the
writes of the NFSv4.0 client keep breaking the layout lease. With a
test-only delay of one second in the pNFS client before
pnfs_layout_process(), 13 of 57 and 14 of 58 blocks written through a
layout read back as zeroes in two runs, each one a block whose
LAYOUTGET reply the recall overtook. With this patch none did (30 and
41 blocks), although the recall overtook the reply 3 times in each run.
The NFSv4.0 client wrote all its 600 blocks in 14.6 and 15.0 seconds
without this patch and in 21.2 and 20.9 seconds with it: each recall
that overtook a reply lasted two seconds longer, and the WRITEs of the
NFSv4.0 client got NFS4ERR_DELAY meanwhile. With fs.lease-break-time at
two seconds or less, a resent recall can outlast the lease break and
have the client fenced, also one that really has no layout, where the
recall used to end at its first answer.
The writer pays also when the client that holds the layout is idle: in a
test where a client read a file, kept it open and did no further I/O
while another client wrote to it, the write ended 11.8 seconds after the
first read with this patch and 8.8 seconds after it without; the writer
starts 8 seconds after the reader starts. The idle client answered
NFS4ERR_NOMATCHING_LAYOUT to the recall and to the resend. That stays so
with the referring call of the next patch: a client that processed the
reply long ago finds the referring call complete and answers
NFS4ERR_NOMATCHING_LAYOUT again (11.9 seconds with the whole series),
and nfsd cannot tell a client that waits for referring calls from one
that does not.
This needs the previous patch, and with it [1], and from "nfsd: layout
recall and lease break fixes" the patches named above: without
"nfsd: keep polling a layout recall answered with NFS4ERR_OLD_STATEID",
the NFS4ERR_OLD_STATEID answer to the resend has the client fenced.
Link: https://lore.kernel.org/r/20260802-nfsd-deleg-destroy-badhandle-v1-8-323aa7196055@kernel.org # [1]
Link: https://lore.kernel.org/r/20261015-nfsd-layout-recall-v3-0-ba3269f8ca22@samsung.com
Fixes: c5c707f96fc9 ("nfsd: implement pNFS layout recalls")
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/nfsd/nfs4layouts.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++++-
fs/nfsd/nfs4proc.c | 12 ++++++
fs/nfsd/pnfs.h | 3 ++
fs/nfsd/state.h | 20 ++++++++++
fs/nfsd/trace.h | 1 +
5 files changed, 141 insertions(+), 2 deletions(-)
diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c
index 679da5ba96..fdce9ddeba 100644
--- a/fs/nfsd/nfs4layouts.c
+++ b/fs/nfsd/nfs4layouts.c
@@ -238,6 +238,7 @@ nfsd4_free_layout_stateid(struct nfs4_stid *stid)
struct nfs4_layout_stateid *ls = layoutstateid(stid);
struct nfs4_client *clp = ls->ls_stid.sc_client;
struct nfs4_file *fp = ls->ls_stid.sc_file;
+ struct nfsd4_layout_grant *lg, *next;
trace_nfsd_layoutstate_free(&ls->ls_stid.sc_stateid);
@@ -254,6 +255,8 @@ nfsd4_free_layout_stateid(struct nfs4_stid *stid)
if (ls->ls_recalled)
atomic_dec(&ls->ls_stid.sc_file->fi_lo_recalls);
+ list_for_each_entry_safe(lg, next, &ls->ls_grants, lg_list)
+ kfree(lg);
kmem_cache_free(nfs4_layout_stateid_cache, ls);
}
@@ -310,6 +313,7 @@ nfsd4_alloc_layout_stateid(struct nfsd4_compound_state *cstate,
INIT_LIST_HEAD(&ls->ls_perfile);
spin_lock_init(&ls->ls_lock);
INIT_LIST_HEAD(&ls->ls_layouts);
+ INIT_LIST_HEAD(&ls->ls_grants);
mutex_init(&ls->ls_mutex);
mutex_init(&ls->ls_fence_mutex);
ls->ls_layout_type = layout_type;
@@ -592,6 +596,66 @@ nfsd4_insert_layout(struct nfsd4_layoutget *lgp, struct nfs4_layout_stateid *ls)
return nfserr;
}
+/* Forget the LAYOUTGETs whose replies the client has seen. */
+static void
+nfsd4_prune_layout_grants(struct nfs4_layout_stateid *ls)
+{
+ struct nfs4_client *clp = ls->ls_stid.sc_client;
+ struct nfsd4_layout_grant *lg, *next;
+
+ lockdep_assert_held(&ls->ls_mutex);
+ list_for_each_entry_safe(lg, next, &ls->ls_grants, lg_list) {
+ if (nfsd41_grant_slot_retired(clp, &lg->lg_slot)) {
+ list_del(&lg->lg_list);
+ kfree(lg);
+ }
+ }
+}
+
+/**
+ * nfsd4_record_layout_grant - remember a LAYOUTGET that added a layout
+ * @ls: layout stateid that the LAYOUTGET added a layout to or merged into
+ * @lg: record for the LAYOUTGET, which @ls takes over
+ * @cstate: compound state of the LAYOUTGET
+ *
+ * RFC 8881 section 2.10.6.3 has a recall carry every such request until
+ * its slot retires. Keeping the latest NFSD4_LAYOUT_MAX_GRANTS instead
+ * bounds the record and the CB_LAYOUTRECALL request. Every LAYOUTGET
+ * after the first one of a layout stateid presents that layout stateid,
+ * so a layout stateid with more than one record is known to the client,
+ * which tells a reply that arrives late by its seqid (RFC 8881 section
+ * 12.5.5.2.1.2); the Linux client drops such a reply
+ * (pnfs_layout_stateid_blocked()). Past NFSD4_LAYOUT_MAX_GRANTS, the
+ * oldest record goes; that is also how records of a destroyed session,
+ * which never show as retired, leave.
+ *
+ * Context: Caller holds ls_mutex, which nfsd4_cb_layout_prepare() takes
+ * as well, so that a recall of the new layout finds the LAYOUTGET
+ * recorded. Takes nn->client_lock.
+ */
+void
+nfsd4_record_layout_grant(struct nfs4_layout_stateid *ls,
+ struct nfsd4_layout_grant *lg,
+ const struct nfsd4_compound_state *cstate)
+{
+ struct nfsd4_layout_grant *old;
+
+ nfsd4_prune_layout_grants(ls);
+ if (list_count_nodes(&ls->ls_grants) >= NFSD4_LAYOUT_MAX_GRANTS) {
+ old = list_first_entry(&ls->ls_grants,
+ struct nfsd4_layout_grant, lg_list);
+ list_del(&old->lg_list);
+ kfree(old);
+ }
+ lg->lg_slot.valid = false;
+ nfsd41_record_grant_slot(&lg->lg_slot, cstate);
+ if (!lg->lg_slot.valid) {
+ kfree(lg);
+ return;
+ }
+ list_add_tail(&lg->lg_list, &ls->ls_grants);
+}
+
static void
nfsd4_free_layouts(struct list_head *reaplist)
{
@@ -883,8 +947,16 @@ nfsd4_cb_layout_prepare(struct nfsd4_callback *cb)
mutex_lock(&ls->ls_mutex);
nfs4_inc_and_copy_stateid(&ls->ls_recall_sid, &ls->ls_stid);
+ /*
+ * nfsd4_layoutget() records each LAYOUTGET that adds a layout
+ * under ls_mutex. With none left whose slot has not retired, there
+ * is no reply to wait for.
+ */
+ nfsd4_prune_layout_grants(ls);
+ ls->ls_recall_retired_at_send = list_empty(&ls->ls_grants);
mutex_unlock(&ls->ls_mutex);
ls->ls_recall_accepted = false;
+ ls->ls_recall_resent = false;
return true;
}
@@ -915,8 +987,10 @@ nfsd4_cb_layout_done(struct nfsd4_callback *cb, struct rpc_task *task)
* The poll reuses the recall's seqid, against RFC 8881
* section 12.5.3, so a client returning its layouts answers
* NFS4ERR_OLD_STATEID too. A client that has not taken the
- * recall up is past that seqid, e.g. after a LAYOUTRETURN,
- * and gets a new one. ls_mutex could block rpciod, so the bump
+ * recall up is past that seqid and gets a new one: after a
+ * LAYOUTRETURN, or when the resend below repeats the seqid of a
+ * recall that the Linux client took in although it matched no
+ * layout segment. ls_mutex could block rpciod, so the bump
* relies on sc_lock.
*/
if (!ls->ls_recall_accepted) {
@@ -994,6 +1068,35 @@ nfsd4_cb_layout_done(struct nfsd4_callback *cb, struct rpc_task *task)
}
return 1;
case -NFS4ERR_NOMATCHING_LAYOUT:
+ /*
+ * The recall can reach the client before the reply to a
+ * LAYOUTGET that added a layout, and a client that has not
+ * processed that reply answers this way too. As the
+ * delegation code does for NFS4ERR_BAD_STATEID, trust the
+ * answer if the slots of the LAYOUTGETs had retired when the
+ * recall was sent, and send the recall once more after a
+ * delay otherwise. Unlike the delegation code, trust the
+ * answer to that resend: not trusting it would end in fencing
+ * the client, and an idle client leaves its slot unretired
+ * also when it has no layout. A client that has not
+ * processed the reply by the resend still has its layout
+ * dropped. A client that has answered NFS4_OK to this recall
+ * knew the layout stateid, so its answer to a poll is trusted
+ * at once.
+ *
+ * The resend repeats the request with its seqid, as the polls
+ * do, although RFC 8881 section 12.5.3 has a new one for each
+ * request: a client that did not take the request in expects
+ * that seqid next, and a client that did answers
+ * NFS4ERR_OLD_STATEID, which the polls handle.
+ */
+ if (!list_empty(&ls->ls_layouts) && !ls->ls_recall_accepted &&
+ !ls->ls_recall_retired_at_send && !ls->ls_recall_resent) {
+ ls->ls_recall_resent = true;
+ trace_nfsd_layout_recall_resend(&ls->ls_recall_sid);
+ rpc_delay(task, 2 * HZ);
+ return 0;
+ }
trace_nfsd_layout_recall_done(&ls->ls_stid.sc_stateid);
task->tk_status = 0;
return 1;
diff --git a/fs/nfsd/nfs4proc.c b/fs/nfsd/nfs4proc.c
index a50f9dcb3a..c4c451dc0a 100644
--- a/fs/nfsd/nfs4proc.c
+++ b/fs/nfsd/nfs4proc.c
@@ -2904,6 +2904,7 @@ nfsd4_layoutget(struct svc_rqst *rqstp,
struct svc_fh *current_fh = &cstate->current_fh;
const struct nfsd4_layout_ops *ops;
struct nfs4_layout_stateid *ls;
+ struct nfsd4_layout_grant *lg = NULL;
__be32 nfserr;
int accmode = NFSD_MAY_READ_IF_EXEC | NFSD_MAY_OWNER_OVERRIDE;
@@ -2962,16 +2963,27 @@ nfsd4_layoutget(struct svc_rqst *rqstp,
if (atomic_read(&ls->ls_stid.sc_file->fi_lo_recalls))
goto out_put_stid;
+ /* Allocate the record first, so that every layout handed out has one */
+ nfserr = nfserr_jukebox;
+ lg = kmalloc_obj(*lg);
+ if (!lg)
+ goto out_put_stid;
+
nfserr = ops->proc_layoutget(rqstp, d_inode(current_fh->fh_dentry),
current_fh, lgp);
if (nfserr)
goto out_put_stid;
nfserr = nfsd4_insert_layout(lgp, ls);
+ if (!nfserr) {
+ nfsd4_record_layout_grant(ls, lg, cstate);
+ lg = NULL;
+ }
out_put_stid:
mutex_unlock(&ls->ls_mutex);
nfs4_put_stid(&ls->ls_stid);
+ kfree(lg);
out:
return nfserr;
}
diff --git a/fs/nfsd/pnfs.h b/fs/nfsd/pnfs.h
index e5f2e11c88..b2fcc28bf2 100644
--- a/fs/nfsd/pnfs.h
+++ b/fs/nfsd/pnfs.h
@@ -61,6 +61,9 @@ __be32 nfsd4_preprocess_layout_stateid(struct svc_rqst *rqstp,
bool create, u32 layout_type, struct nfs4_layout_stateid **lsp);
__be32 nfsd4_insert_layout(struct nfsd4_layoutget *lgp,
struct nfs4_layout_stateid *ls);
+void nfsd4_record_layout_grant(struct nfs4_layout_stateid *ls,
+ struct nfsd4_layout_grant *lg,
+ const struct nfsd4_compound_state *cstate);
__be32 nfsd4_return_file_layouts(struct svc_rqst *rqstp,
struct nfsd4_compound_state *cstate,
struct nfsd4_layoutreturn *lrp);
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 2c00b334c3..b3338cf171 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -848,6 +848,15 @@ static inline struct nfs4_ol_stateid *openlockstateid(struct nfs4_stid *s)
struct nfsd4_fence_script;
+/* A LAYOUTGET that added a layout, until its slot retires */
+struct nfsd4_layout_grant {
+ struct list_head lg_list;
+ struct nfsd4_grant_slot lg_slot;
+};
+
+/* The most LAYOUTGETs that a layout stateid keeps records of */
+#define NFSD4_LAYOUT_MAX_GRANTS 8
+
struct nfs4_layout_stateid {
struct nfs4_stid ls_stid;
struct list_head ls_perclnt;
@@ -863,6 +872,17 @@ struct nfs4_layout_stateid {
bool ls_recall_accepted;
bool ls_recalled;
struct mutex ls_mutex;
+ /*
+ * The latest NFSD4_LAYOUT_MAX_GRANTS LAYOUTGETs that added a
+ * layout to this layout stateid or merged into one, and whose
+ * slots had not retired when last looked at. Under ls_mutex.
+ * Until a slot retires, the client may not have processed the reply
+ * to the LAYOUTGET it carried.
+ */
+ struct list_head ls_grants;
+ /* Recall state for ->done, reset in ->prepare */
+ bool ls_recall_retired_at_send;
+ bool ls_recall_resent;
struct delayed_work ls_fence_work;
struct mutex ls_fence_mutex; /* serializes fence shutdown */
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index 951af645d1..a2c21d491a 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -830,6 +830,7 @@ DEFINE_STATEID_EVENT(layout_return_lookup_fail);
DEFINE_STATEID_EVENT(layout_recall);
DEFINE_STATEID_EVENT(layout_recall_empty);
DEFINE_STATEID_EVENT(layout_recall_done);
+DEFINE_STATEID_EVENT(layout_recall_resend);
DEFINE_STATEID_EVENT(layout_recall_new_seqid);
DEFINE_STATEID_EVENT(layout_recall_fail);
DEFINE_STATEID_EVENT(layout_recall_release);
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 3/4] nfsd: send a referring call with CB_LAYOUTRECALL
2026-10-23 2:10 [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 1/4] nfsd: share the grant slot record between delegations and layouts Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 2/4] nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT Daejun Park via B4 Relay
@ 2026-10-23 2:10 ` Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 4/4] nfsd: do not fail a LAYOUTGET on a lease break by the same client Daejun Park via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-23 2:10 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey
Cc: Christoph Hellwig, Benjamin Coddington, Trond Myklebust,
Anna Schumaker, linux-nfs, linux-kernel, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
The previous patch resends a layout recall once when the client answers
NFS4ERR_NOMATCHING_LAYOUT before it can have processed the reply to a
LAYOUTGET, which covers a client that takes the reply in within two
seconds. RFC 8881 section 2.10.6.3 has the server name the requests that
created the state in the referring call list of the callback instead,
so that the client can hold the callback back until it has processed
the reply, and section 12.5.5.2.1, which is about this race, lets the
server name the LAYOUTGET in a CB_LAYOUTRECALL. Commit 4f3c8d8c9e10
("NFSD: Implement CB_SEQUENCE referring call lists") added the lists,
noting that layout recalls need them, and "NFSD: Send referring calls
with CB_RECALL" [1] uses them for delegations.
Do the same for layouts: nfsd4_cb_layout_prepare() names each LAYOUTGET
that the previous patch has on record as a referring call, which are
those whose slots the client had not retired when the recall was sent.
The resend keeps the list without testing the slots again: a client
that has not processed a reply yet holds the recall back, and one that
has finds the call complete. The Linux client holds a callback back
with "NFSv4.1: Return NFS4ERR_DELAY when a referring call wait times
out" [2]. Without that, it waits half a second for the LAYOUTGET, then
answers as before, and the resend still covers it when it takes the
reply in within about three seconds: half a second of waiting, the two
second delay, and half a second again.
NFS4_enc_cb_layout_sz, from which the RPC client sizes the request
buffer, has room for one referring call. Each record can come from a
session of its own, so make room for NFSD4_LAYOUT_MAX_GRANTS referring
calls, each in a list of its own.
nfsd41_cb_referring_call() leaves a referring call out when it cannot
allocate memory for it. Have it and nfsd41_cb_referring_grant() say so.
A client cannot hold back a recall that does not name the LAYOUTGET, so
for such a recall ->done does not trust NFS4ERR_NOMATCHING_LAYOUT while
a record was left at the send. It sends the recall again every two
seconds until the client answers otherwise, the last layout is
returned, the fence worker of the lease break has run, or two lease
periods have passed since the first answer as for the polls, and then
fences the client if layouts are left. The test for the end of the
polls and the fencing move into helpers for that.
A client that holds the recall back answers CB_SEQUENCE with
NFS4ERR_DELAY, which nfsd4_cb_sequence_done() retries every two seconds
without calling ->done, so the limit of two lease periods in
nfsd4_cb_layout_done() does not apply: the recall lasts until the client
has processed the reply, and every LAYOUTGET for the file fails
meanwhile. A lease break still has the client fenced after
fs.lease-break-time.
This needs the referring call lists, which are in v6.16, and [1], which
is queued for v7.4. It should not go to kernels without
commit f52792f484ba ("NFSD: Enforce timeout on layout recall and
integrate lease manager fencing"), which is in v7.1: a client that holds
the recall back with NFS4ERR_DELAY while it takes the layout in can need
a GETDEVICEINFO for that, and before v7.1 a lease break that holds every
nfsd thread does not give up, so that GETDEVICEINFO would never be
served. From v7.1 on, that case stalls the server for the client until
fs.lease-break-time and then has the client fenced, where the client
would have lost the data before; with "nfsd: answer NFS4ERR_DELAY
instead of waiting for a layout recall", which is on nfsd-testing, a
WRITE or SETATTR no longer waits for a layout recall, which leaves that
case to a LAYOUTGET that completes between that patch's check and the
recall in XFS, and to the kthread of an asynchronous COPY.
A pynfs test (not posted) has one client take two layouts of one layout
stateid with LAYOUTGETs on two slots and send nothing more on either,
and another client cause a recall: the CB_SEQUENCE names both
LAYOUTGETs in each of five runs.
With the test of the previous patch and its one second delay, this patch
lost no block (20 and 28 blocks, the recall overtaking the reply 8 and 4
times). With [2] on the client and the next patch as well, which does
not act in this test as the lease breaks come from the other client, no
recall was processed before the client had taken the LAYOUTGET reply in
(22 and 24 blocks). With a delay of four seconds, longer than the resend
covers, a client without [2] loses every block whose reply the recall
overtakes, with this patch as with the previous one: 15 of 15 in each of
two runs here, 8 of 15 in each with the previous patch alone, where each
such recall is about a second shorter and the NFSv4.0 client was done
after 31.5 and 31.2 seconds, so later blocks met no recall. With [2] and
the whole series, none (11 and 7 blocks). For a client without [2], this
patch saves the two second resend when the client processes the reply
within half a second, and the data when it does so in two to about three
seconds; it adds half a second to each such recall when the client takes
half a second to two seconds, as in the test above, and a second when it
takes longer than about three seconds.
The cost is on the writer that breaks the lease. A recall that names a
LAYOUTGET the client has not processed within half a second now lasts at
least two and a half seconds: half a second of waiting on the client,
then the two second delay before nfsd sends the callback again, after
NFS4ERR_DELAY in CB_SEQUENCE with [2] or as the resend without it.
WRITEs through the metadata server get NFS4ERR_DELAY meanwhile. With the
one second delay, the NFSv4.0 client wrote all its 600 blocks in 35.8
and 25.2 seconds with this patch, against 14.6 and 15.0 seconds without
the series and 21.2 and 20.9 seconds with the previous patch alone. It
needs about 15 seconds plus two and a half seconds for each recall that
meets an unprocessed reply, two with the previous patch alone; there
were 8 and 4 such recalls here and 3 and 3 with the previous patch. How
many there are varies between runs of the same code: with the whole
series, which behaves the same in this test, there were 23 and 24, and
the NFSv4.0 client wrote 62 and 9 blocks in a minute.
With a test-only switch that makes every referring call fail and the
four second delay in a client without [2], the client lost none of its
15 blocks in each of two runs, although every recall overtook the reply;
the previous patch alone, which trusts the answer to the resend, lost 8
of 15 in each. A failed allocation costs the writer instead: the NFSv4.0
client wrote 18 and 14 blocks in a minute. In the test of the previous
patch, where the reading client reads the file again 20 seconds after
its first read, the switch kept the recall going for two lease periods
although that client answered NFS4ERR_NOMATCHING_LAYOUT 89 times: the
write ended 203.9 seconds after the first read and the second read 200.1
seconds after it, as every LAYOUTGET for the file failed meanwhile, and
the recall ended in the fence path although the client held no layout.
Link: https://lore.kernel.org/r/20260802-nfsd-deleg-destroy-badhandle-v1-9-323aa7196055@kernel.org # [1]
Link: https://lore.kernel.org/r/20261020-nfs-referring-call-delay-v1-1-0be8f0608f7c@samsung.com # [2]
Fixes: c5c707f96fc9 ("nfsd: implement pNFS layout recalls")
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/nfsd/nfs4callback.c | 13 ++--
fs/nfsd/nfs4layouts.c | 163 ++++++++++++++++++++++++++++++++-----------------
fs/nfsd/nfs4state.c | 11 ++--
fs/nfsd/state.h | 16 +++--
fs/nfsd/xdr4cb.h | 6 +-
5 files changed, 140 insertions(+), 69 deletions(-)
diff --git a/fs/nfsd/nfs4callback.c b/fs/nfsd/nfs4callback.c
index 70ab4fd321..6edf53be25 100644
--- a/fs/nfsd/nfs4callback.c
+++ b/fs/nfsd/nfs4callback.c
@@ -1471,14 +1471,16 @@ static void nfsd41_destroy_cb(struct nfsd4_callback *cb)
*
* Caller serializes access to @cb.
*
- * NB: If memory allocation fails, the referring call is not added.
+ * Return: true if @cb names the referring call, false if memory
+ * allocation failed and the referring call was not added.
*/
-void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
+bool nfsd41_cb_referring_call(struct nfsd4_callback *cb,
struct nfs4_sessionid *sessionid,
u32 slotid, u32 seqno)
{
struct nfsd4_referring_call_list *rcl;
struct nfsd4_referring_call *rc;
+ bool added = true;
bool found;
might_sleep();
@@ -1494,7 +1496,7 @@ void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
if (!found) {
rcl = kmalloc_obj(*rcl);
if (!rcl)
- return;
+ return false;
memcpy(rcl->rcl_sessionid.data, sessionid->data,
NFS4_MAX_SESSIONID_LEN);
rcl->__nr_referring_calls = 0;
@@ -1512,8 +1514,10 @@ void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
}
if (!found) {
rc = kmalloc_obj(*rc);
- if (!rc)
+ if (!rc) {
+ added = false;
goto out;
+ }
rc->rc_sequenceid = seqno;
rc->rc_slotid = slotid;
rcl->__nr_referring_calls++;
@@ -1526,6 +1530,7 @@ void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
list_del(&rcl->__list);
kfree(rcl);
}
+ return added;
}
/**
diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c
index fdce9ddeba..7a0353149e 100644
--- a/fs/nfsd/nfs4layouts.c
+++ b/fs/nfsd/nfs4layouts.c
@@ -257,6 +257,8 @@ nfsd4_free_layout_stateid(struct nfs4_stid *stid)
list_for_each_entry_safe(lg, next, &ls->ls_grants, lg_list)
kfree(lg);
+ /* The list outlives one send of the recall. */
+ nfsd41_cb_destroy_referring_call_list(&ls->ls_recall);
kmem_cache_free(nfs4_layout_stateid_cache, ls);
}
@@ -944,6 +946,8 @@ nfsd4_cb_layout_prepare(struct nfsd4_callback *cb)
{
struct nfs4_layout_stateid *ls =
container_of(cb, struct nfs4_layout_stateid, ls_recall);
+ struct nfs4_client *clp = ls->ls_stid.sc_client;
+ struct nfsd4_layout_grant *lg;
mutex_lock(&ls->ls_mutex);
nfs4_inc_and_copy_stateid(&ls->ls_recall_sid, &ls->ls_stid);
@@ -954,6 +958,22 @@ nfsd4_cb_layout_prepare(struct nfsd4_callback *cb)
*/
nfsd4_prune_layout_grants(ls);
ls->ls_recall_retired_at_send = list_empty(&ls->ls_grants);
+
+ /*
+ * The recall can reach the client before the reply to a LAYOUTGET
+ * that handed out a layout. Name each LAYOUTGET whose slot has not
+ * retired as a referring call (RFC 8881 section 2.10.6.3), so that
+ * a client that honors them holds the recall back instead of
+ * answering NFS4ERR_NOMATCHING_LAYOUT. ->prepare runs once per
+ * recall; the list stays for the polls and the resend that follow
+ * and is freed with @ls.
+ */
+ ls->ls_recall_unnamed = false;
+ list_for_each_entry(lg, &ls->ls_grants, lg_list) {
+ if (!nfsd41_cb_referring_grant(&ls->ls_recall, clp,
+ &lg->lg_slot))
+ ls->ls_recall_unnamed = true;
+ }
mutex_unlock(&ls->ls_mutex);
ls->ls_recall_accepted = false;
ls->ls_recall_resent = false;
@@ -970,15 +990,63 @@ static bool nfsd4_layout_fenced(struct nfs4_layout_stateid *ls)
return fenced;
}
+/*
+ * Client gets 2 lease periods to return its layouts, counted from its
+ * first answer: the first call starts the clock. task->tk_start is reset
+ * each time the RPC task is restarted, so it cannot anchor the limit.
+ */
+static bool
+nfsd4_layout_recall_expired(struct nfs4_layout_stateid *ls)
+{
+ struct nfsd_net *nn = net_generic(ls->ls_stid.sc_client->net,
+ nfsd_net_id);
+ ktime_t now = ktime_get_boottime();
+
+ if (!ls->ls_recall_start)
+ ls->ls_recall_start = now;
+ return !ktime_before(now, ktime_add_ns(ls->ls_recall_start,
+ (u64)nn->nfsd4_lease * NSEC_PER_SEC * 2));
+}
+
+static void
+nfsd4_cb_layout_give_up(struct nfs4_layout_stateid *ls)
+{
+ const struct nfsd4_layout_ops *ops;
+ struct nfsd_file *fl;
+
+ /*
+ * Nothing to fence for once the layouts are gone: a
+ * LAYOUTRETURN can return the last one while the recall is
+ * still out.
+ */
+ if (list_empty(&ls->ls_layouts)) {
+ trace_nfsd_layout_recall_done(&ls->ls_stid.sc_stateid);
+ return;
+ }
+ /*
+ * Unknown error or non-responding client, we'll need to fence.
+ */
+ trace_nfsd_layout_recall_fail(&ls->ls_stid.sc_stateid);
+ if (nfsd4_layout_fenced(ls))
+ return;
+ rcu_read_lock();
+ fl = nfsd_file_get(ls->ls_file);
+ rcu_read_unlock();
+ if (fl) {
+ ops = nfsd4_layout_ops[ls->ls_layout_type];
+ if (ops->fence_client)
+ ops->fence_client(ls, fl);
+ else
+ nfsd4_cb_layout_fail(ls, fl);
+ nfsd_file_put(fl);
+ }
+}
+
static int
nfsd4_cb_layout_done(struct nfsd4_callback *cb, struct rpc_task *task)
{
struct nfs4_layout_stateid *ls =
container_of(cb, struct nfs4_layout_stateid, ls_recall);
- struct nfsd_net *nn;
- ktime_t now, cutoff;
- const struct nfsd4_layout_ops *ops;
- struct nfsd_file *fl;
trace_nfsd_cb_layout_done(&ls->ls_stid.sc_stateid, task);
switch (task->tk_status) {
@@ -1021,77 +1089,60 @@ nfsd4_cb_layout_done(struct nfsd4_callback *cb, struct rpc_task *task)
}
/* Poll the client until it's done with the layout */
- now = ktime_get_boottime();
- nn = net_generic(ls->ls_stid.sc_client->net, nfsd_net_id);
-
- /*
- * Client gets 2 lease periods to return it, counted from its
- * first answer. task->tk_start is reset each time the RPC task
- * is restarted, so it cannot anchor the limit.
- */
- if (!ls->ls_recall_start)
- ls->ls_recall_start = now;
- cutoff = ktime_add_ns(ls->ls_recall_start,
- (u64)nn->nfsd4_lease * NSEC_PER_SEC * 2);
-
- if (ktime_before(now, cutoff)) {
+ if (!nfsd4_layout_recall_expired(ls)) {
rpc_delay(task, HZ/100); /* 10 mili-seconds */
return 0;
}
fallthrough;
default:
- /*
- * Nothing to fence for once the layouts are gone: a
- * LAYOUTRETURN can return the last one while the recall is
- * still out.
- */
- if (list_empty(&ls->ls_layouts)) {
- trace_nfsd_layout_recall_done(&ls->ls_stid.sc_stateid);
- return 1;
- }
- /*
- * Unknown error or non-responding client, we'll need to fence.
- */
- trace_nfsd_layout_recall_fail(&ls->ls_stid.sc_stateid);
- if (nfsd4_layout_fenced(ls))
- return 1;
- rcu_read_lock();
- fl = nfsd_file_get(ls->ls_file);
- rcu_read_unlock();
- if (fl) {
- ops = nfsd4_layout_ops[ls->ls_layout_type];
- if (ops->fence_client)
- ops->fence_client(ls, fl);
- else
- nfsd4_cb_layout_fail(ls, fl);
- nfsd_file_put(fl);
- }
+ nfsd4_cb_layout_give_up(ls);
return 1;
case -NFS4ERR_NOMATCHING_LAYOUT:
/*
- * The recall can reach the client before the reply to a
- * LAYOUTGET that added a layout, and a client that has not
- * processed that reply answers this way too. As the
- * delegation code does for NFS4ERR_BAD_STATEID, trust the
- * answer if the slots of the LAYOUTGETs had retired when the
- * recall was sent, and send the recall once more after a
- * delay otherwise. Unlike the delegation code, trust the
- * answer to that resend: not trusting it would end in fencing
- * the client, and an idle client leaves its slot unretired
- * also when it has no layout. A client that has not
- * processed the reply by the resend still has its layout
+ * A client that has not processed the reply to a LAYOUTGET
+ * named as referring call answers this way too: one that
+ * ignores referring calls, and one that gives up waiting for
+ * the LAYOUTGET. As the delegation code does for
+ * NFS4ERR_BAD_STATEID, trust the answer if the slots of the
+ * LAYOUTGETs had retired when the recall was sent, and send
+ * the recall once more after a delay otherwise. Unlike the
+ * delegation code, trust the answer to that resend: not
+ * trusting it would end in fencing the client, and an idle
+ * client leaves its slot unretired also when it has no
+ * layout. A client that neither holds the recall back nor
+ * processes the reply before the resend still has its layout
* dropped. A client that has answered NFS4_OK to this recall
* knew the layout stateid, so its answer to a poll is trusted
* at once.
*
+ * When memory ran out in ->prepare and a LAYOUTGET is not
+ * named, the client has no way to hold the recall back, and
+ * its answers are not trusted: the recall is sent again every
+ * two seconds until the client answers otherwise, the last
+ * layout is returned, the fence worker of the lease break has
+ * run or the recall times out.
+ *
* The resend repeats the request with its seqid, as the polls
* do, although RFC 8881 section 12.5.3 has a new one for each
* request: a client that did not take the request in expects
* that seqid next, and a client that did answers
* NFS4ERR_OLD_STATEID, which the polls handle.
+ *
+ * Unlike the delegation code, ->done does not test the slots
+ * again, as ls_grants needs ls_mutex: a referring call that
+ * has completed by the resend is harmless, as the client finds
+ * it complete, but a recall that could not name a LAYOUTGET is
+ * not trusted even after its slots retire.
*/
if (!list_empty(&ls->ls_layouts) && !ls->ls_recall_accepted &&
- !ls->ls_recall_retired_at_send && !ls->ls_recall_resent) {
+ !ls->ls_recall_retired_at_send &&
+ (!ls->ls_recall_resent || ls->ls_recall_unnamed)) {
+ if (ls->ls_recall_unnamed &&
+ (nfsd4_layout_fenced(ls) ||
+ nfsd4_layout_recall_expired(ls))) {
+ nfsd4_cb_layout_give_up(ls);
+ return 1;
+ }
ls->ls_recall_resent = true;
trace_nfsd_layout_recall_resend(&ls->ls_recall_sid);
rpc_delay(task, 2 * HZ);
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 735eaa4b7f..36fa7259b1 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -6329,18 +6329,21 @@ bool nfsd41_grant_slot_retired(struct nfs4_client *clp,
*
* Context: May sleep. Caller serializes access to @cb, as for
* nfsd41_cb_referring_call().
+ *
+ * Return: false if memory allocation failed and @cb does not name the
+ * request, true otherwise.
*/
-void nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
+bool nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
struct nfs4_client *clp,
const struct nfsd4_grant_slot *grant)
{
struct nfsd4_sessionid sid;
if (!grant->valid)
- return;
+ return true;
nfsd41_grant_slot_sessionid(clp, grant, &sid);
- nfsd41_cb_referring_call(cb, (struct nfs4_sessionid *)&sid,
- grant->slotid, grant->seqid);
+ return nfsd41_cb_referring_call(cb, (struct nfs4_sessionid *)&sid,
+ grant->slotid, grant->seqid);
}
static bool nfsd4_recall_grant_slot_retired(struct nfs4_delegation *dp)
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index b3338cf171..5cf3a5ac46 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -854,7 +854,12 @@ struct nfsd4_layout_grant {
struct nfsd4_grant_slot lg_slot;
};
-/* The most LAYOUTGETs that a layout stateid keeps records of */
+/*
+ * The most LAYOUTGETs that a layout stateid keeps records of. A layout
+ * recall names each as a referring call, and the CB_LAYOUTRECALL request
+ * (NFS4_enc_cb_layout_sz) has room for this many, each in a referring
+ * call list of its own.
+ */
#define NFSD4_LAYOUT_MAX_GRANTS 8
struct nfs4_layout_stateid {
@@ -877,12 +882,15 @@ struct nfs4_layout_stateid {
* layout to this layout stateid or merged into one, and whose
* slots had not retired when last looked at. Under ls_mutex.
* Until a slot retires, the client may not have processed the reply
- * to the LAYOUTGET it carried.
+ * to the LAYOUTGET it carried, and a recall names the LAYOUTGET as a
+ * referring call (RFC 8881 section 2.10.6.3).
*/
struct list_head ls_grants;
/* Recall state for ->done, reset in ->prepare */
bool ls_recall_retired_at_send;
bool ls_recall_resent;
+ /* The recall could not name a LAYOUTGET in ls_grants */
+ bool ls_recall_unnamed;
struct delayed_work ls_fence_work;
struct mutex ls_fence_mutex; /* serializes fence shutdown */
@@ -968,7 +976,7 @@ extern __be32 nfs4_check_open_reclaim(struct nfs4_client *);
extern void nfsd4_probe_callback(struct nfs4_client *clp);
extern void nfsd4_probe_callback_sync(struct nfs4_client *clp);
extern void nfsd4_change_callback(struct nfs4_client *clp, struct nfs4_cb_conn *);
-extern void nfsd41_cb_referring_call(struct nfsd4_callback *cb,
+extern bool nfsd41_cb_referring_call(struct nfsd4_callback *cb,
struct nfs4_sessionid *sessionid,
u32 slotid, u32 seqno);
extern void nfsd41_cb_destroy_referring_call_list(struct nfsd4_callback *cb);
@@ -976,7 +984,7 @@ extern void nfsd41_record_grant_slot(struct nfsd4_grant_slot *grant,
const struct nfsd4_compound_state *cstate);
extern bool nfsd41_grant_slot_retired(struct nfs4_client *clp,
const struct nfsd4_grant_slot *grant);
-extern void nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
+extern bool nfsd41_cb_referring_grant(struct nfsd4_callback *cb,
struct nfs4_client *clp,
const struct nfsd4_grant_slot *grant);
extern void nfsd4_init_cb(struct nfsd4_callback *cb, struct nfs4_client *clp,
diff --git a/fs/nfsd/xdr4cb.h b/fs/nfsd/xdr4cb.h
index 838f862982..514404d2e1 100644
--- a/fs/nfsd/xdr4cb.h
+++ b/fs/nfsd/xdr4cb.h
@@ -25,8 +25,12 @@
#define NFS4_dec_cb_recall_sz (cb_compound_dec_hdr_sz + \
cb_sequence_dec_sz + \
op_dec_sz)
+/* NFSD4_LAYOUT_MAX_GRANTS referring calls, each in a list of its own */
+#define cb_layout_sequence_enc_sz (cb_sequence_enc_sz + \
+ (NFSD4_LAYOUT_MAX_GRANTS - 1) * \
+ enc_referring_call_list4_sz)
#define NFS4_enc_cb_layout_sz (cb_compound_enc_hdr_sz + \
- cb_sequence_enc_sz + \
+ cb_layout_sequence_enc_sz + \
op_enc_sz + 3 + 1 + \
enc_nfs4_fh_sz + 4 + \
enc_stateid_sz)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 4/4] nfsd: do not fail a LAYOUTGET on a lease break by the same client
2026-10-23 2:10 [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply Daejun Park via B4 Relay
` (2 preceding siblings ...)
2026-10-23 2:10 ` [PATCH v2 3/4] nfsd: send a referring call with CB_LAYOUTRECALL Daejun Park via B4 Relay
@ 2026-10-23 2:10 ` Daejun Park via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Daejun Park via B4 Relay @ 2026-10-23 2:10 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey
Cc: Christoph Hellwig, Benjamin Coddington, Trond Myklebust,
Anna Schumaker, linux-nfs, linux-kernel, Daejun Park
From: Daejun Park <daejun7.park@samsung.com>
"nfsd: fail a LAYOUTGET in progress when the layout lease is broken"
fails a LAYOUTGET whose layout stateid has no layouts yet when the lease
is broken, so that no layout is handed out after the break without a
recall. Since "nfsd: answer NFS4ERR_DELAY instead of waiting for a
layout recall", a WRITE through the metadata server that meets a layout
of its own client gets NFS4ERR_DELAY, and the Linux client resends it
every 0.1 seconds; a resend breaks the lease of any LAYOUTGET that the
same client has in progress. A client that writes through the metadata
server while it does I/O through a layout of the same file, as the Linux
client does for O_DIRECT writes that are not aligned to a page, then
gets NFS4ERR_RECALLCONFLICT for LAYOUTGET after LAYOUTGET, and waits up
to 15 seconds between them.
When the break comes from a request of the client that holds the layout
stateid, nfsd4_break_file_layout() notes that the break met a LAYOUTGET
in progress and when the break times out (fl_break_time), and
nfsd4_insert_layout() recalls the layout as soon as it adds it. The
LAYOUTGET succeeds and the client is asked for the layout back at once.
nfsd4_layout_breaker_is_holder() finds the client of the breaker as
nfsd_breaker_owns_lease() does for delegations. The layout is still
recalled: a write through the metadata server can change the block
mapping that the layout describes. A break by another client or by a
local process fails the LAYOUTGET as before, and the conflicting
operation goes ahead without waiting for a recall, unless a break by the
holder came first. The new case is traced as nfsd_layout_break_pending.
A LAYOUTGET that would add its layout after the break has timed out
fails as before, whether or not the timeout has been noticed yet:
time_out_leases() notices it only when another lease operation on the
file comes, and a layout added in between would be recalled when it is
already due and have its client fenced if that operation comes before
the layout is back. When the timeout is noticed first,
nfsd4_layout_lm_breaker_timedout() marks the layout stateid recalled, so
that the LAYOUTGET fails. A layout added shortly before the break times
out is recalled with what is left of fs.lease-break-time, and its client
is fenced if it does not return the layout by then, where the LAYOUTGET
failed before this patch. With the default of 45 seconds that takes a
LAYOUTGET that has been in progress for most of that time.
The recall is queued before the LAYOUTGET reply is sent and can reach
the client first; a client that answers NFS4ERR_NOMATCHING_LAYOUT then
would have its layout thrown away while it uses it. The resent recall of
"nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT"
and the referring call that CB_LAYOUTRECALL carries since "nfsd: send a
referring call with CB_LAYOUTRECALL" cover that, for a client that takes
the reply in within about three seconds or holds the recall back, which
is why this comes after them. A recall that cannot be sent because the
backchannel is down ends at once and drops the layouts, as any layout
recall does today; with this patch that can take the layout that the
LAYOUTGET is handing out.
One client writing 1 MiB blocks with O_DIRECT through its block layout
and 512 bytes at the middle of a page through the metadata server to the
same file, for 30 seconds: before this patch 51, 54 and 12 of the 1 MiB
writes were done in three runs, as the LAYOUTGETs failed and the client
backed off; with it 240, 249 and 244, with no 1 MiB block lost. The 512
byte writes went from 5172, 5137 and 7589 to 1473, 1214 and 1397: the
client now holds a layout most of the time, and such a WRITE meets it
and gets NFS4ERR_DELAY. The same change without the two patches above
lost 75 of 600, 79 of 589 and 87 of 600 blocks.
With fs.lease-break-time at one second and a test-only delay before
nfsd4_insert_layout(), a 512 byte write through the metadata server 0.3
seconds into the LAYOUTGET broke the lease and was resent every 0.1
seconds; the first resend after the break timed out came 37 to 44 ms
after it. A layout added 25 to 29 ms before the break timed out was
recalled at once and returned in each of five runs, without fencing.
LAYOUTGETs that would add their layout about 17 or 30 ms after the break
had timed out, before that resend, failed with NFS4ERR_RECALLCONFLICT in
each of five runs at each offset. With the test of the deadline taken
out, they handed the layout out. At 17 ms the client returned it before
the resend in each of five runs, and nothing was fenced; at 30 ms the
resend timed the break out while the client still held the layout, and
had it fenced, in four of five, and came first in the fifth.
With a test-only delay of three seconds and a local write on the server
that breaks the lease during that delay and goes ahead one second later,
the LAYOUTGET fails with NFS4ERR_RECALLCONFLICT with this patch in each
of three runs: a local process is not the holder, so this patch leaves
that case alone. With another client breaking the lease, as in the test
of "nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT"
without the delay, the 1 MiB writes went on with the whole series as
without it, 600 blocks in each of three runs of 30 seconds, and no
recall overtook a reply; without the series, one did in one of three
runs, and that block read back as zeroes.
No Fixes: tag, as "nfsd: fail a LAYOUTGET in progress when the layout
lease is broken" is not upstream yet. Where that patch is backported,
this one is needed too, but only together with the two patches before
it, so only where "NFSD: Send referring calls with CB_RECALL" and its
series are, and not before v7.1.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
fs/nfsd/nfs4layouts.c | 81 ++++++++++++++++++++++++++++++++++++++++++++-------
fs/nfsd/state.h | 11 +++++++
fs/nfsd/trace.h | 1 +
3 files changed, 82 insertions(+), 11 deletions(-)
diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c
index 7a0353149e..c797bc7d93 100644
--- a/fs/nfsd/nfs4layouts.c
+++ b/fs/nfsd/nfs4layouts.c
@@ -336,6 +336,8 @@ nfsd4_alloc_layout_stateid(struct nfsd4_compound_state *cstate,
}
ls->ls_recall_start = 0;
+ ls->ls_break_pending = false;
+ ls->ls_break_time = 0;
ls->ls_fenced = false;
ls->ls_fence_inflight = false;
ls->ls_fence_stopped = false;
@@ -459,16 +461,42 @@ nfsd4_recall_file_layout(struct nfs4_layout_stateid *ls)
spin_unlock(&ls->ls_lock);
}
+/* Whether the lease break comes from a request of the client of @ls */
+static bool
+nfsd4_layout_breaker_is_holder(const struct nfs4_layout_stateid *ls)
+{
+ struct svc_rqst *rqstp = nfsd_current_rqst();
+ struct nfsd_thread_local_info *ntli;
+
+ if (!nfsd_v4client(rqstp))
+ return false;
+ ntli = rqstp->rq_private;
+ return *ntli->ntli_lease_breaker == ls->ls_stid.sc_client;
+}
+
/*
- * A layout stateid with no layouts belongs to a LAYOUTGET in progress
- * and already holds the lease. Mark the stateid recalled so that
- * nfsd4_insert_layout() fails the LAYOUTGET and the lease is released.
+ * A lease break also has to reach a LAYOUTGET in progress, whose layout
+ * stateid holds the lease but has no layouts yet. Mark the stateid
+ * recalled so that nfsd4_insert_layout() fails the LAYOUTGET and the
+ * lease is released. When the break comes from a request of the same
+ * client, as a WRITE through the metadata server that the Linux client
+ * sends next to its I/O through the layout, failing its LAYOUTGET would
+ * have it back off and fail again: note the break and its deadline
+ * instead, and let nfsd4_insert_layout() recall the layout as soon as it
+ * adds it, unless the break is past its deadline by then.
+ * ->lm_break() is not called again for a lease that is already breaking.
*/
static void
-nfsd4_break_file_layout(struct nfs4_layout_stateid *ls)
+nfsd4_break_file_layout(struct nfs4_layout_stateid *ls,
+ unsigned long break_time)
{
spin_lock(&ls->ls_lock);
- if (!ls->ls_recalled && list_empty(&ls->ls_layouts)) {
+ if (!ls->ls_recalled && list_empty(&ls->ls_layouts) &&
+ nfsd4_layout_breaker_is_holder(ls)) {
+ ls->ls_break_pending = true;
+ ls->ls_break_time = break_time;
+ trace_nfsd_layout_break_pending(&ls->ls_stid.sc_stateid);
+ } else if (!ls->ls_recalled && list_empty(&ls->ls_layouts)) {
ls->ls_recalled = true;
atomic_inc(&ls->ls_stid.sc_file->fi_lo_recalls);
trace_nfsd_layout_recall_empty(&ls->ls_stid.sc_stateid);
@@ -478,6 +506,18 @@ nfsd4_break_file_layout(struct nfs4_layout_stateid *ls)
spin_unlock(&ls->ls_lock);
}
+/*
+ * Whether the lease break noted by nfsd4_break_file_layout() is past its
+ * deadline. A layout added now would be recalled too late, whether or
+ * not time_out_leases() has noticed the timeout yet.
+ */
+static bool
+nfsd4_layout_break_expired(const struct nfs4_layout_stateid *ls)
+{
+ return ls->ls_break_pending && ls->ls_break_time &&
+ time_after(jiffies, ls->ls_break_time);
+}
+
static inline u64
layout_end(struct nfsd4_layout_seg *seg)
{
@@ -553,7 +593,7 @@ nfsd4_insert_layout(struct nfsd4_layoutget *lgp, struct nfs4_layout_stateid *ls)
if (nfserr)
goto out;
spin_lock(&ls->ls_lock);
- if (ls->ls_recalled) {
+ if (ls->ls_recalled || nfsd4_layout_break_expired(ls)) {
nfserr = nfserr_recallconflict;
goto out_unlock_ls;
}
@@ -575,7 +615,7 @@ nfsd4_insert_layout(struct nfsd4_layoutget *lgp, struct nfs4_layout_stateid *ls)
if (nfserr)
goto out;
spin_lock(&ls->ls_lock);
- if (ls->ls_recalled) {
+ if (ls->ls_recalled || nfsd4_layout_break_expired(ls)) {
nfserr = nfserr_recallconflict;
goto out_unlock_ls;
}
@@ -589,6 +629,10 @@ nfsd4_insert_layout(struct nfsd4_layoutget *lgp, struct nfs4_layout_stateid *ls)
new = NULL;
done:
nfs4_inc_and_copy_stateid(&lgp->lg_sid, &ls->ls_stid);
+ if (ls->ls_break_pending) {
+ ls->ls_break_pending = false;
+ nfsd4_recall_file_layout_locked(ls);
+ }
out_unlock_ls:
spin_unlock(&ls->ls_lock);
out:
@@ -1182,7 +1226,7 @@ nfsd4_layout_lm_break(struct file_lease *fl)
* Enforce break lease timeout to prevent NFSD
* thread from hanging in __break_lease.
*/
- nfsd4_break_file_layout(fl->c.flc_owner);
+ nfsd4_break_file_layout(fl->c.flc_owner, fl->fl_break_time);
return false;
}
@@ -1373,7 +1417,9 @@ static void nfsd4_layout_fence_worker(struct work_struct *work)
*
* Schedule a worker to fence the client from accessing the block device.
* If the layout type has no fence operation, the worker runs the fence
- * script instead.
+ * script instead. If a lease break of its own client met the LAYOUTGET
+ * of a layout stateid that has no layouts yet, mark it recalled instead,
+ * so that the LAYOUTGET fails.
*
* This function runs under the protection of the spin_lock flc_lock.
* At this time, the file_lease associated with the layout stateid is
@@ -1413,8 +1459,21 @@ nfsd4_layout_lm_breaker_timedout(struct file_lease *fl)
spin_unlock(&ls->ls_lock);
return false;
}
- if (list_empty(&ls->ls_layouts) ||
- !refcount_inc_not_zero(&ls->ls_stid.sc_count)) {
+ if (list_empty(&ls->ls_layouts)) {
+ /*
+ * The lease goes away and the conflicting operation goes
+ * ahead, so a LAYOUTGET that the break met must not add a
+ * layout any more: fail it as nfsd4_insert_layout() fails one
+ * on a recalled layout stateid.
+ */
+ if (ls->ls_break_pending && !ls->ls_recalled) {
+ ls->ls_recalled = true;
+ atomic_inc(&ls->ls_stid.sc_file->fi_lo_recalls);
+ }
+ spin_unlock(&ls->ls_lock);
+ return true;
+ }
+ if (!refcount_inc_not_zero(&ls->ls_stid.sc_count)) {
spin_unlock(&ls->ls_lock);
return true;
}
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 5cf3a5ac46..ec325825d7 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -876,6 +876,17 @@ struct nfs4_layout_stateid {
/* The client has answered NFS4_OK to the recall */
bool ls_recall_accepted;
bool ls_recalled;
+ /*
+ * A lease break of this client met a LAYOUTGET in progress: recall
+ * the layout it adds, or fail it once the break is past its time.
+ * Under ls_lock.
+ */
+ bool ls_break_pending;
+ /*
+ * fl_break_time of that break: 0 if it does not time out.
+ * Under ls_lock.
+ */
+ unsigned long ls_break_time;
struct mutex ls_mutex;
/*
* The latest NFSD4_LAYOUT_MAX_GRANTS LAYOUTGETs that added a
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index a2c21d491a..2110e95ee1 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -829,6 +829,7 @@ DEFINE_STATEID_EVENT(layout_commit_lookup_fail);
DEFINE_STATEID_EVENT(layout_return_lookup_fail);
DEFINE_STATEID_EVENT(layout_recall);
DEFINE_STATEID_EVENT(layout_recall_empty);
+DEFINE_STATEID_EVENT(layout_break_pending);
DEFINE_STATEID_EVENT(layout_recall_done);
DEFINE_STATEID_EVENT(layout_recall_resend);
DEFINE_STATEID_EVENT(layout_recall_new_seqid);
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-23 2:10 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-23 2:10 [PATCH v2 0/4] nfsd: keep a layout whose recall overtakes the LAYOUTGET reply Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 1/4] nfsd: share the grant slot record between delegations and layouts Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 2/4] nfsd: resend a layout recall once before trusting NOMATCHING_LAYOUT Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 3/4] nfsd: send a referring call with CB_LAYOUTRECALL Daejun Park via B4 Relay
2026-10-23 2:10 ` [PATCH v2 4/4] nfsd: do not fail a LAYOUTGET on a lease break by the same client Daejun Park via B4 Relay
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®