@@ -367,21 +367,71 @@ async def run_worker(self) -> WorkerTurn:
367367 """Run one leased turn. Safe to call from cron, a loop, or a supervisor.
368368
369369 Returns immediately with an empty turn when nothing is leasable, so a
370- caller can back off rather than spin.
370+ caller can back off rather than spin. Standalone PostgreSQL turns retry
371+ deadlocks and lock timeouts at most twice, after rollback and fenced
372+ release. Each retry must acquire a new lease; other failures propagate.
371373 """
372- with source_turn ():
373- return await self ._run_leased_turn ()
374+ from adcp .reporting .ledger .pg import PgReportingLedgerStore
375+
376+ retryable = (
377+ self ._store ._worker_lock_errors ()
378+ if isinstance (self ._store , PgReportingLedgerStore )
379+ else ()
380+ )
381+ for attempt in range (3 ):
382+ leased = None
383+ with source_turn ():
384+ try :
385+ now = self ._clock ()
386+ leased = await self ._store .lease_period_close (
387+ worker_id = self ._worker_id , now = now , lease_seconds = self ._lease_seconds
388+ )
389+ turn = WorkerTurn (leased = leased )
390+ if leased is None :
391+ return turn
392+ await self ._work_leased_configuration (leased , turn , now = now )
393+ except retryable :
394+ # Store transactions have already exited/rolled back. A
395+ # committed lease is released only under its original fence;
396+ # a competitor may win before the next acquisition.
397+ if leased is not None :
398+ await self ._release_worker_lease (leased , retryable )
399+ if attempt == 2 :
400+ raise
401+ except BaseException as original :
402+ if leased is not None :
403+ try :
404+ await self ._release_worker_lease (leased , retryable )
405+ except retryable :
406+ # Never turn an immutable-content conflict or
407+ # cancellation into a retryable cleanup failure.
408+ raise original from None
409+ raise
410+ else :
411+ # Exhausted release retries escape from here, rather than
412+ # starting another turn or reporting a false empty success.
413+ await self ._release_worker_lease (leased , retryable )
414+ return turn
415+ await asyncio .sleep (0.05 * (2 ** attempt ))
416+ raise AssertionError ("unreachable worker retry" ) # pragma: no cover
417+
418+ async def _release_worker_lease (
419+ self , leased : LeasedConfiguration , retryable : tuple [type [Exception ], ...]
420+ ) -> None :
421+ for attempt in range (3 ):
422+ try :
423+ await self ._store .release_period_close (leased , worker_id = self ._worker_id )
424+ return
425+ except retryable :
426+ if attempt == 2 :
427+ raise
428+ await asyncio .sleep (0.05 * (2 ** attempt ))
374429
375- async def _run_leased_turn (self ) -> WorkerTurn :
430+ async def _work_leased_configuration (
431+ self , leased : LeasedConfiguration , turn : WorkerTurn , * , now : datetime
432+ ) -> None :
376433 from adcp .reporting .production .contracts import _SourceAuthorizationRevokedError
377434
378- now = self ._clock ()
379- leased = await self ._store .lease_period_close (
380- worker_id = self ._worker_id , now = now , lease_seconds = self ._lease_seconds
381- )
382- turn = WorkerTurn (leased = leased )
383- if leased is None :
384- return turn
385435 try :
386436 configuration = next (
387437 (
@@ -395,17 +445,14 @@ async def _run_leased_turn(self) -> WorkerTurn:
395445 None ,
396446 )
397447 if configuration is None :
398- return turn
448+ return
399449 require_account_work (configuration .account_id )
400450 await self ._close_elapsed_periods (configuration , turn , now = now )
401451 await self ._acquire_pending (configuration , turn , now = now )
402452 except _SourceAuthorizationRevokedError :
403453 # Keep pending acquisitions retryable. Authorization may be
404454 # restored on the next turn; it is not a history failure.
405- return turn
406- finally :
407- await self ._store .release_period_close (leased , worker_id = self ._worker_id )
408- return turn
455+ return
409456
410457 async def run_configuration (
411458 self ,
0 commit comments