Skip to content

Commit 52f1a88

Browse files
committed
Add API query reliability improvements with retries and keep-alive
Enhanced the do_api_query function with a retry mechanism (up to 3 attempts) for transient connection errors, configurable retry delays, and keep-alive headers. It resets the HTTP session on failures, differentiates between network errors and API errors, and maintains backward compatibility while improving reliability with optimized timeouts (90s for long-polling, 15s default).
1 parent 8c27331 commit 52f1a88

File tree

1 file changed

+44
-1
lines changed

1 file changed

+44
-1
lines changed

zulip/zulip/__init__.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,9 @@ def do_api_query(
565565
longpolling: bool = False,
566566
files: Optional[List[IO[Any]]] = None,
567567
timeout: Optional[float] = None,
568+
max_retries: int = 3, # Max retry attempts
569+
retry_delay: float = 2.0, # Delay between retries
570+
keep_alive: bool = True, # Enable keep-alive mechanism
568571
) -> Dict[str, Any]:
569572
if files is None:
570573
files = []
@@ -585,12 +588,52 @@ def do_api_query(
585588
self.ensure_session()
586589
assert self.session is not None
587590

591+
592+
headers = {
593+
"Connection": "keep-alive" if keep_alive else "close",
594+
"Keep-Alive": "timeout=120, max=100", # Keep connection alive
595+
}
596+
588597
query_state: Dict[str, Any] = {
589598
"had_error_retry": False,
590599
"request": request,
591600
"failures": 0,
592601
}
593602

603+
for attempt in range(max_retries):
604+
try:
605+
if method.upper() == "GET":
606+
response = self.session.get(
607+
url, params=request, timeout=request_timeout, headers=headers
608+
)
609+
elif method.upper() == "POST":
610+
response = self.session.post(
611+
url, data=request, files=req_files, timeout=request_timeout, headers=headers
612+
)
613+
614+
response_data = response.json()
615+
616+
if response.status_code == 200:
617+
return response_data
618+
else:
619+
logging.error("API error: %s", response_data)
620+
return response_data # No retry on API-level errors
621+
622+
except requests.exceptions.ConnectionError:
623+
query_state["failures"] += 1
624+
logging.warning("Connection lost. Retrying %d/%d in %.1f seconds...", attempt + 1, max_retries, retry_delay)
625+
626+
# Reset the session to establish a new connection
627+
self.session.close()
628+
self.ensure_session()
629+
630+
time.sleep(retry_delay)
631+
except requests.exceptions.RequestException as e:
632+
logging.error("Request failed: %s", str(e))
633+
return {"result": "error", "msg": str(e)}
634+
635+
logging.error("Failed after %d retries.", max_retries)
636+
594637
def error_retry(error_string: str) -> bool:
595638
if not self.retry_on_errors or query_state["failures"] >= 10:
596639
return False
@@ -617,7 +660,7 @@ def end_error_retry(succeeded: bool) -> None:
617660
print("Success!")
618661
else:
619662
print("Failed!")
620-
663+
621664
while True:
622665
try:
623666
kwarg = "params" if method == "GET" else "data"

0 commit comments

Comments
 (0)