7070
7171logger = init_logger (__name__ )
7272
73+ # Max concurrency for parallel S3 HEAD requests in batched_contains().
74+ _CONTAINS_BATCH_SIZE = 16
75+
7376
7477@dataclass
7578class NixlStorageConfig :
@@ -557,6 +560,40 @@ def nixl_desc_exists(self, meta_info: str) -> bool:
557560 logger .warning (f"NIXL Desc { meta_info } query failed: { exc } " )
558561 return False
559562
563+ def batched_nixl_desc_exists (
564+ self , reg_list : List [tuple [int , int , int , str ]]
565+ ) -> int :
566+ """Check if multiple descriptors exist via a single ``query_memory`` call.
567+
568+ :param reg_list: List of tuples ``(0, 0, 0, meta_info)`` where
569+ *meta_info* is the formatted object-key string.
570+ :return: Number of consecutive descriptors that exist from the
571+ start of the list.
572+ :raises: No exceptions are raised. Errors from the underlying
573+ ``query_memory`` call are caught internally and logged as
574+ warnings; the method returns ``0`` in that case.
575+ """
576+ if not reg_list :
577+ return 0
578+
579+ try :
580+ resp = self .nixl_agent .query_memory (
581+ reg_list , self .backend , mem_type = self .mem_type
582+ )
583+ # nixl api query_memory returns a list of nixlRegDesc
584+ # Count consecutive descriptors that exist (resp[i] is not None)
585+ consecutive_count = 0
586+ for reg_desc in resp :
587+ if reg_desc is not None :
588+ consecutive_count += 1
589+ else :
590+ break
591+
592+ return consecutive_count
593+ except Exception as exc :
594+ logger .warning (f"NIXL batched query failed: { exc } " )
595+ return 0
596+
560597 def close (self ):
561598 self .nixl_agent .release_dlist_handle (self .mem_xfer_handler )
562599 self .nixl_agent .deregister_memory (self .mem_reg_descs )
@@ -1323,6 +1360,29 @@ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
13231360 with self .progress_lock :
13241361 return key in self .progress_set
13251362
1363+ def _exists_in_put_tasks_or_cache (self , key : CacheEngineKey ) -> tuple [bool , bool ]:
1364+ """Check whether key exists in put tasks or presence cache.
1365+
1366+ This method only checks the local data structures and does not
1367+ call the expensive key_exists operation.
1368+
1369+ :param key: The key to check
1370+ :return: Tuple of (found, result) where:
1371+ - found: True if we determined the result locally
1372+ - result: True if key exists, False if key doesn't exist
1373+ (in put tasks)
1374+ """
1375+ # Check if already in progress
1376+ if self .exists_in_put_tasks (key ):
1377+ logger .debug (f"Key { key .chunk_hash :x} is in put tasks" )
1378+ return True , False
1379+
1380+ # Check presence cache before hitting remote storage if not prefetching
1381+ if self ._cache_contains (key .chunk_hash ):
1382+ return True , True
1383+
1384+ return False , False
1385+
13261386 def contains (self , key : CacheEngineKey , pin : bool = False ) -> bool :
13271387 """
13281388 Check whether key is in the storage backend.
@@ -1336,21 +1396,70 @@ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
13361396
13371397 :return: True if the key exists, False otherwise
13381398 """
1339- # Check if already in progress
1340- if self .exists_in_put_tasks (key ):
1341- logger .debug (f"Key { key .chunk_hash :x} is in put tasks" )
1342- return False
1343-
1344- # Check presence cache before hitting remote storage if not prefetching
1345- if self ._cache_contains (key .chunk_hash ):
1346- return True
1399+ # Check local data structures first
1400+ found , local_result = self ._exists_in_put_tasks_or_cache (key )
1401+ if found :
1402+ return local_result
13471403
13481404 xfer_state = self .key_exists (key )
13491405 if xfer_state :
13501406 self ._cache_add (key .chunk_hash )
13511407
13521408 return xfer_state
13531409
1410+ def batched_contains (
1411+ self ,
1412+ keys : List [CacheEngineKey ],
1413+ pin : bool = False ,
1414+ ) -> int :
1415+ """Check whether the keys are in the storage backend.
1416+
1417+ Overrides the sequential base-class implementation to issue a
1418+ single batched ``query_memory`` call for the keys that cannot
1419+ be resolved from local data structures (put-task set and
1420+ presence cache).
1421+
1422+ :param List[CacheEngineKey] keys: The keys of the MemoryObj.
1423+ :param bool pin: Whether to pin the key (not implemented).
1424+ :return: Number of contiguous hit chunks from the start of *keys*.
1425+ :raises: No exceptions are raised. Errors from the underlying
1426+ NIXL batched query are caught internally and logged as
1427+ warnings.
1428+ """
1429+ if not keys :
1430+ return 0
1431+
1432+ # First, do fast sequential check of local data structures
1433+ true_count = 0
1434+ for key in keys :
1435+ found , result = self ._exists_in_put_tasks_or_cache (key )
1436+ if found :
1437+ if result :
1438+ true_count += 1
1439+ else :
1440+ # Found in put tasks (False), stop the loop
1441+ return true_count
1442+ else :
1443+ # Not found locally, break to do expensive checks
1444+ break
1445+
1446+ # If we checked all keys locally, return the count
1447+ if true_count == len (keys ):
1448+ return true_count
1449+
1450+ # For remaining keys, use the new batched_nixl_desc_exists method
1451+ remaining_keys = keys [true_count :]
1452+ reg_list = [(0 , 0 , 0 , self ._format_object_key (key )) for key in remaining_keys ]
1453+
1454+ # Use the agent's batched_nixl_desc_exists method
1455+ consecutive_hits = self .agent .batched_nixl_desc_exists (reg_list )
1456+
1457+ # Update cache for the hits and return total count
1458+ for i in range (consecutive_hits ):
1459+ self ._cache_add (remaining_keys [i ].chunk_hash )
1460+
1461+ return true_count + consecutive_hits
1462+
13541463 async def batched_async_contains (
13551464 self ,
13561465 lookup_id : str ,
@@ -1368,7 +1477,7 @@ async def batched_async_contains(
13681477 """
13691478 n = len (keys )
13701479 idx = 0
1371- batch_size = 16
1480+ batch_size = _CONTAINS_BATCH_SIZE
13721481
13731482 while idx < n :
13741483 batch = keys [idx : idx + batch_size ]
0 commit comments