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