Merge "[AMQP 1.0] Add link credit configuration options" into feature/amqp-dispatch-router

This commit is contained in:
Jenkins 2016-08-16 01:01:01 +00:00 committed by Gerrit Code Review
commit 5d5596a43d
3 changed files with 89 additions and 44 deletions

View File

@ -459,7 +459,7 @@ class Replies(pyngus.ReceiverEventHandler):
routed to the proper incoming queue using the correlation-id header in the routed to the proper incoming queue using the correlation-id header in the
message. message.
""" """
def __init__(self, connection, on_ready, on_down): def __init__(self, connection, on_ready, on_down, capacity):
self._correlation = {} # map of correlation-id to response queue self._correlation = {} # map of correlation-id to response queue
self._on_ready = on_ready self._on_ready = on_ready
self._on_down = on_down self._on_down = on_down
@ -469,13 +469,13 @@ class Replies(pyngus.ReceiverEventHandler):
event_handler=self, event_handler=self,
name=rname) name=rname)
# capacity determines the maximum number of reply messages this link # capacity determines the maximum number of reply messages this link is
# can receive. As messages are received and credit is consumed, this # willing to receive. As messages are received and capacity is
# driver will 'top up' the credit back to max capacity. This number # consumed, this driver will 'top up' the capacity back to max
# should be large enough to avoid needlessly flow-controlling the # capacity. This number should be large enough to avoid needlessly
# replies. # flow-controlling the replies.
self.capacity = 100 # TODO(kgiusti) guesstimate - make configurable self._capacity = capacity
self._credit = 0 self._capacity_low = (capacity + 1) / 2
self._receiver.open() self._receiver.open()
def detach(self): def detach(self):
@ -526,7 +526,7 @@ class Replies(pyngus.ReceiverEventHandler):
messages. messages.
""" """
LOG.debug("Replies link active src=%s", self._receiver.source_address) LOG.debug("Replies link active src=%s", self._receiver.source_address)
self._update_credit() receiver_link.add_capacity(self._capacity)
self._on_ready() self._on_ready()
def receiver_remote_closed(self, receiver, pn_condition): def receiver_remote_closed(self, receiver, pn_condition):
@ -551,26 +551,20 @@ class Replies(pyngus.ReceiverEventHandler):
"""This is a Pyngus callback, invoked by Pyngus when a new message """This is a Pyngus callback, invoked by Pyngus when a new message
arrives on this receiver link from the peer. arrives on this receiver link from the peer.
""" """
self._credit = self._credit - 1
self._update_credit()
key = message.correlation_id key = message.correlation_id
if key in self._correlation: LOG.debug("Received response for msg id=%s", key)
LOG.debug("Received response for msg id=%s", key) try:
self._correlation[key](message) self._correlation[key](message)
# cleanup (only need one response per request) # cleanup (only need one response per request)
del self._correlation[key] del self._correlation[key]
receiver.message_accepted(handle) receiver.message_accepted(handle)
else: except KeyError:
LOG.warning(_LW("Can't find receiver for response msg id=%s, " LOG.warning(_LW("Can't find receiver for response msg id=%s, "
"dropping!"), key) "dropping!"), key)
receiver.message_modified(handle, True, True, None) receiver.message_modified(handle, True, True, None)
def _update_credit(self):
# ensure we have enough credit # ensure we have enough credit
if self._credit < self.capacity / 2: if receiver.capacity <= self._capacity_low:
self._receiver.add_capacity(self.capacity - self._credit) receiver.add_capacity(self._capacity - receiver.capacity)
self._credit = self.capacity
class Server(pyngus.ReceiverEventHandler): class Server(pyngus.ReceiverEventHandler):
@ -578,11 +572,12 @@ class Server(pyngus.ReceiverEventHandler):
from a given target. Messages arriving on the links are placed on the from a given target. Messages arriving on the links are placed on the
'incoming' queue. 'incoming' queue.
""" """
def __init__(self, target, incoming, scheduler, delay): def __init__(self, target, incoming, scheduler, delay, capacity):
self._target = target self._target = target
self._incoming = incoming self._incoming = incoming
self._addresses = [] self._addresses = []
self._capacity = 500 # credit per link self._capacity = capacity # credit per each link
self._capacity_low = (capacity + 1) / 2
self._receivers = [] self._receivers = []
self._scheduler = scheduler self._scheduler = scheduler
self._delay = delay # for link re-attach self._delay = delay # for link re-attach
@ -660,7 +655,7 @@ class Server(pyngus.ReceiverEventHandler):
receiver.message_released(handle) receiver.message_released(handle)
else: else:
receiver.message_accepted(handle) receiver.message_accepted(handle)
if receiver.capacity < self._capacity / 2: if receiver.capacity <= self._capacity_low:
receiver.add_capacity(self._capacity - receiver.capacity) receiver.add_capacity(self._capacity - receiver.capacity)
else: else:
LOG.debug("Can't find receiver for settlement") LOG.debug("Can't find receiver for settlement")
@ -676,11 +671,6 @@ class Server(pyngus.ReceiverEventHandler):
event_handler=self, event_handler=self,
name=name, name=name,
properties=props) properties=props)
# TODO(kgiusti) Hardcoding credit here is sub-optimal. A better
# approach would monitor for a back-up of inbound messages to be
# processed by the consuming application and backpressure the
# sender based on configured thresholds.
r.add_capacity(self._capacity) r.add_capacity(self._capacity)
r.open() r.open()
return r return r
@ -701,8 +691,9 @@ class Server(pyngus.ReceiverEventHandler):
class RPCServer(Server): class RPCServer(Server):
"""Subscribes to RPC addresses""" """Subscribes to RPC addresses"""
def __init__(self, target, incoming, scheduler, delay): def __init__(self, target, incoming, scheduler, delay, capacity):
super(RPCServer, self).__init__(target, incoming, scheduler, delay) super(RPCServer, self).__init__(target, incoming, scheduler, delay,
capacity)
def attach(self, connection, addresser): def attach(self, connection, addresser):
# Generate the AMQP 1.0 addresses for the base class # Generate the AMQP 1.0 addresses for the base class
@ -717,9 +708,9 @@ class RPCServer(Server):
class NotificationServer(Server): class NotificationServer(Server):
"""Subscribes to Notification addresses""" """Subscribes to Notification addresses"""
def __init__(self, target, incoming, scheduler, delay): def __init__(self, target, incoming, scheduler, delay, capacity):
super(NotificationServer, self).__init__(target, incoming, scheduler, super(NotificationServer, self).__init__(target, incoming, scheduler,
delay) delay, capacity)
def attach(self, connection, addresser): def attach(self, connection, addresser):
# Generate the AMQP 1.0 addresses for the base class # Generate the AMQP 1.0 addresses for the base class
@ -841,6 +832,10 @@ class Controller(pyngus.ConnectionEventHandler):
# prevent queuing up multiple requests to run _process_tasks() # prevent queuing up multiple requests to run _process_tasks()
self._process_tasks_scheduled = False self._process_tasks_scheduled = False
self._process_tasks_lock = threading.Lock() self._process_tasks_lock = threading.Lock()
# credit levels for incoming links
self._reply_credit = _opts.reply_link_credit
self._rpc_credit = _opts.rpc_server_credit
self._notify_credit = _opts.notify_server_credit
def connect(self): def connect(self):
"""Connect to the messaging service.""" """Connect to the messaging service."""
@ -899,13 +894,15 @@ class Controller(pyngus.ConnectionEventHandler):
server = NotificationServer(subscribe_task._target, server = NotificationServer(subscribe_task._target,
subscribe_task._in_queue, subscribe_task._in_queue,
self.processor, self.processor,
self.link_retry_delay) self.link_retry_delay,
self._notify_credit)
else: else:
t = "RPC" t = "RPC"
server = RPCServer(subscribe_task._target, server = RPCServer(subscribe_task._target,
subscribe_task._in_queue, subscribe_task._in_queue,
self.processor, self.processor,
self.link_retry_delay) self.link_retry_delay,
self._rpc_credit)
LOG.debug("Subscribing to %(type)s target %(target)s", LOG.debug("Subscribing to %(type)s target %(target)s",
{'type': t, 'target': subscribe_task._target}) {'type': t, 'target': subscribe_task._target})
@ -1061,7 +1058,8 @@ class Controller(pyngus.ConnectionEventHandler):
self.addresser) self.addresser)
self.reply_link = Replies(self._socket_connection.connection, self.reply_link = Replies(self._socket_connection.connection,
self._reply_link_ready, self._reply_link_ready,
self._reply_link_down) self._reply_link_down,
self._reply_credit)
self._delay = 1 self._delay = 1
def connection_closed(self, connection): def connection_closed(self, connection):

