PageRenderTime 29ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/python-service/state_update_service.py

https://bitbucket.org/stateupdateservice/state-update-service
Python | 408 lines | 317 code | 50 blank | 41 comment | 74 complexity | ae934d594db27cbf308fa97d3051fb8e MD5 | raw file
  1. import zmq
  2. from service_message_pb2 import *
  3. from state_update_service_pb2 import *
  4. import sys
  5. import uuid
  6. import traceback
  7. from google.protobuf import reflection
  8. from google.protobuf import descriptor
  9. from google.protobuf import message
  10. def MakeDescriptor(protobufDescriptor):
  11. newDescriptor = descriptor.MakeDescriptor( protobufDescriptor )
  12. for desc in protobufDescriptor.nested_type:
  13. nested_type = MakeDescriptor(desc) #descriptor.MakeDescriptor(desc)
  14. class concrete(message.Message):
  15. __metaclass__ = reflection.GeneratedProtocolMessageType
  16. DESCRIPTOR = nested_type
  17. nested_type._concrete_class = concrete
  18. newDescriptor.nested_types.append(nested_type )
  19. newDescriptor.nested_types_by_name = dict((t.name, t) for t in newDescriptor.nested_types)
  20. for f in protobufDescriptor.field:
  21. type_name = f.type_name.split('.')[-1]
  22. if newDescriptor.nested_types_by_name.has_key(type_name):
  23. newDescriptor.fields_by_number[f.number].message_type = newDescriptor.nested_types_by_name[type_name]
  24. return newDescriptor
  25. class StoreItem:
  26. def __init__( self, key, session ):
  27. self.key = key
  28. self.value = None
  29. # Current revision number of this store item
  30. self.revision = 0
  31. # The earliest revision number we can generate partial updates for
  32. self.revisionHistory = 0
  33. # The partial updates we have stored for this key
  34. self.history = []
  35. # Our current unique session key
  36. self.session = session
  37. # Listener addresses who are interested in this key
  38. self.listeners = {}
  39. # Set protobuf descriptor
  40. self.protobufDescriptor = None
  41. def log( self, message ):
  42. print "Log:",message
  43. def needsReply( self, requestObj ):
  44. if requestObj.HasField('session') and self.session != requestObj.session:
  45. return True
  46. if requestObj.HasField('revision') and self.revision == requestObj.revision:
  47. return False
  48. return True
  49. def updateValue( self, updateObj, replyItems ):
  50. # If somebody is trying to update this to a value that it is already at, just do nothing
  51. if not updateObj.HasField( 'partialUpdate' ) and self.value == updateObj.value:
  52. return True
  53. # Save any specified descriptor
  54. if updateObj.HasField( 'protobufDescriptor' ):
  55. try:
  56. self.protobufDescriptor = MakeDescriptor(updateObj.protobufDescriptor)
  57. except:
  58. traceback.print_exc(file=sys.stdout)
  59. return False
  60. # Do partial update, if it's sent
  61. if updateObj.HasField( 'partialUpdate' ):
  62. newValue = self.applyPartialUpdate( self.value, updateObj.partialUpdate )
  63. if newValue == None:
  64. return False
  65. self.value = newValue
  66. self.revision += 1
  67. # Leave revision history where it is, since we are able to build a partial update from there
  68. self.history.append( updateObj.partialUpdate )
  69. # This is a simple, update value
  70. elif updateObj.HasField( 'value' ):
  71. self.value = updateObj.value
  72. self.revision += 1
  73. self.revisionHistory = self.revision
  74. self.history = []
  75. else:
  76. # If they don't set a value OR a partial update, what are they expecting?
  77. self.log("Client didn't set value or partial update")
  78. return False
  79. # Add list of listeners to reply mapping
  80. for listener, originalRequest in self.listeners.items():
  81. if not replyItems.has_key( listener ):
  82. replyItems[ listener ] = []
  83. replyItems[ listener ].append( ( self, originalRequest ) )
  84. self.listeners = {}
  85. return True
  86. def applyPartialUpdate( self, value, partialUpdate ):
  87. if partialUpdate.type == PartialUpdate.APPEND and partialUpdate.HasField("appendBytes"):
  88. # Append the two messages
  89. return value + partialUpdate.appendBytes;
  90. # If we need to limit the number of fields, let's do it
  91. elif partialUpdate.type == PartialUpdate.PROTOBUF_MERGE and \
  92. partialUpdate.HasField("appendBytes") and self.protobufDescriptor != None:
  93. try:
  94. protoMsg = reflection.ParseMessage( self.protobufDescriptor, value )
  95. protoMsg.MergeFromString( partialUpdate.appendBytes )
  96. except:
  97. traceback.print_exc(file=sys.stdout)
  98. return None
  99. return protoMsg.SerializePartialToString()
  100. else:
  101. if not partialUpdate.HasField("appendBytes"):
  102. self.log("Client didn't set appendBytes field")
  103. elif self.protobufDescriptor == None:
  104. self.log("No protobuf descriptor has been set for this key: "+self.key)
  105. else:
  106. self.log("Client didn't specify any partial update operation")
  107. return None
  108. def addListener( self, listener, requestObj ):
  109. self.listeners[ listener ] = requestObj
  110. def removeListener( self, listener ):
  111. if self.listeners.has_key( listener ):
  112. del self.listeners[ listener ]
  113. class StateUpdateService:
  114. def __init__( self, context, listenAddress ):
  115. self.context = context
  116. self.currentSession = str(uuid.uuid4())
  117. self.service = self.context.socket(zmq.ROUTER)
  118. self.service.bind( listenAddress )
  119. # TODO: Load from db
  120. self.kvstore = {}
  121. def log( self, message ):
  122. print "Log:",message
  123. def sendMessage( self, address, message ):
  124. self.service.send(address, zmq.SNDMORE)
  125. self.service.send('', zmq.SNDMORE)
  126. self.service.send( message )
  127. def flushListeners( self, replyItems ):
  128. # Go through each listener that we are replying to
  129. for listener in replyItems.keys():
  130. # Build response
  131. rep = FetchStateResponseSet()
  132. ind = 0
  133. for targetItem, requestObj in replyItems[ listener ]:
  134. # Assume, until proven otherwise that the history for the relevant key can support version 1 clients
  135. maxVersionNeededByHistory = 1
  136. if requestObj.HasField("partialVersion") and requestObj.partialVersion >= maxVersionNeededByHistory \
  137. and requestObj.HasField("revision") and requestObj.HasField("session") \
  138. and requestObj.session == targetItem.session \
  139. and requestObj.revision >= targetItem.revisionHistory \
  140. and requestObj.revision < targetItem.revision:
  141. # If we find a historical entry that needs version 2 of partial updates, we might need to bail
  142. for historicalEntry in targetItem.history[ requestObj.revision - targetItem.revisionHistory : ]:
  143. if historicalEntry.type not in [ PartialUpdate.APPEND, PartialUpdate.PROTOBUF_MERGE ]:
  144. maxVersionNeededByHistory = 2
  145. # Check whether this request is eligible for a partial update
  146. # 1. The partial version support is compatible
  147. # 2. Make sure we are talking about same session
  148. # 3. Make sure our history goes back far enough
  149. # 4. Make sure they are asking for a reasonable revision
  150. if requestObj.HasField("partialVersion") and requestObj.partialVersion >= maxVersionNeededByHistory \
  151. and requestObj.HasField("revision") and requestObj.HasField("session") \
  152. and requestObj.session == targetItem.session \
  153. and requestObj.revision >= targetItem.revisionHistory \
  154. and requestObj.revision < targetItem.revision:
  155. # Iterate over relevant history items and make responses for them
  156. for historicalEntry in targetItem.history[ requestObj.revision - targetItem.revisionHistory : ]:
  157. # Build fields for partial update
  158. rep.responses.add()
  159. # Set all required fields
  160. rep.responses[ ind ].session = targetItem.session
  161. rep.responses[ ind ].revision = targetItem.revision
  162. rep.responses[ ind ].key = targetItem.key
  163. # Copy partialUpdate object from history
  164. rep.responses[ ind ].partialUpdate.CopyFrom( historicalEntry )
  165. ind += 1
  166. # Otherwise, just send the value back, like normal
  167. else:
  168. rep.responses.add()
  169. # Set all required fields
  170. rep.responses[ ind ].session = targetItem.session
  171. rep.responses[ ind ].revision = targetItem.revision
  172. rep.responses[ ind ].key = targetItem.key
  173. # Set normal response
  174. rep.responses[ ind ].value = targetItem.value
  175. ind += 1
  176. # Build packet
  177. packet = ServicePacket()
  178. packet.packetType = ServicePacket.FETCH_STATE_RESPONSE_SET
  179. packet.packetData = rep.SerializeToString()
  180. # Send response packet
  181. self.sendMessage( listener, packet.SerializeToString() )
  182. def main( self ):
  183. # Keep track of info for all request sets
  184. requestSetIdMap = {}
  185. reverseRequestSetIdMap = {}
  186. # Keep track of what entries have listeners
  187. listenerCacheMap = {}
  188. while True:
  189. [address, blank, message] = self.service.recv_multipart()
  190. try:
  191. packet = ServicePacket()
  192. packet.ParseFromString( message )
  193. except:
  194. traceback.print_exc(file=sys.stdout)
  195. continue
  196. # This is an update state message
  197. if packet.packetType == ServicePacket.UPDATE_STATE_REQUEST_SET:
  198. try:
  199. # Unserialize the request protobuf
  200. req = UpdateStateRequestSet()
  201. req.ParseFromString( packet.packetData )
  202. except:
  203. traceback.print_exc(file=sys.stdout)
  204. continue
  205. replyItems = {}
  206. # Build map of replies that need sent because of this update
  207. successes = []
  208. for requestObj in req.updates:
  209. # If this key doesn't exist yet, initialize new store item and update it
  210. if not self.kvstore.has_key( requestObj.key ):
  211. self.kvstore[ requestObj.key ] = StoreItem( requestObj.key, self.currentSession )
  212. # We must pass the reply map so it can add other listeners that need this update, if necessary
  213. successes.append( self.kvstore[ requestObj.key ].updateValue( requestObj, replyItems ) )
  214. # Build response
  215. rep = UpdateStateResponseSet()
  216. ind = 0
  217. for item in req.updates:
  218. rep.responses.add()
  219. rep.responses[ ind ].session = self.currentSession
  220. rep.responses[ ind ].revision = self.kvstore[ item.key ].revision
  221. rep.responses[ ind ].key = item.key
  222. rep.responses[ ind ].success = successes[ ind ]
  223. ind += 1
  224. # Build packet
  225. packet = ServicePacket()
  226. packet.packetType = ServicePacket.UPDATE_STATE_RESPONSE_SET
  227. packet.packetData = rep.SerializeToString()
  228. # Send response
  229. self.sendMessage( address, packet.SerializeToString() )
  230. # Now trigger all updates to be sent
  231. self.flushListeners( replyItems )
  232. for listener in replyItems.keys():
  233. # Cleanup hanging listeners
  234. if listenerCacheMap.has_key( listener ):
  235. for targetKey in listenerCacheMap[ listener ]:
  236. if self.kvstore.has_key( targetKey ):
  237. self.kvstore[ targetKey ].removeListener( listener )
  238. del listenerCacheMap[ listener ]
  239. # Cleanup requestSetIdMap
  240. if reverseRequestSetIdMap.has_key( listener ):
  241. if reverseRequestSetIdMap.has_key( listener ):
  242. del requestSetIdMap[ reverseRequestSetIdMap[ listener ] ]
  243. del reverseRequestSetIdMap[ listener ]
  244. # This is a fetch state message
  245. elif packet.packetType == ServicePacket.FETCH_STATE_REQUEST_SET:
  246. try:
  247. # Unserialize the request protobuf
  248. req = FetchStateRequestSet()
  249. req.ParseFromString( packet.packetData )
  250. except:
  251. traceback.print_exc(file=sys.stdout)
  252. continue
  253. if req.blocking:
  254. replyingNow = False
  255. else:
  256. replyingNow = True
  257. addingOperation = False
  258. removingOperation = False
  259. # If this is an operation on a previous requestset, let's overwrite the address, just in case we must reply
  260. if req.HasField("requestSetId"):
  261. if req.HasField("requestSetModify"):
  262. # Send blank response, since this is just a modify operation
  263. rep = FetchStateResponseSet()
  264. packet = ServicePacket()
  265. packet.packetType = ServicePacket.FETCH_STATE_RESPONSE_SET
  266. packet.packetData = rep.SerializeToString()
  267. # Send response packet
  268. self.sendMessage( address, packet.SerializeToString() )
  269. # If this is a well-formed modify packet, with a valid operation and a setId that hasn't been replied to yet, let's go forward
  270. if requestSetIdMap.has_key( req.requestSetId ) and req.requestSetModify in [ FetchStateRequestSet.REQUEST_SET_ADD, FetchStateRequestSet.REQUEST_SET_REMOVE ]:
  271. if req.requestSetModify == FetchStateRequestSet.REQUEST_SET_ADD:
  272. addingOperation = True
  273. else:
  274. removingOperation = True
  275. # Overwrite address with address from map
  276. address = requestSetIdMap[ req.requestSetId ]
  277. replyingNow = False # Force blocking
  278. else: # Something was badly formed, do nothing
  279. continue
  280. elif req.blocking:
  281. # Store this for future use, only if it's blocking request
  282. if requestSetIdMap.has_key( req.requestSetId ):
  283. # Somebody is sending the same requestsetid again, before first one has been replied to, we must cleanup old listener address
  284. oldListener = requestSetIdMap[ req.requestSetId ]
  285. if listenerCacheMap.has_key( oldListener ):
  286. for targetKey in listenerCacheMap[ oldListener ]:
  287. if self.kvstore.has_key( targetKey ):
  288. self.kvstore[ targetKey ].removeListener( oldListener )
  289. del listenerCacheMap[ oldListener ]
  290. # Send the old listener a blank reply, just to flush requests through the system
  291. blankrep = FetchStateResponseSet()
  292. blankpacket = ServicePacket()
  293. blankpacket.packetType = ServicePacket.FETCH_STATE_RESPONSE_SET
  294. blankpacket.packetData = blankrep.SerializeToString()
  295. self.sendMessage( oldListener, blankpacket.SerializeToString() )
  296. requestSetIdMap[ req.requestSetId ] = address
  297. reverseRequestSetIdMap[ address ] = req.requestSetId
  298. # Find out if we must reply now or later
  299. thisListenerRequests = { }
  300. replyItems = { address: [] }
  301. if not removingOperation:
  302. for item in req.requests:
  303. if self.kvstore.has_key( item.key ):
  304. thisListenerRequests[ item.key ] = item
  305. # Does this request need a reply right now?
  306. if self.kvstore[ item.key ].needsReply( item ):
  307. replyingNow = True # In case it wasn't already set
  308. replyItems[ address ].append( ( self.kvstore[ item.key ], item ) )
  309. if replyingNow:
  310. # Send off all our replies
  311. self.flushListeners( replyItems )
  312. # On the off-chance that this is being triggered by a modify operation, let's cleanup any other keys being watched
  313. if listenerCacheMap.has_key( address ):
  314. for targetKey in listenerCacheMap[ address ]:
  315. if self.kvstore.has_key( targetKey ):
  316. self.kvstore[ targetKey ].removeListener( address )
  317. del listenerCacheMap[ address ]
  318. # Cleanup requestSetIdMap
  319. if reverseRequestSetIdMap.has_key( address ):
  320. del requestSetIdMap[ reverseRequestSetIdMap[ address ] ]
  321. del reverseRequestSetIdMap[ address ]
  322. else:
  323. if removingOperation:
  324. # Ok, we are removing listeners, let's clean everything up
  325. for requestObj in req.requests:
  326. if self.kvstore.has_key( requestObj.key ):
  327. self.kvstore[ requestObj.key ].removeListener( address )
  328. if listenerCacheMap.has_key( address ):
  329. if requestObj.key in listenerCacheMap[ address ]:
  330. listenerCacheMap[ address ].remove( requestObj.key )
  331. else:
  332. # If we're not replying now, store this request as a listener on each of the keys he wants updates on
  333. if not listenerCacheMap.has_key( address ):
  334. listenerCacheMap[ address ] = []
  335. for requestObj in req.requests:
  336. if self.kvstore.has_key( requestObj.key ):
  337. self.kvstore[ requestObj.key ].addListener( address, requestObj )
  338. # Save in cache for later cleanup
  339. listenerCacheMap[ address ].append( requestObj.key )
  340. if __name__ == "__main__":
  341. context = zmq.Context(1)
  342. service = StateUpdateService( context, sys.argv[1] )
  343. service.main( )