launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05256
[Merge] lp:~lifeless/python-oops-amqp/0.0.2 into lp:python-oops-amqp
Robert Collins has proposed merging lp:~lifeless/python-oops-amqp/0.0.2 into lp:python-oops-amqp.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #875976 in python-oops-amqp: "pydoc oops_amqp misleading"
https://bugs.launchpad.net/python-oops-amqp/+bug/875976
Bug #875984 in python-oops-amqp: "pydoc refers to nonexisting OOPSDateDirRepo"
https://bugs.launchpad.net/python-oops-amqp/+bug/875984
For more details, see:
https://code.launchpad.net/~lifeless/python-oops-amqp/0.0.2/+merge/79637
0.0.2
-----
* Fix documentation warts from initial release, updated to 0.0.2 and prepare
for making the receiver deal with interrupted services.
(Robert Collins, #875976, #875984)
* Fix Receiver.run_forever to actually run forever. (Robert Collins)
* Change API for constructing Receiver to take a connection factory rather than
a channel. This will permit handling transient faults internally rather than
forcing a restart. (Robert Collins)
* Implement resiliency for the Receiver: automatically reconnect if a socket
error is received from rabbit, for up to two minutes of downtime.
(Robert Collins)
--
https://code.launchpad.net/~lifeless/python-oops-amqp/0.0.2/+merge/79637
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/python-oops-amqp/0.0.2 into lp:python-oops-amqp.
=== modified file 'NEWS'
--- NEWS 2011-10-10 21:49:50 +0000
+++ NEWS 2011-10-18 00:05:27 +0000
@@ -6,6 +6,23 @@
NEXT
----
+0.0.2
+-----
+
+* Fix documentation warts from initial release, updated to 0.0.2 and prepare
+ for making the receiver deal with interrupted services.
+ (Robert Collins, #875976, #875984)
+
+* Fix Receiver.run_forever to actually run forever. (Robert Collins)
+
+* Change API for constructing Receiver to take a connection factory rather than
+ a channel. This will permit handling transient faults internally rather than
+ forcing a restart. (Robert Collins)
+
+* Implement resiliency for the Receiver: automatically reconnect if a socket
+ error is received from rabbit, for up to two minutes of downtime.
+ (Robert Collins)
+
0.0.1
-----
=== modified file 'README'
--- README 2011-10-10 21:49:50 +0000
+++ README 2011-10-18 00:05:27 +0000
@@ -98,17 +98,16 @@
AMQP. To use it you need to configure a local config to publish the received
reports. A full config is used because that includes support for filtering
(which can be useful if you need to throttle volume, for instance).
-Additionally you need an amqp channel object and a queue name to receive from.
+Additionally you need an amqp connection factory (to handle the amqp server
+being restarted) and a queue name to receive from.
-This example uses the OOPSDateDirRepo publisher, telling it to accept whatever
+This example uses the DateDirRepo publisher, telling it to accept whatever
id was assigned by the process publishing to AMQP::
- >>> publisher = oops_datedir_repo.OOPSDateDirRepo('.', inherit_id=True)
+ >>> publisher = oops_datedir_repo.DateDirRepo('.', inherit_id=True)
>>> config = oops.Config()
>>> config.publishers.append(publisher.publish)
- >>> connection = amqp.Connection(host="localhost:5672",
- ... userid="guest", password="guest", virtual_host="/", insist=False)
- >>> receiver = oops_amqp.Receiver(config, connection, "my queue")
+ >>> receiver = oops_amqp.Receiver(config, factory, "my queue")
>>> receiver.run_forever()
For more information see pydoc oops_amqp.
=== modified file 'oops_amqp/__init__.py'
--- oops_amqp/__init__.py 2011-10-10 21:49:50 +0000
+++ oops_amqp/__init__.py 2011-10-18 00:05:27 +0000
@@ -73,14 +73,16 @@
AMQP. To use it you need to configure a local config to publish the received
reports. A full config is used because that includes support for filtering
(which can be useful if you need to throttle volume, for instance).
+Additionally you need an amqp connection factory (to handle the amqp server
+being restarted) and a queue name to receive from.
-This example uses the OOPSDateDirRepo publisher, telling it to accept whatever
+This example uses the DateDirRepo publisher, telling it to accept whatever
id was assigned by the process publishing to AMQP::
- >>> publisher = oops_datedir_repo.OOPSDateDirRepo('.', inherit_id=True)
+ >>> publisher = oops_datedir_repo.DateDirRepo('.', inherit_id=True)
>>> config = oops.Config()
>>> config.publishers.append(publisher.publish)
- >>> receiver = oops_amqp.Receiver(config)
+ >>> receiver = oops_amqp.Receiver(config, factory, "my queue")
>>> receiver.run_forever()
"""
@@ -95,7 +97,7 @@
# established at this point, and setup.py will use a version of next-$(revno).
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
-__version__ = (0, 0, 1, 'beta', 0)
+__version__ = (0, 0, 2, 'beta', 0)
__all__ = [
'Publisher',
=== modified file 'oops_amqp/receiver.py'
--- oops_amqp/receiver.py 2011-10-10 21:49:50 +0000
+++ oops_amqp/receiver.py 2011-10-18 00:05:27 +0000
@@ -18,8 +18,13 @@
__metaclass__ = type
+import socket
+import time
+
import bson
+from utils import close_ignoring_EPIPE
+
__all__ = [
'Receiver',
]
@@ -28,23 +33,25 @@
"""Republish OOPS reports from AMQP to a local oops.Config.
:ivar stopping: When True will cause Receiver to break out of run_forever.
+ Calls to run_forever reset this to False.
"""
- def __init__(self, config, channel, queue_name):
+ def __init__(self, config, connection_factory, queue_name):
"""Create a Receiver.
:param config: An oops.Config to republish the OOPS reports.
- :param channel: An amqplib Channel to listen for reports on.
+ :param connection: An amqplib connection factory, used to make the
+ initial connection and to reconnect if that connection is
+ interrupted.
:param queue_name: The queue to listen for reports on.
"""
self.config = config
- self.channel = channel
+ self.connection = None
+ self.channel = None
+ self.connection_factory = connection_factory
self.queue_name = queue_name
- self.stopping = False
def handle_report(self, message):
- if self.stopping:
- self.channel.basic_cancel(self.consume_tag)
try:
report = bson.loads(message.body)
except KeyError:
@@ -56,6 +63,38 @@
self.channel.basic_ack(message.delivery_tag)
def run_forever(self):
- self.consume_tag = self.channel.basic_consume(
- self.queue_name, callback=self.handle_report)
- self.channel.wait()
+ """Run in a loop handling messages.
+
+ If the amqp server is down or uncontactable for > 120 seconds, error
+ out.
+ """
+ self.stopping = False
+ self.connection_start = time.time()
+ while not self.stopping and time.time() < self.connection_start + 120:
+ try:
+ self._run_forever()
+ except socket.error:
+ # Don't probe immediately, give the network/process time to
+ # come back.
+ time.sleep(0.1)
+
+ def _run_forever(self):
+ self.connection = self.connection_factory()
+ # A successful connection: record this so run_forever won't bail early.
+ self.connection_start = time.time()
+ try:
+ self.channel = self.connection.channel()
+ try:
+ self.consume_tag = self.channel.basic_consume(
+ self.queue_name, callback=self.handle_report)
+ try:
+ while True:
+ self.channel.wait()
+ if self.stopping:
+ break
+ finally:
+ self.channel.basic_cancel(self.consume_tag)
+ finally:
+ close_ignoring_EPIPE(self.channel)
+ finally:
+ close_ignoring_EPIPE(self.connection)
=== modified file 'oops_amqp/tests/__init__.py'
--- oops_amqp/tests/__init__.py 2011-10-10 21:49:50 +0000
+++ oops_amqp/tests/__init__.py 2011-10-18 00:05:27 +0000
@@ -16,8 +16,6 @@
"""Tests for oops_amqp."""
-import errno
-import socket
from unittest import TestLoader
from amqplib import client_0_8 as amqp
@@ -91,13 +89,6 @@
self.rabbit.config.port), userid="guest", password="guest",
virtual_host="/")
- def close_ignoring_EPIPE(self, closable):
- try:
- closable.close()
- except socket.error, e:
- if e.errno != errno.EPIPE:
- raise
-
def test_suite():
test_mod_names = [
=== modified file 'oops_amqp/tests/test_publisher.py'
--- oops_amqp/tests/test_publisher.py 2011-10-10 21:49:50 +0000
+++ oops_amqp/tests/test_publisher.py 2011-10-18 00:05:27 +0000
@@ -21,6 +21,7 @@
import bson
from oops_amqp import Publisher
+from oops_amqp.utils import close_ignoring_EPIPE
from oops_amqp.tests import (
QueueFixture,
TestCase,
@@ -64,9 +65,9 @@
# indicate that publication failed - and publishing after it comes back
# works.
connection = self.connection_factory()
- self.addCleanup(self.close_ignoring_EPIPE, connection)
+ self.addCleanup(close_ignoring_EPIPE, connection)
channel = connection.channel()
- self.addCleanup(self.close_ignoring_EPIPE, channel)
+ self.addCleanup(close_ignoring_EPIPE, channel)
queue = self.useFixture(QueueFixture(channel, self.getUniqueString))
# The private method use and the restart of rabbit before it gets torn
# down are bugs in rabbitfixture that will be fixed in a future
@@ -87,9 +88,9 @@
# to indicate that publication failed - and publishing after it comes
# back works.
connection = self.connection_factory()
- self.addCleanup(self.close_ignoring_EPIPE, connection)
+ self.addCleanup(close_ignoring_EPIPE, connection)
channel = connection.channel()
- self.addCleanup(self.close_ignoring_EPIPE, channel)
+ self.addCleanup(close_ignoring_EPIPE, channel)
queue = self.useFixture(QueueFixture(channel, self.getUniqueString))
publisher = Publisher(self.connection_factory, queue.exchange_name, "")
oops = {'akey': 42}
=== modified file 'oops_amqp/tests/test_receiver.py'
--- oops_amqp/tests/test_receiver.py 2011-10-10 21:49:50 +0000
+++ oops_amqp/tests/test_receiver.py 2011-10-18 00:05:27 +0000
@@ -16,6 +16,9 @@
"""Tests for AMQP receiving."""
+import errno
+import socket
+
from amqplib import client_0_8 as amqp
import bson
from oops import Config
@@ -45,33 +48,112 @@
channel.basic_publish(message, queue.exchange_name, routing_key="")
config = Config()
config.publishers.append(capture)
- receiver_channel = connection.channel()
- self.addCleanup(receiver_channel.close)
- receiver = Receiver(config, receiver_channel, queue.queue_name)
# We don't want to loop forever: patch the channel so that after one
# call to wait (which will get our injected message) the loop will shut
- # down. This also checks we use the consume_tag correctly.
- old_wait = receiver_channel.wait
- def new_wait(allowed_methods=None):
- receiver.stopping = True
- return old_wait(allowed_methods=allowed_methods)
- receiver_channel.wait = new_wait
+ # down.
+ def patching_factory():
+ connection = self.connection_factory()
+ old_channel = connection.channel
+ def new_channel():
+ result = old_channel()
+ old_wait = result.wait
+ def new_wait(allowed_methods=None):
+ receiver.stopping = True
+ return old_wait(allowed_methods=allowed_methods)
+ result.wait = new_wait
+ return result
+ connection.channel = new_channel
+ return connection
+ receiver = Receiver(config, patching_factory, queue.queue_name)
receiver.run_forever()
self.assertEqual([expected_report], reports)
def test_run_forever(self):
- # run_forever subscribes and then calls wait.
+ # run_forever subscribes and then calls wait in a loop.
config = None
+ calls = []
class FakeChannel:
- def __init__(self):
- self.calls = []
+ def __init__(self, calls):
+ self.calls = calls
def basic_consume(self, queue_name, callback=None):
self.calls.append(('basic_consume', queue_name, callback))
+ return 'tag'
def wait(self):
self.calls.append(('wait',))
- channel = FakeChannel()
- receiver = Receiver(None, channel, 'foo')
+ if len(self.calls) > 2:
+ receiver.stopping = True
+ def basic_cancel(self, tag):
+ self.calls.append(('basic_cancel', tag))
+ def close(self):
+ pass
+ class FakeConnection:
+ def channel(self):
+ return FakeChannel(calls)
+ def close(self):
+ pass
+ receiver = Receiver(None, FakeConnection, 'foo')
receiver.run_forever()
self.assertEqual(
- [('basic_consume', 'foo', receiver.handle_report), ('wait',)],
- channel.calls)
+ [('basic_consume', 'foo', receiver.handle_report),
+ ('wait',),
+ ('wait',),
+ ('basic_cancel', 'tag')],
+ calls)
+
+ def test_tolerates_amqp_trouble(self):
+ # If the AMQP server is unavailable for a short period, the receiver
+ # will automatically reconnect.
+ # Break a connection to raise socket.error (which we know from the
+ # publisher tests is what le aks through when rabbit is shutdown).
+ # We raise it the first time on each amqp method call.
+ reports = []
+ def capture(report):
+ reports.append(report)
+ return report['id']
+ expected_report = {'id': 'foo', 'otherkey': 42}
+ message = amqp.Message(bson.dumps(expected_report))
+ connection = self.connection_factory()
+ self.addCleanup(connection.close)
+ channel = connection.channel()
+ self.addCleanup(channel.close)
+ queue = self.useFixture(QueueFixture(channel, self.getUniqueString))
+ channel.basic_publish(message, queue.exchange_name, routing_key="")
+ config = Config()
+ config.publishers.append(capture)
+ state = {}
+ def error_once(func):
+ def wrapped(*args, **kwargs):
+ func_ref = func.func_code
+ if func_ref in state:
+ return func(*args, **kwargs)
+ else:
+ state[func_ref] = True
+ # Use EPIPE because the close() code checks that (though
+ # the rest doesn't)
+ raise socket.error(errno.EPIPE, "booyah")
+ return wrapped
+ @error_once
+ def patching_factory():
+ connection = self.connection_factory()
+ old_channel = connection.channel
+ @error_once
+ def new_channel():
+ result = old_channel()
+ result.wait = error_once(result.wait)
+ result.basic_consume = error_once(result.basic_consume)
+ result.basic_cancel = error_once(result.basic_cancel)
+ result.close = error_once(result.close)
+ return result
+ connection.channel = new_channel
+ connection.close = error_once(connection.close)
+ return connection
+ receiver = Receiver(config, patching_factory, queue.queue_name)
+ # After we've successfully handled a message, we can be fairly sure
+ # we'll handle things.
+ old_handle = receiver.handle_report
+ def handle_and_stop(message):
+ old_handle(message)
+ receiver.stopping = True
+ receiver.handle_report = handle_and_stop
+ receiver.run_forever()
+ self.assertEqual([expected_report], reports)
=== added file 'oops_amqp/utils.py'
--- oops_amqp/utils.py 1970-01-01 00:00:00 +0000
+++ oops_amqp/utils.py 2011-10-18 00:05:27 +0000
@@ -0,0 +1,32 @@
+# Copyright (c) 2011, Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Utility functions for oops_amqp."""
+
+import errno
+import socket
+
+__all__ = [
+ 'close_ignoring_EPIPE',
+ ]
+
+
+def close_ignoring_EPIPE(closable):
+ try:
+ return closable.close()
+ except socket.error, e:
+ if e.errno != errno.EPIPE:
+ raise
=== modified file 'setup.py'
--- setup.py 2011-10-10 21:49:50 +0000
+++ setup.py 2011-10-18 00:05:27 +0000
@@ -23,7 +23,7 @@
os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
setup(name="oops_amqp",
- version="0.0.1",
+ version="0.0.2",
description=\
"OOPS AMQP transport.",
long_description=description,
Follow ups