View File

@ -208,5 +208,22 @@ amqp1_opts = [
"Target.exchange if set\n" "Target.exchange if set\n"
"else default_rpc_exchange if set\n" "else default_rpc_exchange if set\n"
"else control_exchange if set\n" "else control_exchange if set\n"
"else 'rpc'") "else 'rpc'"),
# Message Credit Levels
cfg.IntOpt('reply_link_credit',
default=200,
min=1,
help='Window size for incoming RPC Reply messages.'),
cfg.IntOpt('rpc_server_credit',
default=100,
min=1,
help='Window size for incoming RPC Request messages'),
cfg.IntOpt('notify_server_credit',
default=100,
min=1,
help='Window size for incoming Notification messages')
] ]

View File

@ -464,6 +464,29 @@ class TestAmqpSend(_AmqpBrokerTestCaseAuto):
driver.cleanup() driver.cleanup()
def test_sender_minimal_credit(self):
# ensure capacity is replenished when only 1 credit is configured
self.config(reply_link_credit=1,
rpc_server_credit=1,
group="oslo_messaging_amqp")
driver = amqp_driver.ProtonDriver(self.conf, self._broker_url)
target = oslo_messaging.Target(topic="test-topic", server="server")
listener = _ListenerThread(driver.listen(target,
None,
None)._poll_style_listener,
4)
for i in range(4):
threading.Thread(target=driver.send,
args=(target,
{"context": "whatever"},
{"method": "echo"}),
kwargs={'wait_for_reply': True}).start()
predicate = lambda: (self._broker.direct_count == 8)
_wait_until(predicate, 30)
self.assertTrue(predicate())
listener.join(timeout=30)
driver.cleanup()
class TestAmqpNotification(_AmqpBrokerTestCaseAuto): class TestAmqpNotification(_AmqpBrokerTestCaseAuto):
"""Test sending and receiving notifications.""" """Test sending and receiving notifications."""
@ -998,7 +1021,7 @@ class TestLinkRecovery(_AmqpBrokerTestCase):
def _on_active(link): def _on_active(link):
# refuse granting credit for the broadcast link # refuse granting credit for the broadcast link
if link.source_address.startswith("broadcast"): if self._broker._addresser._is_multicast(link.source_address):
self._blocked_links.add(link) self._blocked_links.add(link)
else: else:
# unblock all link when RPC call is made # unblock all link when RPC call is made
@ -1018,13 +1041,17 @@ class TestLinkRecovery(_AmqpBrokerTestCase):
target.fanout = True target.fanout = True
target.server = None target.server = None
# these threads will share the same link # these threads will share the same link
th = []
for i in range(3): for i in range(3):
threading.Thread(target=driver.send, t = threading.Thread(target=driver.send,
args=(target, {"context": "whatever"}, args=(target, {"context": "whatever"},
{"msg": "n=%d" % i}), {"msg": "n=%d" % i}),
kwargs={'wait_for_reply': False}).start() kwargs={'wait_for_reply': False})
t.start()
time.sleep(0.5) t.join(timeout=1)
self.assertTrue(t.isAlive())
th.append(t)
self.assertEqual(self._broker.fanout_sent_count, 0)
# this will trigger the release of credit for the previous links # this will trigger the release of credit for the previous links
target.fanout = False target.fanout = False
rc = driver.send(target, {"context": "whatever"}, rc = driver.send(target, {"context": "whatever"},
@ -1035,6 +1062,9 @@ class TestLinkRecovery(_AmqpBrokerTestCase):
listener.join(timeout=30) listener.join(timeout=30)
self.assertTrue(self._broker.fanout_count == 3) self.assertTrue(self._broker.fanout_count == 3)
self.assertFalse(listener.isAlive()) self.assertFalse(listener.isAlive())
for t in th:
t.join(timeout=30)
self.assertFalse(t.isAlive())
driver.cleanup() driver.cleanup()
@ -1495,7 +1525,7 @@ class FakeBroker(threading.Thread):
"""Forward this message out the proper sending link.""" """Forward this message out the proper sending link."""
self.server.forward_message(message, handle, receiver_link) self.server.forward_message(message, handle, receiver_link)
if self.link.capacity < 1: if self.link.capacity < 1:
self.server.credit_exhausted(self.link) self.server.on_credit_exhausted(self.link)
def __init__(self, cfg, def __init__(self, cfg,
sock_addr="", sock_port=0, sock_addr="", sock_port=0,