Skip to content

Commit e895c7a

Browse files
authored
Merge pull request #445 from rstudio/tdstein/442-Adjust-the-Default-Timeout-for-Long-Lived-Task
Adds CONNECT_TASK_TIMEOUT environment variable.
2 parents 1d09793 + 8722693 commit e895c7a

File tree

5 files changed

+93
-23
lines changed

5 files changed

+93
-23
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
9+
### Added
10+
11+
- The `CONNECT_TASK_TIMEOUT` environment variable, which configures the timeout for [task based operations](https://docs.posit.co/connect/api/#get-/v1/tasks/-id-). This value translates into seconds (e.g., `CONNECT_TASK_TIMEOUT=60` is equivalent to 60 seconds.) By default, this value is set to [`sys.maxsize`](https://docs.python.org/3/library/sys.html#sys.maxsize).
12+
713
## [1.18.0] - 2023-06-27
814

915
### Added

rsconnect/api.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from .metadata import ServerStore, AppStore
2929
from .exception import RSConnectException
3030
from .bundle import _default_title, fake_module_file_from_directory
31-
from .timeouts import get_timeout
31+
from .timeouts import get_task_timeout
3232

3333

3434
class AbstractRemoteServer:
@@ -292,21 +292,27 @@ def get_content(self, content_guid):
292292
return results
293293

294294
def wait_for_task(
295-
self, task_id, log_callback, abort_func=lambda: False, timeout=get_timeout(), poll_wait=0.5, raise_on_error=True
295+
self,
296+
task_id,
297+
log_callback,
298+
abort_func=lambda: False,
299+
timeout=get_task_timeout(),
300+
poll_wait=0.5,
301+
raise_on_error=True,
296302
):
297-
last_status = None
298-
ending = time.time() + timeout if timeout else 999999999999
299303

300304
if log_callback is None:
301305
log_lines = []
302306
log_callback = log_lines.append
303307
else:
304308
log_lines = None
305309

310+
last_status = None
311+
start_time = time.time()
306312
sleep_duration = 0.5
307313
time_slept = 0
308314
while True:
309-
if time.time() >= ending:
315+
if (time.time() - start_time) > timeout:
310316
raise RSConnectException("Task timed out after %d seconds" % timeout)
311317
elif abort_func():
312318
raise RSConnectException("Task aborted.")
@@ -741,7 +747,7 @@ def emit_task_log(
741747
task_id: int = None,
742748
log_callback=connect_logger,
743749
abort_func: Callable[[], bool] = lambda: False,
744-
timeout: int = None,
750+
timeout: int = get_task_timeout(),
745751
poll_wait: float = 0.5,
746752
raise_on_error: bool = True,
747753
):
@@ -1171,7 +1177,7 @@ def get_current_user(self):
11711177
self._server.handle_bad_response(response)
11721178
return response
11731179

1174-
def wait_until_task_is_successful(self, task_id, timeout=get_timeout()):
1180+
def wait_until_task_is_successful(self, task_id, timeout=get_task_timeout()):
11751181
print()
11761182
print("Waiting for task: {}".format(task_id))
11771183
start_time = time.time()
@@ -1448,7 +1454,7 @@ def emit_task_log(
14481454
task_id,
14491455
log_callback,
14501456
abort_func=lambda: False,
1451-
timeout=get_timeout(),
1457+
timeout=get_task_timeout(),
14521458
poll_wait=0.5,
14531459
raise_on_error=True,
14541460
):

rsconnect/http_support.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from six.moves.urllib_parse import urlparse, urlencode, urljoin
1414
import base64
1515

16-
from .timeouts import get_timeout
16+
from .timeouts import get_request_timeout
1717

1818
_user_agent = "rsconnect-python/%s" % VERSION
1919

@@ -31,7 +31,7 @@ def _create_plain_connection(host_name, port, disable_tls_check, ca_data):
3131
:param ca_data: any certificate authority information to use (ignored).
3232
:return: a plain HTTP connection.
3333
"""
34-
timeout = get_timeout()
34+
timeout = get_request_timeout()
3535
logger.debug(f"The HTTPConnection timeout is set to '{timeout}' seconds")
3636
return http.HTTPConnection(host_name, port=(port or http.HTTP_PORT), timeout=timeout)
3737

@@ -77,7 +77,7 @@ def _create_ssl_connection(host_name, port, disable_tls_check, ca_data):
7777
raise ValueError("Cannot both disable TLS checking and provide a custom certificate")
7878
_, _, proxyHost, proxyPort = _get_proxy()
7979
headers = _get_proxy_headers()
80-
timeout = get_timeout()
80+
timeout = get_request_timeout()
8181
logger.debug(f"The HTTPSConnection timeout is set to '{timeout}' seconds")
8282
if ca_data is not None:
8383
return http.HTTPSConnection(

rsconnect/timeouts.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import os
2+
import sys
23
from typing import Union
34

45
from rsconnect.exception import RSConnectException
56

6-
_CONNECT_REQUEST_TIMEOUT_KEY = "CONNECT_REQUEST_TIMEOUT"
7-
_CONNECT_REQUEST_TIMEOUT_DEFAULT_VALUE = "300"
7+
_CONNECT_REQUEST_TIMEOUT_KEY: str = "CONNECT_REQUEST_TIMEOUT"
8+
_CONNECT_REQUEST_TIMEOUT_DEFAULT_VALUE: str = "300"
89

10+
_CONNECT_TASK_TIMEOUT_KEY: str = "CONNECT_TASK_TIMEOUT"
11+
_CONNECT_TASK_TIMEOUT_DEFAULT_VALUE: str = str(sys.maxsize)
912

10-
def get_timeout() -> int:
13+
14+
def get_request_timeout() -> int:
1115
"""Gets the timeout from the CONNECT_REQUEST_TIMEOUT env variable.
1216
1317
The timeout value is intended to be interpreted in seconds. A value of 60 is equal to sixty seconds, or one minute.
@@ -30,12 +34,40 @@ def get_timeout() -> int:
3034
timeout = int(timeout)
3135
except ValueError:
3236
raise RSConnectException(
33-
f"'CONNECT_REQUEST_TIMEOUT' is set to '{timeout}'. The value must be a natural number."
37+
f"'CONNECT_REQUEST_TIMEOUT' is set to '{timeout}'. The value must be a non-negative integer."
3438
)
3539

3640
if timeout < 0:
3741
raise RSConnectException(
38-
f"'CONNECT_REQUEST_TIMEOUT' is set to '{timeout}'. The value must be a natural number."
42+
f"'CONNECT_REQUEST_TIMEOUT' is set to '{timeout}'. The value must be a non-negative integer."
3943
)
4044

4145
return timeout
46+
47+
48+
def get_task_timeout() -> int:
49+
"""Gets the timeout from the CONNECT_TASK_TIMEOUT env variable.
50+
51+
The timeout value is intended to be interpreted in seconds. A value of 60 is equal to sixty seconds, or one minute.
52+
53+
If CONNECT_TASK_TIMEOUT is unset, a default value of sys.maxsize is used.
54+
55+
If CONNECT_TASK_TIMEOUT is set to a value less or equal to 0, an `RSConnectException` is raised.
56+
57+
The primary intent for this method is for usage with the `api` module. Specifically, for setting the timeout
58+
parameter in the method `wait_for_task`.
59+
60+
:raises: `RSConnectException` if CONNECT_TASK_TIMEOUT is not a positive integer.
61+
:return: the timeout value
62+
"""
63+
timeout: Union[int, str] = os.environ.get(_CONNECT_TASK_TIMEOUT_KEY, _CONNECT_TASK_TIMEOUT_DEFAULT_VALUE)
64+
65+
try:
66+
timeout = int(timeout)
67+
except ValueError:
68+
raise RSConnectException(f"'CONNECT_TASK_TIMEOUT' is set to '{timeout}'. The value must be a positive integer.")
69+
70+
if timeout <= 0:
71+
raise RSConnectException(f"'CONNECT_TASK_TIMEOUT' is set to '{timeout}'. The value must be a positive integer.")
72+
73+
return timeout

tests/test_timeouts.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,59 @@
11
import os
2+
import sys
23

34
from unittest import TestCase
45
from unittest.mock import patch
56

67
from rsconnect.exception import RSConnectException
7-
from rsconnect.timeouts import get_timeout
8+
from rsconnect.timeouts import get_request_timeout, get_task_timeout
89

910

10-
class GetTimeoutTestCase(TestCase):
11+
class GetRequestTimeoutTestCase(TestCase):
1112
def test_get_default_timeout(self):
12-
timeout = get_timeout()
13+
timeout = get_request_timeout()
1314
self.assertEqual(300, timeout)
1415

1516
def test_get_valid_timeout_from_environment(self):
1617
with patch.dict(os.environ, {"CONNECT_REQUEST_TIMEOUT": "24"}):
17-
timeout = get_timeout()
18+
timeout = get_request_timeout()
1819
self.assertEqual(24, timeout)
1920

2021
def test_get_zero_timeout_from_environment(self):
2122
with patch.dict(os.environ, {"CONNECT_REQUEST_TIMEOUT": "0"}):
22-
timeout = get_timeout()
23+
timeout = get_request_timeout()
2324
self.assertEqual(0, timeout)
2425

2526
def test_get_invalid_timeout_from_environment(self):
2627
with patch.dict(os.environ, {"CONNECT_REQUEST_TIMEOUT": "foobar"}):
2728
with self.assertRaises(RSConnectException):
28-
get_timeout()
29+
get_request_timeout()
2930

3031
def test_get_negative_timeout_from_environment(self):
3132
with patch.dict(os.environ, {"CONNECT_REQUEST_TIMEOUT": "-24"}):
3233
with self.assertRaises(RSConnectException):
33-
get_timeout()
34+
get_request_timeout()
35+
36+
class GetTaskTimeoutTestCase(TestCase):
37+
def test_get_default_timeout(self):
38+
timeout = get_task_timeout()
39+
self.assertEqual(sys.maxsize, timeout)
40+
41+
def test_get_valid_timeout_from_environment(self):
42+
with patch.dict(os.environ, {"CONNECT_TASK_TIMEOUT": "24"}):
43+
timeout = get_task_timeout()
44+
self.assertEqual(24, timeout)
45+
46+
def test_get_zero_timeout_from_environment(self):
47+
with patch.dict(os.environ, {"CONNECT_TASK_TIMEOUT": "0"}):
48+
with self.assertRaises(RSConnectException):
49+
get_task_timeout()
50+
51+
def test_get_invalid_timeout_from_environment(self):
52+
with patch.dict(os.environ, {"CONNECT_TASK_TIMEOUT": "foobar"}):
53+
with self.assertRaises(RSConnectException):
54+
get_task_timeout()
55+
56+
def test_get_negative_timeout_from_environment(self):
57+
with patch.dict(os.environ, {"CONNECT_TASK_TIMEOUT": "-24"}):
58+
with self.assertRaises(RSConnectException):
59+
get_task_timeout()

0 commit comments

Comments
 (0)