Use URL to define RabbitMQ connection

* kombu have been updated to be compatible with global
requirements
* Unit test have been modified due to breaking changes
introduced by kombu library
* kombu will be replaced later by oslo.messaging
* Add new config option to define multiple connection
URL separated by a semi-colon
* Use secret parameter to avoid any password leaks in log
files
* Add option for connection heartbeat

Change-Id: Ia1db99f5804a0b3cdd496485f6a9410758d567fe
This commit is contained in:
Frédéric Guillot 2016-11-01 09:31:23 -04:00
parent 07cb2a0441
commit f1cb73cab3
7 changed files with 40 additions and 101 deletions

View File

@ -34,11 +34,8 @@ def main():
database_driver.connect() database_driver.connect()
application_controller = controller.Controller(config, database_driver) application_controller = controller.Controller(config, database_driver)
connection = kombu.Connection(hostname=config.collector.rabbit_host,
port=config.collector.rabbit_port, connection = kombu.Connection(config.collector.url, heartbeat=config.collector.heartbeat)
userid=config.collector.rabbit_username,
password=config.collector.rabbit_password,
heartbeat=540)
retry_listener = retry_adapter.RetryAdapter(config, connection) retry_listener = retry_adapter.RetryAdapter(config, connection)
bus_listener = bus_adapter.BusAdapter(config, application_controller, bus_listener = bus_adapter.BusAdapter(config, application_controller,
connection, retry_listener) connection, retry_listener)

View File

@ -20,18 +20,18 @@ LOG = log.getLogger(__name__)
class RetryAdapter(object): class RetryAdapter(object):
def __init__(self, config, connection): def __init__(self, config, connection, retry_producer=None, dead_producer=None):
self.config = config self.config = config
self.connection = connection self.connection = connection
retry_exchange = self._configure_retry_exchanges(self.connection) retry_exchange = self._configure_retry_exchanges(self.connection)
dead_exchange = self._configure_dead_exchange(self.connection) dead_exchange = self._configure_dead_exchange(self.connection)
self._retry_producer = kombu.Producer(self.connection, exchange=retry_exchange) self._retry_producer = retry_producer or kombu.Producer(self.connection, exchange=retry_exchange)
self._dead_producer = kombu.Producer(self.connection, exchange=dead_exchange) self._dead_producer = dead_producer or kombu.Producer(self.connection, exchange=dead_exchange)
def publish_to_dead_letter(self, message): def publish_to_dead_letter(self, message):
death_count = self._rejected_count(message) death_count = self._get_rejected_count(message)
LOG.info('Message die %d times', death_count) LOG.info('Message die %d times', death_count)
if death_count < self.config.collector.max_retries: if death_count < self.config.collector.max_retries:
@ -114,7 +114,7 @@ class RetryAdapter(object):
"x-dead-letter-routing-key": self.config.collector.routing_key, "x-dead-letter-routing-key": self.config.collector.routing_key,
} }
def _rejected_count(self, message): def _get_rejected_count(self, message):
if 'x-death' in message.headers: if 'x-death' in message.headers:
return len(message.headers['x-death']) return len(message.headers['x-death'])
return 0 return 0

View File

