Files
2026-07-13 13:22:34 +08:00

254 lines
8.6 KiB
Python

import socket
import subprocess
import sys
from unittest import mock
import pytest
from requests.adapters import HTTPAdapter
from mlflow.utils import request_utils
from mlflow.utils.request_utils import (
TCPKeepAliveHTTPAdapter,
_build_socket_options,
)
def test_request_utils_does_not_import_mlflow(tmp_path):
file_content = f"""
import importlib.util
import os
import sys
file_path = r"{request_utils.__file__}"
module_name = "mlflow.utils.request_utils"
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
assert "mlflow" not in sys.modules
assert "mlflow.utils.request_utils" in sys.modules
"""
test_file = tmp_path.joinpath("test_request_utils_does_not_import_mlflow.py")
test_file.write_text(file_content)
subprocess.check_call([sys.executable, str(test_file)])
class IncompleteResponse:
def __init__(self):
self.headers = {"Content-Length": "100"}
raw = mock.MagicMock()
raw.tell.return_value = 50
self.raw = raw
def __enter__(self):
return self
def __exit__(self, *args):
pass
def test_download_chunk_incomplete_read(tmp_path):
with mock.patch.object(
request_utils, "cloud_storage_http_request", return_value=IncompleteResponse()
):
download_path = tmp_path / "chunk"
download_path.touch()
with pytest.raises(IOError, match="Incomplete read"):
request_utils.download_chunk(
range_start=0,
range_end=999,
headers={},
download_path=download_path,
http_uri="https://example.com",
)
@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
def test_redirects_disabled_if_env_var_set(monkeypatch, env_value):
monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
with mock.patch("requests.Session.request") as mock_request:
mock_request.return_value.status_code = 302
mock_request.return_value.text = "mock response"
response = request_utils.cloud_storage_http_request("GET", "http://localhost:5000")
assert response.text == "mock response"
mock_request.assert_called_once_with(
"GET",
"http://localhost:5000",
allow_redirects=False,
timeout=None,
)
@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
def test_redirects_enabled_if_env_var_set(monkeypatch, env_value):
monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
with mock.patch("requests.Session.request") as mock_request:
mock_request.return_value.status_code = 302
mock_request.return_value.text = "mock response"
response = request_utils.cloud_storage_http_request(
"GET",
"http://localhost:5000",
)
assert response.text == "mock response"
mock_request.assert_called_once_with(
"GET",
"http://localhost:5000",
allow_redirects=True,
timeout=None,
)
@pytest.mark.parametrize("env_value", ["0", "false", "False", "FALSE"])
def test_redirect_kwarg_overrides_env_value_false(monkeypatch, env_value):
monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
with mock.patch("requests.Session.request") as mock_request:
mock_request.return_value.status_code = 302
mock_request.return_value.text = "mock response"
response = request_utils.cloud_storage_http_request(
"GET", "http://localhost:5000", allow_redirects=True
)
assert response.text == "mock response"
mock_request.assert_called_once_with(
"GET",
"http://localhost:5000",
allow_redirects=True,
timeout=None,
)
@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
def test_redirect_kwarg_overrides_env_value_true(monkeypatch, env_value):
monkeypatch.setenv("MLFLOW_ALLOW_HTTP_REDIRECTS", env_value)
with mock.patch("requests.Session.request") as mock_request:
mock_request.return_value.status_code = 302
mock_request.return_value.text = "mock response"
response = request_utils.cloud_storage_http_request(
"GET", "http://localhost:5000", allow_redirects=False
)
assert response.text == "mock response"
mock_request.assert_called_once_with(
"GET",
"http://localhost:5000",
allow_redirects=False,
timeout=None,
)
def test_redirects_enabled_by_default():
with mock.patch("requests.Session.request") as mock_request:
mock_request.return_value.status_code = 302
mock_request.return_value.text = "mock response"
response = request_utils.cloud_storage_http_request(
"GET",
"http://localhost:5000",
)
assert response.text == "mock response"
mock_request.assert_called_once_with(
"GET",
"http://localhost:5000",
allow_redirects=True,
timeout=None,
)
# --- TCP Keepalive tests ---
def test_build_socket_options_includes_keepalive():
options = _build_socket_options()
assert (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) in options
def test_build_socket_options_platform_specific():
options = _build_socket_options()
if hasattr(socket, "TCP_KEEPIDLE"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30) in options
elif hasattr(socket, "TCP_KEEPALIVE"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 30) in options
if hasattr(socket, "TCP_KEEPINTVL"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) in options
if hasattr(socket, "TCP_KEEPCNT"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) in options
def test_build_socket_options_disabled_via_env(monkeypatch):
monkeypatch.setenv("MLFLOW_HTTP_TCP_KEEPALIVE", "false")
options = _build_socket_options()
assert (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) not in options
def test_build_socket_options_custom_values_via_env(monkeypatch):
monkeypatch.setenv("MLFLOW_HTTP_TCP_KEEPALIVE_IDLE", "60")
monkeypatch.setenv("MLFLOW_HTTP_TCP_KEEPALIVE_INTERVAL", "20")
monkeypatch.setenv("MLFLOW_HTTP_TCP_KEEPALIVE_COUNT", "5")
options = _build_socket_options()
if hasattr(socket, "TCP_KEEPIDLE"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) in options
elif hasattr(socket, "TCP_KEEPALIVE"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 60) in options
if hasattr(socket, "TCP_KEEPINTVL"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 20) in options
if hasattr(socket, "TCP_KEEPCNT"):
assert (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) in options
def test_tcp_keepalive_adapter_init_poolmanager():
adapter = TCPKeepAliveHTTPAdapter()
with mock.patch.object(HTTPAdapter, "init_poolmanager") as mock_init:
adapter.init_poolmanager(1, 1)
mock_init.assert_called_once()
_, kwargs = mock_init.call_args
assert "socket_options" in kwargs
assert (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) in kwargs["socket_options"]
def test_tcp_keepalive_adapter_proxy_manager_for():
adapter = TCPKeepAliveHTTPAdapter()
with mock.patch.object(HTTPAdapter, "proxy_manager_for") as mock_proxy:
adapter.proxy_manager_for("http://proxy:8080")
mock_proxy.assert_called_once()
_, kwargs = mock_proxy.call_args
assert "socket_options" in kwargs
assert (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) in kwargs["socket_options"]
def test_tcp_keepalive_adapter_proxy_respects_explicit_options():
adapter = TCPKeepAliveHTTPAdapter()
custom_options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 0)]
with mock.patch.object(HTTPAdapter, "proxy_manager_for") as mock_proxy:
adapter.proxy_manager_for("http://proxy:8080", socket_options=custom_options)
_, kwargs = mock_proxy.call_args
assert kwargs["socket_options"] == custom_options
def test_session_uses_tcp_keepalive_adapter():
request_utils._cached_get_request_session.cache_clear()
session = request_utils._get_request_session(
max_retries=3,
backoff_factor=1,
backoff_jitter=0.5,
retry_codes=(500,),
raise_on_status=True,
respect_retry_after_header=True,
)
assert isinstance(session.get_adapter("https://example.com"), TCPKeepAliveHTTPAdapter)
assert isinstance(session.get_adapter("http://example.com"), TCPKeepAliveHTTPAdapter)
request_utils._cached_get_request_session.cache_clear()