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

/plugin/src/main/java/com/atlassian/plugin/remotable/plugin/util/http/bigpipe/BigPipeImpl.java

https://bitbucket.org/morzechowski/remotable-plugins-jradev-16952
Java | 370 lines | 300 code | 43 blank | 27 comment | 18 complexity | 9e71d946e1b48aa60c4b60e064fa901c MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.atlassian.plugin.remotable.plugin.util.http.bigpipe;
  2. import com.atlassian.plugin.remotable.spi.http.bigpipe.BigPipe;
  3. import com.atlassian.plugin.remotable.spi.http.bigpipe.BigPipeContentHandler;
  4. import com.atlassian.plugin.remotable.spi.http.bigpipe.RequestIdAccessor;
  5. import com.atlassian.security.random.SecureRandomFactory;
  6. import com.atlassian.util.concurrent.CopyOnWriteMap;
  7. import com.atlassian.util.concurrent.ForwardingPromise;
  8. import com.atlassian.util.concurrent.Promise;
  9. import com.google.common.base.Function;
  10. import com.google.common.util.concurrent.FutureCallback;
  11. import org.json.JSONArray;
  12. import org.json.JSONException;
  13. import org.json.JSONObject;
  14. import org.slf4j.Logger;
  15. import org.slf4j.LoggerFactory;
  16. import org.springframework.stereotype.Component;
  17. import java.security.SecureRandom;
  18. import java.util.Collections;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.concurrent.TimeUnit;
  22. import static com.google.common.collect.Lists.newArrayList;
  23. import static com.google.common.collect.Sets.newHashSet;
  24. /**
  25. * Manages big pipe instances
  26. */
  27. @Component
  28. public final class BigPipeImpl implements BigPipe
  29. {
  30. private final Logger log = LoggerFactory.getLogger(this.getClass());
  31. private final SecureRandom secureRandom = SecureRandomFactory.newInstance();
  32. private final RequestIdAccessorImpl requestIdAccessor = new RequestIdAccessorImpl();
  33. // fixme: add big pipe cleaner thread to clean expired request sets
  34. private final Map<String, RequestContentSet> requestContentSets = CopyOnWriteMap.newHashMap();
  35. @Override
  36. public RequestIdAccessor getRequestIdAccessor()
  37. {
  38. return requestIdAccessor;
  39. }
  40. @Override
  41. public BigPipeContentHandler createContentHandler(String requestId, Promise<String> responsePromise)
  42. {
  43. // give each content its own unique id for replacing later
  44. String contentId = String.valueOf(secureRandom.nextLong());
  45. return createContentHandler(requestId, contentId, responsePromise);
  46. }
  47. @Override
  48. public BigPipeContentHandler createContentHandler(final String requestId, final String contentId, Promise<String> responsePromise)
  49. {
  50. final Promise<String> errorHandlingPromise = responsePromise.recover(new Function<Throwable, String>()
  51. {
  52. @Override
  53. public String apply(Throwable input)
  54. {
  55. log.debug("Error handling bigpipe request for id '" + requestId + "'", input);
  56. return "<span class=\"bp-error\">Error: " + input.getMessage() + "</span>";
  57. }
  58. });
  59. RequestContentSet requestContentSet = requestContentSets.get(requestId);
  60. if (requestContentSet == null)
  61. {
  62. requestContentSet = new RequestContentSet(requestId);
  63. requestContentSets.put(requestId, requestContentSet);
  64. }
  65. Promise<String> cleanedUpPromise = new CleanupPromise(errorHandlingPromise, requestContentSet, contentId);
  66. InternalHandler handler = new InternalHandler(contentId, requestContentSet, cleanedUpPromise);
  67. return requestContentSet.addHandler(handler);
  68. }
  69. @Override
  70. public Iterable<BigPipeContentHandler> waitForCompletedHandlers(String requestId)
  71. {
  72. RequestContentSet request = requestContentSets.get(requestId);
  73. if (request != null)
  74. {
  75. return request.waitForFinishedContent();
  76. }
  77. else
  78. {
  79. return Collections.emptyList();
  80. }
  81. }
  82. @Override
  83. public String convertContentHandlersToJson(Iterable<BigPipeContentHandler> handlers)
  84. {
  85. JSONArray result = new JSONArray();
  86. for (BigPipeContentHandler handler : handlers)
  87. {
  88. String html = handler.getContent().claim();
  89. JSONObject content = new JSONObject();
  90. try
  91. {
  92. content.put("id", handler.getContentId());
  93. content.put("html", html);
  94. }
  95. catch (JSONException e)
  96. {
  97. throw new IllegalArgumentException("Unable to serialize content", e);
  98. }
  99. result.put(content);
  100. }
  101. return result.toString();
  102. }
  103. public Iterable<BigPipeContentHandler> consumeCompletedHandlers(String requestId)
  104. {
  105. RequestContentSet request = requestContentSets.get(requestId);
  106. if (request != null)
  107. {
  108. return request.consumeFinishedContent();
  109. }
  110. else
  111. {
  112. return Collections.emptyList();
  113. }
  114. }
  115. private static class CleanupPromise extends ForwardingPromise<String>
  116. {
  117. private final Promise<String> promise;
  118. private final RequestContentSet requestContentSet;
  119. private final String contentId;
  120. public CleanupPromise(Promise<String> promise, RequestContentSet requestContentSet, String contentId)
  121. {
  122. this.promise = promise;
  123. this.requestContentSet = requestContentSet;
  124. this.contentId = contentId;
  125. }
  126. @Override
  127. protected Promise<String> delegate()
  128. {
  129. return promise;
  130. }
  131. @Override
  132. public String claim()
  133. {
  134. try
  135. {
  136. return super.claim();
  137. }
  138. finally
  139. {
  140. requestContentSet.removeContent(contentId);
  141. }
  142. }
  143. }
  144. /**
  145. * Manages individual bit of content
  146. */
  147. private class InternalHandler implements BigPipeContentHandler
  148. {
  149. private final String contentId;
  150. private final RequestContentSet request;
  151. private final Promise<String> responsePromise;
  152. public InternalHandler(String contentId, RequestContentSet requestContentSet, Promise<String> responsePromise)
  153. {
  154. this.contentId = contentId;
  155. this.request = requestContentSet;
  156. this.responsePromise = responsePromise.then(new FutureCallback<String>()
  157. {
  158. @Override
  159. public void onSuccess(String result)
  160. {
  161. request.notifyConsumers();
  162. }
  163. @Override
  164. public void onFailure(Throwable t)
  165. {
  166. request.notifyConsumers();
  167. }
  168. });
  169. }
  170. @Override
  171. public String getCurrentContent()
  172. {
  173. if (responsePromise.isDone())
  174. {
  175. return responsePromise.claim();
  176. }
  177. else
  178. {
  179. return "<span class=\"bp-" + contentId + " bp-loading\"></span>";
  180. }
  181. }
  182. @Override
  183. public String getContentId()
  184. {
  185. return contentId;
  186. }
  187. @Override
  188. public Promise<String> getContent()
  189. {
  190. return responsePromise;
  191. }
  192. public boolean isFinished()
  193. {
  194. return responsePromise.isDone();
  195. }
  196. }
  197. /**
  198. * Manages a set of content for a single page. Content for the page can be waited upon in a blocking call to be
  199. * woken up when new content is available.
  200. */
  201. private class RequestContentSet
  202. {
  203. private final Map<String, InternalHandler> handlers = CopyOnWriteMap.newHashMap();
  204. /**
  205. * The expiration of the page content in the case where the xhr call to retrieve the content is never made.
  206. */
  207. private final long expiry;
  208. private final String requestId;
  209. private final Object lock = new Object();
  210. public RequestContentSet(String requestId)
  211. {
  212. this.requestId = requestId;
  213. this.expiry = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30);
  214. }
  215. /**
  216. * Ensures only one handler per contentid
  217. *
  218. * @return The handler to actually use
  219. */
  220. public InternalHandler addHandler(InternalHandler handler)
  221. {
  222. if (!handlers.containsKey(handler.contentId))
  223. {
  224. handlers.put(handler.contentId, handler);
  225. return handler;
  226. }
  227. else
  228. {
  229. log.debug("Content id '{}' already assigned for request '{}'", handler.contentId, requestId);
  230. return handlers.get(handler.contentId);
  231. }
  232. }
  233. public void removeContent(String contentId)
  234. {
  235. handlers.remove(contentId);
  236. }
  237. public Iterable<BigPipeContentHandler> consumeFinishedContent()
  238. {
  239. List<BigPipeContentHandler> result = newArrayList();
  240. synchronized (lock)
  241. {
  242. if (handlers.isEmpty())
  243. {
  244. return Collections.emptyList();
  245. }
  246. removeAllFinishedHandlers(result);
  247. if (!hasMoreContent())
  248. {
  249. // we've expired
  250. log.info("All content has been consumed for request id {}", requestId);
  251. removeRequestContentSet();
  252. }
  253. }
  254. return result;
  255. }
  256. private void removeRequestContentSet()
  257. {
  258. handlers.clear();
  259. requestContentSets.remove(requestId);
  260. }
  261. private boolean hasMoreContent()
  262. {
  263. return !handlers.isEmpty();
  264. }
  265. /**
  266. * Waits for content to be finished and removes it from the set of outstanding content if done. It will only
  267. * wait until the timeout for the page has been reached.
  268. */
  269. public Iterable<BigPipeContentHandler> waitForFinishedContent()
  270. {
  271. List<BigPipeContentHandler> result = newArrayList();
  272. synchronized (lock)
  273. {
  274. if (handlers.isEmpty())
  275. {
  276. return Collections.emptyList();
  277. }
  278. removeAllFinishedHandlers(result);
  279. if (result.isEmpty() && !isExpired())
  280. {
  281. try
  282. {
  283. long timeout = expiry - System.currentTimeMillis();
  284. if (timeout > 0)
  285. {
  286. lock.wait(timeout);
  287. }
  288. removeAllFinishedHandlers(result);
  289. }
  290. catch (InterruptedException e)
  291. {
  292. // ignore
  293. }
  294. }
  295. }
  296. if (isExpired())
  297. {
  298. // we've expired
  299. log.info("Timeout waiting for {} jobs for request id {}", handlers.size(), requestId);
  300. removeRequestContentSet();
  301. }
  302. return result;
  303. }
  304. private void removeAllFinishedHandlers(List<BigPipeContentHandler> result)
  305. {
  306. for (InternalHandler handler : newHashSet(handlers.values()))
  307. {
  308. if (handler.isFinished())
  309. {
  310. removeContent(handler.contentId);
  311. result.add(handler);
  312. }
  313. }
  314. }
  315. public boolean isExpired()
  316. {
  317. return System.currentTimeMillis() >= expiry;
  318. }
  319. public void notifyConsumers()
  320. {
  321. synchronized (lock)
  322. {
  323. lock.notifyAll();
  324. }
  325. }
  326. }
  327. }