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.

1248 line
41 KiB

  1. package altk.comm.engine;
  2. import java.io.IOException;
  3. import java.io.PrintWriter;
  4. import java.util.ArrayList;
  5. import java.util.Arrays;
  6. import java.util.HashMap;
  7. import java.util.List;
  8. import java.util.Map;
  9. import java.util.Queue;
  10. import java.util.concurrent.Executors;
  11. import java.util.concurrent.LinkedBlockingQueue;
  12. import java.util.concurrent.ScheduledExecutorService;
  13. import java.util.concurrent.ScheduledFuture;
  14. import java.util.concurrent.TimeUnit;
  15. import javax.servlet.http.HttpServletRequest;
  16. import javax.servlet.http.HttpServletResponse;
  17. import org.apache.log4j.Logger;
  18. import altk.comm.engine.exception.BroadcastError;
  19. import altk.comm.engine.exception.BroadcastException;
  20. import altk.comm.engine.exception.EngineException;
  21. import altk.comm.engine.exception.PlatformError;
  22. import altk.comm.engine.exception.PlatformException;
  23. import altk.comm.engine.postback.PostBack;
  24. /**
  25. * Broadcast class absorbs what was formerly known as Dispatcher class.
  26. *
  27. * @author Yuk-Ming
  28. *
  29. */
  30. public abstract class Broadcast
  31. {
  32. private static final int SCHEDULER_THREAD_POOL_SIZE = 5;
  33. private static final String ACTIVITY_RECORD_ID_PARAM_NAME_DEFAULT = "activity_record_id";
  34. private static final long SLEEP_BETWEEN_JOBS_DEFAULT = 0;
  35. public final String broadcastType;
  36. private String broadcastId;
  37. private BroadcastState state = BroadcastState.ACCEPTED;
  38. String haltReason;
  39. String stateErrorText;
  40. public final long receiveTime;
  41. public long changeStateTime;
  42. /**
  43. * Count of jobs that are completed (excluding those that are
  44. * being rescheduled).
  45. */
  46. private int completedJobCount = 0;
  47. /**
  48. * Dynamically keeps count of the total number jobs scheduled
  49. * in readyQueue. Initially it is set to be the size of the
  50. * recipientList. Then as jobs are processed, and when one is
  51. * to be repeated by re-adding it to the readyQueue, then this
  52. * number is incremented by 1.
  53. */
  54. private int effectiveJobCount = 0;
  55. protected String activityRecordIdParamName;
  56. private String jobReportRootNodeName;
  57. /**
  58. * Set when reading request XML, but never used.
  59. */
  60. private String launchRecordId;
  61. // protected XPath xpathEngine;
  62. protected String postBackURL;
  63. private PostBack postBack;
  64. public long expireTime;
  65. /**
  66. * Sleep time in milliseconds between consecutive job processing (actualliy batch)
  67. */
  68. protected long sleepBetweenJobs;
  69. protected static Logger myLogger = Logger.getLogger(Broadcast.class);
  70. private Queue<Job> readyQueue;
  71. protected List<Service> serviceThreadPool;
  72. private Object resumeFlag; // Semaphore for dispatcher threads to resume.
  73. protected List<Recipient> recipientList;
  74. //private int remainingJobs;
  75. private ScheduledExecutorService scheduler;
  76. private int serviceThreadPoolSize;
  77. public static enum BroadcastState
  78. {
  79. ACCEPTED,
  80. RUNNING,
  81. PAUSING,
  82. PAUSED,
  83. CANCELING,
  84. CANCELED(true), // Final state
  85. PURGED(true), // Final state
  86. ABORTED(true), // final state
  87. EXPIRED(true), // final state
  88. COMPLETED(true); // Final state
  89. final public boolean isFinal;
  90. private BroadcastState()
  91. {
  92. isFinal = false;
  93. }
  94. private BroadcastState(boolean isFinal)
  95. {
  96. this.isFinal = isFinal;
  97. }
  98. }
  99. public enum StateChangeStatus
  100. {
  101. SUCCESS,
  102. NO_CHANGE,
  103. FORBIDDEN
  104. }
  105. /**
  106. * When a Broadcast is first created, its state is INSTALLING, during which
  107. * time, the broadcast request XML is digested. After this period, the
  108. * state of the broadcast enters into RUNNING, and it begins with adding
  109. * all VoiceJobs in the request to the Dispatcher's queues. During this period
  110. * calls are dispatched, setup, and terminate. When the last call is terminated,
  111. * the broadcast enters into the COMPLETED state.
  112. *
  113. * Transitions from INSTALLING to RUNNING, and from RUNNING to COMPLETED happen
  114. * automatically without any external influence.
  115. *
  116. * At any time, the broadcast may be canceled by user action, which causes
  117. * the broadcast to transition from the RUNNING state into the CANCELED state.
  118. *
  119. * Since the RUNNING state may go to the COMLETED or to the CANCELED state,
  120. * each due to a different thread, there needs to be a mutex to guarantee
  121. * state and data integrity.
  122. *
  123. * A INSTALLING or RUNNING broadcast may be paused by user action, stopping the
  124. * Dispatcher from making new calls and causing the broadcast to go to
  125. * the HALTED state. Certain error conditions result in the HALTED state.
  126. *
  127. * User may order a broadcast to be removed entirely with a purge command,
  128. * causing the broadcast to go to the PURGED state. This state is there so
  129. * objects, that has reference to a broadcast which has been purged, know
  130. * what to do in that situation.
  131. *
  132. * We need the pause operation to set a RUNNING machine to be in the PAUSING state,
  133. * allow ongoing jobs to proceed to normal termination,
  134. * whereas no new jobs are started. State goes from PAUSING to HALTED.
  135. *
  136. * Cancel-nice operation is pause, followed by the automatic transition
  137. * from HALTED to CANCELED.
  138. *
  139. * Cancel operation forces all jobs to abort, and the state transitions to CANCELED
  140. * immediately.
  141. *
  142. * Because the Dispatcher and Broadcast is one-to-one, and because they access each other's
  143. * data, Dispatcher should be combined into Broadcast.
  144. *
  145. * @author Yuk-Ming
  146. *
  147. */
  148. static Map<BroadcastState, List<BroadcastState>> toStates;
  149. static
  150. {
  151. // Initialize legal transitions of state machine.
  152. // For each state, define a list of legal states that this state can transition to
  153. toStates = new HashMap<BroadcastState, List<BroadcastState>>();
  154. // Transitions from INSTALLING
  155. toStates.put(BroadcastState.ACCEPTED, Arrays.asList(
  156. BroadcastState.RUNNING, // Normal transition
  157. BroadcastState.CANCELING, // User action
  158. BroadcastState.CANCELED, // User action
  159. BroadcastState.PAUSING, // User action
  160. BroadcastState.PAUSED, // User action
  161. BroadcastState.PURGED, // User action
  162. BroadcastState.ABORTED, // TTS error
  163. BroadcastState.EXPIRED,
  164. BroadcastState.COMPLETED // When recipient list is empty
  165. ));
  166. // Transitions from RUNNING
  167. toStates.put(BroadcastState.RUNNING, Arrays.asList(
  168. BroadcastState.CANCELING, // User action
  169. BroadcastState.CANCELED, // User action
  170. BroadcastState.PAUSING, // User action
  171. BroadcastState.PAUSED, // User action
  172. BroadcastState.PURGED, // User action
  173. BroadcastState.ABORTED, // Service provider irrecoverable error
  174. BroadcastState.EXPIRED,
  175. BroadcastState.COMPLETED // Natural transition, if all ongoing calls complete and no more calls in Dispatcher queues.
  176. ));
  177. // Transitions from CANCELING
  178. toStates.put(BroadcastState.CANCELING, Arrays.asList(
  179. BroadcastState.CANCELED, // User action
  180. BroadcastState.PURGED, // User action
  181. BroadcastState.COMPLETED // Natural transition, if all ongoing calls complete and no more calls in Dispatcher queues.
  182. ));
  183. // Transitions from HALTING
  184. toStates.put(BroadcastState.PAUSING, Arrays.asList(
  185. BroadcastState.RUNNING, // User action
  186. BroadcastState.CANCELED, // User action
  187. BroadcastState.PAUSED,
  188. BroadcastState.PURGED, // User action
  189. BroadcastState.COMPLETED // Natural transition, if all ongoing jobs complete and no more calls in Dispatcher queues.
  190. ));
  191. // Transitions from HALTED
  192. toStates.put(BroadcastState.PAUSED, Arrays.asList(
  193. BroadcastState.RUNNING, // User action
  194. BroadcastState.CANCELED, // User action
  195. BroadcastState.CANCELING, // User action
  196. BroadcastState.PURGED, // User action
  197. BroadcastState.COMPLETED // Natural transition, if all ongoing jobs complete and no more calls in Dispatcher queues.
  198. ));
  199. }
  200. public static class StateChangeResult
  201. {
  202. public StateChangeStatus stateChangeStatus;
  203. public BroadcastState currentState;
  204. public BroadcastState previousState;
  205. public StateChangeResult(StateChangeStatus stateChangeStatus,
  206. BroadcastState currentState, BroadcastState previousState)
  207. {
  208. this.stateChangeStatus = stateChangeStatus;
  209. this.currentState = currentState;
  210. this.previousState = previousState;
  211. }
  212. }
  213. protected class Service extends Thread
  214. {
  215. Object serviceProviderPeer;
  216. protected Service(String name) throws BroadcastException
  217. {
  218. serviceProviderPeer = getInitializedServiceProviderPeer();
  219. setName(name);
  220. }
  221. public void run()
  222. {
  223. myLogger.info("Thread starting...");
  224. for (;;)
  225. {
  226. if (threadsShouldStop())
  227. {
  228. closeServiceProvider(serviceProviderPeer);
  229. return;
  230. }
  231. synchronized (resumeFlag)
  232. {
  233. if (threadsShouldPause())
  234. {
  235. try
  236. {
  237. resumeFlag.wait();
  238. }
  239. catch (InterruptedException e)
  240. {
  241. myLogger.warn("Dispatcher thread interrupted while waiting to resume");
  242. return;
  243. }
  244. }
  245. }
  246. List<Job> batch = null;
  247. /**
  248. * Includes allocation from capacity. Only returns when the required allocation
  249. * is obtained. Example, RTP port allocation, limit due to total number of allowable calls.
  250. */
  251. ServicePrerequisites prerequisites = null;
  252. synchronized(readyQueue)
  253. {
  254. // get a batch of jobs
  255. Job job = readyQueue.peek();
  256. if (job == null)
  257. try
  258. {
  259. readyQueue.wait();
  260. continue;
  261. }
  262. catch (InterruptedException e)
  263. {
  264. return;
  265. }
  266. myLogger.debug("Found some jobs");
  267. // Check if expired
  268. if (System.currentTimeMillis() >= expireTime)
  269. {
  270. setState(BroadcastState.EXPIRED);
  271. continue;
  272. }
  273. /**
  274. * Includes allocation from capacity. Only returns when the required allocation
  275. * is obtained. Example, RTP port allocation, limit due to total number of allowable calls.
  276. */
  277. prerequisites = secureServicePrerequisites();
  278. if (threadsShouldStop() || threadsShouldPause())
  279. {
  280. returnPrerequisites(prerequisites);
  281. continue;
  282. }
  283. // Check again if expired
  284. if (System.currentTimeMillis() >= expireTime)
  285. {
  286. returnPrerequisites(prerequisites);
  287. setState(BroadcastState.EXPIRED);
  288. continue;
  289. }
  290. // Now that we can go ahead with this job, let us remove this from queue
  291. readyQueue.poll();
  292. batch = new ArrayList<Job>();
  293. batch.add(job);
  294. // We we are to get a batch of more than one, let us fill in the rest.
  295. for (int i = 1; i < getJobBatchSize(); i++)
  296. {
  297. job = readyQueue.poll();
  298. if (job == null) break;
  299. batch.add(job);
  300. }
  301. }
  302. if (batch != null && batch.size() > 0)
  303. {
  304. // Mark start time
  305. long now = System.currentTimeMillis();
  306. for (Job job : batch)
  307. {
  308. job.startTime = now;
  309. }
  310. // Service the jobs
  311. try
  312. {
  313. processJobs(batch, serviceProviderPeer, prerequisites);
  314. }
  315. catch (EngineException e)
  316. {
  317. terminate(BroadcastState.ABORTED, e.getMessage());
  318. }
  319. catch (Throwable t)
  320. {
  321. // This is unexpected. Log stack trace
  322. myLogger.error("Caught unexpected Throwable", t);
  323. terminate(BroadcastState.ABORTED, t + ": " + t.getMessage());
  324. }
  325. }
  326. if (sleepBetweenJobs > 0)
  327. {
  328. try
  329. {
  330. Thread.sleep(sleepBetweenJobs);
  331. }
  332. catch (InterruptedException e1)
  333. {
  334. // Do nothing?
  335. }
  336. }
  337. }
  338. }
  339. }
  340. /**
  341. *
  342. * @param broadcastType
  343. * @param activityRecordIdParamName - if null, default is used.
  344. * @param jobReportRootNodeName
  345. */
  346. protected Broadcast(String broadcastType, String activityRecordIdParamName, String jobReportRootNodeName)
  347. {
  348. this.broadcastType = broadcastType;
  349. this.activityRecordIdParamName = activityRecordIdParamName == null? ACTIVITY_RECORD_ID_PARAM_NAME_DEFAULT : activityRecordIdParamName;
  350. this.jobReportRootNodeName = jobReportRootNodeName;
  351. sleepBetweenJobs = SLEEP_BETWEEN_JOBS_DEFAULT;
  352. readyQueue = new LinkedBlockingQueue<Job>();
  353. serviceThreadPool = new ArrayList<Service>();
  354. recipientList = new ArrayList<Recipient>();
  355. scheduler = Executors.newScheduledThreadPool(SCHEDULER_THREAD_POOL_SIZE);
  356. resumeFlag = new Object();
  357. receiveTime = System.currentTimeMillis();
  358. }
  359. /**
  360. * Experimental formulation where it takes over directing
  361. * the activity of a Broadcast, as it should, instead of relegating
  362. * it to CommEngine. This is directly invoked by CommEngine.doPost method,
  363. * and is much easier to read and comprehend.
  364. * <p>It is responsible for
  365. * <ul>
  366. * <li>Replying with HTTP response, and closing HTTP request and response
  367. * <li>Does broadcast and posts real-time progress, using post back queues
  368. * from CommEngine.
  369. * </ul>
  370. * Strategy of execution:
  371. * <ul>
  372. * <li>Decode xml request
  373. * <li>Set up Service threads
  374. * <li> Invite derived class to set up contexts, one for each of the Service threads.
  375. * <li>Do broadcast.
  376. * </ul>
  377. * This method is to be invoked by the CommEngine.doPost method, which fields a HTTP POST.
  378. * <p>
  379. * Adopting the use of this method by CommEngine
  380. * is 100% compatible with derived classes VoiceBroadcast and EmailBroadcast
  381. * existing today, 6/18/2016.
  382. *
  383. *
  384. * @param request
  385. * @param response
  386. * @param commEngine
  387. */
  388. protected void doPost(HttpServletRequest request, HttpServletResponse response,
  389. CommEngine commEngine)
  390. {
  391. myLogger.debug("Entering Broadcast.doPost method");
  392. BroadcastException myException = null;
  393. this.serviceThreadPoolSize = commEngine.getServiceThreadPoolSize();
  394. try
  395. {
  396. boolean notInService = commEngine.notInService();
  397. decode(request, notInService);
  398. // Now that have decoded the id of this broadcast,
  399. // ask CommEngine to install it with its id.
  400. commEngine.installBroadcast(this);
  401. if (notInService)
  402. {
  403. throw new PlatformException(PlatformError.RUNTIME_ERROR,
  404. "Not in service");
  405. }
  406. if (recipientList.size() == 0)
  407. {
  408. // TODO: Got to return HTTP content before returning.
  409. CommonLogger.activity.info("Broadcast " + getBroadcastId() + ": No recipients");
  410. setState(BroadcastState.COMPLETED, "No recipients", null);
  411. return;
  412. }
  413. postBack = (PostBack)commEngine.getPostBack(getPostBackURL(), broadcastType);
  414. initSync(commEngine.getResources());
  415. init(postBack);
  416. if (getState() == BroadcastState.COMPLETED) return;
  417. }
  418. catch (BroadcastException e)
  419. {
  420. // TODO: Got to return HTTP content before returning.
  421. myException = e;
  422. setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText);
  423. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  424. myLogger.error("Broadcast aborted", e);
  425. return;
  426. }
  427. catch (Throwable t)
  428. {
  429. // Caught stray unexpected runtime problem
  430. CommonLogger.alarm.error("Broadcast aborted: " + t);
  431. myLogger.error("Broadcast aborted", t);
  432. myException = new BroadcastException(BroadcastError.PLATFORM_ERROR, t.getMessage());
  433. setState(BroadcastState.ABORTED, myException.errorCodeText, myException.errorText);
  434. }
  435. finally
  436. {
  437. // Return regular or error response
  438. String responseXML = getResponseXML(myException);
  439. PrintWriter writer;
  440. try
  441. {
  442. writer = response.getWriter();
  443. writer.write(responseXML);
  444. writer.close();
  445. }
  446. catch (IOException e)
  447. {
  448. myLogger.error("Failed to write reponse to requester. Aborts broadcast." );
  449. if (state != BroadcastState.ABORTED)
  450. {
  451. setState(BroadcastState.ABORTED, "Failed to reply to requester", e.getMessage());
  452. }
  453. return;
  454. }
  455. if (myException != null) return;
  456. }
  457. // So far so good, we now go ahead with completing
  458. // initialization.
  459. try
  460. {
  461. initAsync();
  462. effectiveJobCount = recipientList.size();
  463. // Create service thread pool to dispatch jobs,
  464. // at the same time, setting up a list of service thread names
  465. // for use by derived class to set up contexts in which
  466. // these threads run.
  467. myLogger.debug("At creating service threads, serviceThreadPoolSize = " + serviceThreadPoolSize);
  468. List<String> serviceThreadNames = new ArrayList<String>();
  469. for (int i = 0; i < serviceThreadPoolSize; i++)
  470. {
  471. String threadName = broadcastId + "_service_thread_" + i;
  472. Service serviceThread = new Service(threadName);
  473. serviceThreadPool.add(serviceThread);
  474. serviceThreadNames.add(threadName);
  475. }
  476. //initServiceThreadContexts(serviceThreadNames);
  477. doBroadcast();
  478. }
  479. catch (BroadcastException e)
  480. {
  481. setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText);
  482. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  483. myLogger.error("Broadcast aborted", e);
  484. }
  485. }
  486. protected abstract void returnPrerequisites(ServicePrerequisites prerequisites);
  487. /**
  488. * Creates and initializes a service provider, to be used by only one service thread.
  489. * If service provider is not thread-specific, then this method may return null, and
  490. * a common service provider is created outside of this method.
  491. *
  492. * @return service provider as a class Object instance.
  493. * @throws BroadcastException
  494. */
  495. protected abstract Object getInitializedServiceProviderPeer() throws BroadcastException;
  496. /**
  497. * Obtains the required components to support a service; e.g. RTP port, or a place
  498. * in maximum total number of calls. Does not return till the reequired prerequisites are obtained.
  499. * @return null, if no prerequisite is required, as in the case of email and sms engines.
  500. */
  501. abstract protected ServicePrerequisites secureServicePrerequisites();
  502. abstract public void closeServiceProvider(Object serviceProvider);
  503. /**
  504. * Makes a state transition to the given newState if the transition from
  505. * the current state is legal.
  506. * @param newState
  507. * @return StateChangeResult
  508. */
  509. public StateChangeResult setState(BroadcastState newState)
  510. {
  511. return setState(newState, null, null);
  512. }
  513. /**
  514. * Makes a state transition to the given newState if the transition from
  515. * the current state is legal. Also posts back a state change notification.
  516. * @param newState
  517. * @return StateChangeResult
  518. */
  519. public synchronized StateChangeResult setState(BroadcastState newState,
  520. String haltReason, String stateErrorText)
  521. {
  522. boolean isLegal;
  523. BroadcastState prev = null;
  524. synchronized (this)
  525. {
  526. if (state == newState) return new StateChangeResult(StateChangeStatus.NO_CHANGE, state, null);
  527. List<BroadcastState> to = toStates.get(state);
  528. isLegal = (to == null? false : to.contains(newState));
  529. prev = state;
  530. if (isLegal)
  531. {
  532. state = newState;
  533. changeStateTime = System.currentTimeMillis();
  534. }
  535. }
  536. if (isLegal)
  537. {
  538. this.haltReason = haltReason;
  539. this.stateErrorText = stateErrorText;
  540. CommonLogger.activity.info(String.format("Broadcast %s: State transitioned from %s to %s", broadcastId, prev, state));
  541. if (postBack != null)
  542. {
  543. postBack.queueReport(mkStatusReport());
  544. }
  545. return new StateChangeResult(StateChangeStatus.SUCCESS, newState, prev);
  546. }
  547. else
  548. {
  549. myLogger.warn(String.format("Broadcast %s: Transition from %s to %s forbidden", broadcastId, prev, newState));
  550. return new StateChangeResult(StateChangeStatus.FORBIDDEN, prev, null);
  551. }
  552. }
  553. protected void setBroadcastId(String broadcastId)
  554. {
  555. if (broadcastId == null)
  556. throw new IllegalArgumentException(
  557. "Argument broadcastId in Broadcast.setBroadcastId method cannot be null");
  558. if (this.broadcastId != null)
  559. throw new IllegalStateException(
  560. "Broadcast.setBroadcastId method cannot be invoked more than once for a Broadcast");
  561. this.broadcastId = broadcastId;
  562. }
  563. protected void setLaunchRecordId(String launchRecordId)
  564. {
  565. if (launchRecordId == null)
  566. throw new IllegalArgumentException(
  567. "Argument launchRecordId in Broadcast.setLaunchRecordId method cannot be null");
  568. if (this.launchRecordId != null)
  569. throw new IllegalStateException(
  570. "Broadcast.setLaunchRecordId method cannot be invoked more than once for a Broadcast");
  571. this.launchRecordId = launchRecordId;
  572. }
  573. public String getBroadcastId()
  574. {
  575. return broadcastId;
  576. }
  577. public String getLaunchRecordId()
  578. {
  579. return launchRecordId;
  580. }
  581. /**
  582. *
  583. * @param e
  584. * @return HTTP response for normal case (when exception e is null), or with exception
  585. */
  586. public String getResponseXML(BroadcastException e)
  587. {
  588. String tagName = broadcastType + "_response";
  589. StringBuffer responseXML = new StringBuffer("<" + tagName);
  590. if (broadcastId != null && broadcastId.length() > 0)
  591. {
  592. responseXML.append(" broadcast_id=\"");
  593. responseXML.append(broadcastId);
  594. responseXML.append("\"");
  595. }
  596. responseXML.append(" accepted='");
  597. responseXML.append(e != null || getState() == BroadcastState.COMPLETED ? "FALSE" : "TRUE");
  598. responseXML.append("'");
  599. if (e == null)
  600. {
  601. if (haltReason != null && haltReason.length() > 0)
  602. {
  603. responseXML.append(" error='" + haltReason + "'");
  604. }
  605. responseXML.append('>');
  606. }
  607. else
  608. {
  609. if (e.errorCode != null)
  610. {
  611. responseXML.append(" error='");
  612. responseXML.append(e.errorCode.toString());
  613. responseXML.append("'");
  614. }
  615. responseXML.append('>');
  616. if (e.errorText != null)
  617. {
  618. responseXML.append("<error_text>");
  619. responseXML.append(e.errorText);
  620. responseXML.append("</error_text>");
  621. }
  622. }
  623. responseXML.append("</" + tagName + '>');
  624. return responseXML.toString();
  625. }
  626. public String getPostBackURL()
  627. {
  628. return postBackURL;
  629. }
  630. protected String mkResponseXML(String errorCode, String errorText)
  631. {
  632. String tagName = broadcastType + "_response";
  633. StringBuffer responseXML = new StringBuffer("<" + tagName);
  634. String broadcastId = getBroadcastId();
  635. if (broadcastId != null && broadcastId.length() > 0)
  636. {
  637. responseXML.append(" broadcast_id=\"");
  638. responseXML.append(broadcastId);
  639. responseXML.append("\"");
  640. }
  641. responseXML.append(" accepted='");
  642. responseXML.append(errorCode == null ? "TRUE" : "FALSE");
  643. responseXML.append("'");
  644. if (errorCode == null)
  645. {
  646. responseXML.append('>');
  647. }
  648. else
  649. {
  650. responseXML.append(" error='");
  651. responseXML.append(errorCode);
  652. responseXML.append("'");
  653. responseXML.append('>');
  654. if (errorText != null)
  655. {
  656. responseXML.append("<error_text>");
  657. responseXML.append(errorText.replaceAll("\\&", "&amp;")
  658. .replaceAll("<", "&lt;"));
  659. responseXML.append("</error_text>");
  660. }
  661. }
  662. responseXML.append("</" + tagName + '>');
  663. return responseXML.toString();
  664. }
  665. /**
  666. * If finalState is final, then this state is set, and dispatcher threads are stopped.
  667. * Overriding implementation may release all other resources, like timers.
  668. * @param finalState
  669. */
  670. public void terminate(BroadcastState finalState)
  671. {
  672. terminate(finalState, null);
  673. }
  674. /**
  675. * If finalState is final, then this state is set, and dispatcher threads are stopped.
  676. * Overriding implementation may release all other resources, like timers.
  677. * @param finalState
  678. */
  679. public void terminate(BroadcastState finalState, String reason)
  680. {
  681. if (!finalState.isFinal) throw new IllegalArgumentException("Argument finalState " + finalState + " in Broadcast.terminate method is not final");
  682. StateChangeResult result = setState(finalState, reason, null);
  683. switch (result.stateChangeStatus)
  684. {
  685. case SUCCESS:
  686. break;
  687. case NO_CHANGE:
  688. return;
  689. case FORBIDDEN:
  690. myLogger.error("Not allow to terminate broadcast in " + result.currentState + " state");
  691. return;
  692. default: // Should not happen
  693. return;
  694. }
  695. // Wake up all dispatcher threads waiting on readyQueue so they will all stop
  696. synchronized(readyQueue)
  697. {
  698. readyQueue.notifyAll();
  699. }
  700. // Wake up all sleeping dispatcher threads for same reason.
  701. for(Thread t : serviceThreadPool)
  702. {
  703. try
  704. {
  705. t.interrupt();
  706. }
  707. catch (Exception e)
  708. {
  709. myLogger.warn("Interrupted while waiting for Thread " + t.getName() + " to terminate");
  710. }
  711. }
  712. // Quiesce scheduler, and terminate it.
  713. scheduler.shutdownNow();
  714. }
  715. /**
  716. * Creates status report.
  717. * @return status report in XML.
  718. */
  719. protected String mkStatusReport()
  720. {
  721. StringBuffer statusBf = new StringBuffer();
  722. String topLevelTag = broadcastType;
  723. String broadcastId = getBroadcastId();
  724. if (broadcastId == null) broadcastId = "";
  725. statusBf.append("<" + topLevelTag + " broadcast_id='" + broadcastId
  726. + "' receive_time='" + receiveTime
  727. + "' recipient_count='" + recipientList.size() + "'");
  728. if (launchRecordId != null)
  729. {
  730. statusBf.append(" launch_record_id='" + launchRecordId + "'");
  731. }
  732. statusBf.append(">\r\n<state>" + state + "</state><state_change_time>" + changeStateTime
  733. + "</state_change_time>\r\n");
  734. if (state == BroadcastState.PAUSED
  735. || state == BroadcastState.ABORTED)
  736. {
  737. if (haltReason != null)
  738. {
  739. statusBf.append("<reason>" + haltReason
  740. + "</reason>\r\n");
  741. }
  742. if (stateErrorText != null)
  743. {
  744. statusBf.append("<error_text>" + stateErrorText
  745. + "</error_text>");
  746. }
  747. }
  748. statusBf.append("<job_summary completed='" + getCompletedJobCount() +
  749. "' ready='" + getReadyJobCount() + "'");
  750. statusBf.append(" active='" + getActiveJobCount() + "'");
  751. statusBf.append("></job_summary></" + topLevelTag + ">\r\n");
  752. String statusReport = statusBf.toString();
  753. return statusReport;
  754. }
  755. protected void onExpire()
  756. {
  757. }
  758. protected void setExpireTime(long expireTime)
  759. {
  760. this.expireTime = expireTime;
  761. }
  762. public long getExpireTime()
  763. {
  764. return expireTime;
  765. }
  766. /**
  767. *
  768. * @return number of active jobs, including those being
  769. * rescheduled by a timer.
  770. * Computed from effectiveJobCount, completedJobCount and readyQueue.size()
  771. */
  772. protected int getActiveJobCount()
  773. {
  774. return effectiveJobCount - completedJobCount - readyQueue.size();
  775. }
  776. /**
  777. * Parses broadcastId and return if notInService is true.
  778. * Otherwise, continue parsing postBackUrl, expireTime, recipientList,
  779. * and implementation-specific data from request.
  780. * Avoid throwing an exception before parsing and setting broadcastId.
  781. * @param notInService
  782. * @throws EngineException
  783. */
  784. protected abstract void decode(HttpServletRequest request, boolean notInService)
  785. throws EngineException;
  786. /**
  787. * Remembers postBack, and
  788. * Creates thread pool of size dictated by broadcast, which determines the size based
  789. * on the chosen service provider.
  790. *
  791. * Overriding implementation must invoke this method at the end, and process information
  792. * contained in the broadcast, in preparation for the invocation of the process
  793. * method.
  794. *
  795. * If there is no error, the overriding implementation must return this base method.
  796. *
  797. * @param commEngine
  798. *
  799. * @throws BroadcastException
  800. */
  801. protected final void init(PostBack postBack)
  802. {
  803. // Remember postBack
  804. this.postBack = postBack;
  805. for (Recipient recipient : recipientList)
  806. {
  807. readyQueue.add(mkJob(recipient));
  808. }
  809. //remainingJobs = readyQueue.size();
  810. }
  811. protected abstract void initSync(EngineResources resources) throws BroadcastException;
  812. protected Job mkJob(Recipient recipient)
  813. {
  814. return new Job(recipient);
  815. }
  816. /**
  817. * Overriding implementation performs time consuming initialization, after returning
  818. * POST http status indicating accepting broadcast for processing.
  819. *
  820. * @throws BroadcastException
  821. */
  822. protected void initAsync() throws BroadcastException
  823. {
  824. // Do nothing in base class.
  825. }
  826. public String getId()
  827. {
  828. return broadcastId;
  829. }
  830. /**
  831. * Sets the stateMachine to CANCEL
  832. */
  833. protected void cancel(PrintWriter out)
  834. {
  835. BroadcastState targetState = getActiveJobCount() == 0?
  836. BroadcastState.CANCELED : BroadcastState.CANCELING;
  837. StateChangeResult result = setState(targetState);
  838. String responseContent = null;
  839. switch (result.stateChangeStatus)
  840. {
  841. case SUCCESS:
  842. responseContent = "OK";
  843. break;
  844. case NO_CHANGE:
  845. responseContent = "Not canceled: Already cancelled";
  846. break;
  847. case FORBIDDEN:
  848. responseContent = "Not canceled: Not allowed to cancel a broadcast in " + result.currentState + " state";
  849. }
  850. out.write(responseContent);
  851. synchronized(resumeFlag)
  852. {
  853. resumeFlag.notifyAll();
  854. }
  855. }
  856. protected void pause()
  857. {
  858. // Sets state to HALTED, which is monitored by Broadcast.Service threads.
  859. setState(BroadcastState.PAUSING);
  860. }
  861. protected void resume()
  862. {
  863. synchronized (resumeFlag)
  864. {
  865. if (threadsShouldPause())
  866. {
  867. setState(BroadcastState.RUNNING);
  868. resumeFlag.notifyAll();
  869. }
  870. }
  871. }
  872. /**
  873. * Derived class may make its own Implementation of JobReport
  874. * @return
  875. */
  876. protected JobReport mkJobReport()
  877. {
  878. return new JobReport();
  879. }
  880. public void addJob(Job job)
  881. {
  882. synchronized(readyQueue)
  883. {
  884. readyQueue.add(job);
  885. readyQueue.notifyAll();
  886. }
  887. }
  888. /**
  889. * For use by the new Broadcast.doPost method.
  890. * It changes state to RUNNING and waits for all Service threads
  891. * to terminate after starting them.
  892. * @throws BroadcastException
  893. */
  894. public void doBroadcast() throws BroadcastException
  895. {
  896. setState(BroadcastState.RUNNING);
  897. // Start the dispatcher threads
  898. for (Service thread : serviceThreadPool)
  899. {
  900. thread.start();
  901. }
  902. for (Service thread : serviceThreadPool)
  903. {
  904. try
  905. {
  906. thread.join();
  907. }
  908. catch (InterruptedException e)
  909. {
  910. myLogger.error("Caught exception while waiting for a Service thread to terminate:" + e);
  911. }
  912. }
  913. close();
  914. myLogger.info("Broadcast " + getId() + " terminated");
  915. }
  916. /**
  917. * Derived may release resources here.
  918. */
  919. protected void close()
  920. {
  921. // Do nothing in base class
  922. }
  923. public void startProcessing() throws BroadcastException
  924. {
  925. effectiveJobCount = recipientList.size();
  926. // Create service thread pool to dispatch jobs
  927. myLogger.debug("At creating service threads, serviceThreadPoolSize = " + serviceThreadPoolSize);
  928. for (int i = 0; i < serviceThreadPoolSize; i++)
  929. {
  930. String threadName = broadcastId + "_service_thread_" + i;
  931. Service serviceThread = new Service(threadName);
  932. serviceThreadPool.add(serviceThread);
  933. }
  934. initServiceThreadContexts();
  935. setState(BroadcastState.RUNNING);
  936. // Start the dispatcher threads
  937. for (Service thread : serviceThreadPool)
  938. {
  939. thread.start();
  940. }
  941. }
  942. /**
  943. * Derived class may set up environment before starting Service threads.
  944. * @param serviceThreadNames
  945. */
  946. protected void initServiceThreadContexts()
  947. {
  948. // Do nothing in base class
  949. }
  950. /**
  951. * Experimental - needed to go with the also experimental method exec.
  952. * @param serviceThreadNames
  953. */
  954. protected void initServiceThreadContexts(List<String> serviceThreadNames)
  955. {
  956. // Do nothing in base class
  957. }
  958. private boolean threadsShouldStop()
  959. {
  960. BroadcastState state = getState();
  961. return state == BroadcastState.CANCELING || state.isFinal;
  962. }
  963. private boolean threadsShouldPause()
  964. {
  965. BroadcastState state = getState();
  966. return state == BroadcastState.PAUSED || state == BroadcastState.PAUSING;
  967. }
  968. /**
  969. * job status is reported back to this broadcast, via the logAndQueueForPostBack method.
  970. * @param batch
  971. * @param prerequisites
  972. */
  973. abstract protected void processJobs(List<Job> batch, Object serviceProvider, ServicePrerequisites prerequisites)
  974. throws EngineException;
  975. /**
  976. * Size of a batch of jobs to be processed together. For email, this may be more than 1,
  977. * and this method should be overridden.
  978. * @return size of a batch of jobs to be processed together.
  979. */
  980. protected int getJobBatchSize()
  981. {
  982. return 1;
  983. }
  984. /**
  985. * Sets jobStatus in job, and post job report.
  986. * If jobStatus is final, and no rescheduling,
  987. * then decrement number of remainingJobs,and increment completedJobCount.
  988. * @param job
  989. * @param jobStatus
  990. * @param errorText
  991. */
  992. public synchronized void postJobStatus(Job job)
  993. {
  994. postJobStatus(job, -1);
  995. /*
  996. if (postBack != null)
  997. {
  998. JobReport report = mkJobReport();
  999. report.initBase(job, broadcastId, launchRecordId, activityRecordIdParamName, jobReportRootNodeName);
  1000. report.init(job);
  1001. postBack.queueReport(report.toString());
  1002. }
  1003. if (job.jobStatus.isTerminal())
  1004. {
  1005. remainingJobs--;
  1006. completedJobCount++;
  1007. if (remainingJobs == 0)
  1008. {
  1009. terminate(BroadcastState.COMPLETED);
  1010. }
  1011. else if (getActiveJobCount() == 0)
  1012. {
  1013. if (state == BroadcastState.CANCELING) setState(BroadcastState.CANCELED);
  1014. else if (state == BroadcastState.PAUSING) setState(BroadcastState.PAUSED);
  1015. }
  1016. }
  1017. */
  1018. }
  1019. /**
  1020. * Sets jobStatus in job, and post job report.
  1021. * Optionally reschedules job.
  1022. * If no rescheduling, then decrement number of remainingJobs,
  1023. * @param job
  1024. * @param rescheduleTimeMS - reschedule time in milliseconds (-1 means do not reschedule).
  1025. */
  1026. protected void postJobStatus(Job job, long rescheduleTimeMS)
  1027. {
  1028. //postJobStatus(job);
  1029. logJobCount("Enering postJobStatus");
  1030. myLogger.debug(job.toString() + ": rescheduleTimeMS " + rescheduleTimeMS);
  1031. if (postBack != null)
  1032. {
  1033. JobReport report = mkJobReport();
  1034. report.initBase(job, broadcastId, launchRecordId, activityRecordIdParamName, jobReportRootNodeName);
  1035. report.init(job);
  1036. postBack.queueReport(report.toString());
  1037. }
  1038. //if (job.jobStatus.isTerminal())
  1039. if (rescheduleTimeMS < 0
  1040. // No more rescheduling on cancel, expire, or pause
  1041. || state == BroadcastState.CANCELING
  1042. || state == BroadcastState.CANCELED
  1043. || state == BroadcastState.EXPIRED
  1044. || state == BroadcastState.PAUSED
  1045. || state == BroadcastState.PAUSING
  1046. )
  1047. {
  1048. //remainingJobs--;
  1049. completedJobCount++;
  1050. logJobCount("Completed a job");
  1051. if (getRemainingJobCount() == 0)
  1052. {
  1053. terminate(BroadcastState.COMPLETED);
  1054. }
  1055. else if (getActiveJobCount() == 0)
  1056. {
  1057. if (state == BroadcastState.CANCELING) setState(BroadcastState.CANCELED);
  1058. else if (state == BroadcastState.PAUSING) setState(BroadcastState.PAUSED);
  1059. }
  1060. }
  1061. else if (rescheduleTimeMS == 0)
  1062. {
  1063. addJob(job);
  1064. effectiveJobCount++;
  1065. logJobCount("Added a job to queue");
  1066. }
  1067. else if (rescheduleTimeMS > 0)
  1068. {
  1069. rescheduleJob(job, rescheduleTimeMS);
  1070. }
  1071. }
  1072. /**
  1073. * Logs effectiveJobCount, completedJobCount, readyQueue.size(),
  1074. * active job count, and total which recipientList.size()
  1075. * Job statistics are collected by length of readyQueue, completedJobCount,
  1076. * and effectiveJobCount.
  1077. */
  1078. private void logJobCount(String title)
  1079. {
  1080. myLogger.debug(String.format("%s: completed: %d, active: %d, ready: %d, total jobs: %d, remaining %d, effectiveJobCount %d",
  1081. title,
  1082. completedJobCount,
  1083. getActiveJobCount(),
  1084. readyQueue.size(),
  1085. recipientList.size(),
  1086. getRemainingJobCount(),
  1087. effectiveJobCount
  1088. ));
  1089. }
  1090. /**
  1091. * Number of jobs to be completed.
  1092. * Computed from effectiveJobCount and completedJobCount
  1093. * @return
  1094. */
  1095. private int getRemainingJobCount()
  1096. {
  1097. return effectiveJobCount - completedJobCount;
  1098. }
  1099. public ScheduledFuture<?> rescheduleJob(final Job job, long rescheduleTimeMS)
  1100. {
  1101. Runnable r = new Runnable() { public void run() { addJob(job);}};
  1102. return scheduler.schedule(r, rescheduleTimeMS, TimeUnit.MILLISECONDS);
  1103. }
  1104. public BroadcastState getState()
  1105. {
  1106. return state;
  1107. }
  1108. public int getReadyJobCount()
  1109. {
  1110. switch (state)
  1111. {
  1112. case RUNNING:
  1113. case PAUSING:
  1114. case PAUSED:
  1115. return readyQueue.size();
  1116. default:
  1117. return 0;
  1118. }
  1119. }
  1120. public int getCompletedJobCount()
  1121. {
  1122. return completedJobCount;
  1123. }
  1124. protected void setServiceThreadPoolsize(int serviceThreadPoolSize)
  1125. {
  1126. this.serviceThreadPoolSize = serviceThreadPoolSize;
  1127. }
  1128. }