Imported from dev1.link2tek.net CommEngine.git
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
16 KiB

  1. package altk.comm.engine.postback;
  2. import java.io.ByteArrayInputStream;
  3. import java.io.IOException;
  4. import java.io.UnsupportedEncodingException;
  5. import java.security.KeyManagementException;
  6. import java.security.KeyStoreException;
  7. import java.security.NoSuchAlgorithmException;
  8. import java.security.cert.X509Certificate;
  9. import java.util.ArrayList;
  10. import java.util.LinkedList;
  11. import java.util.List;
  12. import java.util.Queue;
  13. import javax.net.ssl.SSLContext;
  14. import javax.xml.parsers.DocumentBuilder;
  15. import javax.xml.parsers.DocumentBuilderFactory;
  16. import javax.xml.xpath.XPath;
  17. import javax.xml.xpath.XPathConstants;
  18. import javax.xml.xpath.XPathExpressionException;
  19. import javax.xml.xpath.XPathFactory;
  20. import org.apache.http.StatusLine;
  21. import org.apache.http.client.methods.CloseableHttpResponse;
  22. import org.apache.http.client.methods.HttpPost;
  23. import org.apache.http.conn.ssl.NoopHostnameVerifier;
  24. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
  25. import org.apache.http.conn.ssl.TrustStrategy;
  26. import org.apache.http.entity.StringEntity;
  27. import org.apache.http.impl.client.CloseableHttpClient;
  28. import org.apache.http.impl.client.HttpClientBuilder;
  29. import org.apache.http.protocol.BasicHttpContext;
  30. import org.apache.http.ssl.SSLContextBuilder;
  31. import org.apache.http.util.EntityUtils;
  32. import org.apache.log4j.Logger;
  33. import org.w3c.dom.Document;
  34. import org.w3c.dom.Node;
  35. import altk.comm.engine.CommonLogger;
  36. /**
  37. * Queues JobReports to be posted back to attribute postBackURL.
  38. * Multiple internal class Sender members consume this postQueue, sending items
  39. * in postQueue to postBackURL.
  40. *
  41. * In the future, if postBackURL has problem, or if
  42. * length of postQueue is more than a MAX_QUEUE_LENGTH, then it starts writing
  43. * everything to backingFile.
  44. *
  45. * @author Kwong
  46. *
  47. */
  48. public class PostBack
  49. {
  50. private static final String XML_VERSION_1_0_ENCODING_UTF_8 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
  51. private static final int QUEUE_WAIT = 300; // seconds
  52. private static final int POSTBACK_SERVER_WAIT_TIME = 10; // seconds
  53. private final String postBackURL;
  54. private final String xmlTopElement;
  55. private Queue<String> postQueue;
  56. private final int maxQueueSize;
  57. private List<Sender> senderPool;
  58. private final String myName;
  59. private int maxBatchSize;
  60. private int threadsWaitingToPost;
  61. private static Logger myLogger = Logger.getLogger(PostBack.class);
  62. public enum PostBackStatus
  63. {
  64. SUCCESS,
  65. SERVER_IO_ERROR,
  66. IRRECOVERABLE_ERROR,
  67. HTTP_STATUS_ERROR
  68. }
  69. class Sender extends Thread
  70. {
  71. private boolean threadShouldStop;
  72. private Sender(String name)
  73. {
  74. setName(name);
  75. start();
  76. }
  77. public void run()
  78. {
  79. threadShouldStop = false;
  80. myLogger.info(getName() + " started");
  81. String report;
  82. for (;;) // Each iteration sends a batch
  83. {
  84. if (threadShouldStop)
  85. {
  86. myLogger.info(getName() + " terminating");
  87. System.out.println(getName() + " terminating");
  88. return;
  89. }
  90. myLogger.debug("Looking for reports");
  91. List<String> reportList = null;
  92. synchronized(postQueue)
  93. {
  94. // Each iteration examines the queue for a batch to send
  95. for (;;)
  96. {
  97. reportList = new ArrayList<String>();
  98. for (int i = 0; i < maxBatchSize ; i++)
  99. {
  100. report = postQueue.poll();
  101. if (report == null) break;
  102. reportList.add(report);
  103. }
  104. if (reportList.size() > 0)
  105. {
  106. myLogger.debug(String.format("Extracted %d reports, reducing postQueue size: %d", reportList.size(), postQueue.size()));
  107. postQueue.notifyAll();
  108. break; // break out to do the work.
  109. }
  110. // Nothing to do, so wait a while, and look at the
  111. // queue again.
  112. try
  113. {
  114. myLogger.debug("Going to wait " + QUEUE_WAIT * 1000);
  115. postQueue.wait(QUEUE_WAIT * 1000);
  116. }
  117. catch (InterruptedException e)
  118. {
  119. CommonLogger.alarm.info("Postback queue interrupted while waiting: " + e);
  120. break;
  121. }
  122. CommonLogger.health.info("Surfacing from wait");
  123. System.out.println(getName() + " surfacing from wait");
  124. continue;
  125. }
  126. } // synchronized()
  127. if (reportList != null && reportList.size() > 0)
  128. {
  129. switch (post(reportList))
  130. {
  131. case IRRECOVERABLE_ERROR:
  132. case SUCCESS:
  133. break;
  134. case SERVER_IO_ERROR:
  135. /* Should not requeue report for this may lead to dead lock on this queu.
  136. // TODO: Limit retries, using rate limiting. Posting can be recovered using the activity log.
  137. // Re-queue these reports
  138. for (String rpt : reportList)
  139. {
  140. queueReport(rpt);
  141. }
  142. */
  143. // Sleep for a while before retrying this PostBack server.
  144. CommonLogger.alarm.warn("Caught server IO error. sleep for " + POSTBACK_SERVER_WAIT_TIME + " seconds");
  145. try
  146. {
  147. Thread.sleep(POSTBACK_SERVER_WAIT_TIME * 1000);
  148. }
  149. catch (InterruptedException e)
  150. {
  151. CommonLogger.alarm.warn("Caught while PostBack thread sleeps: " + e);
  152. }
  153. default:
  154. }
  155. }
  156. }
  157. }
  158. /**
  159. *
  160. * @param reportList
  161. * @return SUCCESS,
  162. * SERVER_IO_ERROR, when postback receiver has problem
  163. * IRRECOVERABLE_ERROR
  164. */
  165. private PostBackStatus post(List<String> reportList)
  166. {
  167. StringBuffer xml = new StringBuffer(XML_VERSION_1_0_ENCODING_UTF_8);
  168. xml.append("<"); xml.append(xmlTopElement); xml.append(">");
  169. for (String report : reportList)
  170. {
  171. xml.append(report + "\r\n");
  172. }
  173. xml.append("</"); xml.append(xmlTopElement); xml.append(">");
  174. TrustStrategy tustAllCerts = new TrustStrategy() { public boolean isTrusted(X509Certificate[] chain, String authType) { return true; } };
  175. HttpPost httpPost = new HttpPost(postBackURL);
  176. StringEntity requestEntity;
  177. CloseableHttpResponse response = null;
  178. byte[] xmlBytes = null;
  179. try
  180. {
  181. SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(tustAllCerts).build();
  182. SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
  183. CloseableHttpClient httpclient = HttpClientBuilder.create().setSSLSocketFactory(connectionFactory).build();
  184. requestEntity = (new StringEntity(xml.toString()));
  185. httpPost.setEntity(requestEntity);
  186. myLogger.debug("Posting to " + postBackURL + ": " + xml);
  187. response = httpclient.execute(httpPost, new BasicHttpContext());
  188. StatusLine statusLine = response.getStatusLine();
  189. int statusCode = statusLine.getStatusCode();
  190. if (statusCode != 200)
  191. {
  192. CommonLogger.alarm.error("Got error status code " + statusCode + " while posting status to broadcast requester");
  193. return PostBackStatus.HTTP_STATUS_ERROR;
  194. }
  195. }
  196. catch (UnsupportedEncodingException e)
  197. {
  198. CommonLogger.alarm.warn("While adding this application/xml content to PostBack: " + xml + " -- " + e);
  199. return PostBackStatus.IRRECOVERABLE_ERROR;
  200. }
  201. catch (IOException e)
  202. {
  203. CommonLogger.alarm.error("While posting back to broadcast requester: " + e);
  204. return PostBackStatus.IRRECOVERABLE_ERROR;
  205. }
  206. catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e)
  207. {
  208. CommonLogger.alarm.error("Totally unexpected. While constructing HttpClient for posting back to broadcast requester: " + e);
  209. return PostBackStatus.IRRECOVERABLE_ERROR;
  210. }
  211. String xmlStr;
  212. try
  213. {
  214. xmlBytes = EntityUtils.toByteArray(response.getEntity());
  215. xmlStr = new String(xmlBytes);
  216. myLogger.debug("Received resposne: " + xmlStr);
  217. }
  218. catch (IOException e)
  219. {
  220. CommonLogger.alarm.error("While getting response from posting to broadcast requester: " + e);
  221. return PostBackStatus.SERVER_IO_ERROR;
  222. }
  223. Document xmlDoc = null;
  224. try
  225. {
  226. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  227. DocumentBuilder builder = factory.newDocumentBuilder();
  228. xmlDoc = builder.parse(new ByteArrayInputStream(xmlBytes));
  229. }
  230. catch (Exception e)
  231. {
  232. CommonLogger.alarm.warn("xml parse problem on received response from " + postBackURL + ": " + xmlStr);
  233. return PostBackStatus.IRRECOVERABLE_ERROR;
  234. }
  235. if (!xmlDoc.getDocumentElement().getNodeName().startsWith(xmlTopElement))
  236. {
  237. CommonLogger.alarm.warn("xml response from " + postBackURL + " not a <" + xmlTopElement + "> response: " + xmlStr);
  238. return PostBackStatus.IRRECOVERABLE_ERROR;
  239. }
  240. XPath xpathEngine = XPathFactory.newInstance().newXPath();
  241. String xpath = null;
  242. try
  243. {
  244. xpath = "@error";
  245. Node errorNode = (Node)xpathEngine.evaluate(xpath, xmlDoc.getDocumentElement(), XPathConstants.NODE);
  246. if (errorNode != null)
  247. {
  248. String errorCode = errorNode.getNodeValue();
  249. xpath = "error_text";
  250. String errorText = (String)xpathEngine.evaluate(xpath,
  251. xmlDoc.getDocumentElement(), XPathConstants.STRING);
  252. CommonLogger.alarm.warn("Error response to <" + xmlTopElement + "> post back to "
  253. + postBackURL + " -- error code=\"" + errorCode + "\", error text = \""
  254. + errorText + "\"");
  255. return PostBackStatus.IRRECOVERABLE_ERROR;
  256. }
  257. }
  258. catch (XPathExpressionException e)
  259. {
  260. CommonLogger.alarm.warn("Bad xpath: " + xpath);
  261. return PostBackStatus.IRRECOVERABLE_ERROR;
  262. }
  263. catch (Exception e)
  264. {
  265. CommonLogger.alarm.warn("While decoding post back response from server: " + e);
  266. return PostBackStatus.IRRECOVERABLE_ERROR;
  267. }
  268. myLogger.debug("returned from posting");
  269. return PostBackStatus.SUCCESS;
  270. }
  271. public void terminate()
  272. {
  273. if (threadShouldStop) return;
  274. threadShouldStop = true;
  275. //Wait for at most 100 ms for thread to stop
  276. interrupt();
  277. }
  278. }
  279. /**
  280. * Constructs a pool of threads doing posting from a common job queue,
  281. * to the supplied postBackURL. The top element of the XML that gets
  282. * posted back has the give name.
  283. *
  284. * Requires these System properties:
  285. * postback_max_queue_size
  286. * postback_threadpool_size
  287. *
  288. * @param postBackURL
  289. * @param xmlTopElementName
  290. * @throws IllegalArgumentException if either postBackURL or xmlTopElementName is
  291. * not supplied nor valid.
  292. */
  293. public PostBack(String postBackURL, String xmlTopElementName,
  294. int maxQueueSize, int senderPoolSize, int maxBatchSize)
  295. throws IllegalArgumentException
  296. {
  297. if (postBackURL == null || postBackURL.length() == 0)
  298. {
  299. throw new IllegalArgumentException("PostBack class given null postBackURL");
  300. }
  301. myName = "Postback-" + postBackURL;
  302. if (xmlTopElementName == null || xmlTopElementName.length() == 0)
  303. {
  304. throw new IllegalArgumentException(myName + ": PostBack class given null xmlTopElement");
  305. }
  306. this.postBackURL = postBackURL;
  307. this.xmlTopElement = xmlTopElementName;
  308. this.maxQueueSize = maxQueueSize;
  309. this.maxBatchSize = maxBatchSize;
  310. postQueue = new LinkedList<String>();
  311. threadsWaitingToPost = 0;
  312. senderPool = new ArrayList<Sender>();
  313. for (int i = 0; i < senderPoolSize; i++)
  314. {
  315. Sender sender = new Sender(myName + '-' + i);
  316. senderPool.add(sender);
  317. }
  318. }
  319. /**
  320. * Queues report to postQueue only if the queue size has not reached the
  321. * maxQueueSize.
  322. * @param report
  323. * @return true if report is added to queue, false otherwise (queue full)
  324. */
  325. public boolean queueReport(String report)
  326. {
  327. // Log for recovery in case of problem in posting report.
  328. CommonLogger.activity.info("Attempting to queue report");
  329. synchronized(postQueue)
  330. {
  331. for (;;)
  332. {
  333. if (postQueue.size() < maxQueueSize)
  334. {
  335. myLogger.debug("Queing report" + report);
  336. postQueue.add(report);
  337. myLogger.debug("Added 1 report - postQueue size: " + postQueue.size());
  338. postQueue.notifyAll();
  339. return true;
  340. }
  341. else
  342. {
  343. myLogger.debug("Waiting for space - postQueue size: " + postQueue.size());
  344. try
  345. {
  346. threadsWaitingToPost++;
  347. myLogger.debug("Threads waiting to post: " + threadsWaitingToPost);
  348. postQueue.wait(QUEUE_WAIT * 1000);
  349. }
  350. catch (InterruptedException e)
  351. {
  352. break;
  353. }
  354. threadsWaitingToPost--;
  355. }
  356. }
  357. }
  358. myLogger.info("Interrupted while waiting for space to queue report");
  359. return false;
  360. }
  361. public void terminate()
  362. {
  363. for (Sender sender : senderPool)
  364. {
  365. sender.terminate();
  366. }
  367. }
  368. }