Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Commit ef61092

Browse files
author
Feng Honglin
authored
1.6.5 (#183)
* Add reload timeout for old process (#174) * Add RELOAD_TIMEOUT to configs file * add timeout to update_helper * update tests * Fix bug where 'timer' wasn't defined * "logger" not "logging" * Update readme * bump version & reformat the code
1 parent 44fdc4a commit ef61092

File tree

5 files changed

+123
-19
lines changed

5 files changed

+123
-19
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ Settings in this part is immutable, you have to redeploy HAProxy service to make
220220
|MONITOR_PORT| |the port number where monitor_uri should be added to. Use together with `MONITOR_URI`. Possible value: `80`|
221221
|MONITOR_URI| |the exact URI which we want to intercept to return HAProxy's health status instead of forwarding the request.See: http://cbonte.github.io/haproxy-dconv/configuration-1.5.html#4-monitor-uri. Possible value: `/ping`|
222222
|OPTION|redispatch|comma-separated list of HAProxy `option` entries to the `default` section.|
223+
|RELOAD_TIMEOUT|0| When haproxy is reconfigured, a new process starts and attaches to the TCP socket for new connections, leaving the old process to handle existing connections. This timeout specifies how long the old process is permitted to continue running before being killed. <br/> `-1`: Old process is killed immediately<br/> `0`: No timeout, old process will run as long as TCP connections last. This could potentially be quite a while as `http-keep-alives` are enabled which will keep TCP connections open.<br/> `>0`: Timeout in secs after which the process will be killed.
223224
|RSYSLOG_DESTINATION|127.0.0.1|the rsyslog destination to where HAProxy logs are sent|
224225
|SKIP_FORWARDED_PROTO||If set to any value, HAProxy will not add an X-Forwarded- headers. This can be used when combining HAProxy with another load balancer|
225226
|SSL_BIND_CIPHERS| |explicitly set which SSL ciphers will be used for the SSL server. This sets the HAProxy `ssl-default-bind-ciphers` configuration setting.|

haproxy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.6.4"
1+
__version__ = "1.6.5"

haproxy/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ def parse_additional_backend_settings(envvars):
8585
TIMEOUT = os.getenv("TIMEOUT", "connect 5000, client 50000, server 50000")
8686
NBPROC = int(os.getenv("NBPROC", 1))
8787
SWARM_MODE_POLLING_INTERVAL = int(os.getenv("SWARM_MODE_POLLING_INTERVAL", 5))
88-
HAPROXY_USER=os.getenv("HAPROXY_USER", "haproxy")
89-
HAPROXY_GROUP=os.getenv("HAPROXY_GROUP", "haproxy")
88+
HAPROXY_USER = os.getenv("HAPROXY_USER", "haproxy")
89+
HAPROXY_GROUP = os.getenv("HAPROXY_GROUP", "haproxy")
90+
RELOAD_TIMEOUT = os.getenv("RELOAD_TIMEOUT", "0")
9091

9192
# global
9293
RUNNING_MODE = None

haproxy/helper/update_helper.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
import logging
22
import subprocess
3-
import thread
3+
import threading
4+
import time
45

5-
from haproxy.config import HAPROXY_RUN_COMMAND
6+
from haproxy.config import HAPROXY_RUN_COMMAND, RELOAD_TIMEOUT
67

78
logger = logging.getLogger("haproxy")
89

910

10-
def run_reload(old_process):
11+
# RELOAD_TIMEOUT has the following values and effect:
12+
# -1 : Reload haproxy with "-st" which will immediately kill the previous process
13+
# 0 : Reload haproxy with "-sf" and no timeout. This can potentially leave
14+
# "broken" processes (where the backends have changed) hanging around
15+
# with existing connections.
16+
# > 0 : Reload haproxy with "-sf" but if it takes longer than RELOAD_TIMEOUT then kill it
17+
# This gives existing connections a chance to finish. RELOAD_TIMEOUT should be set to
18+
# the approximate time it takes docker to finish updating services. By this point the
19+
# existing configuration will be invalid, and any connections still using it will
20+
# have invalid backends.
21+
#
22+
def run_reload(old_process, timeout=int(RELOAD_TIMEOUT)):
1123
if old_process:
1224
# Reload haproxy
1325
logger.info("Reloading HAProxy")
14-
new_process = subprocess.Popen(HAPROXY_RUN_COMMAND + ["-sf", str(old_process.pid)])
15-
thread.start_new_thread(wait_pid, (old_process,))
16-
logger.info("HAProxy has been reloaded(PID: %s)", str(new_process.pid))
26+
if timeout == -1:
27+
flag = "-st"
28+
logger.info("Restarting HAProxy immediately")
29+
else:
30+
flag = "-sf"
31+
logger.info("Restarting HAProxy gracefully")
32+
33+
new_process = subprocess.Popen(HAPROXY_RUN_COMMAND + [flag, str(old_process.pid)])
34+
logger.info("HAProxy is reloading (new PID: %s)", str(new_process.pid))
35+
36+
thread = threading.Thread(target=wait_pid, args=[old_process, timeout])
37+
thread.start()
38+
39+
# Block only if we have a timeout. If we don't it could take forever, and so
40+
# returning immediately maintains the original behaviour of no timeout.
41+
if timeout > 0:
42+
thread.join()
43+
1744
else:
1845
# Launch haproxy
1946
logger.info("Launching HAProxy")
@@ -23,6 +50,29 @@ def run_reload(old_process):
2350
return new_process
2451

2552

26-
def wait_pid(process):
53+
def wait_pid(process, timeout):
54+
start = time.time()
55+
56+
timer = None
57+
58+
if timeout > 0:
59+
timer = threading.Timer(timeout, timeout_handler, [process])
60+
timer.start()
61+
2762
process.wait()
28-
logger.info("HAProxy(PID:%s) has been terminated" % str(process.pid))
63+
64+
if timer is not None:
65+
timer.cancel();
66+
67+
duration = time.time() - start
68+
logger.info("Old HAProxy(PID: %s) ended after %s sec", str(process.pid), str(duration))
69+
70+
71+
def timeout_handler(processs):
72+
if processs.poll() is None:
73+
try:
74+
processs.terminate()
75+
logger.info("Old HAProxy process taking too long to complete - terminating")
76+
except OSError as e:
77+
if e.errno != errno.ESRCH:
78+
raise

tests/unit/helper/test_update_helper.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,75 @@
66

77

88
class UpdateHelperTestCase(unittest.TestCase):
9+
class blockingObject(object):
10+
terminated = False
11+
timeout = None
12+
13+
def __init__(self, timeout):
14+
self.timeout = timeout
15+
16+
def wait(self):
17+
startTime = time.time()
18+
# block until waiting == true or we've hit the timeout
19+
while self.terminated == False and time.time() < startTime + self.timeout:
20+
time.sleep(0.5)
21+
22+
def poll(self):
23+
return None
24+
25+
def terminate(self):
26+
self.terminated = True
27+
28+
pass
29+
930
class Object(object):
1031
pass
1132

12-
@mock.patch("haproxy.helper.update_helper.thread.start_new_thread")
1333
@mock.patch("haproxy.helper.update_helper.subprocess.Popen")
14-
def test_run_reload_with_old_process(self, mock_popen, mock_new_thread):
34+
def test_run_graceful_reload_within_timeout(self, mock_popen):
35+
old_process = UpdateHelperTestCase.blockingObject(2)
36+
old_process.pid = "old_pid"
37+
new_process = UpdateHelperTestCase.Object()
38+
new_process.pid = "new_pid"
39+
mock_popen.return_value = new_process
40+
run_reload(old_process, 5)
41+
self.assertFalse(old_process.terminated)
42+
43+
@mock.patch("haproxy.helper.update_helper.subprocess.Popen")
44+
def test_run_graceful_reload_exceeding_timeout(self, mock_popen):
45+
old_process = UpdateHelperTestCase.blockingObject(10)
46+
old_process.pid = "old_pid"
47+
new_process = UpdateHelperTestCase.Object()
48+
new_process.pid = "new_pid"
49+
mock_popen.return_value = new_process
50+
run_reload(old_process, 5)
51+
self.assertTrue(old_process.terminated)
52+
53+
@mock.patch("haproxy.helper.update_helper.threading.Thread")
54+
@mock.patch("haproxy.helper.update_helper.subprocess.Popen")
55+
def test_run_graceful_reload_with_old_process(self, mock_popen, mock_new_thread):
56+
old_process = UpdateHelperTestCase.Object()
57+
old_process.pid = "old_pid"
58+
new_process = UpdateHelperTestCase.Object()
59+
new_process.pid = "new_pid"
60+
mock_popen.return_value = new_process
61+
run_reload(old_process, 0)
62+
mock_popen.assert_called_with(HAPROXY_RUN_COMMAND + ['-sf', 'old_pid'])
63+
mock_new_thread.assert_called_with(target=wait_pid, args=[old_process, 0])
64+
65+
@mock.patch("haproxy.helper.update_helper.threading.Thread")
66+
@mock.patch("haproxy.helper.update_helper.subprocess.Popen")
67+
def test_run_brutal_reload_with_old_process(self, mock_popen, mock_new_thread):
1568
old_process = UpdateHelperTestCase.Object()
16-
old_process.pid = "pid"
69+
old_process.pid = "old_pid"
1770
new_process = UpdateHelperTestCase.Object()
1871
new_process.pid = "new_pid"
1972
mock_popen.return_value = new_process
20-
mock_popen.return_value = old_process
21-
run_reload(old_process)
22-
mock_popen.assert_called_with(HAPROXY_RUN_COMMAND + ['-sf', 'pid'])
23-
mock_new_thread.caslled_with(wait_pid, (old_process,))
73+
run_reload(old_process, -1)
74+
mock_popen.assert_called_with(HAPROXY_RUN_COMMAND + ['-st', 'old_pid'])
75+
mock_new_thread.assert_called_with(target=wait_pid, args=[old_process, -1])
2476

25-
@mock.patch("haproxy.helper.update_helper.thread.start_new_thread")
77+
@mock.patch("haproxy.helper.update_helper.threading.Thread")
2678
@mock.patch("haproxy.helper.update_helper.subprocess.Popen")
2779
def test_run_reload_with_empty_old_process(self, mock_popen, mock_new_thread):
2880
new_process = UpdateHelperTestCase.Object()

0 commit comments

Comments
 (0)