fix: extend polling budget for slow WorldQuant PnL
This commit is contained in:
@@ -24,6 +24,7 @@ class Settings(BaseSettings):
|
||||
wq_base_url: str = "https://api.worldquantbrain.com"
|
||||
request_timeout: float = 30
|
||||
retry_attempts: int = Field(default=4, ge=1, le=8)
|
||||
pnl_poll_attempts: int = Field(default=120, ge=1, le=1000)
|
||||
enable_runner: bool = True
|
||||
mcp_enabled: bool = False
|
||||
ai_request_limit: int = Field(default=12, ge=1, le=30)
|
||||
|
||||
@@ -268,14 +268,15 @@ class WqClient:
|
||||
async def get(self, path: str, params=None, headers=None):
|
||||
return await self._read_json("GET", path, params=params, headers=headers)
|
||||
|
||||
async def _read_json(self, method: str, path: str, *, allow_list=False, **kwargs):
|
||||
async def _read_json(self, method: str, path: str, *, allow_list=False, poll_attempts=None, **kwargs):
|
||||
"""Authenticated read with shared refresh/retry handling; callers use GET or OPTIONS."""
|
||||
if not self.credentials:
|
||||
raise WqError("请先连接 WorldQuant", "disconnected")
|
||||
if not self.authenticated:
|
||||
await self.authenticate(*self.credentials)
|
||||
refreshed = False
|
||||
for attempt in range(self.settings.retry_attempts):
|
||||
attempts = self.settings.retry_attempts if poll_attempts is None else poll_attempts
|
||||
for attempt in range(attempts):
|
||||
generation = self.auth_generation
|
||||
response = await self._request(method, path, **kwargs)
|
||||
if response.status_code == 401 and not refreshed:
|
||||
@@ -293,7 +294,7 @@ class WqClient:
|
||||
response.headers.get("Retry-After")
|
||||
and self.retry_delay(response.headers["Retry-After"], 0) > 0
|
||||
):
|
||||
if attempt + 1 == self.settings.retry_attempts:
|
||||
if attempt + 1 == attempts:
|
||||
raise WqError("平台数据仍在准备,请稍后重试", "pending")
|
||||
delay = self.retry_delay(response.headers["Retry-After"], attempt)
|
||||
if self.on_retry:
|
||||
@@ -390,7 +391,14 @@ class WqClient:
|
||||
return await self.get(f"/alphas/{alpha_id}")
|
||||
|
||||
async def pnl(self, alpha_id):
|
||||
return await self.get(f"/alphas/{alpha_id}/recordsets/pnl")
|
||||
"""Wait for slow PnL generation separately from transport-error retries.
|
||||
|
||||
Each pending response uses the platform's Retry-After delay. Exhausting
|
||||
the bounded polling budget raises WqError with code 'pending'.
|
||||
"""
|
||||
return await self._read_json(
|
||||
"GET", f"/alphas/{alpha_id}/recordsets/pnl", poll_attempts=self.settings.pnl_poll_attempts
|
||||
)
|
||||
|
||||
async def catalog_page(self, scope, dataset_id, offset):
|
||||
"""Read a single scoped page. IDs are query parameters, never upstream paths."""
|
||||
|
||||
@@ -265,3 +265,49 @@ async def test_concurrent_expiry_authenticates_once(settings):
|
||||
assert await asyncio.gather(client.profile(), client.profile()) == [{"id": "user"}, {"id": "user"}]
|
||||
assert auth_count == 2 # Initial connection plus one shared reauthentication.
|
||||
await client.close()
|
||||
|
||||
|
||||
async def test_slow_pnl_waits_beyond_network_retry_budget(settings):
|
||||
calls, delays = 0, []
|
||||
|
||||
async def sleep(delay):
|
||||
delays.append(delay)
|
||||
|
||||
def handler(request):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
assert request.url.path == '/alphas/LL977PqL/recordsets/pnl'
|
||||
if calls <= 6:
|
||||
return httpx.Response(200, headers={'Retry-After': '1.0'})
|
||||
return httpx.Response(200, json={'records': []})
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler), sleep=sleep)
|
||||
client.credentials, client.authenticated = ('test@example.com', 'secret'), True
|
||||
try:
|
||||
assert await client.pnl('LL977PqL') == {'records': []}
|
||||
assert calls == 7
|
||||
assert delays == [1.0] * 6
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
async def test_pnl_polling_remains_bounded(settings):
|
||||
settings.pnl_poll_attempts = 5
|
||||
delays = []
|
||||
|
||||
async def sleep(delay):
|
||||
delays.append(delay)
|
||||
|
||||
client = WqClient(
|
||||
settings,
|
||||
transport=httpx.MockTransport(lambda r: httpx.Response(200, headers={'Retry-After': '1.0'})),
|
||||
sleep=sleep,
|
||||
)
|
||||
client.credentials, client.authenticated = ('test@example.com', 'secret'), True
|
||||
try:
|
||||
with pytest.raises(WqError) as error:
|
||||
await client.pnl('LL977PqL')
|
||||
assert error.value.code == 'pending'
|
||||
assert delays == [1.0] * 4
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
Reference in New Issue
Block a user