35
35
import java .util .Iterator ;
36
36
import java .util .Map ;
37
37
import java .util .Optional ;
38
+ import java .util .concurrent .ConcurrentHashMap ;
38
39
import java .util .concurrent .ConcurrentLinkedDeque ;
39
40
import java .util .concurrent .ExecutionException ;
40
41
import java .util .concurrent .ExecutorService ;
41
42
import java .util .concurrent .Executors ;
42
43
import java .util .concurrent .TimeUnit ;
43
44
import java .util .concurrent .atomic .AtomicBoolean ;
45
+ import java .util .concurrent .locks .ReentrantLock ;
44
46
45
47
/**
46
48
* Manages native memory allocations made by JNI.
@@ -56,6 +58,7 @@ public class NativeMemoryCacheManager implements Closeable {
56
58
57
59
private Cache <String , NativeMemoryAllocation > cache ;
58
60
private Deque <String > accessRecencyQueue ;
61
+ private final ConcurrentHashMap <String , ReentrantLock > indexLocks = new ConcurrentHashMap <>();
59
62
private final ExecutorService executor ;
60
63
private AtomicBoolean cacheCapacityReached ;
61
64
private long maxWeight ;
@@ -306,6 +309,55 @@ public CacheStats getCacheStats() {
306
309
return cache .stats ();
307
310
}
308
311
312
+ /**
313
+ * Opens a vector index with proper locking mechanism to ensure thread safety.
314
+ * The method uses a ReentrantLock to synchronize access to the index file and
315
+ * cleans up the lock when no other threads are waiting.
316
+ *
317
+ * @param key the unique identifier for the index
318
+ * @param nativeMemoryEntryContext the context containing vector index information
319
+ */
320
+ private void open (String key , NativeMemoryEntryContext nativeMemoryEntryContext ) {
321
+ ReentrantLock indexFileLock = indexLocks .computeIfAbsent (key , k -> new ReentrantLock ());
322
+ try {
323
+ indexFileLock .lock ();
324
+ nativeMemoryEntryContext .open ();
325
+ } finally {
326
+ indexFileLock .unlock ();
327
+ if (!indexFileLock .hasQueuedThreads ()) {
328
+ indexLocks .remove (key , indexFileLock );
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Retrieves an entry from the cache and updates its access recency if found.
335
+ * This method combines cache access with recency queue management to maintain
336
+ * the least recently used (LRU) order of cached entries.
337
+ *
338
+ * @param key the unique identifier for the cached entry
339
+ * @return the cached NativeMemoryAllocation if present, null otherwise
340
+ */
341
+ private NativeMemoryAllocation getFromCacheAndUpdateRecency (String key ) {
342
+ NativeMemoryAllocation result = cache .getIfPresent (key );
343
+ if (result != null ) {
344
+ updateAccessRecency (key );
345
+ }
346
+ return result ;
347
+ }
348
+
349
+ /**
350
+ * Updates the access recency of a cached entry by moving it to the end of the queue.
351
+ * This method maintains the least recently used (LRU) order by removing the entry
352
+ * from its current position and adding it to the end of the queue.
353
+ *
354
+ * @param key the unique identifier for the cached entry whose recency needs to be updated
355
+ */
356
+ private void updateAccessRecency (String key ) {
357
+ accessRecencyQueue .remove (key );
358
+ accessRecencyQueue .addLast (key );
359
+ }
360
+
309
361
/**
310
362
* Retrieves NativeMemoryAllocation associated with the nativeMemoryEntryContext.
311
363
*
@@ -338,23 +390,28 @@ public NativeMemoryAllocation get(NativeMemoryEntryContext<?> nativeMemoryEntryC
338
390
// In case of a cache miss, least recently accessed entries are evicted in a blocking manner
339
391
// before the new entry can be added to the cache.
340
392
String key = nativeMemoryEntryContext .getKey ();
341
- NativeMemoryAllocation result = cache .getIfPresent (key );
342
393
343
394
// Cache Hit
344
395
// In case of a cache hit, moving the item to the end of the recency queue adds
345
396
// some overhead to the get operation. This can be optimized further to make this operation
346
397
// as lightweight as possible. Multiple approaches and their outcomes were documented
347
398
// before moving forward with the current solution.
348
399
// The details are outlined here: https://github.com/opensearch-project/k-NN/pull/2015#issuecomment-2327064680
400
+ NativeMemoryAllocation result = getFromCacheAndUpdateRecency (key );
349
401
if (result != null ) {
350
- accessRecencyQueue .remove (key );
351
- accessRecencyQueue .addLast (key );
352
402
return result ;
353
403
}
354
404
355
405
// Cache Miss
356
406
// Evict before put
407
+ // open the graph file before proceeding to load the graph into memory
408
+ open (key , nativeMemoryEntryContext );
357
409
synchronized (this ) {
410
+ // recheck if another thread already loaded this entry into the cache
411
+ result = getFromCacheAndUpdateRecency (key );
412
+ if (result != null ) {
413
+ return result ;
414
+ }
358
415
if (getCacheSizeInKilobytes () + nativeMemoryEntryContext .calculateSizeInKB () >= maxWeight ) {
359
416
Iterator <String > lruIterator = accessRecencyQueue .iterator ();
360
417
while (lruIterator .hasNext ()
@@ -376,7 +433,12 @@ public NativeMemoryAllocation get(NativeMemoryEntryContext<?> nativeMemoryEntryC
376
433
return result ;
377
434
}
378
435
} else {
379
- return cache .get (nativeMemoryEntryContext .getKey (), nativeMemoryEntryContext ::load );
436
+ // open graphFile before load
437
+ try (nativeMemoryEntryContext ) {
438
+ String key = nativeMemoryEntryContext .getKey ();
439
+ open (key , nativeMemoryEntryContext );
440
+ return cache .get (key , nativeMemoryEntryContext ::load );
441
+ }
380
442
}
381
443
}
382
444
0 commit comments