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