Skip to content

Commit ae0f007

Browse files
awaelchlilantiga
andcommitted
Fix HTTPClient retry for flow/work queue (#19837)
Co-authored-by: Luca Antiga <[email protected]>
1 parent f4cd9df commit ae0f007

File tree

3 files changed

+69
-22
lines changed

3 files changed

+69
-22
lines changed

src/lightning/app/utilities/network.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,32 @@ def _find_free_network_port_cloudspace():
8989
_DEFAULT_REQUEST_TIMEOUT = 30 # seconds
9090

9191

92+
def create_retry_strategy():
93+
return Retry(
94+
# wait time between retries increases exponentially according to: backoff_factor * (2 ** (retry - 1))
95+
# but the the maximum wait time is 120 secs. By setting a large value (2880), we'll make sure clients
96+
# are going to be alive for a very long time (~ 4 days) but retries every 120 seconds
97+
total=_CONNECTION_RETRY_TOTAL,
98+
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
99+
status_forcelist={
100+
408, # Request Timeout
101+
429, # Too Many Requests
102+
*range(500, 600), # Any 5xx Server Error status
103+
},
104+
allowed_methods={
105+
"POST", # Default methods are idempotent, add POST here
106+
*Retry.DEFAULT_ALLOWED_METHODS,
107+
},
108+
)
109+
110+
92111
def _configure_session() -> Session:
93112
"""Configures the session for GET and POST requests.
94113
95114
It enables a generous retrial strategy that waits for the application server to connect.
96115
97116
"""
98-
retry_strategy = Retry(
99-
# wait time between retries increases exponentially according to: backoff_factor * (2 ** (retry - 1))
100-
total=_CONNECTION_RETRY_TOTAL,
101-
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
102-
status_forcelist=[429, 500, 502, 503, 504],
103-
)
117+
retry_strategy = create_retry_strategy()
104118
adapter = HTTPAdapter(max_retries=retry_strategy)
105119
http = requests.Session()
106120
http.mount("https://", adapter)
@@ -157,21 +171,7 @@ def __init__(
157171
self, base_url: str, auth_token: Optional[str] = None, log_callback: Optional[Callable] = None
158172
) -> None:
159173
self.base_url = base_url
160-
retry_strategy = Retry(
161-
# wait time between retries increases exponentially according to: backoff_factor * (2 ** (retry - 1))
162-
# but the the maximum wait time is 120 secs. By setting a large value (2880), we'll make sure clients
163-
# are going to be alive for a very long time (~ 4 days) but retries every 120 seconds
164-
total=_CONNECTION_RETRY_TOTAL,
165-
backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
166-
status_forcelist=[
167-
408, # Request Timeout
168-
429, # Too Many Requests
169-
500, # Internal Server Error
170-
502, # Bad Gateway
171-
503, # Service Unavailable
172-
504, # Gateway Timeout
173-
],
174-
)
174+
retry_strategy = create_retry_strategy()
175175
adapter = CustomRetryAdapter(max_retries=retry_strategy, timeout=_DEFAULT_REQUEST_TIMEOUT)
176176
self.session = requests.Session()
177177

tests/tests_app/cli/test_cmd_install.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from lightning.app.testing.helpers import _RunIf
1010

1111

12+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
1213
@mock.patch("lightning.app.cli.cmd_install.subprocess", mock.MagicMock())
1314
def test_valid_org_app_name():
1415
"""Valid organization name."""
@@ -69,6 +70,7 @@ def test_app_install(tmpdir, monkeypatch):
6970
assert test_app_pip_name in str(new_env_output), f"{test_app_pip_name} should be in the env"
7071

7172

73+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
7274
@mock.patch("lightning.app.cli.cmd_install.subprocess", mock.MagicMock())
7375
def test_valid_org_component_name():
7476
runner = CliRunner()
@@ -135,6 +137,7 @@ def test_component_install(real_component, test_component_pip_name):
135137
), f"{test_component_pip_name} should not be in the env after cleanup"
136138

137139

140+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
138141
def test_prompt_actions():
139142
# TODO: each of these installs must check that a package is installed in the environment correctly
140143
app_to_use = "lightning/invideo"
@@ -164,6 +167,7 @@ def test_prompt_actions():
164167
# result = runner.invoke(lightning_cli.cmd_install.install_app, [app_to_use], input='')
165168

