mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
feat(jina): honor bounded Retry-After recovery (#6352)
* feat(jina): honor bounded Retry-After recovery Closes #6237 Honor server-directed waits for 503 and validly hinted 429 responses within the existing attempt and time budgets. Preserve cancellation, default single-attempt behavior and the tool schema. Add deterministic protocol regressions and stabilize mock setup in the real deadline test. Signed-off-by: 97three <2212371308@qq.com> * test(jina): pin local backoff for hinted 502 and 504 Signed-off-by: 97three <2212371308@qq.com> --------- Signed-off-by: 97three <2212371308@qq.com>
This commit is contained in:
parent
e325c90b24
commit
2afd1211a1
@ -165,7 +165,7 @@ For Google's official Gemini OpenAI-compatible endpoint, use the
|
||||
|
||||
Jina, Browserless, and InfoQuest web fetches resolve relative links and image sources using the requested page URL (or a usable HTML base URL), so returned Markdown includes complete destinations. Link resolution preserves the surrounding HTML source, including malformed-page formatting.
|
||||
|
||||
Jina fetches support opt-in bounded retries via `max_retries` (default `0`) and `retry_budget_seconds` (default `30`) in the tool configuration. Retry waits are randomized within the time budget. Retries may increase upstream requests and cost; see [Jina fetch retries](backend/docs/CONFIGURATION.md#jina-fetch-retries).
|
||||
Jina fetches support opt-in bounded retries via `max_retries` (default `0`) and `retry_budget_seconds` (default `30`) in the tool configuration. Valid `Retry-After` hints set a minimum wait for HTTP 429/503; 429 without a valid hint stays terminal. Hints that cannot fit the remaining budget stop retries. Local backoff remains randomized. Retries may increase upstream requests and cost; see [Jina fetch retries](backend/docs/CONFIGURATION.md#jina-fetch-retries).
|
||||
|
||||
Run `make doctor` at any time to verify your setup and get actionable fix hints.
|
||||
If you are opening a GitHub issue about a local setup or runtime problem, run
|
||||
|
||||
@ -617,13 +617,29 @@ request timeout is capped by the remaining budget; the outer deadline also bound
|
||||
responses that keep delivering data. The existing `timeout` remains Jina's
|
||||
`X-Timeout` header and the per-request HTTP timeout limit.
|
||||
|
||||
Only HTTP 502/503/504 and HTTPX connection-establishment errors (`ConnectError`,
|
||||
`ConnectTimeout`) are retried. Authentication/client errors, 429, other statuses,
|
||||
empty successful responses, read/write timeouts and arbitrary exceptions are not
|
||||
retried. `Retry-After` is not interpreted. Backoff ceilings start at 0.5 seconds,
|
||||
double to 1 and 2 seconds, then stay at 4 seconds. Each asynchronous wait caps its
|
||||
ceiling by the remaining budget and independently samples a uniform factor from
|
||||
0.5 to 1.0, reducing synchronized retries without increasing the wait cap.
|
||||
HTTP 502/503/504 and HTTPX connection-establishment errors (`ConnectError`,
|
||||
`ConnectTimeout`) are retryable. HTTP 429 is retried **only** with a valid
|
||||
`Retry-After` header; missing or malformed hints leave it terminal. HTTP 503
|
||||
uses the same hints, falling back to local backoff when they are absent or invalid.
|
||||
Authentication, payment/credit and other statuses remain terminal even with hints;
|
||||
error-body prose never enables retries. Empty successful responses, read/write
|
||||
timeouts and arbitrary exceptions are not retried.
|
||||
|
||||
`Retry-After` accepts non-negative ASCII integer seconds or an HTTP-date
|
||||
(including obsolete HTTP date forms); past dates mean a zero server floor.
|
||||
Signed/fractional delays and non-HTTP dates are invalid. Local backoff ceilings
|
||||
start at 0.5 seconds, double to 1 and 2, then stay at 4. Each wait caps the local
|
||||
ceiling by the remaining budget and samples a uniform factor from 0.5 to 1.0.
|
||||
The actual wait is the greater of that local pacing and the server floor. A
|
||||
server floor is never reduced by jitter or the 4-second local ceiling. If the
|
||||
hinted wait equals or exceeds the remaining budget (including enormous valid
|
||||
integers), the last HTTP status error is returned without another request.
|
||||
Dates use wall time to compute a delay; requests and waits share one monotonic
|
||||
deadline. Each attempt uses only its own hint.
|
||||
|
||||
Offline mocked regressions cover these policies; they do not establish that
|
||||
Jina's hosted service always supplies recovery hints. No paid-provider testing
|
||||
is required to enable this option.
|
||||
Cancellation propagates during requests and waits. This stops local work; it
|
||||
cannot cancel work already started by Jina. Enabling retries can send up to `1 + max_retries` upstream requests
|
||||
and incur additional cost. Successful content and final `Error:` results retain
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
# Jina web fetch
|
||||
|
||||
Jina opt-in retries stay provider-local: `max_retries=0` preserves one attempt;
|
||||
`retry_budget_seconds` bounds enabled request sequences and asynchronous backoff
|
||||
with one asyncio deadline. Only 502/503/504 and connection-establishment failures
|
||||
retry; cancellation propagates. Backoff ceilings double from 0.5 to 4 seconds.
|
||||
Each wait caps the ceiling by the remaining budget, then multiplies it by a fresh
|
||||
uniform random factor from 0.5 to 1.0 to avoid synchronized retries.
|
||||
Offline coverage: `tests/test_jina_retries.py`.
|
||||
Retries stay provider-local; default `max_retries=0` sends once. One monotonic
|
||||
budget bounds requests/waits; cancellation propagates. Retry 502/503/504 and
|
||||
connection-establishment failures; 429 requires valid Retry-After. Parse ASCII
|
||||
integer seconds or HTTP dates (including obsolete forms); past dates floor at
|
||||
zero. Wait max(server floor, existing budget-capped jittered 0.5–4s backoff).
|
||||
Never reduce server floors; unfit waits return the HTTP error. Reset hints each
|
||||
attempt. Auth/payment errors stay terminal. Tests: `test_jina_retries.py` and
|
||||
`test_jina_retry_after.py` (offline, not hosted-provider validation).
|
||||
|
||||
@ -3,6 +3,10 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
import httpx
|
||||
|
||||
@ -11,6 +15,51 @@ logger = logging.getLogger(__name__)
|
||||
_api_key_warned = False
|
||||
|
||||
|
||||
_DAY = r"(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
|
||||
_MONTH = r"(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
|
||||
_CLOCK = r"[0-9]{2}:[0-9]{2}:[0-9]{2}"
|
||||
_HTTP_DATE = re.compile(
|
||||
rf"(?:{_DAY}, [0-9]{{2}} {_MONTH} [0-9]{{4}} {_CLOCK} GMT"
|
||||
rf"|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), [0-9]{{2}}-{_MONTH}-[0-9]{{2}} {_CLOCK} GMT"
|
||||
rf"|{_DAY} {_MONTH} (?:[0-9]{{2}}| [0-9]) {_CLOCK} [0-9]{{4}})"
|
||||
)
|
||||
|
||||
|
||||
def _retry_after(value: str | None) -> float | None:
|
||||
"""Return a server floor, or None for an invalid HTTP Retry-After value."""
|
||||
if value is None:
|
||||
return None
|
||||
value = value.strip(" \t")
|
||||
if value and value.isascii() and value.isdecimal():
|
||||
digits = value.lstrip("0") or "0"
|
||||
# Bound integer conversion work. Infinity is a valid, unfit floor, not
|
||||
# a parsing failure that could cause an early fallback retry.
|
||||
if len(digits) > 309:
|
||||
return math.inf
|
||||
seconds = int(digits)
|
||||
try:
|
||||
floor = float(seconds)
|
||||
except OverflowError:
|
||||
return math.inf
|
||||
return math.nextafter(floor, math.inf) if floor < seconds else floor
|
||||
if not _HTTP_DATE.fullmatch(value):
|
||||
return None
|
||||
try:
|
||||
date = parsedate_to_datetime(value).replace(tzinfo=UTC)
|
||||
now = time.time()
|
||||
if "-" in value:
|
||||
# RFC 850 two-digit years: choose the most recent matching year
|
||||
# no more than 50 years in the future (RFC 9110 section 5.6.7).
|
||||
current = datetime.fromtimestamp(now, UTC)
|
||||
year = (current.year + 50) // 100 * 100 + date.year % 100
|
||||
if (year, date.month, date.day, date.hour, date.minute, date.second) > (current.year + 50, current.month, current.day, current.hour, current.minute, current.second):
|
||||
year -= 100
|
||||
date = date.replace(year=year)
|
||||
return max(0.0, date.timestamp() - now)
|
||||
except (ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
class JinaClient:
|
||||
async def crawl(self, url: str, return_format: str = "html", timeout: int = 10, proxy: str | None = None, trust_env: bool = True, *, max_retries: int = 0, retry_budget_seconds: float = 30.0) -> str:
|
||||
"""Fetch with optional bounded retries; cancellation always propagates."""
|
||||
@ -46,6 +95,7 @@ class JinaClient:
|
||||
if remaining is not None and remaining <= 0:
|
||||
raise TimeoutError
|
||||
request_timeout = min(timeout, remaining) if remaining is not None else timeout
|
||||
server_floor = None
|
||||
try:
|
||||
response = await client.post("https://r.jina.ai/", headers=headers, json=data, timeout=request_timeout)
|
||||
except (httpx.ConnectError, httpx.ConnectTimeout):
|
||||
@ -58,16 +108,26 @@ class JinaClient:
|
||||
error_message = "Jina API returned empty response"
|
||||
logger.error(error_message)
|
||||
return f"Error: {error_message}"
|
||||
if response.status_code not in {502, 503, 504} or attempt == max_retries:
|
||||
if response.status_code in {429, 503} and attempt < max_retries:
|
||||
server_floor = _retry_after(response.headers.get("Retry-After"))
|
||||
retryable = response.status_code in {502, 503, 504} or (response.status_code == 429 and server_floor is not None)
|
||||
if not retryable or attempt == max_retries:
|
||||
error_message = f"Jina API returned status {response.status_code}: {response.text}"
|
||||
logger.error(error_message)
|
||||
return f"Error: {error_message}"
|
||||
|
||||
# Only allowlisted failures reach the non-blocking wait.
|
||||
remaining = deadline - asyncio.get_running_loop().time()
|
||||
if server_floor is not None and server_floor >= remaining:
|
||||
return f"Error: Jina API returned status {response.status_code}: {response.text}"
|
||||
if remaining <= 0:
|
||||
raise TimeoutError
|
||||
await asyncio.sleep(min(delay, remaining) * random.uniform(0.5, 1.0))
|
||||
wait = min(delay, remaining) * random.uniform(0.5, 1.0)
|
||||
if server_floor is not None:
|
||||
wait = max(wait, server_floor)
|
||||
if wait >= remaining:
|
||||
return f"Error: Jina API returned status {response.status_code}: {response.text}"
|
||||
await asyncio.sleep(wait)
|
||||
delay = min(delay * 2, 4.0)
|
||||
except Exception as e:
|
||||
if isinstance(e, TimeoutError) and max_retries and asyncio.get_running_loop().time() >= deadline:
|
||||
|
||||
@ -100,8 +100,17 @@ async def test_backoff_jitter_is_capped_by_remaining_budget(requests, monkeypatc
|
||||
@pytest.mark.parametrize("during_backoff", [False, True])
|
||||
async def test_shared_deadline(requests, monkeypatch, during_backoff):
|
||||
monkeypatch.setattr(random, "uniform", lambda low, high: 1.0)
|
||||
loop = asyncio.get_running_loop()
|
||||
real_time = loop.time
|
||||
start = 0.0
|
||||
# Freeze only mock-client setup. Once the request starts, the actual asyncio
|
||||
# deadline runs for 50ms; host scheduling cannot consume it before this test
|
||||
# reaches the request/backoff operation it intends to exercise.
|
||||
monkeypatch.setattr(loop, "time", lambda: start)
|
||||
|
||||
async def post(*args, **kwargs):
|
||||
offset = real_time() - start
|
||||
monkeypatch.setattr(loop, "time", lambda: real_time() - offset)
|
||||
assert 0 < kwargs["timeout"] <= 0.05
|
||||
if not during_backoff:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
217
backend/tests/test_jina_retry_after.py
Normal file
217
backend/tests/test_jina_retry_after.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Deterministic, offline provider-directed retry regressions."""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from deerflow.community.jina_ai import jina_client, tools
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def rig(monkeypatch):
|
||||
loop = asyncio.get_running_loop()
|
||||
now = [1000.0] # Exact origin keeps simulated budget arithmetic reproducible.
|
||||
waits = []
|
||||
post = AsyncMock()
|
||||
client = AsyncMock()
|
||||
client.__aenter__.return_value = client
|
||||
client.post = post
|
||||
options = []
|
||||
|
||||
def factory(**kwargs):
|
||||
options.append(kwargs)
|
||||
return client
|
||||
|
||||
async def sleep(delay):
|
||||
waits.append(delay)
|
||||
now[0] += delay
|
||||
|
||||
monkeypatch.setattr(loop, "time", lambda: now[0])
|
||||
monkeypatch.setattr(time, "time", lambda: 784111777.0) # Sun, 06 Nov 1994 08:49:37 GMT
|
||||
monkeypatch.setattr(asyncio, "sleep", sleep)
|
||||
monkeypatch.setattr(jina_client.random, "uniform", lambda low, high: 1.0)
|
||||
monkeypatch.setattr(httpx, "AsyncClient", factory)
|
||||
monkeypatch.setenv("JINA_API_KEY", "offline-test")
|
||||
return SimpleNamespace(post=post, client=client, waits=waits, now=now, options=options)
|
||||
|
||||
|
||||
def response(status, hint=None):
|
||||
return httpx.Response(status, text="provider result", headers={} if hint is None else {"Retry-After": hint})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 503])
|
||||
@pytest.mark.parametrize("hint,wait", [("7", 7), ("0007", 7), (" 7\t", 7), ("0", 0.5), ("Sun, 06 Nov 1994 08:49:44 GMT", 7), ("Sunday, 06-Nov-94 08:49:44 GMT", 7), ("Sun Nov 6 08:49:44 1994", 7), ("Sun, 06 Nov 1994 08:49:30 GMT", 0.5)])
|
||||
async def test_recovery(rig, status, hint, wait):
|
||||
rig.post.side_effect = [response(status, hint), response(200)]
|
||||
assert await jina_client.JinaClient().crawl("https://example.com", max_retries=1) == "provider result"
|
||||
assert rig.waits == [wait]
|
||||
assert rig.post.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 503])
|
||||
@pytest.mark.parametrize("hint", [None, "", "-1", "+1", "1.5", "NaN", "inf", "1e2", "tomorrow", "06 Nov 1994 08:49:44 GMT", "Sun, 06 Nov 1994 08:49:44 +0000", "Sun, 06 Nov 1994 08:49:44 GMT junk", "Sun, 31 Feb 1994 08:49:44 GMT"])
|
||||
async def test_missing_or_malformed(rig, status, hint):
|
||||
rig.post.side_effect = [response(status, hint), response(200)]
|
||||
result = await jina_client.JinaClient().crawl("https://example.com", max_retries=1)
|
||||
assert rig.post.await_count == (1 if status == 429 else 2)
|
||||
assert result == ("Error: Jina API returned status 429: provider result" if status == 429 else "provider result")
|
||||
assert rig.waits == ([] if status == 429 else [0.5])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 503])
|
||||
@pytest.mark.parametrize("hint", ["10", "11", "9" * 5000])
|
||||
async def test_cannot_fit_budget(rig, status, hint):
|
||||
rig.post.return_value = response(status, hint)
|
||||
result = await jina_client.JinaClient().crawl("https://example.com", max_retries=2, retry_budget_seconds=10)
|
||||
assert result == f"Error: Jina API returned status {status}: provider result"
|
||||
assert rig.post.await_count == 1
|
||||
assert rig.waits == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("elapsed", [3, 10])
|
||||
async def test_request_time_consumes_budget(rig, elapsed):
|
||||
async def post(*args, **kwargs):
|
||||
rig.now[0] += elapsed
|
||||
return response(503, "7")
|
||||
|
||||
rig.post.side_effect = post
|
||||
assert "status 503" in await jina_client.JinaClient().crawl("https://example.com", max_retries=2, retry_budget_seconds=10)
|
||||
assert rig.post.await_count == 1
|
||||
assert not rig.waits
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 503])
|
||||
async def test_exhaustion_and_defaults(rig, status):
|
||||
rig.post.return_value = response(status, "7")
|
||||
assert "Error:" in await jina_client.JinaClient().crawl("https://example.com")
|
||||
assert rig.post.await_count == 1
|
||||
assert not rig.waits
|
||||
rig.post.reset_mock()
|
||||
assert f"status {status}" in await jina_client.JinaClient().crawl("https://example.com", max_retries=2)
|
||||
assert rig.post.await_count == 3
|
||||
assert rig.waits == [7, 7]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [502, 504])
|
||||
async def test_other_retryable_statuses_ignore_retry_after(rig, status):
|
||||
rig.post.side_effect = [response(status, "7"), response(200)]
|
||||
assert await jina_client.JinaClient().crawl("https://example.com", max_retries=1) == "provider result"
|
||||
assert rig.waits == [0.5]
|
||||
assert rig.post.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [401, 402, 403, 409, 500, 418])
|
||||
async def test_other_statuses_terminal(rig, status):
|
||||
rig.post.return_value = response(status, "1")
|
||||
assert f"status {status}" in await jina_client.JinaClient().crawl("https://example.com", max_retries=2)
|
||||
assert rig.post.await_count == 1
|
||||
assert not rig.waits
|
||||
|
||||
|
||||
async def test_changing_hints_and_connection_failure(rig):
|
||||
rig.post.side_effect = [response(503, "7"), httpx.ConnectError("offline"), response(429, "3"), response(503), response(200)]
|
||||
assert await jina_client.JinaClient().crawl("https://example.com", max_retries=4, retry_budget_seconds=20) == "provider result"
|
||||
assert rig.waits == [7, 1, 3, 4]
|
||||
assert [call.kwargs["timeout"] for call in rig.post.await_args_list] == [10, 10, 10, 9, 5]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("during_sleep", [True, False])
|
||||
async def test_cancellation(rig, monkeypatch, during_sleep):
|
||||
async def cancel(*args, **kwargs):
|
||||
raise asyncio.CancelledError
|
||||
|
||||
if during_sleep:
|
||||
rig.post.return_value = response(429, "7")
|
||||
monkeypatch.setattr(asyncio, "sleep", cancel)
|
||||
else:
|
||||
rig.post.side_effect = cancel
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await jina_client.JinaClient().crawl("https://example.com", max_retries=2)
|
||||
assert rig.post.await_count == 1
|
||||
|
||||
|
||||
async def test_web_fetch_integration(rig, monkeypatch):
|
||||
config = SimpleNamespace(model_extra={"max_retries": 1, "retry_budget_seconds": 8, "proxy": "http://proxy:8080", "trust_env": False, "timeout": 6})
|
||||
monkeypatch.setattr(tools, "get_app_config", lambda: SimpleNamespace(get_tool_config=lambda name: config))
|
||||
rig.post.side_effect = [response(429, "7"), response(402, "1")]
|
||||
assert await tools.web_fetch_tool.ainvoke({"url": "https://example.com"}) == "Error: Jina API returned status 402: provider result"
|
||||
assert rig.waits == [7]
|
||||
assert rig.options == [{"proxy": "http://proxy:8080", "trust_env": False}]
|
||||
assert [call.kwargs["timeout"] for call in rig.post.await_args_list] == [6, 1]
|
||||
assert set(tools.web_fetch_tool.args) == {"url"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["١", "1", "²", "\n7", "7\n"])
|
||||
async def test_non_ascii_or_non_http_whitespace(value):
|
||||
assert jina_client._retry_after(value) is None
|
||||
|
||||
|
||||
async def test_obsolete_year_window(rig):
|
||||
# At the fixture's 1994 wall time, 00 means 2000, not 1900.
|
||||
assert jina_client._retry_after("Saturday, 01-Jan-00 00:00:00 GMT") > 0
|
||||
assert jina_client._retry_after("Sunday, 06-Nov-94 08:49:44 GMT") == 7
|
||||
assert jina_client._retry_after("Monday, 01-Jan-45 00:00:00 GMT") == 0
|
||||
|
||||
|
||||
async def test_local_pacing_cannot_fit_hinted_budget(rig):
|
||||
rig.post.return_value = response(503, "0")
|
||||
assert "status 503" in await jina_client.JinaClient().crawl("https://example.com", max_retries=1, retry_budget_seconds=0.25)
|
||||
assert not rig.waits
|
||||
assert rig.post.await_count == 1
|
||||
|
||||
|
||||
async def test_later_429_without_hint_is_terminal(rig):
|
||||
rig.post.side_effect = [response(503, "7"), response(429)]
|
||||
assert "status 429" in await jina_client.JinaClient().crawl("https://example.com", max_retries=3)
|
||||
assert rig.waits == [7]
|
||||
assert rig.post.await_count == 2
|
||||
|
||||
|
||||
async def test_future_date_cannot_fit_budget(rig):
|
||||
rig.post.return_value = response(503, "Sun, 06 Nov 1994 08:49:47 GMT")
|
||||
assert "status 503" in await jina_client.JinaClient().crawl("https://example.com", max_retries=1, retry_budget_seconds=10)
|
||||
assert rig.post.await_count == 1
|
||||
assert not rig.waits
|
||||
|
||||
|
||||
async def test_oversleep_does_not_send_after_deadline(rig, monkeypatch):
|
||||
async def oversleep(delay):
|
||||
rig.now[0] += 10
|
||||
|
||||
monkeypatch.setattr(asyncio, "sleep", oversleep)
|
||||
rig.post.return_value = response(429, "7")
|
||||
assert "budget exhausted" in await jina_client.JinaClient().crawl("https://example.com", max_retries=2, retry_budget_seconds=10)
|
||||
assert rig.post.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 503])
|
||||
@pytest.mark.parametrize("jitter", [0.5, 0.75])
|
||||
async def test_jitter_never_shortens_server_floor(rig, monkeypatch, status, jitter):
|
||||
monkeypatch.setattr(jina_client.random, "uniform", lambda low, high: jitter)
|
||||
rig.post.side_effect = [response(status, "7"), response(200)]
|
||||
assert await jina_client.JinaClient().crawl("https://example.com", max_retries=1) == "provider result"
|
||||
assert rig.waits == [7]
|
||||
assert rig.post.await_count == 2
|
||||
|
||||
|
||||
async def test_client_setup_consumes_shared_budget(rig):
|
||||
async def enter():
|
||||
rig.now[0] += 10
|
||||
return rig.client
|
||||
|
||||
rig.client.__aenter__.side_effect = enter
|
||||
result = await jina_client.JinaClient().crawl("https://example.com", max_retries=1, retry_budget_seconds=10)
|
||||
assert "budget exhausted" in result
|
||||
rig.post.assert_not_awaited()
|
||||
assert not rig.waits
|
||||
Loading…
x
Reference in New Issue
Block a user