PageRenderTime 55ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/python-api/state_update_service_api.py

https://bitbucket.org/stateupdateservice/state-update-service
Python | 316 lines | 248 code | 34 blank | 34 comment | 22 complexity | 732caf58548f3faf1e58ce9443dcebba MD5 | raw file
  1. import sys
  2. import copy
  3. import uuid
  4. from google.protobuf.descriptor import FieldDescriptor
  5. sys.path.append('../python-service')
  6. from service_message_pb2 import *
  7. from state_update_service_pb2 import *
  8. from descriptor_pb2 import DescriptorProto
  9. import urllib2
  10. def sendRequest( service, packetType, packetData ):
  11. # Build packet structure
  12. p = ServicePacket()
  13. p.packetType = packetType
  14. p.packetData = packetData
  15. # Send to backend
  16. service.send( p.SerializeToString() )
  17. # Get response, parse back into ServicePAcket
  18. p.ParseFromString( service.recv() )
  19. # Return type, data
  20. return p.packetType, p.packetData
  21. class CacheEntry:
  22. def __init__( self, key, value, session, revision ):
  23. self.key = key
  24. self.value = value
  25. self.session = session
  26. self.revision = revision
  27. class HttpService:
  28. def __init__( self, url ):
  29. self.url = url
  30. self.response = None
  31. def send( self, data ):
  32. req = urllib2.Request( self.url, data )
  33. req.add_header('Content-Type', 'application/octet-stream')
  34. while True:
  35. try:
  36. handle = urllib2.urlopen( req, timeout = 300 )
  37. except IOError:
  38. continue
  39. break
  40. self.response = handle.read()
  41. handle.close()
  42. def recv( self ):
  43. if self.response:
  44. return self.response
  45. else:
  46. raise IOError("You must successfully send before trying to receive")
  47. class StateService:
  48. supportedPartialVersion = 1 # This client can deal with partial update version 2 feature set
  49. def __init__( self, context, server ):
  50. if server.startswith('tcp') or server.startswith('ipc'):
  51. import zmq
  52. self.service = context.socket( zmq.REQ )
  53. self.service.connect( server )
  54. self.watcher = context.socket( zmq.REQ )
  55. self.watcher.connect( server )
  56. elif server.startswith('http'):
  57. self.service = HttpService( server )
  58. self.watcher = self.service
  59. self.watchKeys = {}
  60. self.lastSession = None
  61. self.cache = {}
  62. self.currentWatchRequestSetId = None
  63. self.currentUpdateStructure = None
  64. self.sentDescriptorToServer = {}
  65. def initKey( self, key, valueObject ):
  66. # Init specified key to blank so we can operate on it
  67. obj = copy.deepcopy( valueObject )
  68. obj.Clear()
  69. # Default to a generic not-valid entry. This needs to be updated
  70. self.cache[ key ] = CacheEntry( key, obj, "" , 0 )
  71. return self
  72. def updateKey( self, key, msg ):
  73. # This queues an update into memory (and flushes to cache)
  74. if not self.currentUpdateStructure:
  75. self.currentUpdateStructure = UpdateStateRequestSet()
  76. # Add new update entry to structure
  77. self.currentUpdateStructure.updates.add()
  78. self.currentUpdateStructure.updates[ -1 ].key = key
  79. self.currentUpdateStructure.updates[ -1 ].value = msg.SerializeToString()
  80. if not self.sentDescriptorToServer.has_key( key ):
  81. msg.DESCRIPTOR.CopyToProto( self.currentUpdateStructure.updates[ -1 ].protobufDescriptor )
  82. # Mark this so we don't need to send the descriptor every time
  83. self.sentDescriptorToServer[ key ] = True
  84. def partialUpdateKey( self, key, msg ):
  85. if not self.currentUpdateStructure:
  86. self.currentUpdateStructure = UpdateStateRequestSet()
  87. # Add new partial update entry to structure
  88. self.currentUpdateStructure.updates.add()
  89. self.currentUpdateStructure.updates[ -1 ].key = key
  90. self.currentUpdateStructure.updates[ -1 ].partialUpdate.type = PartialUpdate.PROTOBUF_MERGE
  91. self.currentUpdateStructure.updates[ -1 ].partialUpdate.appendBytes = msg.SerializePartialToString()
  92. def flushUpdates( self ):
  93. # Flush any state updates in queue to server (maybe loaded from cache, if we've crashed)
  94. # returns map of what updates failed, empty map otherwise
  95. # Check whether there is anything to flush
  96. if self.currentUpdateStructure == None:
  97. return {}
  98. repType, rep = sendRequest( self.service, ServicePacket.UPDATE_STATE_REQUEST_SET, self.currentUpdateStructure.SerializeToString() )
  99. assert repType == ServicePacket.UPDATE_STATE_RESPONSE_SET
  100. res = UpdateStateResponseSet()
  101. res.ParseFromString( rep )
  102. statusInfoMap = {} # Build a map, detailing what failed, if anything
  103. for response in res.responses:
  104. if not response.success:
  105. statusInfoMap[ response.key ] = False
  106. # If nothing failed, reset structure
  107. if len(statusInfoMap.keys()) == 0:
  108. self.currentUpdateStructure = None
  109. return statusInfoMap # If this map has nothing in it, all updates were successful
  110. def _updateCache( self, responseObj ):
  111. if responseObj.value:
  112. # Replace current value with new value
  113. self.cache[ responseObj.key ].value.ParseFromString( responseObj.value )
  114. elif responseObj.partialUpdate:
  115. if responseObj.partialUpdate.type == PartialUpdate.PROTOBUF_MERGE:
  116. # Simple merge update
  117. self.cache[ responseObj.key ].value.MergeFromString( responseObj.partialUpdate.appendBytes )
  118. self.cache[ responseObj.key ].revision = responseObj.revision
  119. self.cache[ responseObj.key ].session = responseObj.session
  120. def getKeys( self, keys, cacheOnly = False, allowPartial = True ):
  121. for ind, key in enumerate(keys):
  122. if not self.cache.has_key( key ): # Make sure this key has been initialized, otherwise, raise error
  123. raise Exception("Key must be initialized before being passed to getKeys")
  124. # Update keys unless we only want to go to cache
  125. if not cacheOnly:
  126. # Return list of values corresponding to the specified keys
  127. rset = FetchStateRequestSet()
  128. for ind, key in enumerate(keys):
  129. rset.requests.add()
  130. rset.requests[ ind ].key = key
  131. if self.cache[ key ].revision > 0:
  132. rset.requests[ ind ].session = self.cache[ key ].session
  133. rset.requests[ ind ].revision = self.cache[ key ].revision
  134. if allowPartial:
  135. rset.requests[ ind ].partialVersion = self.supportedPartialVersion
  136. # No blocking for new updates, let's just get any new updates to keys
  137. rset.blocking = False
  138. repType, rep = sendRequest( self.service, ServicePacket.FETCH_STATE_REQUEST_SET, rset.SerializeToString() )
  139. assert repType == ServicePacket.FETCH_STATE_RESPONSE_SET
  140. res = FetchStateResponseSet()
  141. res.ParseFromString( rep )
  142. # Apply all updates to cache, if it's not here, either it didn't need any updates or was not found
  143. for response in res.responses:
  144. if self.cache.has_key( response.key ): # Sanity check... server should NOT reply with something that we didn't ask for
  145. self._updateCache( response )
  146. else:
  147. raise Exception("Server respoded with key that we didn't ask for")
  148. # Now build responses
  149. responses = []
  150. for ind, key in enumerate(keys):
  151. if self.cache.has_key( key ) and self.cache[ key ].revision > 0:
  152. responses.append( copy.deepcopy( self.cache[ key ].value ) )
  153. else:
  154. # Key not found
  155. responses.append( None )
  156. return responses
  157. def watchKey( self, key, allowPartial = True ):
  158. if not self.cache.has_key( key ): # Make sure this key has been initialized, otherwise, raise error
  159. raise Exception("Key must be initialized before being passed to watchKey")
  160. # Add key to watch list
  161. # Threadsafe TODO: Wait for add/remove lock, aquire it when possible
  162. if not key in self.watchKeys.keys():
  163. self.watchKeys[ key ] = { "key": key, "allowPartial" : allowPartial }
  164. # Update the server with the news, if necessary
  165. if self.currentWatchRequestSetId != None:
  166. rset = FetchStateRequestSet()
  167. rset.requests.add()
  168. rset.requests[ 0 ].key = key
  169. if self.cache[ key ].revision > 0:
  170. rset.requests[ 0 ].session = self.cache[ key ].session
  171. rset.requests[ 0 ].revision = self.cache[ key ].revision
  172. if allowPartial:
  173. rset.requests[ 0 ].partialVersion = self.supportedPartialVersion
  174. # Tell server we are ADDING this key to earlier request
  175. rset.requestSetModify = FetchStateRequestSet.REQUEST_SET_ADD
  176. # Don't block for new updates
  177. rset.blocking = False
  178. rset.requestSetId = self.currentWatchRequestSetId
  179. # Threadsafe TODO: Release add/remove lock
  180. # Update to server
  181. repType, rep = sendRequest( self.service, ServicePacket.FETCH_STATE_REQUEST_SET, rset.SerializeToString() )
  182. assert repType == ServicePacket.FETCH_STATE_RESPONSE_SET
  183. else:
  184. pass
  185. # Threadsafe TODO: Release add/remove lock
  186. return self
  187. def unWatchKey( self, key ):
  188. if not self.cache.has_key( key ): # Make sure this key has been initialized, otherwise, raise error
  189. raise Exception("Key must be initialized before being passed to unWatchKey")
  190. # Threadsafe TODO: Wait for add/remove lock, aquire it when possible
  191. # Remove key from watch list
  192. if key in self.watchKeys.keys():
  193. del self.watchKeys[ key ]
  194. # Update the server with the news, if necessary
  195. if self.currentWatchRequestSetId != None:
  196. rset = FetchStateRequestSet()
  197. rset.requests.add()
  198. rset.requests[ 0 ].key = key
  199. # Tell server we are REMOVING this key from earlier request
  200. rset.requestSetModify = FetchStateRequestSet.REQUEST_SET_REMOVE
  201. # Don't block for new updates
  202. rset.blocking = False
  203. rset.requestSetId = self.currentWatchRequestSetId
  204. # Threadsafe TODO: Release add/remove lock
  205. # Update to server
  206. repType, rep = sendRequest( self.service, ServicePacket.FETCH_STATE_REQUEST_SET, rset.SerializeToString() )
  207. assert repType == ServicePacket.FETCH_STATE_RESPONSE_SET
  208. else:
  209. pass
  210. # Threadsafe TODO: Release add/remove lock
  211. return self
  212. def doWatch( self ):
  213. # Block for updates on watched keys, then return map of updated keys that were being watched and changed
  214. # Return list of values corresponding to the specified keys
  215. # Threadsafe TODO: Lock watching, fail if we are already watching
  216. # Threadsafe TODO: Lock add/remove watch, so we don't allow other threads to add/remove before it is safe
  217. rset = FetchStateRequestSet()
  218. for ind, key in enumerate( self.watchKeys.keys() ):
  219. rset.requests.add()
  220. rset.requests[ ind ].key = key
  221. if self.cache[ key ].revision > 0:
  222. rset.requests[ ind ].session = self.cache[ key ].session
  223. rset.requests[ ind ].revision = self.cache[ key ].revision
  224. if self.watchKeys[ key ]["allowPartial"]:
  225. rset.requests[ ind ].partialVersion = self.supportedPartialVersion
  226. # Block for new updates
  227. rset.blocking = True
  228. # Set ID for this requestset, so that we can amend it later without interrupting this send
  229. rset.requestSetId = str( uuid.uuid4() )
  230. self.currentWatchRequestSetId = rset.requestSetId
  231. # Threadsafe TODO: Release add/remove watch
  232. # This could block for a while, until new update comes
  233. repType, rep = sendRequest( self.watcher, ServicePacket.FETCH_STATE_REQUEST_SET, rset.SerializeToString() )
  234. # Threadsafe TODO: Lock add/remove watch, so we don't change current watch request in the middle of an add/remove operation
  235. # Now unset this, so other threads don't think we still have this request going
  236. self.currentWatchRequestSetId = None
  237. # Threadsafe TODO: Release add/remove watch
  238. # Threadsafe TODO: Release watching
  239. assert repType == ServicePacket.FETCH_STATE_RESPONSE_SET
  240. res = FetchStateResponseSet()
  241. res.ParseFromString( rep )
  242. # Apply all updates to cache
  243. responses = {}
  244. # Threadsafe TODO: Lock add/remove watch, only for TRULY threaded languages
  245. for response in res.responses:
  246. if self.cache.has_key( response.key ): # Sanity check... server should NOT reply with something that we didn't ask for
  247. self._updateCache( response )
  248. # Add to the map of responses, if we have multiple updates for a single key, the old structure will be overwritten
  249. if self.watchKeys.has_key( response.key ):
  250. responses[ response.key ] = copy.deepcopy( self.cache[ response.key ].value )
  251. else:
  252. # Threadsafe TODO: Release add/remove watch, only for TRULY threaded languages
  253. raise Exception("Server respoded with key that we didn't ask for")
  254. # Threadsafe TODO: Release add/remove watch, only for TRULY threaded languages
  255. return responses