Skip to content

Commit 1a0cb4a

Browse files
committed
Implemented timeout
1 parent 39eb5ba commit 1a0cb4a

File tree

8 files changed

+79
-14
lines changed

8 files changed

+79
-14
lines changed

arca/backend/base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from git import Repo
99

1010
import arca
11-
from arca.exceptions import BuildError
11+
from arca.exceptions import BuildError, BuildTimeoutError
1212
from arca.result import Result
1313
from arca.task import Task
1414
from arca.utils import NOT_SET, LazySettingProperty, logger
@@ -161,7 +161,7 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
161161
stderr=subprocess.PIPE,
162162
cwd=cwd)
163163

164-
out_stream, err_stream = process.communicate()
164+
out_stream, err_stream = process.communicate(timeout=task.timeout)
165165

166166
out_output = out_stream.decode("utf-8")
167167
err_output = err_stream.decode("utf-8")
@@ -170,6 +170,8 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
170170
logger.debug(out_output)
171171

172172
return Result(out_output)
173+
except subprocess.TimeoutExpired:
174+
raise BuildTimeoutError(f"The task timeouted after {task.timeout} seconds.")
173175
except BuildError: # can be raised by :meth:`Result.__init__`
174176
raise
175177
except Exception as e:

arca/backend/docker.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from requests.exceptions import ConnectionError
2020

2121
import arca
22-
from arca.exceptions import ArcaMisconfigured, BuildError, PushToRegistryError
22+
from arca.exceptions import ArcaMisconfigured, BuildError, PushToRegistryError, BuildTimeoutError
2323
from arca.result import Result
2424
from arca.task import Task
2525
from arca.utils import logger, LazySettingProperty
@@ -744,11 +744,16 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
744744
res = None
745745

