Skip to content

Commit e1ebfdf

Browse files
committed
Use cachetools.TTLCache for IWC manifest
Per Marius' review: we already depend on cachetools, so swap the hand-rolled module-globals + threading.Lock cache for the standard @cached(cache=TTLCache(maxsize=1, ttl=...)) pattern. clear_manifest_cache() now just calls TTLCache.clear() under the same lock cachetools uses for read/write. Pre-warming via celery beat remains as possible follow-up.
1 parent 4eb591f commit e1ebfdf

1 file changed

Lines changed: 20 additions & 27 deletions

File tree

lib/galaxy/agents/iwc.py

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,49 @@
11
"""IWC (Intergalactic Workflows Commission) manifest fetching and search helpers.
22
3-
The previous attempt at IWC integration kept its manifest cache on
4-
``AgentOperationsManager``, which is instantiated per request, so the cache
5-
never hit. Putting the cache at module scope keeps it shared across requests
6-
within a worker process.
3+
Manifest fetches go through a process-wide TTL cache so the per-request
4+
``AgentOperationsManager`` instances all share the same hit. Pre-warming
5+
the cache via celery beat is a reasonable follow-up.
76
"""
87

98
import logging
109
import re
11-
import threading
12-
import time
10+
from threading import Lock
1311
from typing import (
1412
Any,
1513
Optional,
1614
)
1715

16+
from cachetools import (
17+
cached,
18+
TTLCache,
19+
)
20+
1821
from galaxy.util import requests
1922

2023
log = logging.getLogger(__name__)
2124

2225
IWC_MANIFEST_URL = "https://iwc.galaxyproject.org/workflow_manifest.json"
2326
CACHE_TTL_SECONDS = 60 * 60 # one hour
2427

25-
_cache_lock = threading.Lock()
26-
_cached_manifest: Optional[list[dict[str, Any]]] = None
27-
_cached_at: float = 0.0
28+
_manifest_cache: TTLCache = TTLCache(maxsize=1, ttl=CACHE_TTL_SECONDS)
29+
_manifest_cache_lock = Lock()
2830

2931

3032
def clear_manifest_cache() -> None:
3133
"""Reset the manifest cache. Tests use this; production normally won't."""
32-
global _cached_manifest, _cached_at
33-
with _cache_lock:
34-
_cached_manifest = None
35-
_cached_at = 0.0
34+
with _manifest_cache_lock:
35+
_manifest_cache.clear()
3636

3737

38+
@cached(cache=_manifest_cache, lock=_manifest_cache_lock)
3839
def fetch_manifest(timeout: float = 30.0) -> list[dict[str, Any]]:
3940
"""Fetch the IWC manifest, returning a cached copy when fresh."""
40-
global _cached_manifest, _cached_at
41-
with _cache_lock:
42-
now = time.monotonic()
43-
if _cached_manifest is not None and (now - _cached_at) < CACHE_TTL_SECONDS:
44-
return _cached_manifest
45-
46-
response = requests.get(IWC_MANIFEST_URL, timeout=timeout)
47-
response.raise_for_status()
48-
manifest = response.json()
49-
if not isinstance(manifest, list):
50-
raise ValueError(f"IWC manifest at {IWC_MANIFEST_URL} did not return a JSON array")
51-
_cached_manifest = manifest
52-
_cached_at = now
53-
return manifest
41+
response = requests.get(IWC_MANIFEST_URL, timeout=timeout)
42+
response.raise_for_status()
43+
manifest = response.json()
44+
if not isinstance(manifest, list):
45+
raise ValueError(f"IWC manifest at {IWC_MANIFEST_URL} did not return a JSON array")
46+
return manifest
5447

5548

5649
def all_workflows(manifest: list[dict[str, Any]]) -> list[dict[str, Any]]:

0 commit comments

Comments
 (0)