Skip to content

Commit 74c8493

Browse files
avoid --abort-on-container-exit to allow containers to react to SIGTERM
1 parent 766d196 commit 74c8493

File tree

4 files changed

+174
-104
lines changed

4 files changed

+174
-104
lines changed

docker-compose.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,9 @@ services:
3434
environment:
3535
- CRON=$CRON
3636
- ROLE=server
37-
- SERVER_PARAMS=$SERVER_PARAMS
3837
- SSLKEYLOGFILE=/logs/keys.log
3938
- QLOGDIR=/logs/qlog/
4039
- TESTCASE=$TESTCASE_SERVER
41-
depends_on:
42-
- sim
4340
cap_add:
4441
- NET_ADMIN
4542
ulimits:
@@ -48,6 +45,8 @@ services:
4845
rightnet:
4946
ipv4_address: 193.167.100.100
5047
ipv6_address: fd00:cafe:cafe:100::100
48+
extra_hosts:
49+
- "sim:193.167.100.2"
5150

5251
client:
5352
image: $CLIENT
@@ -61,13 +60,10 @@ services:
6160
environment:
6261
- CRON=$CRON
6362
- ROLE=client
64-
- CLIENT_PARAMS=$CLIENT_PARAMS
6563
- SSLKEYLOGFILE=/logs/keys.log
6664
- QLOGDIR=/logs/qlog/
6765
- TESTCASE=$TESTCASE_CLIENT
6866
- REQUESTS=$REQUESTS
69-
depends_on:
70-
- sim
7167
cap_add:
7268
- NET_ADMIN
7369
ulimits:
@@ -81,6 +77,7 @@ services:
8177
- "server6:fd00:cafe:cafe:100::100"
8278
- "server46:193.167.100.100"
8379
- "server46:fd00:cafe:cafe:100::100"
80+
- "sim:193.167.0.2"
8481

8582
iperf_server:
8683
image: martenseemann/quic-interop-iperf-endpoint
@@ -89,8 +86,6 @@ services:
8986
tty: true
9087
environment:
9188
- ROLE=server
92-
depends_on:
93-
- sim
9489
cap_add:
9590
- NET_ADMIN
9691
networks:
@@ -110,8 +105,6 @@ services:
110105
tty: true
111106
environment:
112107
- ROLE=client
113-
depends_on:
114-
- sim
115108
cap_add:
116109
- NET_ADMIN
117110
networks:

docker.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import io
2+
import logging
3+
import shutil
4+
import subprocess
5+
import threading
6+
7+
8+
class DockerRunner:
9+
_containers = None
10+
_cond = None
11+
_timeout = 0 # in seconds
12+
_expired = False
13+
14+
def __init__(self, timeout: int):
15+
self._containers = []
16+
self._cond = threading.Condition()
17+
self._timeout = timeout
18+
19+
def add_container(self, name: str, env: dict):
20+
self._containers.append({"name": name, "env": env})
21+
22+
def _run_container(self, cmd: str, env: dict, name: str):
23+
self._execute(cmd, env, name)
24+
with self._cond:
25+
logging.debug("%s container returned.", name)
26+
self._cond.notify()
27+
28+
def _execute(self, cmd: str, env: dict = {}, name: str = ""):
29+
p = subprocess.Popen(
30+
cmd.split(" "),
31+
bufsize=1,
32+
env=env,
33+
stdout=subprocess.PIPE,
34+
stderr=subprocess.STDOUT,
35+
universal_newlines=True,
36+
)
37+
for line in p.stdout:
38+
ll = ""
39+
if name:
40+
ll = name + ": "
41+
ll = ll + line.rstrip()
42+
logging.debug(ll)
43+
44+
def _run_timer(self):
45+
logging.debug("Timer expired. Stopping all containers.")
46+
self._expired = True
47+
with self._cond:
48+
self._cond.notify()
49+
50+
def run(self) -> (str, bool): # returns if the timer expired
51+
# also log to a string, so we can parse the output later
52+
output_string = io.StringIO()
53+
output_stream = logging.StreamHandler(output_string)
54+
output_stream.setLevel(logging.DEBUG)
55+
logging.getLogger().addHandler(output_stream)
56+
57+
threads = []
58+
# Start all containers (in separate threads)
59+
docker_compose = shutil.which("docker-compose")
60+
for e in self._containers:
61+
t = threading.Thread(
62+
target=self._run_container,
63+
kwargs={
64+
"cmd": docker_compose + " up " + e["name"],
65+
"env": e["env"],
66+
"name": e["name"],
67+
},
68+
)
69+
t.start()
70+
threads.append(t)
71+
# set a timer
72+
timer = threading.Timer(self._timeout, self._run_timer)
73+
timer.start()
74+
75+
# Wait for the first container to exit.
76+
# Then stop all other docker containers.
77+
with self._cond:
78+
self._cond.wait()
79+
names = [x["name"] for x in self._containers]
80+
self._execute(
81+
shutil.which("docker-compose") + " stop -t 5 " + " ".join(names)
82+
)
83+
# wait for all threads to finish
84+
for t in threads:
85+
t.join()
86+
timer.cancel()
87+
88+
output = output_string.getvalue()
89+
output_string.close()
90+
logging.getLogger().removeHandler(output_stream)
91+
return output, self._expired