166169

170+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
167171
@mock.patch("lightning.app.cli.cmd_install.subprocess", mock.MagicMock())
168172
def test_version_arg_component(tmpdir, monkeypatch):
169173
monkeypatch.chdir(tmpdir)
@@ -186,6 +190,7 @@ def test_version_arg_component(tmpdir, monkeypatch):
186190
assert result.exit_code == 0
187191

188192

193+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
189194
@mock.patch("lightning.app.cli.cmd_install.subprocess", mock.MagicMock())
190195
@mock.patch("lightning.app.cli.cmd_install.os.chdir", mock.MagicMock())
191196
def test_version_arg_app(tmpdir):
@@ -237,6 +242,7 @@ def test_install_resolve_latest_version(mock_show_install_app_prompt, tmpdir):
237242
assert mock_show_install_app_prompt.call_args[0][0]["version"] == "0.0.4"
238243

239244

245+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
240246
def test_proper_url_parsing():
241247
name = "lightning/invideo"
242248

@@ -311,12 +317,14 @@ def test_install_app_shows_error(tmpdir):
311317
# os.chdir(cwd)
312318

313319

320+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
314321
def test_app_and_component_gallery_app(monkeypatch):
315322
monkeypatch.setattr(cmd_install, "_install_app_from_source", mock.MagicMock())
316323
path = cmd_install.gallery_apps_and_components("lightning/flashy", True, "latest")
317324
assert path == os.path.join(os.getcwd(), "app.py")
318325

319326

327+
@pytest.mark.xfail(strict=False, reason="lightning app cli was deprecated")
320328
def test_app_and_component_gallery_component(monkeypatch):
321329
monkeypatch.setattr(cmd_install, "_install_app_from_source", mock.MagicMock())
322330
path = cmd_install.gallery_apps_and_components("lightning/lit-jupyter", True, "latest")

tests/tests_app/utilities/test_network.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from http.client import HTTPMessage
12
from unittest import mock
23

34
import pytest
45
from lightning.app.core import constants
5-
from lightning.app.utilities.network import find_free_network_port
6+
from lightning.app.utilities.network import HTTPClient, find_free_network_port
67

78

89
def test_find_free_network_port():
@@ -42,3 +43,41 @@ def test_find_free_network_port_cloudspace(_, patch_constants):
4243

4344
# Shouldn't use the APP_SERVER_PORT
4445
assert constants.APP_SERVER_PORT not in ports
46+
47+
48+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._get_conn")
49+
def test_http_client_retry_post(getconn_mock):
50+
getconn_mock.return_value.getresponse.side_effect = [
51+
mock.Mock(status=500, msg=HTTPMessage()),
52+
mock.Mock(status=429, msg=HTTPMessage()),
53+
mock.Mock(status=200, msg=HTTPMessage()),
54+
]
55+
56+
client = HTTPClient(base_url="http://test.url")
57+
r = client.post("/test")
58+
r.raise_for_status()
59+
60+
assert getconn_mock.return_value.request.mock_calls == [
61+
mock.call("POST", "/test", body=None, headers=mock.ANY),
62+
mock.call("POST", "/test", body=None, headers=mock.ANY),
63+
mock.call("POST", "/test", body=None, headers=mock.ANY),
64+
]
65+
66+
67+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._get_conn")
68+
def test_http_client_retry_get(getconn_mock):
69+
getconn_mock.return_value.getresponse.side_effect = [
70+
mock.Mock(status=500, msg=HTTPMessage()),
71+
mock.Mock(status=429, msg=HTTPMessage()),
72+
mock.Mock(status=200, msg=HTTPMessage()),
73+
]
74+
75+
client = HTTPClient(base_url="http://test.url")
76+
r = client.get("/test")
77+
r.raise_for_status()
78+
79+
assert getconn_mock.return_value.request.mock_calls == [
80+
mock.call("GET", "/test", body=None, headers=mock.ANY),
81+
mock.call("GET", "/test", body=None, headers=mock.ANY),
82+
mock.call("GET", "/test", body=None, headers=mock.ANY),
83+
]

0 commit comments

Comments
 (0)