@ -24,6 +24,7 @@ database_opts = [
default='mongodb', default='mongodb',
help='Database driver'), help='Database driver'),
cfg.StrOpt('connection_url', cfg.StrOpt('connection_url',
secret=True,
default='mongodb://almanach:almanach@localhost:27017/almanach', default='mongodb://almanach:almanach@localhost:27017/almanach',
help='Database connection URL'), help='Database connection URL'),
] ]
@ -38,16 +39,13 @@ api_opts = [
] ]
collector_opts = [ collector_opts = [
cfg.HostnameOpt('rabbit_host', cfg.StrOpt('url',
default='localhost', secret=True,
help='RabbitMQ Hostname'), default='amqp://guest:guest@localhost:5672',
cfg.PortOpt('rabbit_port', help='RabbitMQ connection URL'),
default=5672, cfg.IntOpt('heartbeat',
help='RabbitMQ TCP port'), default=540,
cfg.StrOpt('rabbit_username', help='RabbitMQ connection heartbeat'),
help='RabbitMQ Username'),
cfg.StrOpt('rabbit_password',
help='RabbitMQ Password'),
cfg.StrOpt('queue', cfg.StrOpt('queue',
default='almanach.info', default='almanach.info',
help='Default queue name'), help='Default queue name'),
@ -85,11 +83,13 @@ auth_opts = [
default='private_key', default='private_key',
help='Authentication driver for the API'), help='Authentication driver for the API'),
cfg.StrOpt('private_key', cfg.StrOpt('private_key',
secret=True,
default='secret', default='secret',
help='Private key for private key authentication'), help='Private key for private key authentication'),
cfg.StrOpt('keystone_username', cfg.StrOpt('keystone_username',
help='Keystone service username'), help='Keystone service username'),
cfg.StrOpt('keystone_password', cfg.StrOpt('keystone_password',
secret=True,
help='Keystone service password'), help='Keystone service password'),
cfg.StrOpt('keystone_tenant', cfg.StrOpt('keystone_tenant',
help='Keystone service tenant'), help='Keystone service tenant'),

View File

@ -47,19 +47,8 @@ bind_port = 8000
# From almanach # From almanach
# #
# RabbitMQ Hostname (hostname value) # RabbitMQ connection URL (string value)
rabbit_host = messaging url = amqp://guest:guest@messaging:5672
# RabbitMQ TCP port (port value)
# Minimum value: 0
# Maximum value: 65535
#rabbit_port = 5672
# RabbitMQ Username (string value)
rabbit_username = guest
# RabbitMQ Password (string value)
rabbit_password = guest
# Default queue name (string value) # Default queue name (string value)
#default_queue = almanach.info #default_queue = almanach.info

View File

@ -3,7 +3,7 @@ Flask==0.10.1
PyYAML==3.11 PyYAML==3.11
jsonpickle==0.7.1 jsonpickle==0.7.1
pymongo>=3.0.2,!=3.1 # Apache-2.0 pymongo>=3.0.2,!=3.1 # Apache-2.0
kombu>=3.0.30 kombu>=3.0.25 # BSD
pytz>=2014.10 pytz>=2014.10
voluptuous==0.8.11 voluptuous==0.8.11
python-keystoneclient>=1.6.0 python-keystoneclient>=1.6.0

View File

@ -12,3 +12,4 @@ sphinxcontrib-httpdomain # BSD
flake8>=2.5.4,<2.6.0 # MIT flake8>=2.5.4,<2.6.0 # MIT
hacking<0.12,>=0.11.0 # Apache-2.0 hacking<0.12,>=0.11.0 # Apache-2.0
testtools>=1.4.0 # MIT testtools>=1.4.0 # MIT
mock>=2.0 # BSD

View File

@ -12,10 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from flexmock import flexmock import mock
from kombu import Connection
from kombu.tests import mocks
from kombu.transport import pyamqp
from almanach.collector import retry_adapter from almanach.collector import retry_adapter
from tests import base from tests import base
@ -25,73 +22,28 @@ class BusAdapterTest(base.BaseTestCase):
def setUp(self): def setUp(self):
super(BusAdapterTest, self).setUp() super(BusAdapterTest, self).setUp()
self.setup_connection_mock() self.connection = mock.Mock()
self.retry_adapter = retry_adapter.RetryAdapter(self.config, self.connection) self.retry_producer = mock.Mock()
self.dead_producer = mock.Mock()
self.retry_adapter = retry_adapter.RetryAdapter(self.config, self.connection,
self.retry_producer, self.dead_producer)
def setup_connection_mock(self): def test_message_is_published_to_retry_queue(self):
mocks.Transport.recoverable_connection_errors = pyamqp.Transport.recoverable_connection_errors message = mock.Mock(headers=dict())
self.connection = flexmock(Connection(transport=mocks.Transport)) message.delivery_info = dict(routing_key='test')
self.channel_mock = flexmock(self.connection.default_channel)
self.connection.should_receive('channel').and_return(self.channel_mock)
def test_declare_retry_exchanges_retries_if_it_fails(self):
connection = flexmock(Connection(transport=mocks.Transport))
connection.should_receive('_establish_connection').times(3)\
.and_raise(IOError)\
.and_raise(IOError)\
.and_return(connection.transport.establish_connection())
self.retry_adapter = retry_adapter.RetryAdapter(self.config, connection)
def test_publish_to_retry_queue_happy_path(self):
message = self.build_message()
self.expect_publish_with(message, 'almanach.retry').once()
self.retry_adapter.publish_to_dead_letter(message)
def test_publish_to_retry_queue_retries_if_it_fails(self):
message = self.build_message()
self.expect_publish_with(message, 'almanach.retry').times(4)\
.and_raise(IOError)\
.and_raise(IOError)\
.and_raise(IOError)\
.and_return(message)
self.retry_adapter.publish_to_dead_letter(message) self.retry_adapter.publish_to_dead_letter(message)
self.connection.ensure.assert_called_with(self.retry_producer, self.retry_producer.publish,
errback=self.retry_adapter._error_callback,
interval_max=30, interval_start=0, interval_step=5)
def build_message(self, headers=dict()): def test_message_is_published_to_dead_queue(self):
message = MyObject() message = mock.Mock(headers={'x-death': [0, 1, 2, 3]})
message.headers = headers message.delivery_info = dict(routing_key='test')
message.body = b'Now that the worst is behind you, it\'s time we get you back. - Mr. Robot'
message.delivery_info = {'routing_key': 42}
message.content_type = 'xml/rapture'
message.content_encoding = 'iso8859-1'
return message
def test_publish_to_dead_letter_messages_retried_more_than_twice(self):
message = self.build_message(headers={'x-death': [0, 1, 2, 3]})
self.expect_publish_with(message, 'almanach.dead').once()
self.retry_adapter.publish_to_dead_letter(message) self.retry_adapter.publish_to_dead_letter(message)
self.assertEqual(self.connection.ensure.call_count, 3)
def expect_publish_with(self, message, exchange): self.connection.ensure.assert_called_with(self.dead_producer, self.dead_producer.publish,
expected_message = {'body': message.body, errback=self.retry_adapter._error_callback,
'priority': 0, interval_max=30, interval_start=0, interval_step=5)
'content_encoding': message.content_encoding,
'content_type': message.content_type,
'headers': message.headers,
'properties': {'delivery_mode': 2}}
return self.channel_mock.should_receive('basic_publish')\
.with_args(expected_message, exchange=exchange, routing_key=message.delivery_info['routing_key'],
mandatory=False, immediate=False)
class MyObject(object):
headers = None
body = None
delivery_info = None
content_type = None
content_encoding = None