Skip to content

Commit 78b2e8c

Browse files
GitHKAndrei Neagu
and
Andrei Neagu
authored
timeouts for http clients (#1918)
* trying 1s timeout for webservr proxy * adding timeout for the rest client * adding timeout for the file manager downloads * adding timeouts for httpx connections * removed autoformatting * added missing dependency * bounded clientsessin requests @sanderegg please check * services are expected to respond in 10 seconds max * bumping timeouts to 20 seconds up from 10 * added timeout for requests in servicelib * bumping to 5 seconds for good measure * bumping to 5 seconds for good measure * timeout moved to enviornment var * timeout moved to enviornment variable * migrated settings to enviornment * fixed broken test * fixed broken test * fixed issue due to missleading naming * fixed type * moving ClientRequestSettings to models-library * settings are recovered via app instance * bumped to 5 seconds from 1 * refactoring arguments @sanderegg * loading form env vars * added changes to autogen code * loading for env via pydantic * added missing commits * updated import path * corrected wrong path * added test to make sure timeouts are in palce * removing beacause it is already in models_library * refactored to use fixture for current directory path * refactored the test - provided a more helpful error - checks for the timeout=timeout parameter Co-authored-by: Andrei Neagu <[email protected]>
1 parent de91cf6 commit 78b2e8c

File tree

22 files changed

+241
-33
lines changed

22 files changed

+241
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Optional
2+
3+
from pydantic import BaseSettings, Field
4+
5+
6+
class ClientRequestSettings(BaseSettings):
7+
# NOTE: when updating the defaults please make sure to search for the env vars
8+
# in all the project, they also need to be updated inside the service-library
9+
total_timeout: Optional[int] = Field(
10+
default=20,
11+
description="timeout used for outgoing http requests",
12+
env="HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT",
13+
)
14+
15+
aiohttp_connect_timeout: Optional[int] = Field(
16+
default=5,
17+
description=(
18+
"aiohttp specific field used in ClientTimeout, timeout for connecting to a "
19+
"peer for a new connection waiting for a free connection from a pool if "
20+
"pool connection limits are exceeded"
21+
),
22+
env="HTTP_CLIENT_REQUEST_AIOHTTP_CONNECT_TIMEOUT",
23+
)
24+
25+
aiohttp_sock_connect_timeout: Optional[int] = Field(
26+
default=5,
27+
description=(
28+
"aiohttp specific field used in ClientTimeout, timeout for connecting to a "
29+
"peer for a new connection not given a pool"
30+
),
31+
env="HTTP_CLIENT_REQUEST_AIOHTTP_SOCK_CONNECT_TIMEOUT",
32+
)

packages/service-library/src/servicelib/client_session.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,40 @@
55
"""
66
import logging
77

8-
from aiohttp import ClientSession, web
8+
from aiohttp import ClientSession, ClientTimeout, web
99

1010
from .application_keys import APP_CLIENT_SESSION_KEY
11+
from .utils import (
12+
get_http_client_request_total_timeout,
13+
get_http_client_request_aiohttp_connect_timeout,
14+
get_http_client_request_aiohttp_sock_connect_timeout,
15+
)
1116

1217
log = logging.getLogger(__name__)
1318

1419

1520
def get_client_session(app: web.Application) -> ClientSession:
16-
""" Lazy initialization of ClientSession
21+
"""Lazy initialization of ClientSession
1722
1823
Ensures unique session
1924
"""
2025
session = app.get(APP_CLIENT_SESSION_KEY)
2126
if session is None or session.closed:
22-
app[APP_CLIENT_SESSION_KEY] = session = ClientSession()
27+
# it is important to have fast connection handshakes
28+
# also requests should be as fast as possible
29+
# some services are not that fast to reply
30+
# Setting the time of a request using this client session to 5 seconds totals
31+
timeout_settings = ClientTimeout(
32+
total=get_http_client_request_total_timeout(),
33+
connect=get_http_client_request_aiohttp_connect_timeout(),
34+
sock_connect=get_http_client_request_aiohttp_sock_connect_timeout(),
35+
)
36+
app[APP_CLIENT_SESSION_KEY] = session = ClientSession(timeout=timeout_settings)
2337
return session
2438

2539

2640
async def persistent_client_session(app: web.Application):
27-
""" Ensures a single client session per application
41+
"""Ensures a single client session per application
2842
2943
IMPORTANT: Use this function ONLY in cleanup context , i.e.
3044
app.cleanup_ctx.append(persistent_client_session)

packages/service-library/src/servicelib/utils.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,26 @@
1515

1616
def is_production_environ() -> bool:
1717
"""
18-
If True, this code most probably
19-
runs in a production container of one of the
20-
osparc-simcore services.
18+
If True, this code most probably
19+
runs in a production container of one of the
20+
osparc-simcore services.
2121
"""
2222
# WARNING: based on a convention that is not constantly verified
2323
return os.environ.get("SC_BUILD_TARGET") == "production"
2424

2525

26+
def get_http_client_request_total_timeout() -> int:
27+
return int(os.environ.get("HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT", "20"))
28+
29+
30+
def get_http_client_request_aiohttp_connect_timeout() -> int:
31+
return int(os.environ.get("HTTP_CLIENT_REQUEST_AIOHTTP_CONNECT_TIMEOUT", "5"))
32+
33+
34+
def get_http_client_request_aiohttp_sock_connect_timeout() -> int:
35+
return int(os.environ.get("HTTP_CLIENT_REQUEST_AIOHTTP_SOCK_CONNECT_TIMEOUT", "5"))
36+
37+
2638
def is_osparc_repo_dir(path: Path) -> bool:
2739
# TODO: implement with git cli
2840
expected = (".github", "packages", "services")
@@ -31,9 +43,9 @@ def is_osparc_repo_dir(path: Path) -> bool:
3143

3244

3345
def search_osparc_repo_dir(start: Union[str, Path], max_iterations=8) -> Optional[Path]:
34-
""" Returns path to root repo dir or None if it does not exists
46+
"""Returns path to root repo dir or None if it does not exists
3547
36-
NOTE: assumes starts is a path within repo
48+
NOTE: assumes starts is a path within repo
3749
"""
3850
max_iterations = max(max_iterations, 1)
3951
root_dir = Path(start)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from models_library.settings.http_clients import ClientRequestSettings
2+
3+
4+
client_request_settings = ClientRequestSettings()

packages/simcore-sdk/src/simcore_sdk/node_ports/filemanager.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66
from typing import Optional
77

8-
from aiohttp import ClientSession
8+
from aiohttp import ClientSession, ClientTimeout
99
from yarl import URL
1010

1111
import aiofiles
@@ -14,6 +14,7 @@
1414
from models_library.settings.services_common import ServicesCommonSettings
1515

1616
from . import config, exceptions
17+
from ..config.http_clients import client_request_settings
1718

1819
log = logging.getLogger(__name__)
1920

@@ -28,7 +29,16 @@ class ClientSessionContextManager:
2829
# See https://github.com/ITISFoundation/osparc-simcore/issues/1098
2930
#
3031
def __init__(self, session=None):
31-
self.active_session = session or ClientSession()
32+
# We are interested in fast connections, if a connection is established
33+
# there is no timeout for file download operations
34+
35+
self.active_session = session or ClientSession(
36+
timeout=ClientTimeout(
37+
total=None,
38+
connect=client_request_settings.aiohttp_connect_timeout,
39+
sock_connect=client_request_settings.aiohttp_sock_connect_timeout,
40+
)
41+
)
3242
self.is_owned = self.active_session is not session
3343

3444
async def __aenter__(self):

services/api-server/requirements/_base.in

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# NOTE: ALL version constraints MUST be commented
55
-c ../../../requirements/constraints.txt
6+
-r ../../../packages/models-library/requirements/_base.in
67
-r ../../../packages/postgres-database/requirements/_base.in
78

89
# fastapi and extensions

services/api-server/requirements/ci.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
-r _test.txt
1212

1313
# installs this repo's packages
14+
../../packages/models-library
1415
../../packages/pytest-simcore/
1516
../../packages/postgres-database/
1617

services/api-server/requirements/dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
-r _tools.txt
1313

1414
# installs this repo's packages
15+
-e ../../packages/models-library
1516
-e ../../packages/pytest-simcore/
1617
-e ../../packages/postgres-database/[migration]
1718

services/api-server/requirements/prod.txt

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
-r _base.txt
1111

1212
# installs this repo's packages
13+
../../packages/models-library
1314
../../packages/postgres-database/
1415

1516
# installs current package

services/api-server/setup.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ def read_reqs(reqs_path: Path):
2424
readme = (current_dir / "README.md").read_text()
2525
version = (current_dir / "VERSION").read_text().strip()
2626

27-
install_requirements = read_reqs(current_dir / "requirements" / "_base.txt")
27+
install_requirements = read_reqs(current_dir / "requirements" / "_base.txt") + [
28+
"simcore-models-library"
29+
]
2830

2931
test_requirements = read_reqs(current_dir / "requirements" / "_test.txt")
3032

services/api-server/src/simcore_service_api_server/core/settings.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pydantic import BaseSettings, Field, SecretStr, validator
66
from yarl import URL
77

8+
from models_library.settings.http_clients import ClientRequestSettings
9+
810

911
class BootModeEnum(str, Enum):
1012
debug = "debug-ptvsd"
@@ -67,7 +69,11 @@ class AppSettings(BaseSettings):
6769
@classmethod
6870
def create_default(cls) -> "AppSettings":
6971
# This call triggers parsers
70-
return cls(postgres=PostgresSettings(), webserver=WebServerSettings())
72+
return cls(
73+
postgres=PostgresSettings(),
74+
webserver=WebServerSettings(),
75+
client_request=ClientRequestSettings(),
76+
)
7177

7278
# pylint: disable=no-self-use
7379
# pylint: disable=no-self-argument
@@ -86,7 +92,6 @@ def match_logging_level(cls, value) -> str:
8692
except AttributeError as err:
8793
raise ValueError(f"{value.upper()} is not a valid level") from err
8894

89-
9095
@property
9196
def loglevel(self) -> int:
9297
return getattr(logging, self.log_level_name)
@@ -97,6 +102,8 @@ def loglevel(self) -> int:
97102
# WEB-SERVER SERVICE
98103
webserver: WebServerSettings
99104

105+
client_request: ClientRequestSettings
106+
100107
# SERVICE SERVER (see : https://www.uvicorn.org/settings/)
101108
host: str = "0.0.0.0" # nosec
102109
port: int = 8000

services/api-server/src/simcore_service_api_server/services/webserver.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ def setup_webserver(app: FastAPI) -> None:
3838
# init client
3939
logger.debug("Setup webserver at %s...", settings.base_url)
4040

41-
client = AsyncClient(base_url=settings.base_url)
41+
client = AsyncClient(
42+
base_url=settings.base_url,
43+
timeout=app.state.settings.client_request.total_timeout,
44+
)
4245
app.state.webserver_client = client
4346

4447
# TODO: raise if attribute already exists
@@ -108,7 +111,7 @@ def _process(cls, resp: Response) -> Optional[Dict]:
108111
# TODO: add ping to healthcheck
109112

110113
async def get(self, path: str) -> Optional[Dict]:
111-
url = path.lstrip('/')
114+
url = path.lstrip("/")
112115
try:
113116
resp = await self.client.get(url, cookies=self.session_cookies)
114117
except Exception as err:
@@ -118,7 +121,7 @@ async def get(self, path: str) -> Optional[Dict]:
118121
return self._process(resp)
119122

120123
async def put(self, path: str, body: Dict) -> Optional[Dict]:
121-
url = path.lstrip('/')
124+
url = path.lstrip("/")
122125
try:
123126
resp = await self.client.put(url, json=body, cookies=self.session_cookies)
124127
except Exception as err:

services/api-server/tests/unit/test_settings.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
BootModeEnum,
99
PostgresSettings,
1010
WebServerSettings,
11+
ClientRequestSettings,
1112
)
1213

1314

@@ -26,7 +27,11 @@ def test_min_environ_for_settings(monkeypatch):
2627
monkeypatch.setenv("SC_BOOT_MODE", "production")
2728

2829
# NOTE: pg and weberver settings parse environ NOW!
29-
settings = AppSettings(postgres=PostgresSettings(), webserver=WebServerSettings())
30+
settings = AppSettings(
31+
postgres=PostgresSettings(),
32+
webserver=WebServerSettings(),
33+
client_request=ClientRequestSettings(),
34+
)
3035

3136
pprint(settings.dict())
3237

services/catalog/src/simcore_service_catalog/core/settings.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pydantic.types import PositiveInt
77
from yarl import URL
88

9+
from models_library.settings.http_clients import ClientRequestSettings
10+
911

1012
class BootModeEnum(str, Enum):
1113
DEBUG = "debug-ptvsd"
@@ -66,7 +68,11 @@ class AppSettings(BaseSettings):
6668
@classmethod
6769
def create_default(cls) -> "AppSettings":
6870
# This call triggers parsers
69-
return cls(postgres=PostgresSettings(), director=DirectorSettings())
71+
return cls(
72+
postgres=PostgresSettings(),
73+
director=DirectorSettings(),
74+
client_request=ClientRequestSettings(),
75+
)
7076

7177
# pylint: disable=no-self-use
7278
# pylint: disable=no-self-argument
@@ -92,6 +98,8 @@ def loglevel(self) -> int:
9298
# POSTGRES
9399
postgres: PostgresSettings
94100

101+
client_request: ClientRequestSettings
102+
95103
# DIRECTOR SERVICE
96104
director: DirectorSettings
97105

services/catalog/src/simcore_service_catalog/services/director.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ def setup_director(app: FastAPI) -> None:
1717

1818
# init client-api
1919
logger.debug("Setup director at %s...", settings.base_url)
20-
app.state.director_api = DirectorApi(
21-
base_url=settings.base_url, vtag=app.state.settings.director.vtag
22-
)
20+
app.state.director_api = DirectorApi(base_url=settings.base_url, app=app)
2321

2422
# does NOT communicate with director service
2523

@@ -97,9 +95,11 @@ class DirectorApi:
9795
SEE services/catalog/src/simcore_service_catalog/api/dependencies/director.py
9896
"""
9997

100-
def __init__(self, base_url: str, vtag: str):
101-
self.client = AsyncClient(base_url=base_url)
102-
self.vtag = vtag
98+
def __init__(self, base_url: str, app: FastAPI):
99+
self.client = AsyncClient(
100+
base_url=base_url, timeout=app.state.settings.client_request.total_timeout
101+
)
102+
self.vtag = app.state.settings.director.vtag
103103

104104
# OPERATIONS
105105
# TODO: policy to retry if NetworkError/timeout?

services/catalog/tests/unit/test_services_director.py

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AppSettings,
1414
DirectorSettings,
1515
PostgresSettings,
16+
ClientRequestSettings,
1617
)
1718
from simcore_service_catalog.services.director import DirectorApi
1819

