Skip to content

Commit 9341061

Browse files
author
Madhav
committed
Add timeouts to Flower workload setup
1 parent 80901cc commit 9341061

4 files changed

Lines changed: 73 additions & 13 deletions

File tree

aiopslab/orchestrator/problems/flower_model_misconfig/model_misconfig.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
"""Model misconfiguration fault in the Flower application."""
55

6+
import os
67
import time
7-
from typing import Any
8+
from typing import Any, Callable
89

910
from aiopslab.orchestrator.tasks import *
1011
from aiopslab.service.dock import Docker
@@ -13,6 +14,13 @@
1314
from aiopslab.session import SessionItem
1415
from aiopslab.generators.fault.inject_virtual import VirtualizationFaultInjector
1516

17+
WORKLOAD_START_TIMEOUT_SECONDS = int(
18+
os.getenv("AIOPSLAB_FLOWER_WORKLOAD_START_TIMEOUT_SECONDS", "300")
19+
)
20+
FAULT_PROPAGATION_TIMEOUT_SECONDS = int(
21+
os.getenv("AIOPSLAB_FLOWER_FAULT_PROPAGATION_TIMEOUT_SECONDS", "300")
22+
)
23+
1624

1725
class FlowerModelMisconfigBaseTask:
1826
def __init__(self, faulty_service: str = "user-service"):
@@ -25,30 +33,48 @@ def __init__(self, faulty_service: str = "user-service"):
2533
def start_workload(self):
2634
print("== Start Workload ==")
2735
command = "flwr run train local-deployment"
28-
self.docker.exec_command(command, cwd=self.train_dir)
36+
self.docker.exec_command(
37+
command,
38+
cwd=self.train_dir,
39+
timeout=WORKLOAD_START_TIMEOUT_SECONDS,
40+
)
2941

3042
path = "/app/.flwr/apps"
3143
check = f""" docker exec -it {self.faulty_service} sh -c "test -d {path} && echo 'exists'" """
3244

3345
print("Waiting for workload to start...")
34-
while True:
35-
exists = self.docker.exec_command(check)
36-
if exists.strip() == "exists":
37-
break
38-
time.sleep(1)
46+
self._wait_until(
47+
"Flower workload start",
48+
lambda: self.docker.exec_command(check).strip() == "exists",
49+
WORKLOAD_START_TIMEOUT_SECONDS,
50+
)
3951
print("Workload started successfully.")
4052

4153
# Inject fault after workload starts, since the required files are created during the workload
4254
print("Injecting fault...")
4355
self.inject_fault(inject=True)
4456

4557
print("Waiting for faults to propagate...")
46-
while True:
47-
logs = self.docker.get_logs(self.faulty_service)
48-
if "error" in logs.lower():
49-
break
50-
time.sleep(1)
58+
self._wait_until(
59+
"Flower fault propagation",
60+
lambda: "error" in self.docker.get_logs(self.faulty_service).lower(),
61+
FAULT_PROPAGATION_TIMEOUT_SECONDS,
62+
)
5163
print("Faults propagated.")
64+
65+
def _wait_until(
66+
self,
67+
description: str,
68+
predicate: Callable[[], bool],
69+
timeout_seconds: int,
70+
interval_seconds: float = 1,
71+
):
72+
deadline = time.monotonic() + timeout_seconds
73+
while time.monotonic() < deadline:
74+
if predicate():
75+
return
76+
time.sleep(interval_seconds)
77+
raise TimeoutError(f"{description} did not complete within {timeout_seconds} seconds.")
5278

5379
def inject_fault(self, inject: bool = False):
5480
print("== Fault Injection ==")

aiopslab/service/dock.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def cleanup(self):
3838
command = "docker container prune -f"
3939
return self.exec_command(command)
4040

41-
def exec_command(self, command: str, input_data=None, cwd=None):
41+
def exec_command(self, command: str, input_data=None, cwd=None, timeout=None):
4242
"""Execute an arbitrary command."""
4343
if input_data is not None:
4444
input_data = input_data.encode("utf-8")
@@ -50,8 +50,13 @@ def exec_command(self, command: str, input_data=None, cwd=None):
5050
stderr=subprocess.PIPE,
5151
shell=True,
5252
cwd=cwd,
53+
timeout=timeout,
5354
)
5455
if out is not None:
5556
return out.stdout.decode("utf-8")
57+
except subprocess.TimeoutExpired as e:
58+
raise RuntimeError(
59+
f"Timed out after {timeout} seconds executing command: {command}"
60+
) from e
5661
except subprocess.CalledProcessError as e:
5762
return e.stderr.decode("utf-8")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import unittest
2+
3+
from aiopslab.orchestrator.problems.flower_model_misconfig.model_misconfig import (
4+
FlowerModelMisconfigBaseTask,
5+
)
6+
7+
8+
class FlowerModelMisconfigTimeoutTest(unittest.TestCase):
9+
def test_wait_until_returns_when_predicate_succeeds(self):
10+
task = object.__new__(FlowerModelMisconfigBaseTask)
11+
12+
task._wait_until("ready", lambda: True, timeout_seconds=1, interval_seconds=0)
13+
14+
def test_wait_until_times_out_when_predicate_never_succeeds(self):
15+
task = object.__new__(FlowerModelMisconfigBaseTask)
16+
17+
with self.assertRaises(TimeoutError):
18+
task._wait_until("ready", lambda: False, timeout_seconds=0, interval_seconds=0)

tests/service/test_docker.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import unittest
2+
3+
from aiopslab.service.dock import Docker
4+
5+
6+
class DockerExecTimeoutTest(unittest.TestCase):
7+
def test_exec_command_times_out(self):
8+
docker = object.__new__(Docker)
9+
10+
with self.assertRaises(RuntimeError):
11+
docker.exec_command("sleep 1", timeout=0.01)

0 commit comments

Comments
 (0)