File size: 1,446 Bytes
dbf7313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from __future__ import annotations

import http.client
import socket
import time
import urllib.error
import urllib.request
from collections.abc import Callable
from typing import Any

TRANSIENT_HTTP_ERRORS = (
    TimeoutError,
    socket.timeout,
    urllib.error.URLError,
    http.client.RemoteDisconnected,
    ConnectionResetError,
)


def urlopen_with_retry(
    request: urllib.request.Request | str,
    *,
    timeout: int,
    max_retries: int = 5,
    log: Callable[[str], None] | None = None,
    label: str | None = None,
    opener: Callable[..., Any] | None = None,
    sleep: Callable[[float], None] = time.sleep,
) -> Any:
    attempt = 0
    target = label or (request if isinstance(request, str) else request.full_url)
    opener = opener or urllib.request.urlopen
    while True:
        try:
            return opener(request, timeout=timeout)
        except urllib.error.HTTPError:
            raise
        except TRANSIENT_HTTP_ERRORS as exc:
            attempt += 1
            if attempt > max_retries:
                raise RuntimeError(
                    f"HTTP request failed after {max_retries} retries: {target} {exc}"
                ) from exc
            sleep_for = min(2**attempt, 30)
            if log is not None:
                log(
                    f"Transient network failure for {target} (attempt {attempt}/{max_retries}); retrying in {sleep_for}s"
                )
            sleep(sleep_for)