@@ -25,6 +26,7 @@ def minimal_app(loop, devel_environ) -> FastAPI:
2526
settings = AppSettings(
2627
postgres=PostgresSettings(enabled=False),
2728
director=DirectorSettings(enabled=False),
29+
client_request=ClientRequestSettings(),
2830
)
2931
app = init_app(settings)
3032

services/director-v2/src/simcore_service_director_v2/core/settings.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from models_library.basic_types import BootModeEnum, PortInt
77
from models_library.settings.celery import CeleryConfig
88
from models_library.settings.postgres import PostgresSettings
9+
from models_library.settings.http_clients import ClientRequestSettings
910
from pydantic import (
1011
BaseSettings,
1112
Field,
@@ -127,6 +128,7 @@ def create_from_env(cls, **settings_kwargs) -> "AppSettings":
127128
registry=RegistrySettings(),
128129
celery=CelerySettings.create_from_env(),
129130
dynamic_services=DynamicServicesSettings(),
131+
client_request=ClientRequestSettings(),
130132
**settings_kwargs,
131133
)
132134

@@ -223,5 +225,7 @@ def loglevel(self) -> int:
223225

224226
remote_debug_port: PortInt = 3000
225227

228+
client_request: ClientRequestSettings
229+
226230
class Config(CommonConfig):
227231
env_prefix = ""

0 commit comments

Comments
 (0)