Imported from dev1.link2tek.net CommEngine.git
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

469 行
18 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.util.ArrayList;
  6. import java.util.LinkedList;
  7. import java.util.List;
  8. import java.util.Queue;
  9. import javax.xml.parsers.DocumentBuilder;
  10. import javax.xml.parsers.DocumentBuilderFactory;
  11. import javax.xml.xpath.XPath;
  12. import javax.xml.xpath.XPathConstants;
  13. import javax.xml.xpath.XPathExpressionException;
  14. import javax.xml.xpath.XPathFactory;
  15. import org.apache.commons.httpclient.ConnectTimeoutException;
  16. import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
  17. import org.apache.commons.httpclient.Header;
  18. import org.apache.commons.httpclient.HttpClient;
  19. import org.apache.commons.httpclient.HttpURL;
  20. import org.apache.commons.httpclient.methods.PostMethod;
  21. import org.apache.commons.httpclient.methods.StringRequestEntity;
  22. import org.apache.commons.httpclient.params.HttpMethodParams;
  23. import org.apache.log4j.Logger;
  24. import org.w3c.dom.Document;
  25. import org.w3c.dom.Node;
  26. import altk.comm.engine.CommonLogger;
  27. /**
  28. * Queues JobReports to be posted back to attribute postBackURL.
  29. * Multiple internal class Sender members consume this postQueue, sending items
  30. * in postQueue to postBackURL.
  31. *
  32. * In the future, if postBackURL has problem, or if
  33. * length of postQueue is more than a MAX_QUEUE_LENGTH, then it starts writing
  34. * everything to backingFile.
  35. *
  36. * @author Kwong
  37. *
  38. */
  39. public class PostBack
  40. {
  41. private static final String XML_VERSION_1_0_ENCODING_UTF_8 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
  42. private static final int QUEUE_WAIT = 300; // seconds
  43. private static final int POSTBACK_SERVER_WAIT_TIME = 10; // seconds
  44. private final String postBackURL;
  45. private final String xmlTopElement;
  46. private Queue<String> postQueue;
  47. private final int maxQueueSize;
  48. private List<Sender> senderPool;
  49. private final String myName;
  50. private int maxBatchSize;
  51. private int threadsWaitingToPost;
  52. private static Logger myLogger = Logger.getLogger(PostBack.class);
  53. public enum PostBackStatus
  54. {
  55. SUCCESS,
  56. SERVER_IO_ERROR,
  57. IRRECOVERABLE_ERROR,
  58. HTTP_STATUS_ERROR
  59. }
  60. class Sender extends Thread
  61. {
  62. private boolean threadShouldStop;
  63. private Sender(String name)
  64. {
  65. setName(name);
  66. start();
  67. }
  68. public void run()
  69. {
  70. threadShouldStop = false;
  71. myLogger.info(getName() + " started");
  72. String report;
  73. for (;;) // Each iteration sends a batch
  74. {
  75. if (threadShouldStop)
  76. {
  77. myLogger.info(getName() + " terminating");
  78. System.out.println(getName() + " terminating");
  79. return;
  80. }
  81. myLogger.debug("Looking for reports");
  82. List<String> reportList = null;
  83. synchronized(postQueue)
  84. {
  85. // Each iteration examines the queue for a batch to send
  86. for (;;)
  87. {
  88. reportList = new ArrayList<String>();
  89. for (int i = 0; i < maxBatchSize ; i++)
  90. {
  91. report = postQueue.poll();
  92. if (report == null) break;
  93. reportList.add(report);
  94. }
  95. if (reportList.size() > 0)
  96. {
  97. myLogger.debug(String.format("Extracted %d reports, reducing postQueue size: %d", reportList.size(), postQueue.size()));
  98. postQueue.notifyAll();
  99. break; // break out to do the work.
  100. }
  101. // Nothing to do, so wait a while, and look at the
  102. // queue again.
  103. try
  104. {
  105. myLogger.debug("Going to wait " + QUEUE_WAIT * 1000);
  106. postQueue.wait(QUEUE_WAIT * 1000);
  107. }
  108. catch (InterruptedException e)
  109. {
  110. CommonLogger.alarm.info("Postback queue interrupted while waiting: " + e);
  111. break;
  112. }
  113. CommonLogger.health.info("Surfacing from wait");
  114. System.out.println(getName() + " surfacing from wait");
  115. continue;
  116. }
  117. } // synchronized()
  118. if (reportList != null && reportList.size() > 0)
  119. {
  120. switch (post(reportList))
  121. {
  122. case IRRECOVERABLE_ERROR:
  123. case SUCCESS:
  124. break;
  125. case SERVER_IO_ERROR:
  126. /* Should not requeue report for this may lead to dead lock on this queu.
  127. // TODO: Limit retries, using rate limiting. Posting can be recovered using the activity log.
  128. // Re-queue these reports
  129. for (String rpt : reportList)
  130. {
  131. queueReport(rpt);
  132. }
  133. */
  134. // Sleep for a while before retrying this PostBack server.
  135. CommonLogger.alarm.warn("Caught server IO error. sleep for " + POSTBACK_SERVER_WAIT_TIME + " seconds");
  136. try
  137. {
  138. Thread.sleep(POSTBACK_SERVER_WAIT_TIME * 1000);
  139. }
  140. catch (InterruptedException e)
  141. {
  142. CommonLogger.alarm.warn("Caught while PostBack thread sleeps: " + e);
  143. }
  144. default:
  145. }
  146. }
  147. }
  148. }
  149. /**
  150. *
  151. * @param reportList
  152. * @return SUCCESS,
  153. * SERVER_IO_ERROR, when postback receiver has problem
  154. * IRRECOVERABLE_ERROR
  155. */
  156. private PostBackStatus post(List<String> reportList)
  157. {
  158. StringBuffer xml = new StringBuffer(XML_VERSION_1_0_ENCODING_UTF_8);
  159. xml.append("<"); xml.append(xmlTopElement); xml.append(">");
  160. for (String report : reportList)
  161. {
  162. xml.append(report + "\r\n");
  163. }
  164. xml.append("</"); xml.append(xmlTopElement); xml.append(">");
  165. PostMethod post = new PostMethod();
  166. String responseBody = null;
  167. StringRequestEntity requestEntity = null;
  168. try
  169. {
  170. requestEntity = new StringRequestEntity(xml.toString(), "application/xml", "utf-8");
  171. }
  172. catch (UnsupportedEncodingException e)
  173. {
  174. CommonLogger.alarm.warn("While adding this application/xml content to PostBack: " + xml + " -- " + e);
  175. return PostBackStatus.IRRECOVERABLE_ERROR;
  176. }
  177. post.setRequestEntity(requestEntity);
  178. post.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
  179. new DefaultHttpMethodRetryHandler(3, false));
  180. /*
  181. CommonLogger.activity.debug("Before calling setFolloweRedirects()");
  182. post.setFollowRedirects(false);
  183. CommonLogger.activity.debug("After calling setFolloweRedirects()");
  184. */
  185. HttpClient client = new HttpClient();
  186. String url = postBackURL;
  187. for (int redirectCount = 0; redirectCount < 3; redirectCount++)
  188. {
  189. try
  190. {
  191. client.getHttpConnectionManager().getParams().setConnectionTimeout(5 * 1000);
  192. post.setURI(new HttpURL(url));
  193. if (redirectCount == 0)
  194. {
  195. CommonLogger.activity.info("posting " + xml.toString() + " to " + url);
  196. }
  197. else
  198. {
  199. CommonLogger.activity.info("redirected to " + url);
  200. }
  201. long beginPost = System.currentTimeMillis();
  202. int statusCode = client.executeMethod(post);
  203. long postingTime = System.currentTimeMillis() - beginPost; // msec
  204. if (statusCode == 302)
  205. {
  206. Header locationHeader = post.getResponseHeader("Location");
  207. if (locationHeader != null)
  208. {
  209. url = locationHeader.getValue();
  210. post.releaseConnection();
  211. continue;
  212. }
  213. else
  214. {
  215. CommonLogger.alarm.warn("When posting to \"" + url + "\": " + " received status 302 but without location header");
  216. return PostBackStatus.IRRECOVERABLE_ERROR;
  217. }
  218. }
  219. else if (statusCode != 200)
  220. {
  221. CommonLogger.alarm.warn("Received problem status code " + statusCode + " from posting to \"" + url + "\": " + xml);
  222. return PostBackStatus.HTTP_STATUS_ERROR;
  223. }
  224. responseBody = post.getResponseBodyAsString().trim();
  225. post.releaseConnection();
  226. myLogger.debug("Postback time (msec): " + postingTime);
  227. CommonLogger.activity.info("Received response: " + (responseBody.length() == 0? "[empty]" : responseBody));
  228. if (responseBody.trim().length() == 0) return PostBackStatus.SUCCESS;
  229. break;
  230. }
  231. catch (ConnectTimeoutException e)
  232. {
  233. CommonLogger.alarm.warn("IO problem while posting to \"" + url + "\": " + xml + " -- " + e.getMessage());
  234. return PostBackStatus.SERVER_IO_ERROR;
  235. }
  236. catch (IOException e)
  237. {
  238. CommonLogger.alarm.warn("IO problem while posting to \"" + url + "\": " + xml + " -- " + e.getMessage());
  239. return PostBackStatus.SERVER_IO_ERROR;
  240. }
  241. catch (IllegalArgumentException e)
  242. {
  243. CommonLogger.alarm.warn("When posting to \"" + url + "\": " + e.getMessage());
  244. return PostBackStatus.IRRECOVERABLE_ERROR;
  245. }
  246. }
  247. if (responseBody == null)
  248. {
  249. CommonLogger.alarm.warn("When posting to \"" + url + "\": " + " Exhausted allowable redirects");
  250. return PostBackStatus.IRRECOVERABLE_ERROR;
  251. }
  252. // parse into xml doc
  253. Document xmlDoc = null;
  254. try
  255. {
  256. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  257. DocumentBuilder builder = factory.newDocumentBuilder();
  258. xmlDoc = builder.parse(new ByteArrayInputStream(responseBody.getBytes()));
  259. }
  260. catch (Exception e)
  261. {
  262. CommonLogger.alarm.warn("xml parse problem on received response from " + postBackURL + ": " + responseBody);
  263. return PostBackStatus.IRRECOVERABLE_ERROR;
  264. }
  265. if (!xmlDoc.getDocumentElement().getNodeName().startsWith(xmlTopElement))
  266. {
  267. CommonLogger.alarm.warn("xml response from " + postBackURL + " not a <" + xmlTopElement + "> response: " + responseBody);
  268. return PostBackStatus.IRRECOVERABLE_ERROR;
  269. }
  270. XPath xpathEngine = XPathFactory.newInstance().newXPath();
  271. String xpath = null;
  272. try
  273. {
  274. xpath = "@error";
  275. Node errorNode = (Node)xpathEngine.evaluate(xpath, xmlDoc.getDocumentElement(), XPathConstants.NODE);
  276. if (errorNode != null)
  277. {
  278. String errorCode = errorNode.getNodeValue();
  279. xpath = "error_text";
  280. String errorText = (String)xpathEngine.evaluate(xpath,
  281. xmlDoc.getDocumentElement(), XPathConstants.STRING);
  282. CommonLogger.alarm.warn("Error response to <" + xmlTopElement + "> post back to "
  283. + postBackURL + " -- error code=\"" + errorCode + "\", error text = \""
  284. + errorText + "\"");
  285. return PostBackStatus.IRRECOVERABLE_ERROR;
  286. }
  287. }
  288. catch (XPathExpressionException e)
  289. {
  290. CommonLogger.alarm.warn("Bad xpath: " + xpath);
  291. return PostBackStatus.IRRECOVERABLE_ERROR;
  292. }
  293. catch (Exception e)
  294. {
  295. CommonLogger.alarm.warn("While decoding post back response from server: " + e);
  296. return PostBackStatus.IRRECOVERABLE_ERROR;
  297. }
  298. myLogger.debug("returned from posting");
  299. return PostBackStatus.SUCCESS;
  300. }
  301. public void terminate()
  302. {
  303. if (threadShouldStop) return;
  304. threadShouldStop = true;
  305. //Wait for at most 100 ms for thread to stop
  306. interrupt();
  307. }
  308. }
  309. /**
  310. * Constructs a pool of threads doing posting from a common job queue,
  311. * to the supplied postBackURL. The top element of the XML that gets
  312. * posted back has the give name.
  313. *
  314. * Requires these System properties:
  315. * postback_max_queue_size
  316. * postback_threadpool_size
  317. *
  318. * @param postBackURL
  319. * @param xmlTopElementName
  320. * @throws IllegalArgumentException if either postBackURL or xmlTopElementName is
  321. * not supplied nor valid.
  322. */
  323. public PostBack(String postBackURL, String xmlTopElementName,
  324. int maxQueueSize, int senderPoolSize, int maxBatchSize)
  325. throws IllegalArgumentException
  326. {
  327. if (postBackURL == null || postBackURL.length() == 0)
  328. {
  329. throw new IllegalArgumentException("PostBack class given null postBackURL");
  330. }
  331. myName = "Postback-" + postBackURL;
  332. if (xmlTopElementName == null || xmlTopElementName.length() == 0)
  333. {
  334. throw new IllegalArgumentException(myName + ": PostBack class given null xmlTopElement");
  335. }
  336. this.postBackURL = postBackURL;
  337. this.xmlTopElement = xmlTopElementName;
  338. this.maxQueueSize = maxQueueSize;
  339. this.maxBatchSize = maxBatchSize;
  340. postQueue = new LinkedList<String>();
  341. threadsWaitingToPost = 0;
  342. senderPool = new ArrayList<Sender>();
  343. for (int i = 0; i < senderPoolSize; i++)
  344. {
  345. Sender sender = new Sender(myName + '-' + i);
  346. senderPool.add(sender);
  347. }
  348. }
  349. /**
  350. * Queues report to postQueue only if the queue size has not reached the
  351. * maxQueueSize.
  352. * @param report
  353. * @return true if report is added to queue, false otherwise (queue full)
  354. */
  355. public boolean queueReport(String report)
  356. {
  357. // Log for recovery in case of problem in posting report.
  358. CommonLogger.activity.info("Attempting to queue report");
  359. synchronized(postQueue)
  360. {
  361. for (;;)
  362. {
  363. if (postQueue.size() < maxQueueSize)
  364. {
  365. myLogger.debug("Queing report" + report);
  366. postQueue.add(report);
  367. myLogger.debug("Added 1 report - postQueue size: " + postQueue.size());
  368. postQueue.notifyAll();
  369. return true;
  370. }
  371. else
  372. {
  373. myLogger.debug("Waiting for space - postQueue size: " + postQueue.size());
  374. try
  375. {
  376. threadsWaitingToPost++;
  377. myLogger.debug("Threads waiting to post: " + threadsWaitingToPost);
  378. postQueue.wait(QUEUE_WAIT * 1000);
  379. }
  380. catch (InterruptedException e)
  381. {
  382. break;
  383. }
  384. threadsWaitingToPost--;
  385. }
  386. }
  387. }
  388. myLogger.info("Interrupted while waiting for space to queue report");
  389. return false;
  390. }
  391. /**
  392. * Queues reports to postQueue only if the queue size has not reached the
  393. * maxQueueSize.
  394. * @param reports to be added back to postQueue
  395. * @return true if all jobs have been added to queue, false otherwise (queue full)
  396. */
  397. /*
  398. @Deprecated
  399. public boolean queueReports(List<String> reports)
  400. {
  401. myLogger.debug(myName + ": postQueue size: " + postQueue.size());
  402. synchronized(postQueue)
  403. {
  404. Iterator<String> iter = reports.iterator();
  405. int count = 0; // Number of reports added back to postQueue
  406. while (iter.hasNext())
  407. {
  408. String report = iter.next();
  409. if (postQueue.size() < maxQueueSize)
  410. {
  411. postQueue.add(report);
  412. count++;
  413. }
  414. }
  415. if (count > 0) postQueue.notify();
  416. boolean returnValue = (count == reports.size());
  417. if (!returnValue)
  418. {
  419. CommonLogger.alarm.warn(myName
  420. + ".queueReport method returning false, having queued "
  421. + count + " out of " + reports.size());
  422. }
  423. return returnValue;
  424. }
  425. }
  426. */
  427. public void terminate()
  428. {
  429. for (Sender sender : senderPool)
  430. {
  431. sender.terminate();
  432. }
  433. }
  434. }