Skip to content

Commit 6a9ba61

Browse files
committed
TUN-4051: Add component-tests to test graceful shutdown
1 parent 848c44b commit 6a9ba61

File tree

4 files changed

+120
-15
lines changed

4 files changed

+120
-15
lines changed

component-tests/test_logging.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class TestLogging:
1313
def test_logging_to_terminal(self, tmp_path, component_tests_config):
1414
config = component_tests_config()
1515
with start_cloudflared(tmp_path, config, new_process=True) as cloudflared:
16-
wait_tunnel_ready()
16+
wait_tunnel_ready(tunnel_url=config.get_url())
1717
self.assert_log_to_terminal(cloudflared)
1818

1919
def test_logging_to_file(self, tmp_path, component_tests_config):
@@ -24,7 +24,7 @@ def test_logging_to_file(self, tmp_path, component_tests_config):
2424
}
2525
config = component_tests_config(extra_config)
2626
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
27-
wait_tunnel_ready()
27+
wait_tunnel_ready(tunnel_url=config.get_url())
2828
self.assert_log_in_file(log_file)
2929
self.assert_json_log(log_file)
3030

@@ -37,7 +37,7 @@ def test_logging_to_dir(self, tmp_path, component_tests_config):
3737
}
3838
config = component_tests_config(extra_config)
3939
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
40-
wait_tunnel_ready()
40+
wait_tunnel_ready(tunnel_url=config.get_url())
4141
self.assert_log_to_dir(config, log_dir)
4242

4343
def assert_log_to_terminal(self, cloudflared):

component-tests/test_reconnect.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from retrying import retry
55
from time import sleep
66

7-
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_ready, send_requests
7+
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected, send_requests
88

99

1010
class TestReconnect():
@@ -35,16 +35,17 @@ def send_reconnect(self, cloudflared, secs):
3535
cloudflared.stdin.flush()
3636

3737
def assert_reconnect(self, config, cloudflared, repeat):
38-
wait_tunnel_ready()
38+
wait_tunnel_ready(tunnel_url=config.get_url())
3939
for _ in range(repeat):
4040
for i in range(self.default_ha_conns):
4141
self.send_reconnect(cloudflared, self.default_reconnect_secs)
4242
expect_connections = self.default_ha_conns-i-1
4343
if expect_connections > 0:
44+
# Don't check if tunnel returns 200 here because there is a race condition between wait_tunnel_ready
45+
# retrying to get 200 response and reconnecting
4446
wait_tunnel_ready(expect_connections=expect_connections)
4547
else:
46-
check_tunnel_not_ready()
48+
check_tunnel_not_connected()
4749

