DennisChan0909 Claude Opus 4.6 commited on
Commit
b4eaf42
·
1 Parent(s): 024c6e1

fix: replace unbounded caches with TTLCache to prevent memory leaks

Browse files

- fetcher._realtime_cache: dict → TTLCache(maxsize=50, ttl=30)
- news_service._news_cache: dict → TTLCache(maxsize=50, ttl=1800)
- LIGHTWEIGHT_MODE already exists (200→50 trees), just needs env var on Render

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

data/fetcher.py CHANGED
@@ -277,7 +277,8 @@ def get_stock_info(stock_no: str) -> dict:
277
  # non-trading-hours requests (twstock price=None) return stale but valid data.
278
  # ---------------------------------------------------------------------------
279
 
280
- _realtime_cache: dict[str, dict] = {}
 
281
 
282
 
283
  def get_realtime_quote(stock_no: str) -> dict:
 
277
  # non-trading-hours requests (twstock price=None) return stale but valid data.
278
  # ---------------------------------------------------------------------------
279
 
280
+ from cachetools import TTLCache
281
+ _realtime_cache: TTLCache = TTLCache(maxsize=50, ttl=30)
282
 
283
 
284
  def get_realtime_quote(stock_no: str) -> dict:
services/news_service.py CHANGED
@@ -18,9 +18,9 @@ logger = logging.getLogger(__name__)
18
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
19
  _GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
20
 
21
- # In-memory cache: {stock_no: (result, timestamp)}
22
- _news_cache: dict = {}
23
- _NEWS_TTL = 1800 # 30 minutes
24
 
25
 
26
  def get_stock_news(stock_no: str, name: str = "", lang: str = "zh-TW") -> Optional[dict]:
@@ -39,12 +39,11 @@ def get_stock_news(stock_no: str, name: str = "", lang: str = "zh-TW") -> Option
39
  if not GEMINI_API_KEY:
40
  return None
41
 
42
- # Check cache
43
  now = time.time()
44
- if stock_no in _news_cache:
45
- cached, ts = _news_cache[stock_no]
46
- if now - ts < _NEWS_TTL:
47
- return {**cached, "cached": True}
48
 
49
  stock_label = f"{name} ({stock_no})" if name and name != stock_no else stock_no
50
 
@@ -95,7 +94,7 @@ def get_stock_news(stock_no: str, name: str = "", lang: str = "zh-TW") -> Option
95
  "summary": summary,
96
  "source": "gemini",
97
  }
98
- _news_cache[stock_no] = (result, now)
99
  return {**result, "cached": False}
100
 
101
  except Exception as exc:
 
18
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
19
  _GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
20
 
21
+ # In-memory cache with TTL and size limit
22
+ from cachetools import TTLCache
23
+ _news_cache: TTLCache = TTLCache(maxsize=50, ttl=1800)
24
 
25
 
26
  def get_stock_news(stock_no: str, name: str = "", lang: str = "zh-TW") -> Optional[dict]:
 
39
  if not GEMINI_API_KEY:
40
  return None
41
 
42
+ # Check cache (TTLCache handles expiry automatically)
43
  now = time.time()
44
+ cached = _news_cache.get(stock_no)
45
+ if cached is not None:
46
+ return {**cached, "cached": True}
 
47
 
48
  stock_label = f"{name} ({stock_no})" if name and name != stock_no else stock_no
49
 
 
94
  "summary": summary,
95
  "source": "gemini",
96
  }
97
+ _news_cache[stock_no] = result
98
  return {**result, "cached": False}
99
 
100
  except Exception as exc:
tests/test_arch_deploy.py CHANGED
@@ -227,7 +227,8 @@ class TestRealtimeQuoteCache:
227
 
228
  def test_realtime_cache_exists(self):
229
  from data.fetcher import _realtime_cache
230
- assert isinstance(_realtime_cache, dict)
 
231
 
232
  def test_cache_and_return_stores_data(self):
233
  """get_realtime_quote should store results in _realtime_cache."""
 
227
 
228
  def test_realtime_cache_exists(self):
229
  from data.fetcher import _realtime_cache
230
+ from collections.abc import MutableMapping
231
+ assert isinstance(_realtime_cache, MutableMapping)
232
 
233
  def test_cache_and_return_stores_data(self):
234
  """get_realtime_quote should store results in _realtime_cache."""