Refactor link handling to use a more event driven approach.

This commit is contained in:
Kenneth Giusti 2014-03-07 15:27:13 -05:00
parent 9935c0cf50
commit ac54d70c02
2 changed files with 273 additions and 178 deletions

@ -51,7 +51,7 @@ class ConnectionEventHandler(object):
def sender_requested(self, connection, link_handle,
name, requested_source,
properties=None):
properties):
"""Peer has requested a SenderLink be created."""
# call accept_sender to accept new link,
# reject_sender to reject it.
@ -147,8 +147,9 @@ class Connection(object):
self._pn_transport.trace(proton.Transport.TRACE_FRM)
# indexed by link-name
self._sender_links = {} # SenderLink or pn_link if pending
self._receiver_links = {} # ReceiverLink or pn_link if pending
self._sender_links = {} # SenderLink
self._receiver_links = {} # ReceiverLink
self._work_queue = set() # either, in need of processing
self._read_done = False
self._write_done = False
@ -240,33 +241,9 @@ class Connection(object):
self._pn_transport = None
self._user_context = None
def _link_requested(self, pn_link):
if pn_link.is_sender and pn_link.name not in self._sender_links:
LOG.debug("Remotely initiated Sender needs init")
self._sender_links[pn_link.name] = pn_link
pn_link.context = None # TODO(kgiusti) update proton.py
req_source = ""
if pn_link.remote_source.dynamic:
req_source = None
elif pn_link.remote_source.address:
req_source = pn_link.remote_source.address
self._handler.sender_requested(self, pn_link.name,
pn_link.name, req_source,
{"target-address":
pn_link.remote_target.address})
elif pn_link.is_receiver and pn_link.name not in self._receiver_links:
LOG.debug("Remotely initiated Receiver needs init")
self._receiver_links[pn_link.name] = pn_link
pn_link.context = None # TODO(kgiusti) update proton.py
req_target = ""
if pn_link.remote_target.dynamic:
req_target = None
elif pn_link.remote_target.address:
req_target = pn_link.remote_target.address
self._handler.receiver_requested(self, pn_link.name,
pn_link.name, req_target,
{"source-address":
pn_link.remote_source.address})
def _add_work(self, link):
"""Add the link to the work queue."""
self._work_queue.add(link)
_REMOTE_REQ = (proton.Endpoint.LOCAL_UNINIT
| proton.Endpoint.REMOTE_ACTIVE)
@ -315,78 +292,40 @@ class Connection(object):
pn_link = self._pn_connection.link_head(self._REMOTE_REQ)
while pn_link:
next_link = pn_link.next(self._LOCAL_UNINIT)
if (pn_link.is_sender and
pn_link.name not in self._sender_links):
LOG.debug("Remotely initiated Sender needs init")
link = SenderLink(self, pn_link)
self._sender_links[pn_link.name] = link
self._add_work(link)
elif (pn_link.is_receiver and
pn_link.name not in self._receiver_links):
LOG.debug("Remotely initiated Receiver needs init")
link = ReceiverLink(self, pn_link)
self._receiver_links[pn_link.name] = link
self._add_work(link)
else:
LOG.debug("Ignoring request link - name in use %s",
pn_link.name)
pn_link = pn_link.next(self._REMOTE_REQ)
if pn_link.state == self._REMOTE_REQ:
self._link_requested(pn_link)
pn_link = next_link
# TODO(kgiusti) won't scale?
# TODO(kgiusti) won't scale - use new Enging event API
pn_link = self._pn_connection.link_head(self._ACTIVE)
while pn_link:
next_link = pn_link.next(self._ACTIVE)
if pn_link.context and not pn_link.context._active:
LOG.debug("Link is up")
pn_link.context._active = True
if pn_link.is_sender:
sender_link = pn_link.context
if sender_link._handler:
sender_link._handler.sender_active(sender_link)
else:
receiver_link = pn_link.context
if receiver_link._handler:
receiver_link._handler.receiver_active(receiver_link)
pn_link = next_link
# process the work queue
pn_delivery = self._pn_connection.work_head
while pn_delivery:
next_delivery = pn_delivery.work_next
if pn_delivery.link.context:
if pn_delivery.link.is_sender:
sender_link = pn_delivery.link.context
sender_link._delivery_updated(pn_delivery)
else:
receiver_link = pn_delivery.link.context
receiver_link._delivery_updated(pn_delivery)
pn_delivery = next_delivery
self._add_work(pn_link.context)
pn_link = pn_link.next(self._ACTIVE)
# do endpoint down handling:
pn_link = self._pn_connection.link_head(self._REMOTE_CLOSE)
while pn_link:
LOG.debug("Link closed remotely")
next_link = pn_link.next(self._REMOTE_CLOSE)
# TODO(kgiusti) error reporting
if pn_link.context:
if pn_link.is_sender:
sender_link = pn_link.context
handler = pn_link.context._handler
if handler:
handler.sender_remote_closed(sender_link, None)
else:
receiver_link = pn_link.context
handler = pn_link.context._handler
if handler:
handler.receiver_remote_closed(receiver_link, None)
pn_link = next_link
self._add_work(pn_link.context)
pn_link = pn_link.next(self._REMOTE_CLOSE)
pn_link = self._pn_connection.link_head(self._CLOSED)
while pn_link:
next_link = pn_link.next(self._CLOSED)
if pn_link.context and pn_link.context._active:
LOG.debug("Link close completed")
pn_link.context._active = False
if pn_link.is_sender:
sender_link = pn_link.context
sender_link._handler.sender_closed(sender_link)
else:
receiver_link = pn_link.context
receiver_link._handler.receiver_closed(receiver_link)
pn_link = next_link
self._add_work(pn_link.context)
pn_link = pn_link.next(self._CLOSED)
pn_session = self._pn_connection.session_head(self._REMOTE_CLOSE)
while pn_session:
@ -401,6 +340,19 @@ class Connection(object):
LOG.debug("Connection close complete")
self._handler.connection_closed(self)
# service links needing attention:
while self._work_queue:
link = self._work_queue.pop()
link._process()
# process the delivery work queue
pn_delivery = self._pn_connection.work_head
while pn_delivery:
next_delivery = pn_delivery.work_next
pn_delivery.link.context._process_delivery(pn_delivery)
pn_delivery = next_delivery
# DEBUG LINK "LEAK"
# count = 0
# link = self._pn_connection.link_head(0)
@ -500,36 +452,32 @@ class Connection(object):
raise KeyError("Sender %s already exists!" % ident)
pn_link = self._pn_session.sender(ident)
if pn_link:
s = SenderLink(self, pn_link,
source_address, target_address,
event_handler, properties)
self._sender_links[ident] = s
return s
return None
sl = SenderLink(self, pn_link)
sl.configure(target_address, source_address, event_handler, properties)
self._sender_links[ident] = sl
return sl
def accept_sender(self, link_handle, source_override=None,
event_handler=None, properties=None):
pn_link = self._sender_links.get(link_handle)
if not pn_link or not isinstance(pn_link, proton.Sender):
link = self._sender_links.get(link_handle)
if not link:
raise Exception("Invalid link_handle: %s" % link_handle)
pn_link = link._pn_link
if pn_link.remote_source.dynamic and not source_override:
raise Exception("A source address must be supplied!")
source_addr = source_override or pn_link.remote_source.address
link = SenderLink(self, pn_link,
source_addr,
pn_link.remote_target.address,
event_handler, properties)
self._sender_links[link_handle] = link
link.configure(pn_link.remote_target.address,
source_addr,
event_handler, properties)
return link
def reject_sender(self, link_handle, reason):
pn_link = self._sender_links.get(link_handle)
if not pn_link or not isinstance(pn_link, proton.Sender):
"""Rejects the SenderLink, and destroys the handle."""
link = self._sender_links.get(link_handle)
if not link:
raise Exception("Invalid link_handle: %s" % link_handle)
del self._sender_links[link_handle]
# TODO(kgiusti) support reason for close
pn_link.close()
link.reject(reason)
link.destroy()
def create_receiver(self, target_address, source_address=None,
event_handler=None, name=None, properties=None):
@ -539,35 +487,31 @@ class Connection(object):
raise KeyError("Receiver %s already exists!" % ident)
pn_link = self._pn_session.receiver(ident)
if pn_link:
r = ReceiverLink(self, pn_link, target_address,
source_address, event_handler, properties)
self._receiver_links[ident] = r
return r
return None
rl = ReceiverLink(self, pn_link)
rl.configure(target_address, source_address, event_handler, properties)
self._receiver_links[ident] = rl
return rl
def accept_receiver(self, link_handle, target_override=None,
event_handler=None, properties=None):
pn_link = self._receiver_links.get(link_handle)
if not pn_link or not isinstance(pn_link, proton.Receiver):
link = self._receiver_links.get(link_handle)
if not link:
raise Exception("Invalid link_handle: %s" % link_handle)
pn_link = link._pn_link
if pn_link.remote_target.dynamic and not target_override:
raise Exception("A target address must be supplied!")
target_addr = target_override or pn_link.remote_target.address
link = ReceiverLink(self, pn_link,
target_addr,
pn_link.remote_source.address,
event_handler, properties)
self._receiver_links[link_handle] = link
link.configure(target_addr,
pn_link.remote_source.address,
event_handler, properties)
return link
def reject_receiver(self, link_handle, reason):
pn_link = self._receiver_links.get(link_handle)
if not pn_link or not isinstance(pn_link, proton.Receiver):
link = self._receiver_links.get(link_handle)
if not link:
raise Exception("Invalid link_handle: %s" % link_handle)
del self._receiver_links[link_handle]
# TODO(kgiusti) support reason for close
pn_link.close()
link.reject(reason)
link.destroy()
def _remove_sender(self, name):
if name in self._sender_links:

