3939
4040logger = logging .getLogger (__name__ )
4141
42+ _MAX_SELECTION_ATTEMPTS = 4
43+
4244_STICKY_GRACE_PERIOD_SECONDS = 10.0
4345_RECOVERABLE_STATUSES = frozenset (
4446 {
@@ -140,12 +142,13 @@ async def load_selection_inputs() -> _SelectionInputs:
140142 )
141143
142144 selected_snapshot : Account | None = None
143- selected_version_at_pick : int = 0
144145 error_message : str | None = None
145146 selected_states : list [AccountState ] = []
146147 selected_account_map : dict [str , Account ] = {}
147148 if sticky_key is None :
149+ attempt = 0
148150 while True :
151+ attempt += 1
149152 self ._prune_runtime (selection_inputs .accounts )
150153 states , account_map = _build_states (
151154 accounts = selection_inputs .accounts ,
@@ -189,11 +192,20 @@ async def load_selection_inputs() -> _SelectionInputs:
189192 selected_snapshot .status = result .account .status
190193 selected_snapshot .deactivation_reason = result .account .deactivation_reason
191194 selected_snapshot .reset_at = selected_reset_at
192- selected_version_at_pick = self ._runtime .get (selected .id , RuntimeState ()).version
193195 else :
194196 error_message = result .error_message
195197
196- pre_persist_versions = {aid : runtime .version for aid , runtime in self ._runtime .items ()}
198+ pre_persist_runtime_state = {
199+ aid : (
200+ runtime .reset_at ,
201+ runtime .cooldown_until ,
202+ runtime .error_count ,
203+ runtime .last_error_at ,
204+ )
205+ for aid , runtime in self ._runtime .items ()
206+ }
207+ pre_persist_cache_generation = self ._selection_inputs_cache .generation
208+
197209 async with self ._repo_factory () as repos :
198210 stale_account_ids = await self ._persist_selection_state (
199211 repos .accounts ,
@@ -202,6 +214,10 @@ async def load_selection_inputs() -> _SelectionInputs:
202214 )
203215 stale_account_ids = stale_account_ids or set ()
204216 if selected_snapshot is not None and selected_snapshot .id in stale_account_ids :
217+ if attempt >= _MAX_SELECTION_ATTEMPTS :
218+ selected_snapshot = None
219+ error_message = None
220+ break
205221 selection_inputs = await load_selection_inputs ()
206222 if selection_inputs .error_code is not None and not selection_inputs .accounts :
207223 return AccountSelection (
@@ -215,37 +231,53 @@ async def load_selection_inputs() -> _SelectionInputs:
215231 selected_account_map = {}
216232 continue
217233
218- if selected_snapshot is not None :
219- _sel_runtime = self ._runtime .get (selected_snapshot .id )
220- _sel_pre_ver = selected_version_at_pick
221- if _sel_runtime is not None and _sel_runtime .version != _sel_pre_ver :
234+ if (
235+ selected_snapshot is not None
236+ and self ._selection_inputs_cache .generation != pre_persist_cache_generation
237+ and attempt < _MAX_SELECTION_ATTEMPTS
238+ ):
239+ selection_inputs = await load_selection_inputs ()
240+ if selection_inputs .error_code is not None and not selection_inputs .accounts :
241+ return AccountSelection (
242+ account = None ,
243+ error_message = selection_inputs .error_message ,
244+ error_code = selection_inputs .error_code ,
245+ )
246+ selected_snapshot = None
247+ error_message = None
248+ selected_states = []
249+ selected_account_map = {}
250+ await asyncio .sleep (0 )
251+ continue
252+
253+ if selected_snapshot is None and error_message == "No available accounts" :
254+ runtime_recovered = any (
255+ self ._runtime .get (account_id , RuntimeState ()).reset_at != before [0 ]
256+ or self ._runtime .get (account_id , RuntimeState ()).cooldown_until != before [1 ]
257+ or self ._runtime .get (account_id , RuntimeState ()).error_count != before [2 ]
258+ or self ._runtime .get (account_id , RuntimeState ()).last_error_at != before [3 ]
259+ for account_id , before in pre_persist_runtime_state .items ()
260+ )
261+ if runtime_recovered and attempt < _MAX_SELECTION_ATTEMPTS :
222262 selection_inputs = await load_selection_inputs ()
223263 if selection_inputs .error_code is not None and not selection_inputs .accounts :
224264 return AccountSelection (
225265 account = None ,
226266 error_message = selection_inputs .error_message ,
227267 error_code = selection_inputs .error_code ,
228268 )
229- selected_snapshot = None
230269 error_message = None
231270 selected_states = []
232271 selected_account_map = {}
272+ await asyncio .sleep (0 )
233273 continue
234274
235- if selected_snapshot is None and error_message == "No available accounts" :
236- runtime_recovered = any (
237- self ._runtime .get (aid , RuntimeState ()).version != pre_persist_versions .get (aid , 0 )
238- for aid in account_map
239- )
240- if runtime_recovered :
241- error_message = None
242- selected_states = []
243- selected_account_map = {}
244- pre_persist_versions = {aid : runtime .version for aid , runtime in self ._runtime .items ()}
245- continue
246275 break
276+
247277 else :
278+ attempt = 0
248279 while True :
280+ attempt += 1
249281 self ._prune_runtime (selection_inputs .accounts )
250282 states , account_map = _build_states (
251283 accounts = selection_inputs .accounts ,
@@ -304,17 +336,20 @@ async def load_selection_inputs() -> _SelectionInputs:
304336 )
305337 stale_account_ids = stale_account_ids or set ()
306338 if selected_snapshot is not None and selected_snapshot .id in stale_account_ids :
339+ selected_snapshot = None
340+ error_message = None
341+ selected_states = []
342+ selected_account_map = {}
343+ if attempt >= _MAX_SELECTION_ATTEMPTS :
344+ break
307345 selection_inputs = await load_selection_inputs ()
308346 if selection_inputs .error_code is not None and not selection_inputs .accounts :
309347 return AccountSelection (
310348 account = None ,
311349 error_message = selection_inputs .error_message ,
312350 error_code = selection_inputs .error_code ,
313351 )
314- selected_snapshot = None
315- error_message = None
316- selected_states = []
317- selected_account_map = {}
352+ await asyncio .sleep (0 )
318353 continue
319354 break
320355
0 commit comments