PageRenderTime 26ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/java-api/src/net/stateupdateservice/StateService.java

https://bitbucket.org/stateupdateservice/state-update-service
Java | 510 lines | 349 code | 84 blank | 77 comment | 72 complexity | 191310dbab4b5656a2e7f526c8dc669d MD5 | raw file
  1. package net.stateupdateservice;
  2. import net.stateupdateservice.StateUpdateService.*;
  3. import net.stateupdateservice.CacheEntry;
  4. import net.stateupdateservice.ServiceMessage.ServicePacket;
  5. import java.io.File;
  6. import java.io.FileInputStream;
  7. import java.io.FileOutputStream;
  8. import java.io.IOException;
  9. import java.io.ObjectInputStream;
  10. import java.io.ObjectOutputStream;
  11. import java.util.HashMap;
  12. import java.util.HashSet;
  13. import java.util.Map;
  14. import java.util.Set;
  15. import java.util.List;
  16. import java.util.LinkedList;
  17. import java.util.UUID;
  18. import com.google.protobuf.*;
  19. public class StateService {
  20. public int supportedPartialVersion = 2;
  21. public StateServiceServer service;
  22. public Map<String, WatchKey> watchKeys;
  23. private Map<String, CacheEntry> cache;
  24. private String currentWatchRequestSetId;
  25. private UpdateStateRequestSet.Builder currentUpdateStructure = null;
  26. private Set<String> sentDescriptorToServer = null;
  27. private String cacheFilePath = null;
  28. public StateService( StateServiceServer server, String cacheFile ) {
  29. this.service = server;
  30. this.cacheFilePath = cacheFile;
  31. watchKeys = new HashMap<String, WatchKey>();
  32. sentDescriptorToServer = new HashSet<String>();
  33. boolean loadedCache = false;
  34. if( cacheFile != null ) {
  35. File fhandle = new File( cacheFile );
  36. if( fhandle.exists() ) {
  37. try {
  38. FileInputStream fis = new FileInputStream( cacheFile );
  39. ObjectInputStream ois = new ObjectInputStream(fis);
  40. int cacheVersion = ois.readInt();
  41. if( cacheVersion == 1 ) {
  42. cache = (HashMap<String, CacheEntry>) ois.readObject();
  43. ByteString currentUpdateStructureString = (ByteString) ois.readObject();
  44. if( currentUpdateStructureString != null ) {
  45. currentUpdateStructure = UpdateStateRequestSet.newBuilder();
  46. currentUpdateStructure.mergeFrom(currentUpdateStructureString);
  47. }
  48. ois.close();
  49. loadedCache = true;
  50. }
  51. } catch ( IOException e) {
  52. } catch ( ClassNotFoundException e ) {
  53. }
  54. }
  55. }
  56. if(!loadedCache) {
  57. cache = new HashMap<String, CacheEntry>();
  58. }
  59. }
  60. public void flushCacheToDisk( ) {
  61. if( cacheFilePath != null ) {
  62. try {
  63. FileOutputStream fos = new FileOutputStream( cacheFilePath );
  64. ObjectOutputStream oos = new ObjectOutputStream(fos);
  65. oos.writeInt( 1 ); // Cache version 1
  66. oos.writeObject( cache );
  67. if( currentUpdateStructure != null ) {
  68. oos.writeObject( currentUpdateStructure.build().toByteString() );
  69. } else {
  70. oos.writeObject( null );
  71. }
  72. oos.close();
  73. } catch (IOException e) {
  74. }
  75. }
  76. }
  77. private class WatchKey {
  78. @SuppressWarnings("unused")
  79. public String key;
  80. public boolean allowPartial;
  81. public WatchKey( String key, boolean allowPartial ) {
  82. this.key = key;
  83. this.allowPartial = allowPartial;
  84. }
  85. }
  86. public class BadServiceResponseException extends IOException {
  87. /**
  88. *
  89. */
  90. private static final long serialVersionUID = 1L;
  91. public BadServiceResponseException( String message ) {
  92. super( message );
  93. }
  94. }
  95. public StateService initKey( String key, Message.Builder valueObject ) {
  96. if( cache.containsKey(key) ) {
  97. // Make sure value is completely loaded
  98. cache.get(key).initValue(valueObject);
  99. } else {
  100. // Init specified keys to blank so we can operate on it
  101. Message.Builder obj = valueObject.clone();
  102. obj.clear();
  103. // Default to a generic not-valid entry. This needs to be updated
  104. cache.put( key, new CacheEntry( key, obj, "", 0 ) );
  105. }
  106. return this;
  107. }
  108. public StateService updateKey( String key, Message.Builder msg ) {
  109. // This queues an update into memory (and flushes to cache)
  110. if( currentUpdateStructure == null ) {
  111. currentUpdateStructure = UpdateStateRequestSet.newBuilder();
  112. }
  113. // Begin building update object
  114. UpdateStateRequestSet.UpdateStateRequest.Builder updateObj =
  115. UpdateStateRequestSet.UpdateStateRequest.newBuilder()
  116. .setKey( key )
  117. .setValue( msg.build().toByteString() );
  118. // Add new update entry to structure
  119. if( !sentDescriptorToServer.contains( key ) ) {
  120. // Set the protobuf descriptor so the server can cache it
  121. updateObj.setProtobufDescriptor( msg.getDescriptorForType().toProto() );
  122. // Mark this so we don't need to send the descriptor every time
  123. sentDescriptorToServer.add( key );
  124. }
  125. currentUpdateStructure.addUpdates( updateObj );
  126. flushCacheToDisk();
  127. return this;
  128. }
  129. public StateService partialUpdateKey( String key, Message.Builder msg ) {
  130. if( currentUpdateStructure == null ) {
  131. currentUpdateStructure = UpdateStateRequestSet.newBuilder();
  132. }
  133. // Build update object
  134. currentUpdateStructure.addUpdates( UpdateStateRequestSet.UpdateStateRequest.newBuilder()
  135. .setKey( key )
  136. .setPartialUpdate(
  137. PartialUpdate.newBuilder()
  138. .setType( PartialUpdate.PartialUpdateType.PROTOBUF_MERGE )
  139. .setAppendBytes( msg.buildPartial().toByteString() )
  140. )
  141. );
  142. return this;
  143. }
  144. public HashMap<String, Boolean> flushUpdates( ) throws BadServiceResponseException, IOException {
  145. // Flush any state updates in queue to server (maybe loaded from cache, if we've crashed)
  146. // returns map of what updates failed, empty map otherwise
  147. // Check whether there is anything to flush
  148. if( currentUpdateStructure == null ) {
  149. return null;
  150. }
  151. // Build packet
  152. ServicePacket request = ServicePacket.newBuilder()
  153. .setPacketType( ServicePacket.ServicePacketType.UPDATE_STATE_REQUEST_SET)
  154. .setPacketData( currentUpdateStructure.build().toByteString() )
  155. .build();
  156. // Run request and get response
  157. ServicePacket response = service.sendRequest( request );
  158. // Make sure proper type was returned
  159. if( response.getPacketType() != ServicePacket.ServicePacketType.UPDATE_STATE_RESPONSE_SET ) {
  160. throw new BadServiceResponseException("Expected UPDATE_STATE_RESPONSE_SET packet");
  161. }
  162. // Unpack response
  163. UpdateStateResponseSet.Builder responseSet = UpdateStateResponseSet.newBuilder();
  164. try {
  165. responseSet.mergeFrom( response.getPacketData() );
  166. } catch( InvalidProtocolBufferException e ) {
  167. throw new BadServiceResponseException("Received invalid UPDATE_STATE_RESPONSE_SET packet: " + e.toString() );
  168. }
  169. // Build a map, detailing what failed, if anything
  170. HashMap<String, Boolean> statusInfoMap = new HashMap<String, Boolean>();
  171. for( UpdateStateResponseSet.UpdateStateResponse responseObj: responseSet.getResponsesList() ) {
  172. if( !responseObj.getSuccess() ) {
  173. statusInfoMap.put( responseObj.getKey(), false );
  174. }
  175. }
  176. // If nothing failed, reset structure
  177. if( statusInfoMap.isEmpty() ) {
  178. currentUpdateStructure = null;
  179. }
  180. flushCacheToDisk();
  181. return statusInfoMap; // If this map has nothing in it, all updates were successful
  182. }
  183. private void updateCache( FetchStateResponseSet.FetchStateResponse responseObj ) throws BadServiceResponseException {
  184. if( responseObj.hasValue() ) {
  185. // Replace current value with new value
  186. try {
  187. cache.get( responseObj.getKey() ).setValueBytes( responseObj.getValue() );
  188. } catch( InvalidProtocolBufferException e ) {
  189. throw new BadServiceResponseException("Received invalid FETCH_STATE_RESPONSE_SET value field: " + e.toString() );
  190. }
  191. } else if( responseObj.hasPartialUpdate() ) {
  192. if( responseObj.getPartialUpdate().getType() == PartialUpdate.PartialUpdateType.PROTOBUF_MERGE ) {
  193. // Simple merge update
  194. try {
  195. cache.get( responseObj.getKey() ).mergeValueBytes( responseObj.getPartialUpdate().getAppendBytes() );
  196. } catch( InvalidProtocolBufferException e ) {
  197. throw new BadServiceResponseException("Received invalid FETCH_STATE_RESPONSE_SET partial update: " + e.toString() );
  198. }
  199. }
  200. }
  201. cache.get( responseObj.getKey() ).revision = responseObj.getRevision();
  202. cache.get( responseObj.getKey() ).session = responseObj.getSession();
  203. }
  204. public List<Message> getCachedKeys( String[] keys ) throws BadServiceResponseException, IOException {
  205. return getKeys( keys, true, true);
  206. }
  207. public List<Message> getKeys( String[] keys, boolean allowPartial ) throws BadServiceResponseException, IOException {
  208. return getKeys( keys, false, allowPartial);
  209. }
  210. private synchronized List<Message> getKeys( String[] keys, boolean cacheOnly, boolean allowPartial ) throws BadServiceResponseException, IOException {
  211. for( String key : keys ) {
  212. if( !cache.containsKey( key ) ) { // Make sure this key has been initialized, otherwise, raise error
  213. throw new IllegalArgumentException("Key must be initialized before being passed to getKeys");
  214. }
  215. }
  216. // Update keys unless we only want to go to cache
  217. if( !cacheOnly ) {
  218. // Return list of values corresponding to the specified keys
  219. FetchStateRequestSet.Builder rset = FetchStateRequestSet.newBuilder();
  220. for( String key : keys ) {
  221. FetchStateRequestSet.FetchStateRequest.Builder req = FetchStateRequestSet.FetchStateRequest.newBuilder()
  222. .setKey( key );
  223. if( cache.get( key ).revision > 0 ) {
  224. req.setSession( cache.get( key ).session );
  225. req.setRevision( cache.get( key ).revision );
  226. if( allowPartial ) {
  227. req.setPartialVersion( supportedPartialVersion );
  228. }
  229. }
  230. rset.addRequests( req.build() );
  231. }
  232. // No blocking for new updates, let's just get any new updates to keys
  233. rset.setBlocking( false );
  234. // Build packet
  235. ServicePacket request = ServicePacket.newBuilder()
  236. .setPacketType( ServicePacket.ServicePacketType.FETCH_STATE_REQUEST_SET)
  237. .setPacketData( rset.build().toByteString() )
  238. .build();
  239. // Run request and get response
  240. ServicePacket response;
  241. response = service.sendRequest( request );
  242. // Make sure proper type was returned
  243. if( response.getPacketType() != ServicePacket.ServicePacketType.FETCH_STATE_RESPONSE_SET ) {
  244. throw new BadServiceResponseException("Expected FETCH_STATE_RESPONSE_SET packet");
  245. }
  246. // Unpack response
  247. FetchStateResponseSet.Builder responseSet = FetchStateResponseSet.newBuilder();
  248. try {
  249. responseSet.mergeFrom( response.getPacketData() );
  250. } catch( InvalidProtocolBufferException e ) {
  251. throw new BadServiceResponseException("Received invalid FETCH_STATE_RESPONSE_SET packet: " + e.toString() );
  252. }
  253. // Apply all updates to cache, if it's not here, either it didn't need any updates or was not found
  254. for( FetchStateResponseSet.FetchStateResponse responseObj: responseSet.getResponsesList() ) {
  255. if( cache.containsKey( responseObj.getKey() ) ) { // Sanity check... server should NOT reply with something that we didn't ask for
  256. updateCache( responseObj );
  257. } else {
  258. throw new BadServiceResponseException("Server respoded with key that we didn't ask for");
  259. }
  260. }
  261. }
  262. // Now build responses
  263. List<Message> returnResponses = new LinkedList<Message>();
  264. for( String key : keys ) {
  265. if( cache.containsKey( key ) && cache.get( key ).revision > 0 ) { // Make sure this key has been initialized, otherwise, raise error
  266. returnResponses.add( cache.get( key ).getValue().build() );
  267. } else {
  268. // Key not found
  269. returnResponses.add( null );
  270. }
  271. }
  272. flushCacheToDisk();
  273. return returnResponses;
  274. }
  275. public StateService watchKey( String key, boolean allowPartial ) throws BadServiceResponseException, IOException {
  276. if( !cache.containsKey( key ) ) { // Make sure this key has been initialized, otherwise, raise error
  277. throw new IllegalArgumentException("Key must be initialized before being passed to watchKey");
  278. }
  279. // Add key to watch list
  280. // Threadsafe TODO: Wait for add/remove lock, aquire it when possible
  281. if( !watchKeys.containsKey( key ) ) {
  282. watchKeys.put( key, new WatchKey( key, allowPartial ) );
  283. }
  284. // Update the server with the news, if necessary
  285. if( currentWatchRequestSetId != null ) {
  286. FetchStateRequestSet.Builder rset = FetchStateRequestSet.newBuilder();
  287. // Add a single item to structure
  288. FetchStateRequestSet.FetchStateRequest.Builder req = FetchStateRequestSet.FetchStateRequest.newBuilder()
  289. .setKey( key );
  290. if( cache.get( key ).revision > 0 ) {
  291. req.setSession( cache.get( key ).session );
  292. req.setRevision( cache.get( key ).revision );
  293. if( allowPartial ) {
  294. req.setPartialVersion( supportedPartialVersion );
  295. }
  296. }
  297. rset.addRequests( req.build() );
  298. // Tell server we are ADDING this key to earlier request
  299. rset.setRequestSetModify( FetchStateRequestSet.RequestSetOperation.REQUEST_SET_ADD );
  300. // Don't block for new updates
  301. rset.setBlocking( false );
  302. rset.setRequestSetId( currentWatchRequestSetId );
  303. // Threadsafe TODO: Release add/remove lock
  304. // Update to server
  305. // Build packet
  306. ServicePacket request = ServicePacket.newBuilder()
  307. .setPacketType( ServicePacket.ServicePacketType.FETCH_STATE_REQUEST_SET)
  308. .setPacketData( rset.build().toByteString() )
  309. .build();
  310. // Run request and get response
  311. ServicePacket response = service.sendRequest( request );
  312. // Make sure proper type was returned
  313. if( response.getPacketType() != ServicePacket.ServicePacketType.FETCH_STATE_RESPONSE_SET ) {
  314. throw new BadServiceResponseException("Expected FETCH_STATE_RESPONSE_SET packet");
  315. }
  316. } else {
  317. // Threadsafe TODO: Release add/remove lock
  318. }
  319. return this;
  320. }
  321. public StateService unWatchKey( String key ) throws BadServiceResponseException, IOException {
  322. if( !cache.containsKey( key ) ) { // Make sure this key has been initialized, otherwise, raise error
  323. throw new IllegalArgumentException("Key must be initialized before being passed to unWatchKey");
  324. }
  325. // Remove key to watch list
  326. // Threadsafe TODO: Wait for add/remove lock, aquire it when possible
  327. if( watchKeys.containsKey( key ) ) {
  328. watchKeys.remove( key );
  329. }
  330. // Update the server with the news, if necessary
  331. if( currentWatchRequestSetId != null ) {
  332. FetchStateRequestSet.Builder rset = FetchStateRequestSet.newBuilder();
  333. // Add a single item to structure
  334. FetchStateRequestSet.FetchStateRequest.Builder req = FetchStateRequestSet.FetchStateRequest.newBuilder()
  335. .setKey( key );
  336. rset.addRequests( req.build() );
  337. // Tell server we are REMOVING this key to earlier request
  338. rset.setRequestSetModify( FetchStateRequestSet.RequestSetOperation.REQUEST_SET_REMOVE );
  339. // Don't block for new updates
  340. rset.setBlocking( false );
  341. rset.setRequestSetId( currentWatchRequestSetId );
  342. // Threadsafe TODO: Release add/remove lock
  343. // Update to server
  344. // Build packet
  345. ServicePacket request = ServicePacket.newBuilder()
  346. .setPacketType( ServicePacket.ServicePacketType.FETCH_STATE_REQUEST_SET)
  347. .setPacketData( rset.build().toByteString() )
  348. .build();
  349. // Run request and get response
  350. ServicePacket response = service.sendRequest( request );
  351. // Make sure proper type was returned
  352. if( response.getPacketType() != ServicePacket.ServicePacketType.FETCH_STATE_RESPONSE_SET ) {
  353. throw new BadServiceResponseException("Expected FETCH_STATE_RESPONSE_SET packet");
  354. }
  355. } else {
  356. // Threadsafe TODO: Release add/remove lock
  357. }
  358. return this;
  359. }
  360. public synchronized Map<String,Message> doWatch( ) throws BadServiceResponseException, IOException {
  361. // Block for updates on watched keys, then return map of updated keys that were being watched and changed
  362. // Threadsafe TODO: Lock watching, fail if we are already watching
  363. // Threadsafe TODO: Lock add/remove watch, so we don't allow other threads to add/remove before it is safe
  364. FetchStateRequestSet.Builder rset = FetchStateRequestSet.newBuilder();
  365. for( String key : watchKeys.keySet() ) {
  366. FetchStateRequestSet.FetchStateRequest.Builder req = FetchStateRequestSet.FetchStateRequest.newBuilder()
  367. .setKey( key );
  368. if( cache.get( key ).revision > 0 ) {
  369. req.setSession( cache.get( key ).session );
  370. req.setRevision( cache.get( key ).revision );
  371. if( watchKeys.get( key ).allowPartial ) {
  372. req.setPartialVersion( supportedPartialVersion );
  373. }
  374. }
  375. rset.addRequests( req.build() );
  376. }
  377. // Block for new updates
  378. rset.setBlocking( true );
  379. // Set ID for this requestset, so that we can amend it later without interrupting this send
  380. currentWatchRequestSetId = UUID.randomUUID().toString();
  381. rset.setRequestSetId( currentWatchRequestSetId );
  382. // Build packet
  383. ServicePacket request = ServicePacket.newBuilder()
  384. .setPacketType( ServicePacket.ServicePacketType.FETCH_STATE_REQUEST_SET)
  385. .setPacketData( rset.build().toByteString() )
  386. .build();
  387. // Threadsafe TODO: Release add/remove watch
  388. // This could block for a while, until new update comes
  389. // Run request and get response
  390. ServicePacket response = service.sendRequest( request );
  391. // Threadsafe TODO: Lock add/remove watch, so we don't change current watch request in the middle of an add/remove operation
  392. // Now unset this, so other threads don't think we still have this request going
  393. currentWatchRequestSetId = null;
  394. // Threadsafe TODO: Release add/remove watch
  395. // Threadsafe TODO: Release watching
  396. // Make sure proper type was returned
  397. if( response.getPacketType() != ServicePacket.ServicePacketType.FETCH_STATE_RESPONSE_SET ) {
  398. throw new BadServiceResponseException("Expected FETCH_STATE_RESPONSE_SET packet");
  399. }
  400. // Unpack response
  401. FetchStateResponseSet.Builder responseSet = FetchStateResponseSet.newBuilder();
  402. try {
  403. responseSet.mergeFrom( response.getPacketData() );
  404. } catch( InvalidProtocolBufferException e ) {
  405. throw new BadServiceResponseException("Received invalid FETCH_STATE_RESPONSE_SET packet: " + e.toString() );
  406. }
  407. // Apply all updates to cache
  408. // Threadsafe TODO: Lock add/remove watch, only for TRULY threaded languages
  409. Map<String, Message> returnResponses = new HashMap<String, Message>();
  410. for( FetchStateResponseSet.FetchStateResponse responseObj: responseSet.getResponsesList() ) {
  411. if( cache.containsKey( responseObj.getKey() ) ) { // Sanity check... server should NOT reply with something that we didn't ask for
  412. updateCache( responseObj );
  413. // Add to the map of responses, if we have multiple updates for a single key, the old structure will be overwritten
  414. if( watchKeys.containsKey( responseObj.getKey() ) ) {
  415. returnResponses.put( responseObj.getKey(), cache.get( responseObj.getKey() ).getValue().build() );
  416. }
  417. } else {
  418. // Threadsafe TODO: Release add/remove watch, only for TRULY threaded languages
  419. throw new BadServiceResponseException("Server respoded with key that we didn't ask for");
  420. }
  421. }
  422. // Threadsafe TODO: Release add/remove watch, only for TRULY threaded languages
  423. flushCacheToDisk();
  424. return returnResponses;
  425. }
  426. }