3131_CACHE : Optional [LocalCache ] = None
3232
3333
34+ def get_cache_entry_dir (root : os .PathLike , key : str ) -> Path :
35+ """
36+ Returns the cache directory for a given key.
37+ A three-character prefix subdirectory is used to avoid hitting filesystem limits on the number of entries per folder.
38+ """
39+ return Path (root ) / key [:3 ] / key
40+
41+
3442@dataclass
3543class CacheEntry :
3644 """Internal representation of a cache entry."""
@@ -41,7 +49,7 @@ class CacheEntry:
4149
4250 @property
4351 def path (self ) -> Path :
44- return self .root / self .key
52+ return get_cache_entry_dir ( self .root , self .key )
4553
4654 @property
4755 def artifact_path (self ) -> Path :
@@ -168,28 +176,14 @@ def _store(self, key: str, source_path: Path, metadata: dict[str, Any]) -> Optio
168176 with self ._lock :
169177 self ._root .mkdir (parents = True , exist_ok = True )
170178 self ._ensure_limits (file_size )
171- final_dir = self ._root / key
172- backup_dir : Optional [Path ] = None
173-
174- try :
175- if final_dir .exists ():
176- backup_dir = final_dir .with_name (
177- f"{ final_dir .name } .bak.{ _timestamp_suffix ()} "
178- )
179- os .replace (final_dir , backup_dir )
180- # move tmp_dir into place
181- os .replace (tmp_dir , final_dir )
182- except Exception :
183- # restore backup if needed
184- if backup_dir and backup_dir .exists ():
185- os .replace (backup_dir , final_dir )
186- raise
187- else :
188- entry = CacheEntry (key = key , root = self ._root , metadata = metadata )
189- if backup_dir and backup_dir .exists ():
190- shutil .rmtree (backup_dir , ignore_errors = True )
191- log .debug ("Stored simulation cache entry '%s' (%d bytes)." , key , file_size )
192- return entry
179+ final_dir = get_cache_entry_dir (self ._root , key )
180+ final_dir .parent .mkdir (parents = True , exist_ok = True )
181+ if final_dir .exists ():
182+ shutil .rmtree (final_dir )
183+ os .replace (tmp_dir , final_dir )
184+ entry = CacheEntry (key = key , root = self ._root , metadata = metadata )
185+ log .debug ("Stored simulation cache entry '%s' (%d bytes)." , key , file_size )
186+ return entry
193187 finally :
194188 try :
195189 if tmp_dir .exists ():
@@ -242,20 +236,33 @@ def _evict_by_size(
242236 log .info (f"Simulation cache evicted entry '{ entry .key } ' to reclaim { size } bytes." )
243237
244238 def _iter_entries (self ) -> Iterable [CacheEntry ]:
239+ """Iterate over all cache entries, including those in prefix subdirectories."""
245240 if not self ._root .exists ():
246241 return []
242+
247243 entries : list [CacheEntry ] = []
248- for child in self . _root . iterdir ():
249- if child . name . startswith ( TMP_PREFIX ) or child . name . startswith ( TMP_BATCH_PREFIX ):
250- continue
251- meta_path = child / CACHE_METADATA_NAME
252- if not meta_path . exists ( ):
244+
245+ for prefix_dir in self . _root . iterdir ( ):
246+ if not prefix_dir . is_dir () or prefix_dir . name . startswith (
247+ ( TMP_PREFIX , TMP_BATCH_PREFIX )
248+ ):
253249 continue
254- try :
255- metadata = json .loads (meta_path .read_text (encoding = "utf-8" ))
256- except Exception :
257- metadata = {}
258- entries .append (CacheEntry (key = child .name , root = self ._root , metadata = metadata ))
250+
251+ for child in prefix_dir .iterdir ():
252+ if not child .is_dir ():
253+ continue
254+
255+ meta_path = child / CACHE_METADATA_NAME
256+ if not meta_path .exists ():
257+ continue
258+
259+ try :
260+ metadata = json .loads (meta_path .read_text (encoding = "utf-8" ))
261+ except Exception :
262+ metadata = {}
263+
264+ entries .append (CacheEntry (key = child .name , root = self ._root , metadata = metadata ))
265+
259266 return entries
260267
261268 def _load_entry (self , key : str ) -> Optional [CacheEntry ]:
0 commit comments