Skip to content

Commit bc4953d

Browse files
fix(tidy3d): FXC-3885-cache-directory-sharding
1 parent 7a0a13e commit bc4953d

File tree

2 files changed

+42
-35
lines changed

2 files changed

+42
-35
lines changed

tests/test_web/test_local_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from tidy3d.web.api import webapi as web
1414
from tidy3d.web.api.container import WebContainer
1515
from tidy3d.web.api.webapi import load_simulation_if_cached
16-
from tidy3d.web.cache import CACHE_ARTIFACT_NAME, clear, resolve_local_cache
16+
from tidy3d.web.cache import CACHE_ARTIFACT_NAME, clear, get_cache_entry_dir, resolve_local_cache
1717

1818
common.CONNECTION_RETRY_TIME = 0.1
1919

@@ -281,7 +281,7 @@ def _test_checksum_mismatch_triggers_refresh(monkeypatch, tmp_path, basic_simula
281281

282282
cache = resolve_local_cache(use_cache=True)
283283
metadata = cache.list()[0]
284-
corrupted_path = cache.root / metadata["cache_key"] / CACHE_ARTIFACT_NAME
284+
corrupted_path = get_cache_entry_dir(cache.root, metadata["cache_key"]) / CACHE_ARTIFACT_NAME
285285
corrupted_path.write_text("corrupted")
286286

287287
cache._fetch(metadata["cache_key"])

tidy3d/web/cache.py

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
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
3543
class 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

Comments
 (0)