4850
sleep(self.default_reconnect_secs + 10)
49-
wait_tunnel_ready()
50-
send_requests(config.get_url(), 1)
51+
wait_tunnel_ready(tunnel_url=config.get_url())
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env python
2+
from contextlib import contextmanager
3+
import requests
4+
import signal
5+
import threading
6+
import time
7+
8+
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected, LOGGER
9+
10+
11+
class TestTermination():
12+
grace_period = 5
13+
timeout = 10
14+
extra_config = {
15+
"grace-period": f"{grace_period}s",
16+
}
17+
signals = [signal.SIGTERM, signal.SIGINT]
18+
sse_endpoint = "/sse?freq=1s"
19+
20+
def test_graceful_shutdown(self, tmp_path, component_tests_config):
21+
config = component_tests_config(self.extra_config)
22+
for sig in self.signals:
23+
with start_cloudflared(
24+
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
25+
wait_tunnel_ready(tunnel_url=config.get_url())
26+
27+
connected = threading.Condition()
28+
in_flight_req = threading.Thread(
29+
target=self.stream_request, args=(config, connected,))
30+
in_flight_req.start()
31+
32+
with connected:
33+
connected.wait(self.timeout)
34+
# Send signal after the SSE connection is established
35+
self.terminate_by_signal(cloudflared, sig)
36+
self.wait_eyeball_thread(
37+
in_flight_req, self.grace_period + self.timeout)
38+
39+
# test cloudflared terminates before grace period expires when all eyeball
40+
# connections are drained
41+
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config):
42+
config = component_tests_config(self.extra_config)
43+
for sig in self.signals:
44+
with start_cloudflared(
45+
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
46+
wait_tunnel_ready(tunnel_url=config.get_url())
47+
48+
connected = threading.Condition()
49+
in_flight_req = threading.Thread(
50+
target=self.stream_request, args=(config, connected, True, ))
51+
in_flight_req.start()
52+
53+
with self.within_grace_period():
54+
with connected:
55+
connected.wait(self.timeout)
56+
# Send signal after the SSE connection is established
57+
self.terminate_by_signal(cloudflared, sig)
58+
self.wait_eyeball_thread(in_flight_req, self.grace_period)
59+
60+
def test_no_connection_shutdown(self, tmp_path, component_tests_config):
61+
config = component_tests_config(self.extra_config)
62+
for sig in self.signals:
63+
with start_cloudflared(
64+
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
65+
wait_tunnel_ready(tunnel_url=config.get_url())
66+
with self.within_grace_period():
67+
self.terminate_by_signal(cloudflared, sig)
68+
69+
def terminate_by_signal(self, cloudflared, sig):
70+
cloudflared.send_signal(sig)
71+
check_tunnel_not_connected()
72+
cloudflared.wait()
73+
74+
def wait_eyeball_thread(self, thread, timeout):
75+
thread.join(timeout)
76+
assert thread.is_alive() == False, "eyeball thread is still alive"
77+
78+
# Using this context asserts logic within the context is executed within grace period
79+
@contextmanager
80+
def within_grace_period(self):
81+
try:
82+
start = time.time()
83+
yield
84+
finally:
85+
duration = time.time() - start
86+
assert duration < self.grace_period
87+
88+
def stream_request(self, config, connected, early_terminate):
89+
expected_terminate_message = "502 Bad Gateway"
90+
url = config.get_url() + self.sse_endpoint
91+
92+
with requests.get(url, timeout=5, stream=True) as resp:
93+
with connected:
94+
connected.notifyAll()
95+
lines = 0
96+
for line in resp.iter_lines():
97+
if expected_terminate_message.encode() == line:
98+
break
99+
lines += 1
100+
if early_terminate and lines == 2:
101+
return
102+
# /sse returns count followed by 2 new lines
103+
assert lines >= (self.grace_period * 2)

component-tests/util.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,26 @@ def run_cloudflared_background(cmd, allow_input, capture_output):
4444

4545

4646
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
47-
def wait_tunnel_ready(expect_connections=4):
48-
url = f'http://localhost:{METRICS_PORT}/ready'
47+
def wait_tunnel_ready(tunnel_url=None, expect_connections=4):
48+
metrics_url = f'http://localhost:{METRICS_PORT}/ready'
4949

5050
with requests.Session() as s:
51-
resp = send_request(s, url, True)
51+
resp = send_request(s, metrics_url, True)
5252
assert resp.json()[
53-
"readyConnections"] == expect_connections, f"Ready endpoint returned {resp.json()} but we expect {expect_connections} ready connections"
53+
"readyConnections"] >= expect_connections, f"Ready endpoint returned {resp.json()} but we expect at least {expect_connections} connections"
54+
if tunnel_url is not None:
55+
send_request(s, tunnel_url, True)
5456

5557

5658
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
57-
def check_tunnel_not_ready():
59+
def check_tunnel_not_connected():
5860
url = f'http://localhost:{METRICS_PORT}/ready'
5961

6062
resp = requests.get(url, timeout=1)
6163
assert resp.status_code == 503, f"Expect {url} returns 503, got {resp.status_code}"
6264

63-
# In some cases we don't need to check response status, such as when sending batch requests to generate logs
64-
6565

66+
# In some cases we don't need to check response status, such as when sending batch requests to generate logs
6667
def send_requests(url, count, require_ok=True):
6768
errors = 0
6869
with requests.Session() as s:

0 commit comments

Comments
 (0)