Skip to content

Commit e1e164e

Browse files
committed
nfs4: add heuristic to issue delegation based on file usage
Motivation: Current logic issues delegation if there is already exist an open on the file. However, the loop like OPEN+READ+CLOSE won't be detected. Modification: Introduce Adaptive Delegation Logic that uses two queues for delegation heuristic. The algorithm works as follows: 1. When a file is accessed for the first time, it is added to the Eviction Queue 2. If a file in the Eviction Queue is accessed again, it is moved to the Active Queue 3. If a file in the Active Queue is accessed again, the system recommends delegation 4. If a file in the Active Queue is not accessed for a specified idle time, it is moved to the Eviction Queue 5. When active queue reaches capacity, the least recently used (LRU) file is pushed into eviction queue 6. When evicted from active queue entry is not accessed for a specified idle time, entry evicted 7. When eviction queue reaches capacity, the least recently used (LRU) file is evicted Result: open-read-close loops are detected and subject for delegation Acked-by: Paul Millar Target: master
1 parent 014c314 commit e1e164e

File tree

2 files changed

+468
-0
lines changed

2 files changed

+468
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/*
2+
* Copyright (c) 2025 Deutsches Elektronen-Synchroton,
3+
* Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY
4+
*
5+
* This library is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU Library General Public License as
7+
* published by the Free Software Foundation; either version 2 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This library is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Library General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Library General Public
16+
* License along with this program (see the file COPYING.LIB for more
17+
* details); if not, write to the Free Software Foundation, Inc.,
18+
* 675 Mass Ave, Cambridge, MA 02139, USA.
19+
*/
20+
package org.dcache.nfs.util;
21+
22+
23+
import com.google.common.annotations.VisibleForTesting;
24+
import org.dcache.nfs.v4.NFS4Client;
25+
import org.dcache.nfs.v4.xdr.clientid4;
26+
import org.dcache.nfs.vfs.Inode;
27+
28+
import java.time.Clock;
29+
import java.time.Duration;
30+
import java.time.Instant;
31+
import java.util.LinkedHashMap;
32+
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.concurrent.locks.Lock;
35+
import java.util.concurrent.locks.ReentrantLock;
36+
37+
/**
38+
* <pre>
39+
///
40+
/// Adaptive Delegation Logic for NFSv4 file delegation heuristic.
41+
///
42+
/// This system uses two queues to track file access patterns:
43+
/// - Eviction Queue: Tracks files that have been accessed only once
44+
/// - Active Queue: Contains files that have been accessed multiple times
45+
///
46+
///
47+
/// The algorithm works as follows:
48+
///
49+
/// 1. When a file is accessed for the first time, it is added to the Eviction Queue
50+
/// 2. If a file in the Eviction Queue is accessed again, it is moved to the Active Queue
51+
/// 3. If a file in the Active Queue is accessed again, the system recommends delegation
52+
/// 4. If a file in the Active Queue is not accessed for a specified idle time, it is moved to the Eviction Queue
53+
/// 5. When active queue reaches capacity, the least recently used (LRU) file is pushed into eviction queue
54+
/// 6. When evicted from active queue entry is not accessed for a specified idle time, entry evicted
55+
/// 7. When eviction queue reaches capacity, the least recently used (LRU) file is evicted
56+
///
57+
///
58+
/// Uses LinkedHashMap to maintain insertion order and LRU eviction policy.
59+
///
60+
///
61+
///
62+
/// +---------------+
63+
/// | File Access |
64+
/// +-------+-------+
65+
/// |
66+
/// v
67+
/// +--------------------+
68+
/// | Is in Active Queue?|
69+
/// +--------+-----------+
70+
/// |
71+
/// +-------------------+-------------------+
72+
/// | No Yes |
73+
/// | |
74+
/// v v
75+
/// +---------------------+ +---------------------+
76+
/// |Is in Eviction Queue?| | is Idletime Excided?|
77+
/// +---------+-----------+ +---------------------+
78+
/// | |
79+
/// +--------------|---------------+ +----------------------+
80+
/// | | | Yes No |
81+
/// | Yes | No | |
82+
/// v v | V
83+
/// +--------------------+ +----------------------+ | +-------------------+
84+
/// |Move to Active Queue| |Add to Eviction Queue | <| | Offer Delegation |
85+
/// +--------------------+ +----------+-----------+ +-------------------+
86+
/// | |
87+
/// v v
88+
/// +---------------+ +---------------+
89+
/// |Is A.Q. Full? | |Is E.Q. Full? |
90+
/// +-------+-------+ +-------+-------+
91+
/// | |
92+
/// +----------+----------+ +----------+----------+
93+
/// | | No | |
94+
/// | Yes +--------+ Yes |
95+
/// v | v
96+
/// +---------------+ v +---------------+
97+
/// |LRU to E.Q. | +--------+ |LRU from E.Q. |
98+
/// +---------------+ | Noop | +---------------+
99+
/// +--------+
100+
///
101+
///
102+
</pre>
103+
*/
104+
105+
public class AdaptiveDelegationLogic {
106+
107+
108+
/**
109+
* Per NFS client queues for file delegation.
110+
*/
111+
private static class ClientQueues {
112+
/**
113+
* Eviction Queue for files accessed once.
114+
*/
115+
final LinkedHashMap<Inode, Instant> evictionQueue;
116+
117+
/**
118+
* Active Queue for files accessed multiple times.
119+
*/
120+
final LinkedHashMap<Inode, Instant> activeQueue;
121+
122+
/**
123+
* Lock for synchronizing access to the queues.
124+
*/
125+
private final Lock lock = new ReentrantLock();
126+
127+
ClientQueues(int maxEvictionQueueSize, int maxActiveQueueSize) {
128+
// Initialize eviction queue with LRU eviction policy
129+
this.evictionQueue = new LinkedHashMap<>(maxEvictionQueueSize, 0.75f, true) {
130+
@Override
131+
protected boolean removeEldestEntry(Map.Entry<Inode, Instant> eldest) {
132+
return size() > maxEvictionQueueSize;
133+
}
134+
};
135+
136+
// Initialize active queue with LRU eviction policy
137+
this.activeQueue = new LinkedHashMap<>(maxActiveQueueSize, 0.75f, true);
138+
}
139+
140+
void lock() {
141+
lock.lock();
142+
}
143+
144+
void unlock() {
145+
lock.unlock();
146+
}
147+
}
148+
149+
150+
/**
151+
* Maximum size of eviction queue.
152+
*/
153+
private final int maxEvictionQueueSize;
154+
155+
/**
156+
* Maximum size of active queue.
157+
*/
158+
private final int maxActiveQueueSize;
159+
160+
/**
161+
* Per-client queue map.
162+
*/
163+
private final Map<clientid4, ClientQueues> clientQueuesMap = new ConcurrentHashMap<>();
164+
165+
/**
166+
* Maximum idle time for files in the active queue.
167+
* Files that exceed this time will be discarded by-passing eviction queue.
168+
*/
169+
private final Duration maxIdleTime;
170+
171+
172+
/**
173+
* Time source for generating timestamps.
174+
* This allows for easier testing and mocking of time-related functionality.
175+
*/
176+
private final Clock clock;
177+
178+
/**
179+
* Creates a new instance of DelegationQueueSystem
180+
*
181+
* @param maxActiveQueueSize Maximum size of active queue.
182+
* @param maxEvictionQueueSize Maximum size of eviction queue.
183+
* @param maxIdleTime Maximum idle time for files in the active queue.
184+
*/
185+
public AdaptiveDelegationLogic(int maxActiveQueueSize, int maxEvictionQueueSize, Duration maxIdleTime) {
186+
this(maxActiveQueueSize, maxEvictionQueueSize, maxIdleTime, Clock.systemDefaultZone());
187+
}
188+
189+
/**
190+
* Creates a new instance of DelegationQueueSystem. For internal use only.
191+
*
192+
* @param maxActiveQueueSize Maximum size of active queue.
193+
* @param maxEvictionQueueSize Maximum size of eviction queue.
194+
* @param maxIdleTime Maximum idle time for files in the active queue.
195+
* @param clock Clock to use for timestamp generation, for testing purposes.
196+
*/
197+
@VisibleForTesting
198+
AdaptiveDelegationLogic(int maxActiveQueueSize, int maxEvictionQueueSize, Duration maxIdleTime, Clock clock) {
199+
this.maxActiveQueueSize = maxActiveQueueSize;
200+
this.maxEvictionQueueSize = maxEvictionQueueSize;
201+
this.maxIdleTime = maxIdleTime;
202+
this.clock = clock;
203+
}
204+
205+
/**
206+
* Get client queues, creating them if they don't exist
207+
*
208+
* @param client NFS client to get queues for
209+
* @return ClientQueues for the specified client
210+
*/
211+
private ClientQueues getClientQueues(NFS4Client client) {
212+
return clientQueuesMap.computeIfAbsent(
213+
client.getId(),
214+
k -> new ClientQueues(maxEvictionQueueSize, maxActiveQueueSize)
215+
);
216+
}
217+
218+
/**
219+
* Check if a file should be delegated based on access frequency.
220+
*
221+
* @param client The NFS client making the request
222+
* @param inode The file to check
223+
* @return true if the file should be delegated, false otherwise
224+
*/
225+
public boolean shouldDelegate(NFS4Client client, Inode inode) {
226+
var clientQueues = getClientQueues(client);
227+
var currentTime = Instant.now(clock);
228+
229+
clientQueues.lock();
230+
try {
231+
// Case 1: File is in Active Queue - offer delegation
232+
var lastAccessTime = clientQueues.activeQueue.get(inode);
233+
if (lastAccessTime != null) {
234+
235+
// Check if the file has been idle for too long
236+
if (lastAccessTime.plus(maxIdleTime).isBefore(currentTime)) {
237+
// Move to Eviction Queue
238+
clientQueues.evictionQueue.put(inode, lastAccessTime);
239+
clientQueues.activeQueue.remove(inode);
240+
return false;
241+
}
242+
243+
// Update access time
244+
clientQueues.activeQueue.put(inode, currentTime);
245+
return true;
246+
}
247+
248+
// Case 2: File is in Eviction Queue - move to Active Queue
249+
if (clientQueues.evictionQueue.containsKey(inode)) {
250+
// Remove from Eviction Queue
251+
clientQueues.evictionQueue.remove(inode);
252+
// Add to Active Queue
253+
clientQueues.activeQueue.put(inode, currentTime);
254+
255+
// Move the least recently used file from Active Queue into Eviction Queue or drop, if too old
256+
if (clientQueues.activeQueue.size() > maxActiveQueueSize) {
257+
258+
var eldestEntry = clientQueues.activeQueue.entrySet().iterator().next();
259+
if (eldestEntry.getValue().plus(maxIdleTime).isAfter(currentTime)) {
260+
clientQueues.evictionQueue.put(eldestEntry.getKey(), eldestEntry.getValue());
261+
}
262+
263+
clientQueues.activeQueue.remove(eldestEntry.getKey());
264+
}
265+
return true;
266+
}
267+
268+
// Case 3: File is not in any queue - add to Eviction Queue
269+
clientQueues.evictionQueue.put(inode, currentTime);
270+
return false;
271+
} finally {
272+
clientQueues.unlock();
273+
}
274+
}
275+
276+
/**
277+
* Check if a file is in the active queue
278+
*
279+
* @param client The NFS client making the request
280+
* @param inode The file to check
281+
* @return true if the file is in the active queue, false otherwise
282+
*/
283+
@VisibleForTesting
284+
synchronized boolean isInActive(NFS4Client client, Inode inode) {
285+
return getClientQueues(client).activeQueue.containsKey(inode);
286+
}
287+
288+
/**
289+
* Check if a file is in the eviction queue
290+
*
291+
* @param client The NFS client making the request
292+
* @param inode The file to check
293+
* @return true if the file is in the eviction queue, false otherwise
294+
*/
295+
@VisibleForTesting
296+
synchronized boolean isInEvictionQueue(NFS4Client client, Inode inode) {
297+
return getClientQueues(client).evictionQueue.containsKey(inode);
298+
}
299+
300+
/**
301+
* Reset queues for a specific client
302+
*
303+
* @param client The NFS client whose queues should be reset
304+
*/
305+
public synchronized void reset(NFS4Client client) {
306+
clientQueuesMap.remove(client.getId());
307+
}
308+
309+
/**
310+
* Reset all queues
311+
*/
312+
public void reset() {
313+
clientQueuesMap.clear();
314+
}
315+
}

0 commit comments

Comments
 (0)