746746
try:
747-
res = container.exec_run(["python",
747+
res = container.exec_run(["timeout",
748+
str(task.timeout),
749+
"python",
748750
"/srv/scripts/runner.py",
749751
f"/srv/scripts/{task_filename}"],
750752
tty=True)
751753

754+
if res.exit_code == 124:
755+
raise BuildTimeoutError(f"The task timeouted after {task.timeout} seconds.")
756+
752757
return Result(res.output)
753758
except BuildError: # can be raised by :meth:`Result.__init__`
754759
raise

arca/backend/vagrant.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import math
12
import re
23
import shutil
34
import subprocess
45
from pathlib import Path
56
from textwrap import dedent
67
from uuid import uuid4
78

9+
from fabric.exceptions import CommandTimeout
810
from git import Repo
911

10-
from arca.exceptions import ArcaMisconfigured, BuildError
12+
from arca.exceptions import ArcaMisconfigured, BuildError, BuildTimeoutError
1113
from arca.result import Result
1214
from arca.task import Task
1315
from arca.utils import LazySettingProperty, logger
@@ -150,7 +152,7 @@ def fabric_task(self):
150152
from fabric import api
151153

152154
@api.task
153-
def run_script(container_name, definition_filename, image_name, image_tag, repository):
155+
def run_script(container_name, definition_filename, image_name, image_tag, repository, timeout):
154156
""" Sequence to run inside the VM.
155157
Starts up the container if the container is not running
156158
(and copies over the data and the runner script)
@@ -184,9 +186,11 @@ def run_script(container_name, definition_filename, image_name, image_tag, repos
184186
api.run(f"docker cp /vagrant/{definition_filename} {container_name}:/srv/scripts/")
185187

186188
output = api.run(" ".join([
187-
"docker", "exec", container_name,
188-
"python", "/srv/scripts/runner.py", f"/srv/scripts/{definition_filename}",
189-
]))
189+
"docker", "exec", container_name,
190+
"python", "/srv/scripts/runner.py", f"/srv/scripts/{definition_filename}",
191+
]),
192+
timeout=math.ceil(timeout)
193+
)
190194

191195
if not self.keep_container_running:
192196
api.run(f"docker kill {container_name}")
@@ -261,9 +265,12 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
261265
definition_filename=task_filename,
262266
image_name=image_name,
263267
image_tag=image_tag,
264-
repository=str(repo_path.relative_to(Path(self._arca.base_dir).resolve() / 'repos')))
268+
repository=str(repo_path.relative_to(Path(self._arca.base_dir).resolve() / 'repos')),
269+
timeout=task.timeout)
265270

266271
return Result(res[self.vagrant.user_hostname_port()].stdout)
272+
except CommandTimeout:
273+
raise BuildTimeoutError(f"The task timeouted after {task.timeout} seconds.")
267274
except BuildError: # can be raised by :meth:`Result.__init__`
268275
raise
269276
except Exception as e:

arca/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ def __str__(self):
4343
)
4444

4545

46+
class BuildTimeoutError(BuildError):
47+
""" Raised if the task takes too long.
48+
"""
49+
50+
4651
class PushToRegistryError(ArcaException):
4752
""" Raised if pushing of images to Docker registry in :class:`DockerBackend` fails.
4853
"""

arca/task.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def ret_argument(value="Value"):
3131
"""
3232

3333
def __init__(self, entry_point: str, *,
34+
timeout: int=5,
3435
args: Optional[Iterable[Any]]=None,
3536
kwargs: Optional[Dict[str, Any]]=None) -> None:
3637

@@ -42,6 +43,11 @@ def __init__(self, entry_point: str, *,
4243
if self._entry_point.object_name is None:
4344
raise TaskMisconfigured("Task entry point must be an object, not a module.")
4445

46+
try:
47+
self._timeout = int(timeout)
48+
except ValueError:
49+
raise TaskMisconfigured("Provided timeout could not be converted to int.")
50+
4551
try:
4652
self._args = list(args or [])
4753
self._kwargs = dict(kwargs or {})
@@ -68,6 +74,10 @@ def args(self):
6874
def kwargs(self):
6975
return self._kwargs
7076

77+
@property
78+
def timeout(self):
79+
return self._timeout
80+
7181
def __repr__(self):
7282
return f"Task({self.entry_point})"
7383

tests/common.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,11 @@ def return_alsaaudio_installed():
5656
def return_platform():
5757
return platform.dist()[0]
5858
"""
59+
60+
WAITING_FUNCTION = """
61+
import time
62+
63+
def return_str_function():
64+
time.sleep(2)
65+
return "Some string"
66+
"""

tests/test_backends.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

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

1011

1112
@pytest.mark.parametrize(
@@ -48,7 +49,7 @@ def test_backends(temp_repo_func, backend, requirements_location, file_location)
4849
temp_repo_func.repo.index.add([str(filepath)])
4950
temp_repo_func.repo.index.commit("Initial")
5051

51-
task = Task("test_file:return_str_function",)
52+
task = Task("test_file:return_str_function")
5253

5354
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task).output == "Some string"
5455

@@ -103,6 +104,18 @@ def test_backends(temp_repo_func, backend, requirements_location, file_location)
103104
kwargs={"kwarg": TEST_UNICODE}
104105
)).output == TEST_UNICODE[::-1]
105106

107+
filepath.write_text(WAITING_FUNCTION)
108+
temp_repo_func.repo.index.add([str(filepath)])
109+
temp_repo_func.repo.index.commit("Waiting function")
110+
111+
task_1_second = Task("test_file:return_str_function", timeout=1)
112+
task_3_seconds = Task("test_file:return_str_function", timeout=3)
113+
114+
with pytest.raises(BuildTimeoutError):
115+
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_1_second).output == "Some string"
116+
117+
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_3_seconds).output == "Some string"
118+
106119
if isinstance(backend, CurrentEnvironmentBackend):
107120
backend._uninstall("colorama")
108121

tests/test_vagrant.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from vagrant import Vagrant
66

77
from arca import VagrantBackend, Arca, Task
8-
from arca.exceptions import ArcaMisconfigured
9-
from common import BASE_DIR, RETURN_COLORAMA_VERSION_FUNCTION, SECOND_RETURN_STR_FUNCTION, TEST_UNICODE
8+
from arca.exceptions import ArcaMisconfigured, BuildTimeoutError
9+
from common import BASE_DIR, RETURN_COLORAMA_VERSION_FUNCTION, SECOND_RETURN_STR_FUNCTION, TEST_UNICODE, \
10+
WAITING_FUNCTION
1011

1112

1213
def test_validation():
@@ -100,6 +101,20 @@ def test_vagrant(temp_repo_func, destroy=False):
100101
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task).output == "0.3.9"
101102
assert arca.run(temp_repo_func.url, "branch", task).output == TEST_UNICODE
102103

104+
# test timeout
105+
temp_repo_func.repo.branches.master.checkout()
106+
temp_repo_func.file_path.write_text(WAITING_FUNCTION)
107+
temp_repo_func.repo.index.add([str(temp_repo_func.file_path)])
108+
temp_repo_func.repo.index.commit("Waiting function")
109+
110+
task_1_second = Task("test_file:return_str_function", timeout=1)
111+
task_3_seconds = Task("test_file:return_str_function", timeout=3)
112+
113+
with pytest.raises(BuildTimeoutError):
114+
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_1_second).output == "Some string"
115+
116+
assert arca.run(temp_repo_func.url, temp_repo_func.branch, task_3_seconds).output == "Some string"
117+
103118
backend.stop_vm()
104119

105120
if destroy:

0 commit comments

Comments
 (0)