@ -28,30 +28,155 @@ import proton
LOG = logging.getLogger(__name__)
# link lifecycle state:
_STATE_UNINIT = 0 # initial state
_STATE_PENDING = 1 # waiting for remote to open
_STATE_REQUESTED = 2 # waiting for local to open
_STATE_ACTIVE = 3
_STATE_NEED_CLOSE = 4 # remote initiated close
_STATE_CLOSING = 5 # locally closed, pending remote
_STATE_CLOSED = 6 # terminal state
# proton endpoint states:
_LOCAL_UNINIT = proton.Endpoint.LOCAL_UNINIT
_LOCAL_ACTIVE = proton.Endpoint.LOCAL_ACTIVE
_LOCAL_CLOSED = proton.Endpoint.LOCAL_CLOSED
_REMOTE_UNINIT = proton.Endpoint.REMOTE_UNINIT
_REMOTE_ACTIVE = proton.Endpoint.REMOTE_ACTIVE
_REMOTE_CLOSED = proton.Endpoint.REMOTE_CLOSED
def _do_pending(link):
"""The application has opened a new link."""
# nothing to do but wait for the remote:
return _STATE_PENDING
def _do_requested(link):
"""The peer has requested a new link be created."""
LOG.debug("Remote has initiated a link")
pn_link = link._pn_link
if isinstance(link, SenderLink):
# has the remote requested a source address?
req_source = ""
if pn_link.remote_source.dynamic:
req_source = None
elif pn_link.remote_source.address:
req_source = pn_link.remote_source.address
handler = link._connection._handler
if handler:
handler.sender_requested(link._connection,
pn_link.name, # handle
pn_link.name,
req_source,
{"target-address":
pn_link.remote_target.address})
else:
# has the remote requested a target address?
req_target = ""
if pn_link.remote_target.dynamic:
req_target = None
elif pn_link.remote_target.address:
req_target = pn_link.remote_target.address
handler = link._connection._handler
if handler:
handler.receiver_requested(link._connection,
pn_link.name, # handle
pn_link.name,
req_target,
{"source-address":
pn_link.remote_source.address})
return _STATE_REQUESTED
def _do_active(link):
"""Both ends of the link have become active."""
LOG.debug("Link is up")
if link._handler:
if isinstance(link, SenderLink):
link._handler.sender_active(link)
else:
link._handler.receiver_active(link)
return _STATE_ACTIVE
def _do_need_close(link):
"""The remote has closed its end of the link."""
# TODO(kgiusti) error reporting
LOG.debug("Link remote closed")
if link._handler:
if isinstance(link, SenderLink):
link._handler.sender_remote_closed(link, None)
else:
link._handler.receiver_remote_closed(link, None)
return _STATE_NEED_CLOSE
def _do_closing(link):
"""Locally closed, remote end still active."""
# nothing to do but wait for the remote
return _STATE_CLOSING
def _do_closed(link):
"""Both ends of the link have closed."""
LOG.debug("Link close completed")
if link._handler:
if isinstance(link, SenderLink):
link._handler.sender_closed(link)
else:
link._handler.receiver_closed(link)
return _STATE_CLOSED
# Given the current endpoint state for the link, move to the next state do any
# state-specific processing:
_EP_STATE_MACHINE = [
# _STATE_UNINIT:
{(_LOCAL_ACTIVE | _REMOTE_UNINIT): _do_pending,
(_LOCAL_UNINIT | _REMOTE_ACTIVE): _do_requested},
# _STATE_PENDING:
{(_LOCAL_ACTIVE | _REMOTE_ACTIVE): _do_active,
(_LOCAL_ACTIVE | _REMOTE_CLOSED): _do_need_close,
(_LOCAL_CLOSED | _REMOTE_UNINIT): _do_closed,
(_LOCAL_CLOSED | _REMOTE_CLOSED): _do_closed},
# _STATE_REQESTED:
{(_LOCAL_ACTIVE | _REMOTE_ACTIVE): _do_active,
(_LOCAL_CLOSED | _REMOTE_ACTIVE): _do_closing,
(_LOCAL_ACTIVE | _REMOTE_CLOSED): _do_need_close},
# _STATE_ACTIVE:
{(_LOCAL_ACTIVE | _REMOTE_CLOSED): _do_need_close,
(_LOCAL_CLOSED | _REMOTE_ACTIVE): _do_closing,
(_LOCAL_CLOSED | _REMOTE_CLOSED): _do_closed},
# _STATE_NEED_CLOSE:
{(_LOCAL_CLOSED | _REMOTE_CLOSED): _do_closed},
# _STATE_CLOSING:
{(_LOCAL_CLOSED | _REMOTE_CLOSED): _do_closed}]
class _Link(object):
"""A generic Link base class."""
def __init__(self, connection, pn_link,
target_address, source_address,
handler, properties):
def __init__(self, connection, pn_link):
self._state = _STATE_UNINIT
# last known endpoint state:
self._ep_state = _LOCAL_UNINIT | _REMOTE_UNINIT
self._connection = connection
self._name = pn_link.name
self._handler = handler
self._properties = properties
self._handler = None
self._properties = None
self._user_context = None
self._active = False
# TODO(kgiusti): raise jira to add 'context' attr to api
self._pn_link = pn_link
pn_link.context = self
def configure(self, target_address, source_address, handler, properties):
"""Assign addresses, properties, etc."""
self._handler = handler
self._properties = properties
if target_address is None:
assert pn_link.is_sender, "Dynamic target not allowed"
if not self._pn_link.is_sender:
raise Exception("Dynamic target not allowed")
self._pn_link.target.dynamic = True
elif target_address:
self._pn_link.target.address = target_address
if source_address is None:
assert pn_link.is_receiver, "Dynamic source not allowed"
if not self._pn_link.is_receiver:
raise Exception("Dynamic source not allowed")
self._pn_link.source.dynamic = True
elif source_address:
self._pn_link.source.address = source_address
@ -73,7 +198,9 @@ class _Link(object):
return self._name
def open(self):
LOG.debug("Opening the link.")
self._pn_link.open()
self._connection._add_work(self)
def _get_user_context(self):
return self._user_context
@ -106,7 +233,9 @@ class _Link(object):
return self._pn_link.remote_target.address
def close(self, error=None):
LOG.debug("Closing the link.")
self._pn_link.close()
self._connection._add_work(self)
@property
def closed(self):
@ -122,6 +251,20 @@ class _Link(object):
self._pn_link.free()
self._pn_link = None
def _process(self):
"""Link state machine processing."""
# check for transitions in Endpoint state:
pn_link = self._pn_link
if self._state != _STATE_CLOSED:
ep_state = pn_link.state
if ep_state != self._ep_state:
LOG.debug("link state: %s old ep: %s new ep: %s",
self._state, hex(self._ep_state), hex(ep_state))
self._ep_state = ep_state
self._state = _EP_STATE_MACHINE[self._state][ep_state](self)
def _process_delivery(self, pn_delivery):
pass
class SenderEventHandler(object):
def sender_active(self, sender_link):
@ -146,11 +289,8 @@ class SenderLink(_Link):
RELEASED = 3
MODIFIED = 4
def __init__(self, connection, pn_link, source_address,
target_address, event_handler, properties):
super(SenderLink, self).__init__(connection, pn_link,
target_address, source_address,
event_handler, properties)
def __init__(self, connection, pn_link):
super(SenderLink, self).__init__(connection, pn_link)
self._pending_sends = collections.deque()
self._pending_acks = {}
self._next_deadline = 0
@ -163,17 +303,18 @@ class SenderLink(_Link):
self._pending_sends.append((message, delivery_callback, handle,
deadline))
# TODO(kgiusti) deadline not supported yet
assert not deadline, "send timeout not supported yet!"
if deadline:
raise NotImplementedError("send timeout not supported yet!")
if deadline and (self._next_deadline == 0 or
self._next_deadline > deadline):
self._next_deadline = deadline
delivery = self._pn_link.delivery("tag-%x" % self._next_tag)
pn_delivery = self._pn_link.delivery("tag-%x" % self._next_tag)
self._next_tag += 1
if delivery.writable:
if pn_delivery.writable:
send_req = self._pending_sends.popleft()
self._write_msg(delivery, send_req)
self._write_msg(pn_delivery, send_req)
return 0
@ -197,14 +338,20 @@ class SenderLink(_Link):
self._pending_acks.clear()
super(SenderLink, self).close()
def reject(self, reason):
"""See Link Reject, AMQP1.0 spec."""
# TODO(kgiusti) support reason for close
self._pn_link.source.type = proton.Terminus.UNSPECIFIED
self._pn_link.open()
self._pn_link.close()
def destroy(self):
self._connection._remove_sender(self._name)
self._connection = None
super(SenderLink, self).destroy()
def _delivery_updated(self, delivery):
# A delivery has changed state.
# Do we need to know?
def _process_delivery(self, pn_delivery):
"""Check if the delivery can be processed."""
_disposition_state_map = {
proton.Disposition.ACCEPTED: SenderLink.ACCEPTED,
proton.Disposition.REJECTED: SenderLink.REJECTED,
@ -212,41 +359,41 @@ class SenderLink(_Link):
proton.Disposition.MODIFIED: SenderLink.MODIFIED,
}
if delivery.tag in self._pending_acks:
if delivery.settled: # remote has finished
LOG.debug("delivery updated, remote state=%s",
str(delivery.remote_state))
send_req = self._pending_acks.pop(delivery.tag)
state = _disposition_state_map.get(delivery.remote_state,
if pn_delivery.tag in self._pending_acks:
if pn_delivery.settled: # remote has finished
LOG.debug("Remote has settled a sent msg")
send_req = self._pending_acks.pop(pn_delivery.tag)
state = _disposition_state_map.get(pn_delivery.remote_state,
self.UNKNOWN)
cb = send_req[1]
handle = send_req[2]
cb(self, handle, state, None)
delivery.settle()
pn_delivery.settle()
else:
# not for a sent msg, use it to send the next
if delivery.writable and self._pending_sends:
if pn_delivery.writable and self._pending_sends:
send_req = self._pending_sends.popleft()
self._write_msg(delivery, send_req)
self._write_msg(pn_delivery, send_req)
else:
# what else is there???
delivery.settle()
pn_delivery.settle()
def _write_msg(self, delivery, send_req):
def _write_msg(self, pn_delivery, send_req):
# given a writable delivery, send a message
# send_req = (msg, cb, handle, deadline)
LOG.debug("Sending a pending message")
msg = send_req[0]
cb = send_req[1]
self._pn_link.send(msg.encode())
self._pn_link.advance()
if cb: # delivery callback given
assert delivery.tag not in self._pending_acks
self._pending_acks[delivery.tag] = send_req
if pn_delivery.tag in self._pending_acks:
raise Exception("Duplicate delivery tag?")
self._pending_acks[pn_delivery.tag] = send_req
else:
# no status required, so settle it now.
delivery.settle()
pn_delivery.settle()
class ReceiverEventHandler(object):
@ -265,11 +412,8 @@ class ReceiverEventHandler(object):
class ReceiverLink(_Link):
def __init__(self, connection, pn_link, target_address,
source_address, event_handler, properties):
super(ReceiverLink, self).__init__(connection, pn_link,
target_address, source_address,
event_handler, properties)
def __init__(self, connection, pn_link):
super(ReceiverLink, self).__init__(connection, pn_link)
self._next_handle = 0
self._unsettled_deliveries = {} # indexed by handle
@ -294,17 +438,24 @@ class ReceiverLink(_Link):
def message_modified(self, handle):
self._settle_delivery(handle, proton.Delivery.MODIFIED)
def reject(self, reason):
"""See Link Reject, AMQP1.0 spec."""
# TODO(kgiusti) support reason for close
self._pn_link.target.type = proton.Terminus.UNSPECIFIED
self._pn_link.open()
self._pn_link.close()
def destroy(self):
self._connection._remove_receiver(self._name)
self._connection = None
super(ReceiverLink, self).destroy()
def _delivery_updated(self, delivery):
# a receive delivery changed state
def _process_delivery(self, pn_delivery):
"""Check if the delivery can be processed."""
# TODO(kgiusti): multi-frame message transfer
LOG.debug("Receive delivery updated")
if delivery.readable:
data = self._pn_link.recv(delivery.pending)
if pn_delivery.readable:
LOG.debug("Receive delivery readable")
data = self._pn_link.recv(pn_delivery.pending)
msg = proton.Message()
msg.decode(data)
self._pn_link.advance()
@ -312,16 +463,16 @@ class ReceiverLink(_Link):
if self._handler:
handle = "rmsg-%s:%x" % (self._name, self._next_handle)
self._next_handle += 1
self._unsettled_deliveries[handle] = delivery
self._unsettled_deliveries[handle] = pn_delivery
self._handler.message_received(self, msg, handle)
else:
# TODO(kgiusti): is it ok to assume Delivery.REJECTED?
delivery.settle()
pn_delivery.settle()
def _settle_delivery(self, handle, result):
# settle delivery associated with a handle
if handle not in self._unsettled_deliveries:
raise Exception("Invalid message handle: %s" % str(handle))
delivery = self._unsettled_deliveries.pop(handle)
delivery.update(result)
delivery.settle()
pn_delivery = self._unsettled_deliveries.pop(handle)
pn_delivery.update(result)
pn_delivery.settle()