2929import secrets
3030import threading
3131import time
32- from dataclasses import dataclass , field
32+ from dataclasses import dataclass , field , replace
3333from datetime import datetime , timezone
3434from typing import Any , Callable
3535
@@ -64,6 +64,7 @@ class ForgetResult:
6464@dataclass (frozen = True , slots = True , kw_only = True )
6565class _RequestSnapshot (RequestAuth ):
6666 connection : Connection
67+ route : str
6768
6869
6970@dataclass
@@ -778,6 +779,7 @@ def request_auth(self, provider: str, *, pinned: tuple[str, str] | None = None)
778779 client's ``(connection_id, generation)``; a mismatch is
779780 ``connection_changed``, never a silent rebind (AUTH-20.1)."""
780781 provider = self .descriptor (provider ).id
782+ route = provider
781783 document = self .store .read ()
782784 slot , material = self ._route_view (document , provider )
783785 provider = slot .provider
@@ -786,17 +788,17 @@ def request_auth(self, provider: str, *, pinned: tuple[str, str] | None = None)
786788 # have died mid-exchange. Only the lock can tell: wait for it,
787789 # re-read, and reuse the sibling's result; a marker still there
788790 # once we hold the lock is an interrupted renewal (AUTH-20.4).
789- return self ._renew (provider , pinned )
791+ return replace ( self ._renew (provider , pinned ), route = route )
790792 self ._check_selected (provider , slot , material , pinned )
791793 flow = flow_for_material (provider , material ) # type: ignore[arg-type]
792794 assert material is not None
793795 expiry = flow .expiry (material )
794796 if expiry in ("never" , None ):
795- return self ._auth_from (provider , flow , material , slot )
797+ return replace ( self ._auth_from (provider , flow , material , slot ), route = route )
796798 now_ms = int (self ._clock () * 1000 )
797799 if now_ms < expiry - self ._lead_ms (flow .lifetime_s (material )):
798- return self ._auth_from (provider , flow , material , slot )
799- return self ._renew (provider , pinned )
800+ return replace ( self ._auth_from (provider , flow , material , slot ), route = route )
801+ return replace ( self ._renew (provider , pinned ), route = route )
800802
801803 def _check_selected (self , provider : str , slot : _Slot , material : Material | None , pinned : tuple [str , str ] | None ) -> None :
802804 if pinned is not None and (slot .connection_id != pinned [0 ] or str (slot .generation ) != pinned [1 ]):
@@ -832,14 +834,14 @@ def _check_selected(self, provider: str, slot: _Slot, material: Material | None,
832834 provider = provider , connection_id = slot .connection_id ,
833835 )
834836
835- def _auth_from (self , provider : str , flow : Any , material : Material , slot : _Slot ) -> RequestAuth :
837+ def _auth_from (self , provider : str , flow : Any , material : Material , slot : _Slot ) -> _RequestSnapshot :
836838 try :
837839 value = flow .request_auth (material , slot .settings )
838840 connection = slot .connection ()
839841 assert connection is not None
840842 return _RequestSnapshot (
841843 credential = value .credential , headers = dict (value .headers ), base_url = value .base_url ,
842- account_id = value .account_id , named = value .named , connection = connection ,
844+ account_id = value .account_id , named = value .named , connection = connection , route = provider ,
843845 )
844846 except LoginDenied as exc :
845847 raise AuthOperationError (f"{ provider } : { exc } " , reason = "login_required" , stage = "resolution" ,
@@ -854,11 +856,11 @@ def _admit(self, snapshot: _RequestSnapshot) -> None:
854856 """
855857 connection = snapshot .connection
856858 with self .store .transaction () as txn :
857- slot , material = self ._view (txn .read (), connection . provider )
858- self ._check_selected (connection . provider , slot , material ,
859+ slot , material = self ._route_view (txn .read (), snapshot . route )
860+ self ._check_selected (snapshot . route , slot , material ,
859861 (connection .id , connection .identity_generation ))
860862
861- def _renew (self , provider : str , pinned : tuple [str , str ] | None ) -> RequestAuth :
863+ def _renew (self , provider : str , pinned : tuple [str , str ] | None ) -> _RequestSnapshot :
862864 """AUTH-20.4: lock, re-read, reuse a sibling's fresh result, else
863865 mark in-flight, exchange, write — all under the lock."""
864866 ctx = LoginContext (ui = _NoUI (), deadline = self ._monotonic () + 60.0 , provider = provider , clock = self ._monotonic ,
@@ -1024,22 +1026,28 @@ async def status(self, provider: str):
10241026
10251027 async def login (self , provider : str , method : Any = None , ** kwargs : Any ):
10261028 import asyncio
1029+ from concurrent .futures import ThreadPoolExecutor
10271030
10281031 cancel = kwargs .get ("cancel" ) or threading .Event ()
10291032 kwargs ["cancel" ] = cancel
10301033 try :
10311034 return await self ._run (self .sync .login , provider , method , ** kwargs )
10321035 except asyncio .CancelledError :
10331036 cancel .set ()
1034- cleanup = asyncio .create_task (self ._run (self .sync ._cancel_login_attempt , provider , cancel ))
1035- # Repeated task cancellation must not let a reservation outlive
1036- # CancelledError. Do not wait for the provider's worker to return.
1037- while not cleanup .done ():
1038- try :
1039- await asyncio .shield (cleanup )
1040- except asyncio .CancelledError :
1041- continue
1042- cleanup .result ()
1037+ # A blocking login can occupy every default-executor worker.
1038+ # Durable cancellation must not queue behind those workers.
1039+ with ThreadPoolExecutor (max_workers = 1 ) as executor :
1040+ cleanup = asyncio .get_running_loop ().run_in_executor (
1041+ executor , self .sync ._cancel_login_attempt , provider , cancel ,
1042+ )
1043+ # Repeated cancellation must not let the reservation outlive
1044+ # CancelledError. The provider's worker need not return first.
1045+ while not cleanup .done ():
1046+ try :
1047+ await asyncio .shield (cleanup )
1048+ except asyncio .CancelledError :
1049+ continue
1050+ cleanup .result ()
10431051 raise
10441052
10451053 async def set_api_key (self , provider : str , key : str , ** kwargs : Any ):
0 commit comments