interop.py

Lines changed: 76 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from termcolor import colored
1717

1818
import testcases
19+
from docker import DockerRunner
1920
from result import TestResult
2021
from testcases import Perspective
2122

@@ -98,9 +99,12 @@ def __init__(
9899
self.measurement_results[server][client][measurement] = {}
99100

100101
def _is_unsupported(self, lines: List[str]) -> bool:
101-
return any("exited with code 127" in str(line) for line in lines) or any(
102-
"exit status 127" in str(line) for line in lines
103-
)
102+
for line in lines:
103+
if "sim exited with code 127" in str(line):
104+
continue
105+
if "exited with code 127" in str(line) or "exit status 127" in str(line):
106+
return True
107+
return False
104108

105109
def _check_impl_is_compliant(self, name: str) -> bool:
106110
""" check if an implementation return UNSUPPORTED for unknown test cases """
@@ -121,46 +125,48 @@ def _check_impl_is_compliant(self, name: str) -> bool:
121125

122126
# check that the client is capable of returning UNSUPPORTED
123127
logging.debug("Checking compliance of %s client", name)
124-
cmd = (
125-
"CERTS=" + certs_dir.name + " "
126-
"TESTCASE_CLIENT=" + random_string(6) + " "
127-
"SERVER_LOGS=/dev/null "
128-
"CLIENT_LOGS=" + client_log_dir.name + " "
129-
"WWW=" + www_dir.name + " "
130-
"DOWNLOADS=" + downloads_dir.name + " "
131-
'SCENARIO="simple-p2p --delay=15ms --bandwidth=10Mbps --queue=25" '
132-
"CLIENT=" + self._implementations[name]["image"] + " "
133-
"docker-compose up --timeout 0 --abort-on-container-exit -V sim client"
134-
)
135-
output = subprocess.run(
136-
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
137-
)
138-
if not self._is_unsupported(output.stdout.splitlines()):
128+
env_sim = {"SCENARIO": "simple-p2p --delay=15ms --bandwidth=10Mbps --queue=25"}
129+
env_client = {
130+
"CERTS": "./certs",
131+
"TESTCASE_CLIENT": random_string(6),
132+
"DOWNLOADS": downloads_dir.name,
133+
"CLIENT_LOGS": client_log_dir.name,
134+
"CLIENT": self._implementations[name]["image"],
135+
}
136+
env = {}
137+
env.update(env_sim)
138+
env.update(env_client)
139+
r = DockerRunner(15)
140+
r.add_container("client", env)
141+
r.add_container("sim", env)
142+
output, expired = r.run()
143+
if expired or not self._is_unsupported(output.splitlines()):
139144
logging.error("%s client not compliant.", name)
140-
logging.debug("%s", output.stdout.decode("utf-8"))
145+
logging.debug("%s", output)
141146
self.compliant[name] = False
142147
return False
143148
logging.debug("%s client compliant.", name)
144149

145150
# check that the server is capable of returning UNSUPPORTED
146151
logging.debug("Checking compliance of %s server", name)
147152
server_log_dir = tempfile.TemporaryDirectory(dir="/tmp", prefix="logs_server_")
148-
cmd = (
149-
"CERTS=" + certs_dir.name + " "
150-
"TESTCASE_SERVER=" + random_string(6) + " "
151-
"SERVER_LOGS=" + server_log_dir.name + " "
152-
"CLIENT_LOGS=/dev/null "
153-
"WWW=" + www_dir.name + " "
154-
"DOWNLOADS=" + downloads_dir.name + " "
155-
"SERVER=" + self._implementations[name]["image"] + " "
156-
"docker-compose up -V server"
157-
)
158-
output = subprocess.run(
159-
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
160-
)
161-
if not self._is_unsupported(output.stdout.splitlines()):
153+
env_server = {
154+
"CERTS": "./certs",
155+
"TESTCASE_SERVER": random_string(6),
156+
"SERVER_LOGS": server_log_dir.name,
157+
"WWW": www_dir.name,
158+
"SERVER": self._implementations[name]["image"],
159+
}
160+
env = {}
161+
env.update(env_sim)
162+
env.update(env_server)
163+
r = DockerRunner(15)
164+
r.add_container("server", env)
165+
r.add_container("sim", env)
166+
output, expired = r.run()
167+
if expired or not self._is_unsupported(output.splitlines()):
162168
logging.error("%s server not compliant.", name)
163-
logging.debug("%s", output.stdout.decode("utf-8"))
169+
logging.debug("%s", output)
164170
self.compliant[name] = False
165171
return False
166172
logging.debug("%s server compliant.", name)
@@ -329,74 +335,54 @@ def _run_test(
329335
server_keylog_file=server_log_dir.name + "/keys.log",
330336
)
331337
print(
332-
"Server: "
333-
+ server
334-
+ ". Client: "
335-
+ client
336-
+ ". Running test case: "
337-
+ str(testcase)
338+
"Server: {}. Client: {}. Running test: {}".format(
339+
server, client, str(testcase)
340+
)
338341
)
339342

340343
reqs = " ".join([testcase.urlprefix() + p for p in testcase.get_paths()])
341344
logging.debug("Requests: %s", reqs)
342-
params = (
343-
"WAITFORSERVER=server:443 "
344-
"CERTS=" + testcase.certs_dir() + " "
345-
"TESTCASE_SERVER=" + testcase.testname(Perspective.SERVER) + " "
346-
"TESTCASE_CLIENT=" + testcase.testname(Perspective.CLIENT) + " "
347-
"WWW=" + testcase.www_dir() + " "
348-
"DOWNLOADS=" + testcase.download_dir() + " "
349-
"SERVER_LOGS=" + server_log_dir.name + " "
350-
"CLIENT_LOGS=" + client_log_dir.name + " "
351-
'SCENARIO="{}" '
352-
"CLIENT=" + self._implementations[client]["image"] + " "
353-
"SERVER=" + self._implementations[server]["image"] + " "
354-
'REQUESTS="' + reqs + '" '
355-
).format(testcase.scenario())
356-
params += " ".join(testcase.additional_envs())
357-
containers = "sim client server " + " ".join(testcase.additional_containers())
358-
cmd = (
359-
params
360-
+ " docker-compose up --abort-on-container-exit --timeout 1 "
361-
+ containers
362-
)
363-
logging.debug("Command: %s", cmd)
345+
r = DockerRunner(timeout=testcase.timeout())
346+
env_server = {
347+
"CERTS": "./certs",
348+
"TESTCASE_SERVER": testcase.testname(Perspective.SERVER),
349+
"WWW": testcase.www_dir(),
350+
"SERVER_LOGS": server_log_dir.name,
351+
"SERVER": self._implementations[server]["image"],
352+
}
353+
env_client = {
354+
"CERTS": "./certs",
355+
"TESTCASE_CLIENT": testcase.testname(Perspective.CLIENT),
356+
"DOWNLOADS": testcase.download_dir(),
357+
"CLIENT_LOGS": client_log_dir.name,
358+
"CLIENT": self._implementations[client]["image"],
359+
"REQUESTS": reqs,
360+
}
361+
env_sim = {
362+
"SCENARIO": testcase.scenario(),
363+
"WAITFORSERVER": "server:443",
364+
}
365+
env = {}
366+
env.update(env_server)
367+
env.update(env_client)
368+
env.update(env_sim)
369+
r.add_container(name="server", env=env)
370+
r.add_container(name="client", env=env)
371+
r.add_container(name="sim", env=env)
372+
for c in testcase.additional_containers():
373+
r.add_container(name=c, env=testcase.additional_envs())
374+
output, expired = r.run()
364375

365376
status = TestResult.FAILED
366-
output = ""
367-
expired = False
368-
try:
369-
r = subprocess.run(
370-
cmd,
371-
shell=True,
372-
stdout=subprocess.PIPE,
373-
stderr=subprocess.STDOUT,
374-
timeout=testcase.timeout(),
375-
)
376-
output = r.stdout
377-
except subprocess.TimeoutExpired as ex:
378-
output = ex.stdout
379-
expired = True
380-
381-
logging.debug("%s", output.decode("utf-8"))
382-
383-
if expired:
384-
logging.debug("Test failed: took longer than %ds.", testcase.timeout())
385-
r = subprocess.run(
386-
"docker-compose stop " + containers,
387-
shell=True,
388-
stdout=subprocess.PIPE,
389-
stderr=subprocess.STDOUT,
390-
timeout=60,
391-
)
392-
logging.debug("%s", r.stdout.decode("utf-8"))
393377

394378
# copy the pcaps from the simulator
395379
self._copy_logs("sim", sim_log_dir)
396380
self._copy_logs("client", client_log_dir)
397381
self._copy_logs("server", server_log_dir)
398382

399-
if not expired:
383+
if expired:
384+
logging.debug("Test failed: took longer than %ds.", testcase.timeout())
385+
else:
400386
lines = output.splitlines()
401387
if self._is_unsupported(lines):
402388
status = TestResult.UNSUPPORTED

testcases.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ def urlprefix() -> str:
106106

107107
@staticmethod
108108
def additional_envs() -> List[str]:
109-
return [""]
109+
return []
110110

111111
@staticmethod
112112
def additional_containers() -> List[str]:
113-
return [""]
113+
return []
114114

115115
def www_dir(self):
116116
if not self._www_dir:
@@ -1406,8 +1406,8 @@ def timeout() -> int:
14061406
return 180
14071407

14081408
@staticmethod
1409-
def additional_envs() -> List[str]:
1410-
return ["IPERF_CONGESTION=cubic"]
1409+
def additional_envs() -> dict:
1410+
return {"IPERF_CONGESTION": "cubic"}
14111411

14121412
@staticmethod
14131413
def additional_containers() -> List[str]:

0 commit comments

Comments
 (0)