* [PATCH v5 1/8] NFSD: Do not send CB_RECALL_ANY to NFSv4.0 clients
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 2/8] NFSD: Count the delegations held by each client Chuck Lever
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
deleg_reaper() sends CB_RECALL_ANY to every ACTIVE client holding
delegations, but CB_RECALL_ANY is an NFSv4.1 operation. An NFSv4.0
client's callback service accepts only CB_GETATTR and CB_RECALL, so it
replies OP_ILLEGAL. The decoder maps the unexpected opnum to -EIO, and
nfsd4_cb_done() marks the client's callback channel down.
Nothing brings the channel back. nfsd4_run_cb_work() sets NFSD4_CB_UP
only for a minor version above zero, and the only nfsd4_probe_callback()
call site an NFSv4.0 client reaches is nfsd4_setclientid_confirm(). One
visit from the reaper therefore leaves the channel marked down until the
client re-establishes its clientid. RENEW then returns
NFS4ERR_CB_PATH_DOWN for as long as the client holds delegations.
nfsd4_cb_channel_good() stops returning true, so the client is granted
no further delegations.
Skip clients at minor version zero.
Fixes: 44df6f439a17 ("NFSD: add delegation reaper to react to low memory condition")
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/nfs4state.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 1ba97e3f65eb..9a3574b853ad 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -7945,6 +7945,8 @@ deleg_reaper(struct nfsd_net *nn)
list_for_each_safe(pos, next, &nn->client_lru) {
clp = list_entry(pos, struct nfs4_client, cl_lru);
+ if (clp->cl_minorversion == 0)
+ continue;
if (clp->cl_state != NFSD4_ACTIVE)
continue;
if (list_empty(&clp->cl_delegations))
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 2/8] NFSD: Count the delegations held by each client
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
2026-08-18 1:08 ` [PATCH v5 1/8] NFSD: Do not send CB_RECALL_ANY to NFSv4.0 clients Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 3/8] NFSD: Name directory delegations in the CB_RECALL_ANY type mask Chuck Lever
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
struct nfs4_client records the delegations it holds on cl_delegations
but keeps no count of them. deleg_reaper() walks nn->client_lru under
nn->client_lock, but cl_delegations is serialized by nn->deleg_lock,
which nests outside nn->client_lock. A caller there cannot take
nn->deleg_lock to count the list. The cost tells against the walk as
well: an O(n) count per client, on a pass that already visits every
client.
Add cl_deleg_count, maintained at the two sites that mutate
cl_delegations. Both hold nn->deleg_lock, so the counter is already
serialized against itself and needs no atomic of its own. The decrement
sits below the delegation_hashed() test, next to the list_del_init it
pairs with, so it runs only when the delegation really leaves the list.
A reader that holds only nn->client_lock is not synchronized against
either update site, so it can see a count that does not match the
list. Such a reader marks the access with data_race() and may not
depend on the value for correctness.
No functional change.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/nfs4state.c | 2 ++
fs/nfsd/state.h | 2 ++
2 files changed, 4 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 9a3574b853ad..fbe073ee5225 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1527,6 +1527,7 @@ hash_delegation_locked(struct nfs4_delegation *dp, struct nfs4_file *fp)
dp->dl_stid.sc_type = SC_TYPE_DELEG;
list_add(&dp->dl_perfile, &fp->fi_delegations);
list_add(&dp->dl_perclnt, &clp->cl_delegations);
+ clp->cl_deleg_count++;
return 0;
}
@@ -1558,6 +1559,7 @@ unhash_delegation_locked(struct nfs4_delegation *dp, unsigned short statusmask)
++dp->dl_time;
spin_lock(&fp->fi_lock);
list_del_init(&dp->dl_perclnt);
+ dp->dl_stid.sc_client->cl_deleg_count--;
list_del_init(&dp->dl_recall_lru);
list_del_init(&dp->dl_perfile);
spin_unlock(&fp->fi_lock);
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index c65b604e29f1..cd9294f024bb 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -633,6 +633,8 @@ struct nfs4_client {
unsigned int cl_state;
atomic_t cl_delegs_in_recall;
+ /* Length of cl_delegations, updated under nn->deleg_lock */
+ unsigned int cl_deleg_count;
struct nfsd4_cb_recall_any *cl_ra;
time64_t cl_ra_time;
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 3/8] NFSD: Name directory delegations in the CB_RECALL_ANY type mask
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
2026-08-18 1:08 ` [PATCH v5 1/8] NFSD: Do not send CB_RECALL_ANY to NFSv4.0 clients Chuck Lever
2026-08-18 1:08 ` [PATCH v5 2/8] NFSD: Count the delegations held by each client Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 4/8] NFSD: Send a meaningful CB_RECALL_ANY keep count Chuck Lever
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
RFC 8881 Section 20.6.3 distinguishes an NFSv4.1 server
implementation that shares one pool among all classes of recallable
objects from one that keeps separate pools per class. NFSD falls
in the former category.
The CB_RECALL_ANY operation's craa_type_mask argument names the
types of objects in the recallable resource pool, but NFSD's
implementation does not name directory delegations, even though
they are allocated through __alloc_init_deleg(), they are counted
against the max_delegations budget, and the state shrinker reclaims
them.
Add RCA4_TYPE_MASK_DIR_DLG to craa_type_mask so clients that
implement directory delegations consider them when choosing which
delegations to return.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/nfs4state.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index fbe073ee5225..2214b4d60c7e 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -7967,7 +7967,8 @@ deleg_reaper(struct nfsd_net *nn)
clp->cl_ra_time = ktime_get_boottime_seconds();
clp->cl_ra->ra_keep = 0;
clp->cl_ra->ra_bmval[0] = BIT(RCA4_TYPE_MASK_RDATA_DLG) |
- BIT(RCA4_TYPE_MASK_WDATA_DLG);
+ BIT(RCA4_TYPE_MASK_WDATA_DLG) |
+ BIT(RCA4_TYPE_MASK_DIR_DLG);
trace_nfsd_cb_recall_any(clp->cl_ra);
nfsd4_run_cb(&clp->cl_ra->ra_cb);
}
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 4/8] NFSD: Send a meaningful CB_RECALL_ANY keep count
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
` (2 preceding siblings ...)
2026-08-18 1:08 ` [PATCH v5 3/8] NFSD: Name directory delegations in the CB_RECALL_ANY type mask Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 5/8] NFSD: Count delegations per network namespace Chuck Lever
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
deleg_reaper() sets craa_objects_to_keep to zero on every
CB_RECALL_ANY. RFC 8881 Section 20.6.3 defines that field as the
number of objects the client may keep, leaving the client to choose
which of the excess to return, because the server cannot read lack
of recent use as lack of usefulness. Zero asks for every delegation
the client holds, including the ones backing files an application
still has open.
There is also no reason NFSD has to reclaim the entire delegation
working set on the first sign of memory pressure.
Derive the keep count from cl_deleg_count so that each callback
asks for one delegation. Both the shrinker and the laundromat re-arm
while their condition lasts, so a client with more to give is asked
again on the next pass.
The Linux client ignores craa_objects_to_keep and returns unused
delegations selected from the type mask alone, so the count changes
nothing for it.
Fixes: 44df6f439a17 ("NFSD: add delegation reaper to react to low memory condition")
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/nfs4state.c | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 2214b4d60c7e..5c2dd6657fac 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -7942,6 +7942,7 @@ deleg_reaper(struct nfsd_net *nn)
{
struct list_head *pos, *next;
struct nfs4_client *clp;
+ unsigned int count;
spin_lock(&nn->client_lock);
list_for_each_safe(pos, next, &nn->client_lru) {
@@ -7951,21 +7952,33 @@ deleg_reaper(struct nfsd_net *nn)
continue;
if (clp->cl_state != NFSD4_ACTIVE)
continue;
- if (list_empty(&clp->cl_delegations))
- continue;
if (atomic_read(&clp->cl_delegs_in_recall))
continue;
if (ktime_get_boottime_seconds() - clp->cl_ra_time < 5)
continue;
if (clp->cl_cb_state != NFSD4_CB_UP)
continue;
+ /*
+ * This read races with hash_delegation_locked() and
+ * unhash_delegation_locked() on other CPUs. A stale
+ * count only skews the keep value; the next
+ * laundromat pass sees a more current one.
+ */
+ count = data_race(READ_ONCE(clp->cl_deleg_count));
+ if (!count)
+ continue;
if (test_and_set_bit(NFSD4_CALLBACK_RUNNING, &clp->cl_ra->ra_cb.cb_flags))
continue;
/* release in nfsd4_cb_recall_any_release */
kref_get(&clp->cl_nfsdfs.cl_ref);
clp->cl_ra_time = ktime_get_boottime_seconds();
- clp->cl_ra->ra_keep = 0;
+ /*
+ * Ask for a single delegation. Recalling one before it
+ * is needed costs the client an OPEN when it next
+ * touches the file.
+ */
+ clp->cl_ra->ra_keep = count - 1;
clp->cl_ra->ra_bmval[0] = BIT(RCA4_TYPE_MASK_RDATA_DLG) |
BIT(RCA4_TYPE_MASK_WDATA_DLG) |
BIT(RCA4_TYPE_MASK_DIR_DLG);
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 5/8] NFSD: Count delegations per network namespace
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
` (3 preceding siblings ...)
2026-08-18 1:08 ` [PATCH v5 4/8] NFSD: Send a meaningful CB_RECALL_ANY keep count Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 6/8] NFSD: Give delegations their own state shrinker Chuck Lever
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
The state shrinker is allocated per network namespace, but
nfsd4_state_shrinker_count() reports num_delegations, which counts
the delegations held by the whole host. Every namespace therefore
reports every delegation on the server. Reclaim sees the
population multiplied by the number of namespaces running NFSD. A
namespace holding no delegations of its own still reports a
nonzero count and queues its reaper, which then finds nothing to
recall.
Count the delegations in each namespace and report that instead.
num_delegations stays for the admission check in
__alloc_init_deleg() and the ceiling check in nfs4_laundromat().
Both compare against max_delegations, which is sized from host
memory and so remains a host-wide limit.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/netns.h | 2 ++
fs/nfsd/nfs4state.c | 7 ++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/fs/nfsd/netns.h b/fs/nfsd/netns.h
index 71eebfea020d..bb62d19430bc 100644
--- a/fs/nfsd/netns.h
+++ b/fs/nfsd/netns.h
@@ -238,6 +238,8 @@ struct nfsd_net {
int nfs4_max_clients;
atomic_t nfsd_courtesy_clients;
+ /* per-namespace; num_delegations in nfs4state.c is host-wide */
+ atomic_long_t nfsd_delegations;
struct shrinker *nfsd_client_shrinker;
struct work_struct nfsd_shrinker_work;
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 5c2dd6657fac..2c169bfabfbe 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1161,6 +1161,7 @@ static struct nfs4_ol_stateid * nfs4_alloc_open_stateid(struct nfs4_client *clp)
*/
static void nfs4_free_deleg(struct nfs4_stid *stid)
{
+ struct nfsd_net *nn = net_generic(stid->sc_client->net, nfsd_net_id);
struct nfs4_delegation *dp = delegstateid(stid);
WARN_ON_ONCE(!list_empty(&stid->sc_cp_list));
@@ -1171,6 +1172,7 @@ static void nfs4_free_deleg(struct nfs4_stid *stid)
nfsd41_cb_destroy_referring_call_list(&dp->dl_recall);
kmem_cache_free(deleg_slab, stid);
atomic_long_dec(&num_delegations);
+ atomic_long_dec(&nn->nfsd_delegations);
}
/*
@@ -1255,6 +1257,7 @@ __alloc_init_deleg(struct nfs4_client *clp, struct nfs4_file *fp,
struct nfs4_clnt_odstate *odstate, u32 dl_type,
void (*sc_free)(struct nfs4_stid *))
{
+ struct nfsd_net *nn = net_generic(clp->net, nfsd_net_id);
struct nfs4_delegation *dp;
struct nfs4_stid *stid;
long n;
@@ -1263,6 +1266,7 @@ __alloc_init_deleg(struct nfs4_client *clp, struct nfs4_file *fp,
return NULL;
n = atomic_long_inc_return(&num_delegations);
+ atomic_long_inc(&nn->nfsd_delegations);
if (n < 0 || n > max_delegations)
goto out_dec;
@@ -1295,6 +1299,7 @@ __alloc_init_deleg(struct nfs4_client *clp, struct nfs4_file *fp,
return dp;
out_dec:
atomic_long_dec(&num_delegations);
+ atomic_long_dec(&nn->nfsd_delegations);
return NULL;
}
@@ -5564,7 +5569,7 @@ nfsd4_state_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
count = atomic_read(&nn->nfsd_courtesy_clients);
if (!count)
- count = atomic_long_read(&num_delegations);
+ count = atomic_long_read(&nn->nfsd_delegations);
if (count)
queue_work(laundry_wq, &nn->nfsd_shrinker_work);
return (unsigned long)count;
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 6/8] NFSD: Give delegations their own state shrinker
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
` (4 preceding siblings ...)
2026-08-18 1:08 ` [PATCH v5 5/8] NFSD: Count delegations per network namespace Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 7/8] NFSD: Pace the state shrinker's scan requests Chuck Lever
2026-08-18 1:08 ` [PATCH v5 8/8] NFSD: Apportion CB_RECALL_ANY recalls among clients Chuck Lever
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
Since commit 44df6f439a17 ("NFSD: add delegation reaper to react to
low memory condition"), nfsd_client_shrinker has managed two
unrelated populations of objects.
One population is courtesy clients. Shrinking that population can
be done synchronously and without risk of deadlock. The shrinker
callback could return a precise count of the number of objects
that were released.
The other population is delegations. Shrinking that population
requires sending a CB_RECALL_ANY; clients are not obligated to
return any delegation. The shrinker callback is structurally
unable to report progress.
What's more, the single shrinker callback falls back to
delegation reaping only when there are no courtesy clients left to
reclaim. A single courtesy client is enough to keep a namespace's
delegations out of the count it reports.
To begin to resolve these issues, refactor the existing state
shrinker into two: one for courtesy clients and one for reaping
delegations. Each manages the size of its own population, and the
shrinker names become namespace-specific.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/netns.h | 6 +++--
fs/nfsd/nfs4state.c | 77 ++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 65 insertions(+), 18 deletions(-)
diff --git a/fs/nfsd/netns.h b/fs/nfsd/netns.h
index bb62d19430bc..ef01a1cf72ac 100644
--- a/fs/nfsd/netns.h
+++ b/fs/nfsd/netns.h
@@ -240,8 +240,10 @@ struct nfsd_net {
atomic_t nfsd_courtesy_clients;
/* per-namespace; num_delegations in nfs4state.c is host-wide */
atomic_long_t nfsd_delegations;
- struct shrinker *nfsd_client_shrinker;
- struct work_struct nfsd_shrinker_work;
+ struct shrinker *nfsd_courtesy_shrinker;
+ struct shrinker *nfsd_deleg_shrinker;
+ struct work_struct nfsd_courtesy_work;
+ struct work_struct nfsd_deleg_work;
/* last time an admin-revoke happened for NFSv4.0 */
time64_t nfs40_last_revoke;
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 2c169bfabfbe..2dbc49a6dcaa 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -5562,16 +5562,27 @@ nfsd4_init_slabs(void)
}
static unsigned long
-nfsd4_state_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
+nfsd4_courtesy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
{
struct nfsd_net *nn = shrink->private_data;
long count;
count = atomic_read(&nn->nfsd_courtesy_clients);
- if (!count)
- count = atomic_long_read(&nn->nfsd_delegations);
if (count)
- queue_work(laundry_wq, &nn->nfsd_shrinker_work);
+ queue_work(laundry_wq, &nn->nfsd_courtesy_work);
+ return (unsigned long)count;
+}
+
+static unsigned long
+nfsd4_deleg_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
+{
+ struct nfsd_net *nn = shrink->private_data;
+ long count;
+
+ count = atomic_long_read(&nn->nfsd_delegations);
+ if (count)
+ queue_work(laundry_wq, &nn->nfsd_deleg_work);
return (unsigned long)count;
}
@@ -5581,6 +5592,25 @@ nfsd4_state_shrinker_scan(struct shrinker *shrink, struct shrink_control *sc)
return SHRINK_STOP;
}
+static struct shrinker *
+nfsd4_alloc_state_shrinker(struct nfsd_net *nn, const char *name,
+ unsigned long (*count)(struct shrinker *,
+ struct shrink_control *))
+{
+ struct shrinker *shrink;
+
+ shrink = shrinker_alloc(0, "%s:%s", name, nn->nfsd_name);
+ if (!shrink)
+ return NULL;
+
+ shrink->count_objects = count;
+ shrink->scan_objects = nfsd4_state_shrinker_scan;
+ shrink->private_data = nn;
+
+ shrinker_register(shrink);
+ return shrink;
+}
+
void
nfsd4_init_leases_net(struct nfsd_net *nn)
{
@@ -7994,12 +8024,20 @@ deleg_reaper(struct nfsd_net *nn)
}
static void
-nfsd4_state_shrinker_worker(struct work_struct *work)
+nfsd4_courtesy_shrinker_worker(struct work_struct *work)
{
struct nfsd_net *nn = container_of(work, struct nfsd_net,
- nfsd_shrinker_work);
+ nfsd_courtesy_work);
courtesy_client_reaper(nn);
+}
+
+static void
+nfsd4_deleg_shrinker_worker(struct work_struct *work)
+{
+ struct nfsd_net *nn = container_of(work, struct nfsd_net,
+ nfsd_deleg_work);
+
deleg_reaper(nn);
}
@@ -9962,21 +10000,26 @@ static int nfs4_state_create_net(struct net *net)
INIT_DELAYED_WORK(&nn->laundromat_work, laundromat_main);
/* Make sure this cannot run until client tracking is initialised */
disable_delayed_work(&nn->laundromat_work);
- INIT_WORK(&nn->nfsd_shrinker_work, nfsd4_state_shrinker_worker);
+ INIT_WORK(&nn->nfsd_courtesy_work, nfsd4_courtesy_shrinker_worker);
+ INIT_WORK(&nn->nfsd_deleg_work, nfsd4_deleg_shrinker_worker);
get_net(net);
- nn->nfsd_client_shrinker = shrinker_alloc(0, "nfsd-client");
- if (!nn->nfsd_client_shrinker)
+ nn->nfsd_courtesy_shrinker =
+ nfsd4_alloc_state_shrinker(nn, "nfsd-courtesy",
+ nfsd4_courtesy_shrinker_count);
+ if (!nn->nfsd_courtesy_shrinker)
goto err_shrinker;
- nn->nfsd_client_shrinker->scan_objects = nfsd4_state_shrinker_scan;
- nn->nfsd_client_shrinker->count_objects = nfsd4_state_shrinker_count;
- nn->nfsd_client_shrinker->private_data = nn;
-
- shrinker_register(nn->nfsd_client_shrinker);
+ nn->nfsd_deleg_shrinker =
+ nfsd4_alloc_state_shrinker(nn, "nfsd-delegation",
+ nfsd4_deleg_shrinker_count);
+ if (!nn->nfsd_deleg_shrinker)
+ goto err_deleg_shrinker;
return 0;
+err_deleg_shrinker:
+ shrinker_free(nn->nfsd_courtesy_shrinker);
err_shrinker:
put_net(net);
kfree(nn->sessionid_hashtbl);
@@ -10077,8 +10120,10 @@ nfs4_state_shutdown_net(struct net *net)
struct list_head *pos, *next, reaplist;
struct nfsd_net *nn = net_generic(net, nfsd_net_id);
- shrinker_free(nn->nfsd_client_shrinker);
- cancel_work_sync(&nn->nfsd_shrinker_work);
+ shrinker_free(nn->nfsd_courtesy_shrinker);
+ shrinker_free(nn->nfsd_deleg_shrinker);
+ cancel_work_sync(&nn->nfsd_courtesy_work);
+ cancel_work_sync(&nn->nfsd_deleg_work);
disable_delayed_work_sync(&nn->laundromat_work);
locks_end_grace(&nn->nfsd4_manager);
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 7/8] NFSD: Pace the state shrinker's scan requests
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
` (5 preceding siblings ...)
2026-08-18 1:08 ` [PATCH v5 6/8] NFSD: Give delegations their own state shrinker Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
2026-08-18 1:08 ` [PATCH v5 8/8] NFSD: Apportion CB_RECALL_ANY recalls among clients Chuck Lever
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
Currently, neither of the scan callback functions records anything
before returning SHRINK_STOP, so the size of each scan request is
discarded. That size is the only real measure NFSD gets of reclaim
pressure. Both count callbacks report their population whether or
not the reaper is already queued to reclaim it, so reclaim asks
again for work that is pending.
Accumulate each courtesy scan request in nfsd_shrink_backlog and
subtract the backlog from what that count callback reports. The
worker retires the backlog once courtesy_client_reaper() has run.
That reaper expires the clients synchronously, so the discount
covers exactly the interval the work is pending.
Delegations need a different bound. This is because deleg_reaper()
only sends CB_RECALL_ANY and does not track how many delegations
were actually returned by the targeted client.
Report the delegations only once NFSD_RECALL_ANY_COOLDOWN_SECS have
passed since the last sweep. deleg_reaper() skips any client it
recalled from within that window, so an earlier scan request cannot
produce another recall.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/netns.h | 6 ++++
fs/nfsd/nfs4state.c | 92 +++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 85 insertions(+), 13 deletions(-)
diff --git a/fs/nfsd/netns.h b/fs/nfsd/netns.h
index ef01a1cf72ac..23923cc4aa47 100644
--- a/fs/nfsd/netns.h
+++ b/fs/nfsd/netns.h
@@ -245,6 +245,12 @@ struct nfsd_net {
struct work_struct nfsd_courtesy_work;
struct work_struct nfsd_deleg_work;
+ /* courtesy scan requests the reaper has not retired yet */
+ atomic_long_t nfsd_shrink_backlog;
+
+ /* when deleg_reaper() last swept the client list */
+ time64_t nfsd_last_recall_any;
+
/* last time an admin-revoke happened for NFSv4.0 */
time64_t nfs40_last_revoke;
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 2dbc49a6dcaa..1e71bb7a29c8 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -5561,41 +5561,88 @@ nfsd4_init_slabs(void)
return -ENOMEM;
}
+#define NFSD_RECALL_ANY_COOLDOWN_SECS 5
+
static unsigned long
nfsd4_courtesy_shrinker_count(struct shrinker *shrink,
struct shrink_control *sc)
{
struct nfsd_net *nn = shrink->private_data;
- long count;
+ long backlog, count;
count = atomic_read(&nn->nfsd_courtesy_clients);
- if (count)
- queue_work(laundry_wq, &nn->nfsd_courtesy_work);
- return (unsigned long)count;
+ if (!count)
+ return 0;
+
+ queue_work(laundry_wq, &nn->nfsd_courtesy_work);
+
+ /* Work already queued is not available to reclaim again. */
+ backlog = atomic_long_read(&nn->nfsd_shrink_backlog);
+ return count > backlog ? count - backlog : 0;
}
static unsigned long
nfsd4_deleg_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
{
struct nfsd_net *nn = shrink->private_data;
+ time64_t elapsed;
long count;
count = atomic_long_read(&nn->nfsd_delegations);
- if (count)
- queue_work(laundry_wq, &nn->nfsd_deleg_work);
- return (unsigned long)count;
+ if (!count)
+ return 0;
+
+ /*
+ * Delegations the last sweep reached stay unreclaimable until
+ * deleg_reaper()'s cooldown expires. CB_RECALL_ANY leaves the
+ * choice of delegations to the client, so there is no return
+ * to wait on instead.
+ */
+ elapsed = ktime_get_boottime_seconds() -
+ READ_ONCE(nn->nfsd_last_recall_any);
+ if (elapsed < NFSD_RECALL_ANY_COOLDOWN_SECS)
+ return 0;
+
+ queue_work(laundry_wq, &nn->nfsd_deleg_work);
+ return count;
}
static unsigned long
-nfsd4_state_shrinker_scan(struct shrinker *shrink, struct shrink_control *sc)
+nfsd4_courtesy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
{
+ struct nfsd_net *nn = shrink->private_data;
+
+ atomic_long_add(sc->nr_to_scan, &nn->nfsd_shrink_backlog);
+ queue_work(laundry_wq, &nn->nfsd_courtesy_work);
+
+ /*
+ * The reaper runs from laundry_wq. Report no progress rather
+ * than claim memory that is not free yet.
+ */
+ return SHRINK_STOP;
+}
+
+static unsigned long
+nfsd4_deleg_shrinker_scan(struct shrinker *shrink, struct shrink_control *sc)
+{
+ struct nfsd_net *nn = shrink->private_data;
+
+ queue_work(laundry_wq, &nn->nfsd_deleg_work);
+
+ /*
+ * The reaper sends CB_RECALL_ANY, so nothing is free when
+ * this returns.
+ */
return SHRINK_STOP;
}
static struct shrinker *
nfsd4_alloc_state_shrinker(struct nfsd_net *nn, const char *name,
unsigned long (*count)(struct shrinker *,
- struct shrink_control *))
+ struct shrink_control *),
+ unsigned long (*scan)(struct shrinker *,
+ struct shrink_control *))
{
struct shrinker *shrink;
@@ -5604,7 +5651,7 @@ nfsd4_alloc_state_shrinker(struct nfsd_net *nn, const char *name,
return NULL;
shrink->count_objects = count;
- shrink->scan_objects = nfsd4_state_shrinker_scan;
+ shrink->scan_objects = scan;
shrink->private_data = nn;
shrinker_register(shrink);
@@ -7989,7 +8036,8 @@ deleg_reaper(struct nfsd_net *nn)
continue;
if (atomic_read(&clp->cl_delegs_in_recall))
continue;
- if (ktime_get_boottime_seconds() - clp->cl_ra_time < 5)
+ if (ktime_get_boottime_seconds() - clp->cl_ra_time <
+ NFSD_RECALL_ANY_COOLDOWN_SECS)
continue;
if (clp->cl_cb_state != NFSD4_CB_UP)
continue;
@@ -8021,6 +8069,12 @@ deleg_reaper(struct nfsd_net *nn)
nfsd4_run_cb(&clp->cl_ra->ra_cb);
}
spin_unlock(&nn->client_lock);
+
+ /*
+ * Stamp the sweep even when no recall went out. A sweep that
+ * found nothing eligible finds nothing on an immediate retry.
+ */
+ WRITE_ONCE(nn->nfsd_last_recall_any, ktime_get_boottime_seconds());
}
static void
@@ -8028,8 +8082,16 @@ nfsd4_courtesy_shrinker_worker(struct work_struct *work)
{
struct nfsd_net *nn = container_of(work, struct nfsd_net,
nfsd_courtesy_work);
+ long backlog;
+ /*
+ * Retire only the requests sampled here, so that requests
+ * arriving while the reaper runs are still discounted by
+ * nfsd4_courtesy_shrinker_count().
+ */
+ backlog = atomic_long_read(&nn->nfsd_shrink_backlog);
courtesy_client_reaper(nn);
+ atomic_long_sub(backlog, &nn->nfsd_shrink_backlog);
}
static void
@@ -10002,17 +10064,21 @@ static int nfs4_state_create_net(struct net *net)
disable_delayed_work(&nn->laundromat_work);
INIT_WORK(&nn->nfsd_courtesy_work, nfsd4_courtesy_shrinker_worker);
INIT_WORK(&nn->nfsd_deleg_work, nfsd4_deleg_shrinker_worker);
+ atomic_long_set(&nn->nfsd_shrink_backlog, 0);
+ nn->nfsd_last_recall_any = 0;
get_net(net);
nn->nfsd_courtesy_shrinker =
nfsd4_alloc_state_shrinker(nn, "nfsd-courtesy",
- nfsd4_courtesy_shrinker_count);
+ nfsd4_courtesy_shrinker_count,
+ nfsd4_courtesy_shrinker_scan);
if (!nn->nfsd_courtesy_shrinker)
goto err_shrinker;
nn->nfsd_deleg_shrinker =
nfsd4_alloc_state_shrinker(nn, "nfsd-delegation",
- nfsd4_deleg_shrinker_count);
+ nfsd4_deleg_shrinker_count,
+ nfsd4_deleg_shrinker_scan);
if (!nn->nfsd_deleg_shrinker)
goto err_deleg_shrinker;
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v5 8/8] NFSD: Apportion CB_RECALL_ANY recalls among clients
2026-08-18 1:08 [PATCH v5 0/8] NFSD: CB_RECALL_ANY fixes and a meaningful keep count Chuck Lever
` (6 preceding siblings ...)
2026-08-18 1:08 ` [PATCH v5 7/8] NFSD: Pace the state shrinker's scan requests Chuck Lever
@ 2026-08-18 1:08 ` Chuck Lever
7 siblings, 0 replies; 9+ messages in thread
From: Chuck Lever @ 2026-08-18 1:08 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, Chuck Lever
deleg_reaper() asks each eligible client to return one delegation
whenever it runs, whether or not anything needs the memory. A
delegation returned before it is needed costs the client an OPEN
when it next touches the file. Nothing sizes the request either.
The delegation scan callback discards nr_to_scan, which is reclaim's
statement of how many objects it wants back.
Record each delegation scan request in nfsd_deleg_backlog and pass
the accumulated total to deleg_reaper(). Handing that total to
every client would ask for it once per client, so scale it by each
client's share of the delegations this sweep can reach. The count
callback reports what is left after the outstanding requests, so
concurrent reclaimers do not each ask for the same delegations.
cl_ra_time keeps the next sweep from returning to the clients this
one reached.
Nothing is recalled until a scan arrives. nfs4_laundromat() is the
exception. It has no scan request to pass, so it computes what must
go for num_delegations to fall below max_delegations, and passes
only this namespace's share.
Signed-off-by: Chuck Lever <cel@kernel.org>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/netns.h | 3 ++
fs/nfsd/nfs4state.c | 131 ++++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 109 insertions(+), 25 deletions(-)
diff --git a/fs/nfsd/netns.h b/fs/nfsd/netns.h
index 23923cc4aa47..0ce7da20aba3 100644
--- a/fs/nfsd/netns.h
+++ b/fs/nfsd/netns.h
@@ -248,6 +248,9 @@ struct nfsd_net {
/* courtesy scan requests the reaper has not retired yet */
atomic_long_t nfsd_shrink_backlog;
+ /* delegation scan requests the reaper has not retired yet */
+ atomic_long_t nfsd_deleg_backlog;
+
/* when deleg_reaper() last swept the client list */
time64_t nfsd_last_recall_any;
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 1e71bb7a29c8..2d10137fa71d 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -93,7 +93,7 @@ static void nfs4_free_ol_stateid(struct nfs4_stid *stid);
static void nfsd4_end_grace(struct nfsd_net *nn);
static void _free_cpntf_state_locked(struct nfsd_net *nn, struct nfs4_cpntf_state *cps);
static void nfsd4_file_hash_remove(struct nfs4_file *fi);
-static void deleg_reaper(struct nfsd_net *nn);
+static void deleg_reaper(struct nfsd_net *nn, unsigned long backlog);
static void nfsd4_drop_revoked_stid(struct nfs4_stid *s)
__releases(&s->sc_client->cl_lock);
@@ -5586,7 +5586,7 @@ nfsd4_deleg_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
{
struct nfsd_net *nn = shrink->private_data;
time64_t elapsed;
- long count;
+ long backlog, count;
count = atomic_long_read(&nn->nfsd_delegations);
if (!count)
@@ -5603,8 +5603,14 @@ nfsd4_deleg_shrinker_count(struct shrinker *shrink, struct shrink_control *sc)
if (elapsed < NFSD_RECALL_ANY_COOLDOWN_SECS)
return 0;
- queue_work(laundry_wq, &nn->nfsd_deleg_work);
- return count;
+ /*
+ * Unlike the courtesy shrinker, this one queues no work.
+ * Nothing is recalled until a scan request arrives. Subtract
+ * the requests already recorded, or concurrent reclaimers
+ * each see the whole namespace and stack a scan on top of it.
+ */
+ backlog = atomic_long_read(&nn->nfsd_deleg_backlog);
+ return count > backlog ? count - backlog : 0;
}
static unsigned long
@@ -5628,6 +5634,7 @@ nfsd4_deleg_shrinker_scan(struct shrinker *shrink, struct shrink_control *sc)
{
struct nfsd_net *nn = shrink->private_data;
+ atomic_long_add(sc->nr_to_scan, &nn->nfsd_deleg_backlog);
queue_work(laundry_wq, &nn->nfsd_deleg_work);
/*
@@ -7877,6 +7884,7 @@ nfs4_laundromat(struct nfsd_net *nn)
struct nfs4_cpntf_state *cps;
struct nfs4_client *clp;
copy_stateid_t *cps_t;
+ long held, host, n;
int i;
if (clients_still_reclaiming(nn)) {
@@ -7990,8 +7998,22 @@ nfs4_laundromat(struct nfsd_net *nn)
/* service the server-to-server copy delayed unmount list */
nfsd4_ssc_expire_umount(nn);
#endif
- if (atomic_long_read(&num_delegations) >= max_delegations)
- deleg_reaper(nn);
+ /*
+ * set_max_delegations() computes a zero max_delegations on a
+ * server with very little memory. @host is a divisor below.
+ */
+ host = atomic_long_read(&num_delegations);
+ if (host && host >= max_delegations) {
+ /*
+ * max_delegations bounds the host, but the laundromat
+ * runs once per network namespace. Requesting the whole
+ * overage in each would multiply the request, so take
+ * only this namespace's share.
+ */
+ held = atomic_long_read(&nn->nfsd_delegations);
+ n = host - max_delegations + 1;
+ deleg_reaper(nn, DIV64_U64_ROUND_UP((u64)n * held, host));
+ }
out:
return max_t(time64_t, lt.new_timeo, NFSD_LAUNDROMAT_MINTIMEOUT);
}
@@ -8019,27 +8041,58 @@ courtesy_client_reaper(struct nfsd_net *nn)
nfs4_process_client_reaplist(&reaplist);
}
+/* The two passes in deleg_reaper() must agree on which clients are asked. */
+static bool
+deleg_reaper_eligible(const struct nfs4_client *clp, time64_t now)
+{
+ if (clp->cl_minorversion == 0)
+ return false;
+ if (clp->cl_state != NFSD4_ACTIVE)
+ return false;
+ if (atomic_read(&clp->cl_delegs_in_recall))
+ return false;
+ if (test_bit(NFSD4_CALLBACK_RUNNING, &clp->cl_ra->ra_cb.cb_flags))
+ return false;
+ if (now - clp->cl_ra_time < NFSD_RECALL_ANY_COOLDOWN_SECS)
+ return false;
+ if (clp->cl_cb_state != NFSD4_CB_UP)
+ return false;
+ return true;
+}
+
static void
-deleg_reaper(struct nfsd_net *nn)
+deleg_reaper(struct nfsd_net *nn, unsigned long backlog)
{
struct list_head *pos, *next;
struct nfs4_client *clp;
+ unsigned long remaining, share, total;
unsigned int count;
+ time64_t now;
+
+ /*
+ * Recalling a delegation before it is needed costs the client
+ * an OPEN when it next touches the file. Leave
+ * nfsd_last_recall_any unstamped so the next sweep is not
+ * delayed.
+ */
+ if (!backlog)
+ return;
+ now = ktime_get_boottime_seconds();
spin_lock(&nn->client_lock);
- list_for_each_safe(pos, next, &nn->client_lru) {
+
+ /*
+ * Only the clients this sweep asks contribute to the
+ * apportionment. Dividing the request among holders that are
+ * skipped under-serves it, and the shortfall goes nowhere:
+ * nfsd4_deleg_shrinker_worker() has already cleared
+ * nfsd_deleg_backlog.
+ */
+ total = 0;
+ list_for_each(pos, &nn->client_lru) {
clp = list_entry(pos, struct nfs4_client, cl_lru);
- if (clp->cl_minorversion == 0)
- continue;
- if (clp->cl_state != NFSD4_ACTIVE)
- continue;
- if (atomic_read(&clp->cl_delegs_in_recall))
- continue;
- if (ktime_get_boottime_seconds() - clp->cl_ra_time <
- NFSD_RECALL_ANY_COOLDOWN_SECS)
- continue;
- if (clp->cl_cb_state != NFSD4_CB_UP)
+ if (!deleg_reaper_eligible(clp, now))
continue;
/*
* This read races with hash_delegation_locked() and
@@ -8047,6 +8100,25 @@ deleg_reaper(struct nfsd_net *nn)
* count only skews the keep value; the next
* laundromat pass sees a more current one.
*/
+ total += data_race(READ_ONCE(clp->cl_deleg_count));
+ }
+ if (!total)
+ goto out;
+
+ /*
+ * Reclaim asks in batches and is not bound by what the count
+ * callback reported, so the backlog can exceed what these
+ * clients hold. Cap it to keep each share within the client's
+ * own count.
+ */
+ backlog = min(backlog, total);
+ remaining = backlog;
+
+ list_for_each_safe(pos, next, &nn->client_lru) {
+ clp = list_entry(pos, struct nfs4_client, cl_lru);
+
+ if (!deleg_reaper_eligible(clp, now))
+ continue;
count = data_race(READ_ONCE(clp->cl_deleg_count));
if (!count)
continue;
@@ -8055,26 +8127,34 @@ deleg_reaper(struct nfsd_net *nn)
/* release in nfsd4_cb_recall_any_release */
kref_get(&clp->cl_nfsdfs.cl_ref);
- clp->cl_ra_time = ktime_get_boottime_seconds();
+ clp->cl_ra_time = now;
/*
- * Ask for a single delegation. Recalling one before it
- * is needed costs the client an OPEN when it next
- * touches the file.
+ * Rounding up guarantees every holder gives up at least
+ * one. The round-up can overshoot @backlog, so stop
+ * once the request is met. client_lru is ordered by
+ * last renewal, so the least active clients are asked
+ * first.
*/
- clp->cl_ra->ra_keep = count - 1;
+ share = DIV64_U64_ROUND_UP((u64)backlog * count, total);
+ share = min(share, remaining);
+ remaining -= share;
+ clp->cl_ra->ra_keep = count - share;
clp->cl_ra->ra_bmval[0] = BIT(RCA4_TYPE_MASK_RDATA_DLG) |
BIT(RCA4_TYPE_MASK_WDATA_DLG) |
BIT(RCA4_TYPE_MASK_DIR_DLG);
trace_nfsd_cb_recall_any(clp->cl_ra);
nfsd4_run_cb(&clp->cl_ra->ra_cb);
+ if (!remaining)
+ break;
}
+out:
spin_unlock(&nn->client_lock);
/*
* Stamp the sweep even when no recall went out. A sweep that
* found nothing eligible finds nothing on an immediate retry.
*/
- WRITE_ONCE(nn->nfsd_last_recall_any, ktime_get_boottime_seconds());
+ WRITE_ONCE(nn->nfsd_last_recall_any, now);
}
static void
@@ -8100,7 +8180,7 @@ nfsd4_deleg_shrinker_worker(struct work_struct *work)
struct nfsd_net *nn = container_of(work, struct nfsd_net,
nfsd_deleg_work);
- deleg_reaper(nn);
+ deleg_reaper(nn, atomic_long_xchg(&nn->nfsd_deleg_backlog, 0));
}
static inline __be32 nfs4_check_fh(struct svc_fh *fhp, struct nfs4_stid *stp)
@@ -10065,6 +10145,7 @@ static int nfs4_state_create_net(struct net *net)
INIT_WORK(&nn->nfsd_courtesy_work, nfsd4_courtesy_shrinker_worker);
INIT_WORK(&nn->nfsd_deleg_work, nfsd4_deleg_shrinker_worker);
atomic_long_set(&nn->nfsd_shrink_backlog, 0);
+ atomic_long_set(&nn->nfsd_deleg_backlog, 0);
nn->nfsd_last_recall_any = 0;
get_net(net);
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread