Skip to content

Commit 6fa4e2e

Browse files
committed
[DISCO-3754] Add background task to refresh snapshot cache
1 parent 0072215 commit 6fa4e2e

File tree

3 files changed

+97
-2
lines changed

3 files changed

+97
-2
lines changed

merino/providers/suggest/finance/backends/polygon/backend.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""A wrapper for Polygon API interactions."""
22

3+
import asyncio
34
import itertools
45
import hashlib
56
import logging
@@ -366,6 +367,28 @@ async def store_snapshots_in_cache(self, snapshots: list[TickerSnapshot]) -> Non
366367
],
367368
)
368369

370+
async def refresh_ticker_cache_entries(
371+
self, tickers: list[str], *, await_store: bool = False
372+
) -> None:
373+
"""Refresh ticker snapshot in cache. Fetches new snapshot from upstream API and
374+
fires a background task to write it to the cache.
375+
376+
Note: Only awaits the cache write process if await_store is true. Used only for testing.
377+
"""
378+
snapshots = await self.get_snapshots(tickers)
379+
380+
# early exit if no snapshots returned.
381+
if len(snapshots) == 0:
382+
return
383+
384+
# this parameter is only used for testing.
385+
if await_store:
386+
await self.store_snapshots_in_cache(snapshots)
387+
else:
388+
task = asyncio.create_task(self.store_snapshots_in_cache(snapshots))
389+
# consume/log
390+
task.add_done_callback(lambda t: t.exception())
391+
369392
# TODO @herraj add unit tests for this
370393
def _parse_cached_data(
371394
self, cached_data: list[bytes | None]

tests/integration/providers/suggest/finance/backends/test_polygon.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from merino.providers.suggest.finance.backends.protocol import TickerSnapshot
2727
from merino.providers.suggest.finance.backends.polygon import PolygonBackend
28+
from merino.providers.suggest.finance.backends.polygon.utils import generate_cache_key_for_ticker
2829

2930
logger = logging.getLogger(__name__)
3031

@@ -82,6 +83,29 @@ def fixture_polygon_parameters(
8283
}
8384

8485

86+
@pytest.fixture(name="polygon_factory")
87+
def fixture_polygon_factory(mocker: MockerFixture, statsd_mock: Any, redis_client: Redis):
88+
"""Return factory fixture to create Polygon backend parameters with overrides."""
89+
90+
def _polygon_parameters(**overrides: Any) -> dict[str, Any]:
91+
params = {
92+
"api_key": "api_key",
93+
"metrics_client": statsd_mock,
94+
"http_client": mocker.AsyncMock(spec=AsyncClient),
95+
"metrics_sample_rate": 1,
96+
"url_param_api_key": "apiKey",
97+
"url_single_ticker_snapshot": URL_SINGLE_TICKER_SNAPSHOT,
98+
"url_single_ticker_overview": URL_SINGLE_TICKER_OVERVIEW,
99+
"gcs_uploader": mocker.MagicMock(),
100+
"cache": RedisAdapter(redis_client),
101+
"ticker_ttl_sec": TICKER_TTL_SEC,
102+
}
103+
params.update(overrides)
104+
return params
105+
106+
return _polygon_parameters
107+
108+
85109
@pytest.fixture(name="polygon")
86110
def fixture_polygon(
87111
polygon_parameters: dict[str, Any],
@@ -118,6 +142,14 @@ def fixture_ticker_snapshot_NFLX() -> TickerSnapshot:
118142
)
119143

120144

145+
async def set_redis_key_expiry(
146+
redis_client: Redis, keys_and_expiry: list[tuple[str, int]]
147+
) -> None:
148+
"""Set redis cache key expiry (TTL seconds)."""
149+
for key, ttl in keys_and_expiry:
150+
await redis_client.expire(key, ttl)
151+
152+
121153
@pytest.mark.asyncio
122154
async def test_get_snapshots_from_cache_success(
123155
polygon: PolygonBackend, ticker_snapshot_AAPL: TickerSnapshot, ticker_snapshot_NFLX
@@ -180,3 +212,44 @@ async def test_get_snapshots_from_cache_raises_cache_error(
180212
assert len(records) == 1
181213
assert records[0].message.startswith("Failed to fetch snapshots from Redis: test cache error")
182214
assert actual == []
215+
216+
217+
@pytest.mark.asyncio
218+
async def test_refresh_ticker_cache_entries_success(
219+
polygon_factory,
220+
ticker_snapshot_AAPL: TickerSnapshot,
221+
ticker_snapshot_NFLX,
222+
redis_client: Redis,
223+
mocker,
224+
) -> None:
225+
"""Test that refresh_ticker_cache_entries method successfully writes snapshots to cache with new TTL."""
226+
polygon = PolygonBackend(**polygon_factory(cache=RedisAdapter(redis_client)))
227+
228+
# Mocking the get_snapshots method to return AAPL and NFLX snapshots fixtures for 2 calls.
229+
get_snapshots_mock = mocker.patch.object(
230+
polygon, "get_snapshots", new_callable=mocker.AsyncMock
231+
)
232+
get_snapshots_mock.return_value = [ticker_snapshot_AAPL, ticker_snapshot_NFLX]
233+
234+
expected = [(ticker_snapshot_AAPL, TICKER_TTL_SEC), (ticker_snapshot_NFLX, TICKER_TTL_SEC)]
235+
236+
# write to cache (this method writes with the default 300 sec TTL)
237+
await polygon.store_snapshots_in_cache([ticker_snapshot_AAPL, ticker_snapshot_NFLX])
238+
239+
# manually modify the TTL for the above cache entries to 100 instead of 300
240+
cache_keys = []
241+
for key in ["AAPL", "NFLX"]:
242+
cache_keys.append(generate_cache_key_for_ticker(key))
243+
await set_redis_key_expiry(redis_client, [(cache_keys[0], 100), (cache_keys[1], 100)])
244+
245+
# refresh the cache entries -- this should reset the TTL to 300
246+
# forcing the await here otherwise this task finishes after test execution
247+
await polygon.refresh_ticker_cache_entries(["AAPL", "NFLX"], await_store=True)
248+
249+
actual = await polygon.get_snapshots_from_cache(["AAPL", "NFLX"])
250+
251+
assert actual is not None
252+
assert actual == expected
253+
254+
assert actual[0] == expected[0]
255+
assert actual[1] == expected[1]

tests/unit/providers/suggest/finance/backends/test_polygon.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,7 @@ async def test_get_snapshots_success(
277277
"""
278278
tickers = ["AAPL", "MSFT", "TSLA"]
279279

280-
# Mocking the fetch_ticker_snapshot method to return single_ticker_snapshot_response fixture for two of the calls.
281-
# Returns None for one of the calls.
280+
# Mocking the fetch_ticker_snapshot method to return single_ticker_snapshot_response fixture for all three calls.
282281
fetch_mock = mocker.patch.object(
283282
polygon,
284283
"fetch_ticker_snapshot",

0 commit comments

Comments
 (0)