spoon.routing.MeshRouter'

Package spoon :: Package routing :: Module MeshRouter' |
[hide private]
[frames] | no frames]

# Source Code for Module spoon.routing.MeshRouter'

      1  #
      2  # Copyright (C) 2006, Matt Sullivan <matts@zarrf.com>
      3  #
      4  # Permission is hereby granted, free of charge, to any person obtaining
      5  # a copy of this software and associated documentation files (the
      6  # "Software"), to deal in the Software without restriction, including
      7  # without limitation the rights to use, copy, modify, merge, publish,
      8  # distribute, sublicense, and/or sell copies of the Software, and to
      9  # permit persons to whom the Software is furnished to do so, subject to
     10  # the following conditions:
     11  #
     12  # The above copyright notice and this permission notice shall be included
     13  # in all copies or substantial portions of the Software.
     14  #
     15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
     16  # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     17  # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
     18  # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
     19  # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
     20  # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
     21  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     22  #
     23
     24  """
     25  Mesh Routing, this is the protocol by which an ad-hoc network of nodes can be created and
     26  how messages may be passed between them.
     27
     28  Routing in SpoonRPC
     29
     30  Baseline:
     31     - a network of nodes (each node is probably a server)
     32     - nodes are connected directly to each other by links (probably TCP)
     33     - from node A to node B, there may be more than one direct route (redundant paths are okay, encouraged in fact,
     34       to avoid pitfalls of IRC)
     35
     36  Route tracking
     37
     38  Each node tracks -- for every other node in the network -- how many hops it is to that node, and which link is used
     39  to get there.
     40
     41  New links
     42
     43  When a node gains a new link, it sends a message to each of its other links announcing the new hop-count for this
     44  new link ("Node T, 1 hop"). Each link compares this with any existing route for T, and if the hop count is smaller,
     45  the route entry is replaced and a new hop count message is sent to each of its local links (except the one it received
     46  the original message from). In this way, the new route will propagate. When such a message loops back on itself around
     47  a redundant link, it will be discarded because the hop count will be larger than the one already stored.
     48
     49  If the hop count is larger, but the link making the announcement is the link already in this node's route table, then
     50  the route's hop-count is changed and the message is propagated anyway. (It means the node's route is now longer than
     51  it was, or possibly infinite.)
     52
     53  An example may help: Node A is linked directly to B and C. It has a route to node X through B, with 4 hops. (All it
     54  stores for "X" is "B, 4 hops".) If it receives a message from C saying "X is 2 hops for me" then it will change X's
     55  route to "C, 3 hops" and send this new route to its other local links (like B). If, on the other hand, C says "X is 5
     56  hops for me" then A will ignore the message. If A receives a message from B saying "X is 5 hops for me" then A changes
     57  its route to "B, 6 hops" and sends this new route to all local links.
     58
     59  Once a new link is established, the two newly connected nodes will each send the other a hopcount update message
     60  containing all of the nodes each knows about. In the case of a node with no connections to the network, connecting
     61  to a node with connections, this will make the new node aware of all of the nodes that are available. In the case of
     62  a link forming between two existing networks (or perhaps fragments of a previous network that got disconnected), these
     63  messages will cause the two networks to be made fully aware of eachother.
     64
     65  Dead links
     66
     67  When a node drops a link, it sends out a message to all other links announcing the hop-count for the dead route as
     68  "infinity". (This can be represented as 0, -1, maxint, or whatever.) If a link receives an "infinity" route for node
     69  X from the link that is its current route to X, then the node must drop that route and propagate the "infinity" route
     70  to all its other links. In this way, if that was the only route to X, everyone will eventually drop the route and have
     71  X unreachable.
     72
     73  If a node receives an "infinity" route for node X from a link that is not its current route to X, it should immediately
     74  propagate its current route to that link. This will end up being propagated as if it were a new link (this is really just
     75  good neighbor policy.)
     76
     77  This rerouting mechanism may leave some nodes with inefficient (winding) routes to nodes that move around. So the
     78  "good neighbor" policy dictates that when you have a route to X, and a link that is not your route sends you info
     79  about a much longer route (at least 2 hops longer than yours), you turn around and tell this link about your route.
     80
     81  """
     82  from spoon.transports import LinkMessage
     83  from spoon import LMTYPE_NETWORK, LMTYPE_NETWORK_PROTO, NMTYPE_MESSAGING
     84  from spoon import serialprop, __SPOONNETMSG_TAG__
     85  from spoon import ber
     86
     87  #from spoon.messaging import SingletonMessaging
     88
     89
     90 -class NetMessage(object):
     91      """
     92      NetMessage is the protocol unit for the networking component of spoon.  It contains a
     93      src (source node id), dest (destination node id), message type and an object attachment.
     94      The object attachment can of course be anything at all that's serializable.
     95      The message type indicates what subsystem the message should be handled by.  Currently
     96      only messaging is implemented, but in the future, who knows.
     97      @todo: At some point in the future I'd like to add an HMAC here, but I have to sort out
     98      how keys and such are shared at a protocol level here.
     99      """
    100 -    def __init__(self, src=None, dst=None, mtype=None, attach=None):
    101          self.src = src
    102          self.dst = dst
    103          self.mtype = mtype
    104          self.attach = attach
    105
    106
    107  @ber.encoder(NetMessage)
    108 -def encode_netmessage(fd, obj):
    109      ber.Tag.from_tag(__SPOONNETMSG_TAG__, None).write(fd)
    110      b = ber.BERStream(fd)
    111      b.add(obj.dst)
    112      b.add(obj.src)
    113      b.add(obj.mtype)
    114      b.add(obj.attach)
    115      b._add_eof()
    116
    117  @ber.decoder(__SPOONNETMSG_TAG__)
    118 -def decode_netmessage(fd, tag):
    119      out = NetMessage()
    120      b = ber.BERStream(fd)
    121      out.dst = b.next()
    122      out.src = b.next()
    123      out.mtype = b.next()
    124      out.attach = b.next()
    125      if b.has_next():
    126          pass
    127      return out
    128
    129
    130 -class RoutingProtocolError(Exception):
    131      pass
    132
    133 -class NodeUnreachable(RoutingProtocolError):
    134      """
    135      Raised when trying to send to a node that is not currently reachable.  This is not necessarily a
    136      permanent condition.  Particularly if a transport has just been lost, a node may become available again
    137      within a few seconds as a new route is found.
    138      """
    139 -    def __init__(self, data, nodeId=None):
    140          self.nodeId = nodeId
    141          RoutingProtocolError.__init__(self, data)
    142
    143
    144  # Just use MAX signed int on a 32 bit scale to represent infinity, AKA, node not reachable.
    145  INFINITY = 2147483647
    146
    147 -class MeshRouter(object):
    148      """
    149      This class handles LMTYPE_NETWORK messages and LMTYPE_NETWORK_PROTO messages.  LMTYPE_NETWORK_PROTO is handled
    150      as a routing update, and LMTYPE_NETWORK will be handled as network traffic to be dealt with or routed.
    151
    152      If there is a SingletonMessaging instance already created (e.g. through the use of
    153      the acceptMsg decorator) and messaging is None
    154
    155      @ivar hub: the TransportHub that the router is associated with, should be set at init time and never changed.
    156      @ivar nodes: This is the routing table.  It's a dictionary whose keys are node ids, and the values are a
    157      tuple consisting of the hop count to the node and the transport used to get there.
    158      @ivar transports:   A set of the transports that are currently active.
    159      """
    160 -    def __init__(self, hub, messaging=None):
    161          self.hub = hub
    162          self.nodes = {}
    163          self.transports = set()
    164          self.nmtypes = {}
    165          if messaging:
    166              self.nmtypes[NMTYPE_MESSAGING] = messaging
    167          #elif SingletonMessaging.hasinstance():
    168          #    msging = SingletonMessaging.getinstance()
    169          #    msging.setNetwork(self)
    170          #    self.nmtypes[NMTYPE_MESSAGING] = messaging
    171
    172
    173 -    def addTransport(self, t, nodeId):
    174          """
    175          Handles the routing protocol initilization.
    176          This involves first exchanging routing tables entirely, then updating
    177          our routing table as appropriate.  While updating our routing table,
    178          we need to construct a routing update message and send it to all transports other than
    179          the transport being added.
    180          @param t: The transport being added to the network
    181          """
    182          # Construct a complete table message to send to the new node.
    183          wholeTable = {}
    184          for k,v in self.nodes.iteritems():
    185              wholeTable[k] = v[0]
    186          wholeTableMessage = LinkMessage(LMTYPE_NETWORK_PROTO, wholeTable)
    187
    188          remoteTable = {}
    189          # Weird hack... but between the two nodes exchanging tables, the one with the lowest nodeId will send first.
    190          if nodeId < self.hub.nodeId:
    191              # Send the table update message to the new node.
    192              t.write(wholeTableMessage)
    193              remoteTable = t.read()
    194          else:
    195              remoteTable = t.read()
    196              t.write(wholeTableMessage)
    197
    198          if type(remoteTable) != LinkMessage:
    199              self.hub._log.error("Invalid protocol message received during transport initialization of routing on transport "+repr(t)+", got '%s'"%(type(remoteTable)))
    200              raise RoutingProtocolError("Invalid protocol message received during transport initialization of routing on transport "+repr(t))
    201          # Process the routing table that we've received from the remote node and update our routing table to indicate that
    202          # we have a route to the remote node with a hopcount of 1.
    203          self.transports.add(t)
    204          self.handleUpdate(t, remoteTable.attach, {t.nodeId: (1, t)})
    205
    206 -    def removeTransport(self, t):
    207          """
    208          Removes the transport from the network.  This involves figuring out which nodes are now unreachable (any routes
    209          we had over that transport), removing any references to the transport from our routing table, and updating
    210          our neighbors about our changed routing table.
    211          @param t: The transport being removed from the network
    212          """
    213          updatedRoutes = {}
    214          self.transports.remove(t)
    215          for k,v in self.nodes.iteritems():
    216              if v[1] == t:
    217                  self.nodes[k] = (INFINITY, None)
    218                  updatedRoutes[k] = INFINITY
    219
    220          # Normally we'd check to see if updatedRoutes actually contains anything, but it has to at this point.
    221          for x in self.transports:
    222              x.write(LinkMessage(LMTYPE_NETWORK_PROTO, updatedRoutes))
    223
    224 -    def handleUpdate(self, t, update, localUpdate=None):
    225          """
    226          Handles a routing table update, builds the update message and sends it out to all except the src of the update.
    227          @param t: The source transport from which the update came (in other words where NOT to send our update)
    228          @param update: A dict containing the routing update.
    229          @param localUpdate: Only used in the specific case of when creating a new transport.  This will include an
    230          entry for the node we've just connected to, so that it may be merged into the update sent out to the
    231          other nodes.
    232          """
    233          # Updated routes will be sent to all transports but t
    234          updatedRoutes = {}
    235          # Good neighbor updates will be sent only to t
    236          goodNeighborUpdate = {}
    237          if localUpdate:
    238              # Update our local table with the info in localUpdate
    239              for k, v in localUpdate.iteritems():
    240                  self.nodes[k] = v
    241                  updatedRoutes[k] = v[0]
    242
    243          for k, v in update.iteritems():
    244              currentRoute = self.nodes.get(k, None)
    245              newCount = v + 1
    246              # Do we have a current route
    247              if currentRoute:
    248                  # We will update our current route and propagate it if the hop count we're being told about is
    249                  # better than what we have.  We will also update our current route and propagate it if the
    250                  # hop count is worse than our current route and it is coming from our current route.
    251                  if (newCount < currentRoute[0]) or ((newCount > currentRoute[0]) and (t == currentRoute[1])):
    252                      updatedRoutes[k] = newCount
    253                      self.nodes[k] = (newCount, t)
    254
    255                  # If the the advertised hopcount +1 is greater than our current hopcount, and the message isn't
    256                  # coming from our current route, we should send a "good neighbor" update to inform the other
    257                  # node of our better route.
    258                  elif(newCount > currentRoute[0]) and (t != currentRoute[1]):
    259                      goodNeighborUpdate[k] = currentRoute[0]
    260              # No current route, so we'll take the first one we hear about until we get a better one
    261              else:
    262                  updatedRoutes[k] = newCount
    263                  self.nodes[k] = (newCount, t)
    264
    265          # Ok, we should have all of our messages now, let's send them if they have anything
    266          if updatedRoutes:
    267              for x in self.transports:
    268                  if x != t:
    269                      x.write(LinkMessage(LMTYPE_NETWORK_PROTO, updatedRoutes))
    270          if goodNeighborUpdate:
    271              t.write(LinkMessage(LMTYPE_NETWORK_PROTO, updatedRoutes))
    272
    273 -    def handleLinkMessage(self, t, lm):
    274          """
    275          Called by the SpoonTransportThread when a link message has been received for one of our
    276          LMTYPEs.
    277          @param t: The transport that it came from
    278          @param lm: The LinkMessage that came in.
    279          """
    280          self.hub._log.debug("Recieving link message "+repr(lm))
    281          if lm.msgtype == LMTYPE_NETWORK_PROTO:
    282              self.handleUpdate(t, lm.attach)
    283          elif lm.msgtype == LMTYPE_NETWORK:
    284              self.handleNetworkTraffic(t, lm.attach)
    285          else:
    286              raise RoutingProtocolError("Received unknown LinkMessage type %d on transport %s"%(lm.msgtype, repr(t)))
    287
    288 -    def handleNetworkTraffic(self, t, obj):
    289          if type(obj) != NetMessage:
    290              raise RoutingProtocolError("Received unknown content in LinkMessage on transport %s"%(repr(t)))
    291          handler = self.nmtypes.get(obj.mtype, None)
    292          # Is this destined to us?  This is where the tricky bit comes in.
    293          if obj.dst == self.hub.nodeId:
    294              if not handler:
    295                  # This is destined for us, but we don't have a clue how to handle it
    296                  self.hub._log.warning("Received network message with a handler type of %s but no handler is registered."%(obj.mtype))
    297                  return
    298              handler.handleMessage(obj.src, obj.attach)
    299          else:
    300              route = self.nodes.get(obj.dst, (INFINITY, None))
    301              if route[0] == INFINITY:
    302                  self.hub._log.debug("Dropped network message to %d, no current route."%obj.dst)
    303                  return
    304              if route[1] == t:
    305                  self.hub._log.error("Immediate routing loop detected, something very bad is happening!\
    306                    Received message for node %d from our route for it, %d"%(obj.dst, t.NodeId))
    307              route[1].write(LinkMessage(LMTYPE_NETWORK, obj))
    308
    309 -    def sendNetMsg(self, dst, mtype, obj):
    310          """
    311          Sends a network message to the destination specified.
    312          A NetMessage object is constructed from the arguments provided to this
    313          method and routed appropriately.
    314          @param dst: The node id of the destination
    315          @param mtype: The message type (probably NMTYPE_MESSAGE)
    316          @param obj: The object attachment of the NetMessage
    317          """
    318          msg = NetMessage(self.hub.nodeId, dst, mtype, obj)
    319          route = self.nodes.get(dst, (INFINITY, None))
    320          if route[0] == INFINITY:
    321              raise NodeUnreachable("NodeId %s is not currently reachable"%dst, dst)
    322          route[1].write(LinkMessage(LMTYPE_NETWORK, msg))
    323