Merge lp:~cjwatson/rabbitfixture/work-around-stop-hang into lp:rabbitfixture

Proposed by Colin Watson
Status: Merged
Merged at revision: 34
Proposed branch: lp:~cjwatson/rabbitfixture/work-around-stop-hang
Merge into: lp:rabbitfixture
Diff against target: 199 lines (+93/-24)
3 files modified
rabbitfixture/server.py (+41/-17)
rabbitfixture/tests/test_server.py (+40/-1)
setup.py (+12/-6)
To merge this branch: bzr merge lp:~cjwatson/rabbitfixture/work-around-stop-hang
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+257361@code.launchpad.net

Commit message

Apply a timeout to all rabbitmqctl calls to work around occasional hangs on stop.

Description of the change

Apply a timeout to all rabbitmqctl calls to work around occasional hangs on stop.

subprocess32 is used because that adds a timeout facility to communicate().

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'rabbitfixture/server.py'
2--- rabbitfixture/server.py 2014-05-29 12:26:57 +0000
3+++ rabbitfixture/server.py 2015-04-24 12:33:24 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2011 Canonical Ltd. This software is licensed under the
6+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Test server fixtures for RabbitMQ."""
10@@ -14,7 +14,10 @@
11 import re
12 import signal
13 import socket
14-import subprocess
15+try:
16+ import subprocess32 as subprocess
17+except ImportError:
18+ import subprocess
19 import time
20
21 from amqplib import client_0_8 as amqp
22@@ -194,18 +197,31 @@
23 yield error
24 yield '\n'
25
26- def rabbitctl(self, command, strip=False):
27+ @property
28+ def ctlbin(self):
29+ return os.path.join(RABBITBIN, "rabbitmqctl")
30+
31+ @property
32+ def ctltimeout(self):
33+ return 15
34+
35+ def rabbitctl(self, command, strip=False, timeout=None):
36 """Executes a ``rabbitctl`` command and returns status."""
37- ctlbin = os.path.join(RABBITBIN, "rabbitmqctl")
38+ if timeout is None:
39+ timeout = self.ctltimeout
40 nodename = self.config.fq_nodename
41 env = dict(os.environ, HOME=self.config.homedir)
42 if isinstance(command, str):
43 command = (command,)
44 ctl = subprocess.Popen(
45- (ctlbin, "-n", nodename) + command, env=env,
46+ (self.ctlbin, "-n", nodename) + command, env=env,
47 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
48 preexec_fn=preexec_fn)
49- outstr, errstr = ctl.communicate()
50+ try:
51+ outstr, errstr = ctl.communicate(timeout=timeout)
52+ except subprocess.TimeoutExpired:
53+ ctl.kill()
54+ raise
55 if strip:
56 return outstr.strip(), errstr.strip()
57 return outstr, errstr
58@@ -213,7 +229,10 @@
59 def is_node_running(self):
60 """Checks that our RabbitMQ node is up and running."""
61 nodename = self.config.fq_nodename
62- outdata, errdata = self.rabbitctl("status")
63+ try:
64+ outdata, errdata = self.rabbitctl("status")
65+ except subprocess.TimeoutExpired:
66+ return False
67 if errdata:
68 self._errors.append(errdata)
69 if not outdata:
70@@ -336,16 +355,21 @@
71
72 def _stop(self):
73 """Stop the running server. Normally called by cleanups."""
74- self._request_stop()
75- # Wait for the node to go down...
76- timeout = time.time() + 15
77- while time.time() < timeout:
78- if not self.environment.is_node_running():
79- break
80- time.sleep(0.3)
81- else:
82- raise Exception(
83- "Timeout waiting for RabbitMQ node to go down.")
84+ try:
85+ self._request_stop()
86+ # Wait for the node to go down...
87+ timeout = time.time() + 15
88+ while time.time() < timeout:
89+ if not self.environment.is_node_running():
90+ break
91+ time.sleep(0.3)
92+ else:
93+ raise Exception(
94+ "Timeout waiting for RabbitMQ node to go down.")
95+ except subprocess.TimeoutExpired:
96+ # Go straight to killing the process directly.
97+ timeout = time.time()
98+
99 # Wait at least 5 more seconds for the process to end...
100 timeout = max(timeout, time.time() + 5)
101 while time.time() < timeout:
102
103=== modified file 'rabbitfixture/tests/test_server.py'
104--- rabbitfixture/tests/test_server.py 2014-05-29 12:33:23 +0000
105+++ rabbitfixture/tests/test_server.py 2015-04-24 12:33:24 +0000
106@@ -7,10 +7,15 @@
107
108 import os.path
109 import socket
110+import stat
111 from textwrap import dedent
112
113 from amqplib import client_0_8 as amqp
114-from fixtures import EnvironmentVariableFixture
115+from fixtures import (
116+ EnvironmentVariableFixture,
117+ MonkeyPatch,
118+ TempDir,
119+ )
120 from rabbitfixture.server import (
121 get_nodename_from_status,
122 RabbitServer,
123@@ -55,6 +60,40 @@
124 # The daemon should be closed now.
125 self.assertRaises(socket.error, amqp.Connection, **connect_arguments)
126
127+ def test_stop_hang(self):
128+ # If rabbitctl hangs on shutdown, the fixture eventually manages to
129+ # stop RabbitMQ anyway.
130+ bindir = self.useFixture(TempDir()).path
131+ fakectlbin = os.path.join(bindir, "rabbitmqctl")
132+ with open(fakectlbin, "w") as f:
133+ f.write("#! /bin/sh\n")
134+ f.write("while :; do sleep 1 || exit; done\n")
135+ os.chmod(fakectlbin, stat.S_IRWXU)
136+
137+ with RabbitServer() as fixture:
138+ try:
139+ connect_arguments = {
140+ "host": 'localhost:%s' % fixture.config.port,
141+ "userid": "guest", "password": "guest",
142+ "virtual_host": "/", "insist": False,
143+ }
144+
145+ self.useFixture(MonkeyPatch(
146+ "rabbitfixture.server.RabbitServerEnvironment.ctlbin",
147+ fakectlbin))
148+ self.useFixture(MonkeyPatch(
149+ "rabbitfixture.server.RabbitServerEnvironment.ctltimeout",
150+ 0.1))
151+ except Exception:
152+ # self.useFixture() is not being used because we want to
153+ # handle the fixture's lifecycle, so we must also be
154+ # responsible for propagating fixture details.
155+ gather_details(fixture.getDetails(), self.getDetails())
156+ raise
157+
158+ # The daemon should be closed now.
159+ self.assertRaises(socket.error, amqp.Connection, **connect_arguments)
160+
161 def test_config(self):
162 # The configuration can be passed in.
163 config = RabbitServerResources()
164
165=== modified file 'setup.py'
166--- setup.py 2014-05-29 13:19:05 +0000
167+++ setup.py 2015-04-24 12:33:24 +0000
168@@ -1,9 +1,20 @@
169 #!/usr/bin/env python
170 """Distutils installer for rabbitfixture."""
171
172+import sys
173+
174 from setuptools import setup, find_packages
175
176
177+install_requires = [
178+ 'amqplib >= 0.6.1',
179+ 'fixtures >= 0.3.6',
180+ 'setuptools',
181+ 'testtools >= 0.9.12',
182+ ]
183+if sys.version_info[0] < 3:
184+ install_requires.append('subprocess32')
185+
186 setup(
187 name='rabbitfixture',
188 version="0.3.5",
189@@ -12,9 +23,4 @@
190 include_package_data=True,
191 zip_safe=False,
192 description='Magic.',
193- install_requires=[
194- 'amqplib >= 0.6.1',
195- 'fixtures >= 0.3.6',
196- 'setuptools',
197- 'testtools >= 0.9.12',
198- ])
199+ install_requires=install_requires)

Subscribers

People subscribed via source and target branches

to all changes: