Skip to content

Commit f7b28f2

Browse files
committed
Timeout for installing requirements
1 parent b50ec44 commit f7b28f2

File tree

8 files changed

+97
-16
lines changed

8 files changed

+97
-16
lines changed

arca/backend/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ class BaseBackend:
2121
2222
* **requirements_location**: Relative path to the requirements file in the target repositories.
2323
(default is ``requirements.txt``)
24-
* **cwd**: Relative path to the required working directory. (default is ``""``, the root of the repo)
24+
* **requirements_timeout**: The maximum time in seconds allowed for installing requirements.
25+
(default is 5 minutes, 300 seconds)
26+
* **cwd**: Relative path to the required working directory.
27+
(default is ``""``, the root of the repo)
2528
"""
2629

2730
RUNNER = Path(__file__).parent.parent.resolve() / "_runner.py"
2831

2932
requirements_location: str = LazySettingProperty(default="requirements.txt")
33+
requirements_timeout: int = LazySettingProperty(default=300, convert=int)
3034
cwd: str = LazySettingProperty(default="")
3135

3236
def __init__(self, **settings):

arca/backend/current_environment.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from git import Repo
88

9-
from arca.exceptions import ArcaMisconfigured, RequirementsMismatch, BuildError
9+
from arca.exceptions import ArcaMisconfigured, RequirementsMismatch, BuildError, BuildTimeoutError
1010
from arca.utils import LazySettingProperty, logger
1111
from .base import BaseRunInSubprocessBackend
1212

@@ -76,7 +76,11 @@ def install_requirements(self, *, path: Optional[Path] = None, requirements: Opt
7676

7777
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
7878

79-
[out_stream, err_stream] = process.communicate()
79+
try:
80+
out_stream, err_stream = process.communicate(timeout=self.requirements_timeout)
81+
except subprocess.TimeoutExpired:
82+
raise BuildTimeoutError(f"Installing of requirements timeouted after {self.requirements_timeout} seconds.")
83+
8084
out_stream = out_stream.decode("utf-8")
8185
err_stream = err_stream.decode("utf-8")
8286

arca/backend/docker.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ class DockerBackend(BaseBackend):
5454
INSTALL_REQUIREMENTS = """
5555
FROM {name}:{tag}
5656
ADD {requirements} /srv/requirements.txt
57-
RUN pip install -r /srv/requirements.txt
57+
RUN if grep -q Alpine /etc/issue; then \
58+
timeout -t {timeout} pip install --no-cache-dir -r /srv/requirements.txt; \
59+
else \
60+
timeout {timeout} pip install --no-cache-dir -r /srv/requirements.txt; \
61+
fi
5862
CMD bash -i
5963
"""
6064

@@ -296,6 +300,11 @@ def get_or_build_image(self, name: str, tag: str, dockerfile: Union[str, Callabl
296300

297301
dockerfile_file.unlink()
298302
except docker.errors.BuildError as e:
303+
for line in e.build_log:
304+
if isinstance(line, dict) and line.get("errorDetail") and line["errorDetail"].get("code") in {124, 143}:
305+
raise BuildTimeoutError(f"Installing of requirements timeouted after "
306+
f"{self.requirements_timeout} seconds.")
307+
299308
logger.exception(e)
300309
raise
301310

@@ -412,7 +421,8 @@ def build_image_from_inherited_image(self, image_name: str, image_tag: str,
412421
install_requirements_dockerfile = self.INSTALL_REQUIREMENTS.format(
413422
name=base_name,
414423
tag=base_tag,
415-
requirements=relative_requirements
424+
requirements=relative_requirements,
425+
timeout=self.requirements_timeout
416426
)
417427

418428
self.get_or_build_image(image_name, image_tag, install_requirements_dockerfile,
@@ -508,7 +518,8 @@ def install_requirements_dockerfile():
508518
return self.INSTALL_REQUIREMENTS.format(
509519
name=dependencies_name,
510520
tag=dependencies_tag,
511-
requirements=relative_requirements
521+
requirements=relative_requirements,
522+
timeout=self.requirements_timeout
512523
)
513524

514525
self.get_or_build_image(image_name, image_tag, install_requirements_dockerfile, build_context=build_context,

arca/backend/venv.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import shutil
12
import subprocess
23
from pathlib import Path
34
from venv import EnvBuilder
45

56
from git import Repo
67

7-
from arca.exceptions import BuildError
8+
from arca.exceptions import BuildError, BuildTimeoutError
89
from arca.utils import logger
910
from .base import BaseRunInSubprocessBackend
1011

@@ -65,7 +66,14 @@ def get_or_create_venv(self, path: Path) -> Path:
6566
str(requirements_file)],
6667
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
6768

68-
[out_stream, err_stream] = process.communicate()
69+
try:
70+
out_stream, err_stream = process.communicate(timeout=self.requirements_timeout)
71+
except subprocess.TimeoutExpired:
72+
shutil.rmtree(venv_path, ignore_errors=True)
73+
74+
raise BuildTimeoutError(f"Installing of requirements timeouted after "
75+
f"{self.requirements_timeout} seconds.")
76+
6977
out_stream = out_stream.decode("utf-8")
7078
err_stream = err_stream.decode("utf-8")
7179

docs/backends.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ or you can use settings (described in more details in :ref:`configuring`). For e
3939
As mentioned in :ref:`options`, there are two options common for all backends. (See that section for more details.)
4040

4141
* **requirements_location**
42+
* **requirements_timeout**
4243
* **cwd**
4344

4445
.. _backends_cur:

docs/changes.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Changes:
1717
* **apk_dependencies** changed to **apt_dependencies**, now installing using `apt-get`
1818

1919
* Vagrant backend only creates one VM, instead of multiple -- see its documentation
20-
* Added timeout to tasks -- 5 being the default
20+
* Added timeout to tasks, 5 seconds by default. Can be set using the argument **timeout** for ``Task``.
21+
* Added timeout to installing requirements, 300 seconds by default. Can be set using the **requirements_timeout** configuration option for backends.
2122

2223
0.1.1 (2018-04-23)
2324
******************

docs/settings.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ This section describes settings that are common for all the backends.
7171
Tells backends where to look for a requirements file in the repositories, so it must be a relative path. You can set it
7272
to ``None`` to indicate there are no requirements. The default is ``requirements.txt``.
7373

74+
**requirements_timeout** (`ARCA_BACKEND_REQUIREMENTS_TIMEOUT`)
75+
76+
Tells backends how long the installing of requirements can take, in seconds.
77+
The default is 120 seconds.
78+
If the limit is exceeded :class:`BuildTimeoutError <arca.exceptions.BuildTimeoutError>` is raised.
79+
7480
**cwd** (`ARCA_BACKEND_CWD`)
7581

7682
Tells Arca in what working directory the tasks should be launched, so again a relative path.

tests/test_backends.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import pytest
55

66
from arca import Arca, VenvBackend, DockerBackend, Task, CurrentEnvironmentBackend
7-
from common import BASE_DIR, RETURN_COLORAMA_VERSION_FUNCTION, SECOND_RETURN_STR_FUNCTION, \
8-
TEST_UNICODE, ARG_STR_FUNCTION, KWARG_STR_FUNCTION, WAITING_FUNCTION
97
from arca.exceptions import BuildTimeoutError
8+
from common import BASE_DIR, RETURN_COLORAMA_VERSION_FUNCTION, SECOND_RETURN_STR_FUNCTION, \
9+
TEST_UNICODE, ARG_STR_FUNCTION, KWARG_STR_FUNCTION, WAITING_FUNCTION, RETURN_STR_FUNCTION
1010

1111

1212
@pytest.mark.parametrize(
@@ -17,6 +17,9 @@
1717
))
1818
)
1919
def test_backends(temp_repo_func, backend, requirements_location, file_location):
20+
""" Tests the basic stuff around backends, if it can install requirements from more locations,
21+
launch stuff with correct cwd, works well with multiple branches, etc
22+
"""
2023
if os.environ.get("TRAVIS", False) and backend == VenvBackend:
2124
pytest.skip("Venv Backend doesn't work on Travis")
2225

@@ -86,6 +89,41 @@ def test_backends(temp_repo_func, backend, requirements_location, file_location)
8689

8790
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task).output == "0.3.8"
8891

92+
# cleanup
93+
94+
if isinstance(backend, CurrentEnvironmentBackend):
95+
backend._uninstall("colorama")
96+
97+
with pytest.raises(ModuleNotFoundError):
98+
import colorama # noqa
99+
100+
101+
@pytest.mark.parametrize(
102+
"backend",
103+
[CurrentEnvironmentBackend, VenvBackend, DockerBackend]
104+
)
105+
def test_advanced_backends(temp_repo_func, backend):
106+
""" Tests the more time-intensive stuff, like timeouts or arguments,
107+
things multiple for runs with different arguments are not neccessary
108+
"""
109+
if os.environ.get("TRAVIS", False) and backend == VenvBackend:
110+
pytest.skip("Venv Backend doesn't work on Travis")
111+
112+
kwargs = {}
113+
114+
if backend == DockerBackend:
115+
kwargs["disable_pull"] = True
116+
if backend == CurrentEnvironmentBackend:
117+
kwargs["current_environment_requirements"] = None
118+
kwargs["requirements_strategy"] = "install_extra"
119+
120+
backend = backend(verbosity=2, **kwargs)
121+
122+
arca = Arca(backend=backend, base_dir=BASE_DIR)
123+
124+
filepath = temp_repo_func.file_path
125+
requirements_path = temp_repo_func.repo_path / backend.requirements_location
126+
89127
filepath.write_text(ARG_STR_FUNCTION)
90128
temp_repo_func.repo.index.add([str(filepath)])
91129
temp_repo_func.repo.index.commit("Argument function")
@@ -104,6 +142,7 @@ def test_backends(temp_repo_func, backend, requirements_location, file_location)
104142
kwargs={"kwarg": TEST_UNICODE}
105143
)).output == TEST_UNICODE[::-1]
106144

145+
# test task timeout
107146
filepath.write_text(WAITING_FUNCTION)
108147
temp_repo_func.repo.index.add([str(filepath)])
109148
temp_repo_func.repo.index.commit("Waiting function")
@@ -112,12 +151,19 @@ def test_backends(temp_repo_func, backend, requirements_location, file_location)
112151
task_3_seconds = Task("test_file:return_str_function", timeout=3)
113152

114153
with pytest.raises(BuildTimeoutError):
115-
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_1_second).output == "Some string"
154+
arca.run(temp_repo_func.url, temp_repo_func.branch, task_1_second)
116155

117156
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_3_seconds).output == "Some string"
118157

119-
if isinstance(backend, CurrentEnvironmentBackend):
120-
backend._uninstall("colorama")
158+
# test requirements timeout
159+
requirements_path.write_text("scipy")
121160

122-
with pytest.raises(ModuleNotFoundError):
123-
import colorama # noqa
161+
filepath.write_text(RETURN_STR_FUNCTION)
162+
163+
temp_repo_func.repo.index.add([str(filepath), str(requirements_path)])
164+
temp_repo_func.repo.index.commit("Updated requirements to something that takes > 1 second to install")
165+
166+
arca.backend.requirements_timeout = 1
167+
168+
with pytest.raises(BuildTimeoutError):
169+
arca.run(temp_repo_func.url, temp_repo_func.branch, Task("test_file:return_str_function"))

0 commit comments

Comments
 (0)