Skip to content

Commit da5aaa5

Browse files
committed
fix: Delay the first big segment status poll by one interval
The status polling task ran its first query as soon as it started, while get_status() also queries the store whenever no status is cached yet. A status request that arrived while that first query was still in flight found no cached status and sent a second query, so startup could produce two metadata queries instead of one. The polling task now waits one interval before its first query. The inline query in get_status() still keeps the first status accurate, so this matches the ticker semantics the Go SDK uses.
1 parent 4d75813 commit da5aaa5

4 files changed

Lines changed: 62 additions & 2 deletions

File tree

‎ldclient/impl/async_big_segments.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(self, config: AsyncBigSegmentsConfig):
6565

6666
if self.__store:
6767
self.__cache = ExpiringDict(max_len=config.context_cache_size, max_age_seconds=config.context_cache_time)
68-
self.__poll_task = AsyncRepeatingTask.at_interval("ldclient.bigsegment.status-poll", config.status_poll_interval, 0, self.poll_store_and_update_status)
68+
self.__poll_task = AsyncRepeatingTask.at_interval("ldclient.bigsegment.status-poll", config.status_poll_interval, config.status_poll_interval, self.poll_store_and_update_status)
6969

7070
def start(self):
7171
"""Starts the status polling task. Separated from __init__ so the manager

‎ldclient/impl/big_segments.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(self, config: BigSegmentsConfig):
6767

6868
if self.__store:
6969
self.__cache = ExpiringDict(max_len=config.context_cache_size, max_age_seconds=config.context_cache_time)
70-
self.__poll_task = RepeatingTask.at_interval("ldclient.bigsegment.status-poll", config.status_poll_interval, 0, self.poll_store_and_update_status)
70+
self.__poll_task = RepeatingTask.at_interval("ldclient.bigsegment.status-poll", config.status_poll_interval, config.status_poll_interval, self.poll_store_and_update_status)
7171
self.__poll_task.start()
7272

7373
def stop(self):

‎ldclient/testing/impl/test_async_big_segments.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ class MockAsyncBigSegmentStore(AsyncBigSegmentStore):
2626

2727
def __init__(self):
2828
self._membership_queries = []
29+
self._metadata_queries = []
2930
self._memberships = {}
3031
self._metadata_fn = lambda: BigSegmentStoreMetadata(int(time.time() * 1000))
32+
self._metadata_delay = 0.0
3133
self._stopped = False
3234

35+
def setup_metadata_delay(self, delay: float):
36+
"""Makes get_metadata await for the given number of seconds, so a test can
37+
keep a metadata query in flight."""
38+
self._metadata_delay = delay
39+
3340
def setup_membership(self, user_hash: str, membership):
3441
self._memberships[user_hash] = membership
3542

@@ -48,6 +55,9 @@ def _raise():
4855
self._metadata_fn = _raise
4956

5057
async def get_metadata(self) -> BigSegmentStoreMetadata:
58+
self._metadata_queries.append(True)
59+
if self._metadata_delay:
60+
await asyncio.sleep(self._metadata_delay)
5161
return self._metadata_fn()
5262

5363
async def get_membership(self, context_hash: str):
@@ -61,6 +71,10 @@ async def stop(self):
6171
def membership_queries(self):
6272
return list(self._membership_queries)
6373

74+
@property
75+
def metadata_queries(self):
76+
return list(self._metadata_queries)
77+
6478

6579
async def make_started_manager(store, **kwargs):
6680
config = AsyncBigSegmentsConfig(store=store, **kwargs)
@@ -348,3 +362,25 @@ async def test_get_status_with_no_store_configured():
348362
assert status.available is False
349363
finally:
350364
await manager.stop()
365+
366+
367+
@pytest.mark.asyncio
368+
async def test_status_query_at_startup_polls_store_only_once():
369+
"""
370+
The polling task waits one interval before its first query, so a status request at
371+
startup is served by a single inline query. If the task queried the store as soon as
372+
it started, a status request arriving while that query was still in flight would find
373+
no cached status and query a second time.
374+
"""
375+
store = MockAsyncBigSegmentStore()
376+
store.setup_metadata_always_up_to_date()
377+
store.setup_metadata_delay(0.25)
378+
379+
manager = await make_started_manager(store, status_poll_interval=10)
380+
try:
381+
status = await manager.get_status()
382+
assert status.available is True
383+
finally:
384+
await manager.stop()
385+
386+
assert len(store.metadata_queries) == 1

‎ldclient/testing/impl/test_big_segments.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,27 @@ def test_status_polling_detects_stale_status():
185185
assert status3.stale is False
186186
finally:
187187
manager.stop()
188+
189+
190+
def test_status_query_at_startup_polls_store_only_once():
191+
# The polling task waits one interval before its first query, so a status
192+
# request at startup is served by a single inline query. If the task queried
193+
# the store as soon as it started, a status request arriving while that query
194+
# was still in flight would find no cached status and query a second time.
195+
metadata_queries = []
196+
197+
def slow_metadata():
198+
metadata_queries.append(True)
199+
time.sleep(0.25)
200+
return BigSegmentStoreMetadata(time.time() * 1000)
201+
202+
store = MockBigSegmentStore()
203+
store.setup_metadata(slow_metadata)
204+
205+
manager = BigSegmentStoreManager(BigSegmentsConfig(store=store, status_poll_interval=10))
206+
try:
207+
assert manager.status_provider.status.available is True
208+
finally:
209+
manager.stop()
210+
211+
assert len(metadata_queries) == 1

0 commit comments

Comments
 (0)