@@ -391,6 +391,7 @@ def __init__(self, token_id: int, user_data_dir: str, db=None):
391391 self ._shared_reuse_count = 0
392392 self ._consecutive_browser_failures = 0
393393 self ._solve_inflight = 0
394+ self ._last_idle_since = time .monotonic ()
394395 self ._refresh_browser_profile ()
395396
396397 def _refresh_browser_profile (self ):
@@ -752,6 +753,7 @@ async def _get_or_create_shared_browser(self, token_proxy_url: Optional[str] = N
752753 self ._shared_proxy_url = (self ._last_fingerprint or {}).get ("proxy_url" )
753754 self ._shared_launch_count += 1
754755 self ._shared_reuse_count = 0
756+ self .note_idle ()
755757 return playwright , browser , context
756758
757759 async def _capture_page_fingerprint (self , page ):
@@ -1431,6 +1433,18 @@ def handle_request_failed(request):
14311433 def is_busy (self ) -> bool :
14321434 return self ._solve_inflight > 0
14331435
1436+ def note_idle (self ):
1437+ if self ._solve_inflight <= 0 :
1438+ self ._last_idle_since = time .monotonic ()
1439+
1440+ def idle_seconds (self ) -> float :
1441+ if self .is_busy ():
1442+ return 0.0
1443+ return max (0.0 , time .monotonic () - self ._last_idle_since )
1444+
1445+ def has_shared_browser (self ) -> bool :
1446+ return bool (self ._shared_browser or self ._shared_context or self ._shared_keepalive_page )
1447+
14341448 def get_last_fingerprint (self ) -> Optional [Dict [str , Any ]]:
14351449 """返回最近一次打码浏览器的指纹快照。"""
14361450 if not self ._last_fingerprint :
@@ -1495,6 +1509,7 @@ async def get_token(
14951509 return None , None
14961510 finally :
14971511 self ._solve_inflight = max (0 , self ._solve_inflight - 1 )
1512+ self .note_idle ()
14981513
14991514 async def get_custom_token (
15001515 self ,
@@ -1555,6 +1570,7 @@ async def get_custom_token(
15551570 return None
15561571 finally :
15571572 self ._solve_inflight = max (0 , self ._solve_inflight - 1 )
1573+ self .note_idle ()
15581574
15591575 async def get_custom_score (
15601576 self ,
@@ -1624,6 +1640,7 @@ async def get_custom_score(
16241640 }
16251641 finally :
16261642 self ._solve_inflight = max (0 , self ._solve_inflight - 1 )
1643+ self .note_idle ()
16271644
16281645
16291646class BrowserCaptchaService :
@@ -1658,7 +1675,36 @@ def __init__(self, db=None):
16581675
16591676 # ?????? _load_browser_count ???????
16601677 self ._token_semaphore = None
1678+ self ._idle_reaper_task : Optional [asyncio .Task ] = None
16611679
1680+ async def _ensure_idle_reaper (self ):
1681+ if self ._idle_reaper_task is None or self ._idle_reaper_task .done ():
1682+ self ._idle_reaper_task = asyncio .create_task (self ._idle_reaper_loop ())
1683+
1684+ async def _idle_reaper_loop (self ):
1685+ while True :
1686+ try :
1687+ await asyncio .sleep (15 )
1688+ idle_ttl = int (getattr (config , "browser_idle_ttl_seconds" , 600 ) or 600 )
1689+ browsers = []
1690+ async with self ._browsers_lock :
1691+ browsers = list (self ._browsers .values ())
1692+ for browser in browsers :
1693+ try :
1694+ if browser .is_busy ():
1695+ continue
1696+ if not browser .has_shared_browser ():
1697+ continue
1698+ if browser .idle_seconds () < idle_ttl :
1699+ continue
1700+ await browser .recycle_browser (reason = f"idle_ttl_{ idle_ttl } s" , rotate_profile = False )
1701+ except Exception as e :
1702+ debug_logger .log_warning (f"[BrowserCaptcha] idle reaper failed: { e } " )
1703+ except asyncio .CancelledError :
1704+ return
1705+ except Exception as e :
1706+ debug_logger .log_warning (f"[BrowserCaptcha] idle reaper loop error: { e } " )
1707+
16621708 @classmethod
16631709 async def get_instance (cls , db = None ) -> 'BrowserCaptchaService' :
16641710 if cls ._instance is None :
@@ -1667,6 +1713,7 @@ async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
16671713 cls ._instance = cls (db )
16681714 # 从数据库加载 browser_count 配置
16691715 await cls ._instance ._load_browser_count ()
1716+ await cls ._instance ._ensure_idle_reaper ()
16701717 return cls ._instance
16711718
16721719 def _check_available (self ):
@@ -1707,6 +1754,7 @@ async def reload_browser_count(self):
17071754 await self ._load_browser_count ()
17081755
17091756 browsers_to_close : List [TokenBrowser ] = []
1757+ await self ._ensure_idle_reaper ()
17101758 if self ._browser_count < old_count :
17111759 async with self ._browsers_lock :
17121760 for browser_id in list (self ._browsers .keys ()):
@@ -2042,6 +2090,13 @@ async def close(self):
20422090 browsers = list (self ._browsers .values ())
20432091 self ._browsers .clear ()
20442092
2093+ if self ._idle_reaper_task and not self ._idle_reaper_task .done ():
2094+ self ._idle_reaper_task .cancel ()
2095+ try :
2096+ await self ._idle_reaper_task
2097+ except asyncio .CancelledError :
2098+ pass
2099+
20452100 for browser in browsers :
20462101 try :
20472102 await browser .force_close_pending_browser (close_all = True )
0 commit comments