1+ import asyncio
12from typing import Awaitable , Callable , Optional , Tuple
23
34from expiringdict import ExpiringDict
@@ -61,6 +62,7 @@ def __init__(self, config: AsyncBigSegmentsConfig):
6162 self .__stale_after_millis = config .stale_after * 1000
6263 self .__status_provider = AsyncBigSegmentStoreStatusProviderImpl (self .get_status )
6364 self .__last_status = None # type: Optional[BigSegmentStoreStatus]
65+ self .__poll_lock = asyncio .Lock ()
6466 self .__poll_task = None # type: Optional[AsyncRepeatingTask]
6567
6668 if self .__store :
@@ -97,26 +99,37 @@ async def get_user_membership(self, user_key: str) -> Tuple[Optional[dict], str]
9799 except Exception as e :
98100 log .exception ("Big Segment store membership query returned error: %s" % e )
99101 return None , BigSegmentsStatus .STORE_ERROR
100- # First-call fallback: if the polling task hasn't run yet, poll inline now
101- status = self .__last_status
102- if status is None :
103- status = await self .poll_store_and_update_status ()
102+ status = await self .get_status ()
104103 if not status .available :
105104 return membership , BigSegmentsStatus .STORE_ERROR
106105 return membership , BigSegmentsStatus .STALE if status .stale else BigSegmentsStatus .HEALTHY
107106
108107 async def get_status (self ) -> BigSegmentStoreStatus :
109108 """Return the most recently polled status.
110109
111- When no status has been cached yet, poll the store inline and wait for the
112- result, so the status is accurate even if called immediately after start().
110+ When no status has been cached yet, poll the store and wait for the result,
111+ so the status is accurate even if called immediately after start().
113112 """
114113 status = self .__last_status
115- if status is None :
116- status = await self .poll_store_and_update_status ()
117- return status
114+ if status is not None :
115+ return status
116+ # Check again under the lock: another caller may have polled while we waited.
117+ async with self .__poll_lock :
118+ status = self .__last_status
119+ if status is not None :
120+ return status
121+ new_status = await self .__query_store_status ()
122+ self .__notify_status (new_status )
123+ return new_status
118124
119125 async def poll_store_and_update_status (self ) -> BigSegmentStoreStatus :
126+ async with self .__poll_lock :
127+ new_status = await self .__query_store_status ()
128+ self .__notify_status (new_status )
129+ return new_status
130+
131+ async def __query_store_status (self ) -> BigSegmentStoreStatus :
132+ """Queries the store and caches the result. Callers must hold the poll lock."""
120133 new_status = BigSegmentStoreStatus (False , False ) # default to "unavailable" if we don't get a new status below
121134 if self .__store :
122135 try :
@@ -125,8 +138,12 @@ async def poll_store_and_update_status(self) -> BigSegmentStoreStatus:
125138 except Exception as e :
126139 log .exception ("Big Segment store status query returned error: %s" % e )
127140 self .__last_status = new_status
128- self .__status_provider ._update_status (new_status )
129141 return new_status
130142
143+ def __notify_status (self , new_status : BigSegmentStoreStatus ):
144+ """Tells the status provider about a new status, outside the poll lock so
145+ that a listener calling back into the manager cannot deadlock."""
146+ self .__status_provider ._update_status (new_status )
147+
131148 def is_stale (self , timestamp ) -> bool :
132149 return is_stale (timestamp , self .__stale_after_millis )
0 commit comments