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.

725 lines
19 KiB

4 years ago
  1. /**
  2. * Support for concurrent task management and synchronization in web
  3. * applications.
  4. *
  5. * @author Dave Longley
  6. * @author David I. Lehn <dlehn@digitalbazaar.com>
  7. *
  8. * Copyright (c) 2009-2013 Digital Bazaar, Inc.
  9. */
  10. var forge = require('./forge');
  11. require('./debug');
  12. require('./log');
  13. require('./util');
  14. // logging category
  15. var cat = 'forge.task';
  16. // verbose level
  17. // 0: off, 1: a little, 2: a whole lot
  18. // Verbose debug logging is surrounded by a level check to avoid the
  19. // performance issues with even calling the logging code regardless if it
  20. // is actually logged. For performance reasons this should not be set to 2
  21. // for production use.
  22. // ex: if(sVL >= 2) forge.log.verbose(....)
  23. var sVL = 0;
  24. // track tasks for debugging
  25. var sTasks = {};
  26. var sNextTaskId = 0;
  27. // debug access
  28. forge.debug.set(cat, 'tasks', sTasks);
  29. // a map of task type to task queue
  30. var sTaskQueues = {};
  31. // debug access
  32. forge.debug.set(cat, 'queues', sTaskQueues);
  33. // name for unnamed tasks
  34. var sNoTaskName = '?';
  35. // maximum number of doNext() recursions before a context swap occurs
  36. // FIXME: might need to tweak this based on the browser
  37. var sMaxRecursions = 30;
  38. // time slice for doing tasks before a context swap occurs
  39. // FIXME: might need to tweak this based on the browser
  40. var sTimeSlice = 20;
  41. /**
  42. * Task states.
  43. *
  44. * READY: ready to start processing
  45. * RUNNING: task or a subtask is running
  46. * BLOCKED: task is waiting to acquire N permits to continue
  47. * SLEEPING: task is sleeping for a period of time
  48. * DONE: task is done
  49. * ERROR: task has an error
  50. */
  51. var READY = 'ready';
  52. var RUNNING = 'running';
  53. var BLOCKED = 'blocked';
  54. var SLEEPING = 'sleeping';
  55. var DONE = 'done';
  56. var ERROR = 'error';
  57. /**
  58. * Task actions. Used to control state transitions.
  59. *
  60. * STOP: stop processing
  61. * START: start processing tasks
  62. * BLOCK: block task from continuing until 1 or more permits are released
  63. * UNBLOCK: release one or more permits
  64. * SLEEP: sleep for a period of time
  65. * WAKEUP: wakeup early from SLEEPING state
  66. * CANCEL: cancel further tasks
  67. * FAIL: a failure occured
  68. */
  69. var STOP = 'stop';
  70. var START = 'start';
  71. var BLOCK = 'block';
  72. var UNBLOCK = 'unblock';
  73. var SLEEP = 'sleep';
  74. var WAKEUP = 'wakeup';
  75. var CANCEL = 'cancel';
  76. var FAIL = 'fail';
  77. /**
  78. * State transition table.
  79. *
  80. * nextState = sStateTable[currentState][action]
  81. */
  82. var sStateTable = {};
  83. sStateTable[READY] = {};
  84. sStateTable[READY][STOP] = READY;
  85. sStateTable[READY][START] = RUNNING;
  86. sStateTable[READY][CANCEL] = DONE;
  87. sStateTable[READY][FAIL] = ERROR;
  88. sStateTable[RUNNING] = {};
  89. sStateTable[RUNNING][STOP] = READY;
  90. sStateTable[RUNNING][START] = RUNNING;
  91. sStateTable[RUNNING][BLOCK] = BLOCKED;
  92. sStateTable[RUNNING][UNBLOCK] = RUNNING;
  93. sStateTable[RUNNING][SLEEP] = SLEEPING;
  94. sStateTable[RUNNING][WAKEUP] = RUNNING;
  95. sStateTable[RUNNING][CANCEL] = DONE;
  96. sStateTable[RUNNING][FAIL] = ERROR;
  97. sStateTable[BLOCKED] = {};
  98. sStateTable[BLOCKED][STOP] = BLOCKED;
  99. sStateTable[BLOCKED][START] = BLOCKED;
  100. sStateTable[BLOCKED][BLOCK] = BLOCKED;
  101. sStateTable[BLOCKED][UNBLOCK] = BLOCKED;
  102. sStateTable[BLOCKED][SLEEP] = BLOCKED;
  103. sStateTable[BLOCKED][WAKEUP] = BLOCKED;
  104. sStateTable[BLOCKED][CANCEL] = DONE;
  105. sStateTable[BLOCKED][FAIL] = ERROR;
  106. sStateTable[SLEEPING] = {};
  107. sStateTable[SLEEPING][STOP] = SLEEPING;
  108. sStateTable[SLEEPING][START] = SLEEPING;
  109. sStateTable[SLEEPING][BLOCK] = SLEEPING;
  110. sStateTable[SLEEPING][UNBLOCK] = SLEEPING;
  111. sStateTable[SLEEPING][SLEEP] = SLEEPING;
  112. sStateTable[SLEEPING][WAKEUP] = SLEEPING;
  113. sStateTable[SLEEPING][CANCEL] = DONE;
  114. sStateTable[SLEEPING][FAIL] = ERROR;
  115. sStateTable[DONE] = {};
  116. sStateTable[DONE][STOP] = DONE;
  117. sStateTable[DONE][START] = DONE;
  118. sStateTable[DONE][BLOCK] = DONE;
  119. sStateTable[DONE][UNBLOCK] = DONE;
  120. sStateTable[DONE][SLEEP] = DONE;
  121. sStateTable[DONE][WAKEUP] = DONE;
  122. sStateTable[DONE][CANCEL] = DONE;
  123. sStateTable[DONE][FAIL] = ERROR;
  124. sStateTable[ERROR] = {};
  125. sStateTable[ERROR][STOP] = ERROR;
  126. sStateTable[ERROR][START] = ERROR;
  127. sStateTable[ERROR][BLOCK] = ERROR;
  128. sStateTable[ERROR][UNBLOCK] = ERROR;
  129. sStateTable[ERROR][SLEEP] = ERROR;
  130. sStateTable[ERROR][WAKEUP] = ERROR;
  131. sStateTable[ERROR][CANCEL] = ERROR;
  132. sStateTable[ERROR][FAIL] = ERROR;
  133. /**
  134. * Creates a new task.
  135. *
  136. * @param options options for this task
  137. * run: the run function for the task (required)
  138. * name: the run function for the task (optional)
  139. * parent: parent of this task (optional)
  140. *
  141. * @return the empty task.
  142. */
  143. var Task = function(options) {
  144. // task id
  145. this.id = -1;
  146. // task name
  147. this.name = options.name || sNoTaskName;
  148. // task has no parent
  149. this.parent = options.parent || null;
  150. // save run function
  151. this.run = options.run;
  152. // create a queue of subtasks to run
  153. this.subtasks = [];
  154. // error flag
  155. this.error = false;
  156. // state of the task
  157. this.state = READY;
  158. // number of times the task has been blocked (also the number
  159. // of permits needed to be released to continue running)
  160. this.blocks = 0;
  161. // timeout id when sleeping
  162. this.timeoutId = null;
  163. // no swap time yet
  164. this.swapTime = null;
  165. // no user data
  166. this.userData = null;
  167. // initialize task
  168. // FIXME: deal with overflow
  169. this.id = sNextTaskId++;
  170. sTasks[this.id] = this;
  171. if(sVL >= 1) {
  172. forge.log.verbose(cat, '[%s][%s] init', this.id, this.name, this);
  173. }
  174. };
  175. /**
  176. * Logs debug information on this task and the system state.
  177. */
  178. Task.prototype.debug = function(msg) {
  179. msg = msg || '';
  180. forge.log.debug(cat, msg,
  181. '[%s][%s] task:', this.id, this.name, this,
  182. 'subtasks:', this.subtasks.length,
  183. 'queue:', sTaskQueues);
  184. };
  185. /**
  186. * Adds a subtask to run after task.doNext() or task.fail() is called.
  187. *
  188. * @param name human readable name for this task (optional).
  189. * @param subrun a function to run that takes the current task as
  190. * its first parameter.
  191. *
  192. * @return the current task (useful for chaining next() calls).
  193. */
  194. Task.prototype.next = function(name, subrun) {
  195. // juggle parameters if it looks like no name is given
  196. if(typeof(name) === 'function') {
  197. subrun = name;
  198. // inherit parent's name
  199. name = this.name;
  200. }
  201. // create subtask, set parent to this task, propagate callbacks
  202. var subtask = new Task({
  203. run: subrun,
  204. name: name,
  205. parent: this
  206. });
  207. // start subtasks running
  208. subtask.state = RUNNING;
  209. subtask.type = this.type;
  210. subtask.successCallback = this.successCallback || null;
  211. subtask.failureCallback = this.failureCallback || null;
  212. // queue a new subtask
  213. this.subtasks.push(subtask);
  214. return this;
  215. };
  216. /**
  217. * Adds subtasks to run in parallel after task.doNext() or task.fail()
  218. * is called.
  219. *
  220. * @param name human readable name for this task (optional).
  221. * @param subrun functions to run that take the current task as
  222. * their first parameter.
  223. *
  224. * @return the current task (useful for chaining next() calls).
  225. */
  226. Task.prototype.parallel = function(name, subrun) {
  227. // juggle parameters if it looks like no name is given
  228. if(forge.util.isArray(name)) {
  229. subrun = name;
  230. // inherit parent's name
  231. name = this.name;
  232. }
  233. // Wrap parallel tasks in a regular task so they are started at the
  234. // proper time.
  235. return this.next(name, function(task) {
  236. // block waiting for subtasks
  237. var ptask = task;
  238. ptask.block(subrun.length);
  239. // we pass the iterator from the loop below as a parameter
  240. // to a function because it is otherwise included in the
  241. // closure and changes as the loop changes -- causing i
  242. // to always be set to its highest value
  243. var startParallelTask = function(pname, pi) {
  244. forge.task.start({
  245. type: pname,
  246. run: function(task) {
  247. subrun[pi](task);
  248. },
  249. success: function(task) {
  250. ptask.unblock();
  251. },
  252. failure: function(task) {
  253. ptask.unblock();
  254. }
  255. });
  256. };
  257. for(var i = 0; i < subrun.length; i++) {
  258. // Type must be unique so task starts in parallel:
  259. // name + private string + task id + sub-task index
  260. // start tasks in parallel and unblock when the finish
  261. var pname = name + '__parallel-' + task.id + '-' + i;
  262. var pi = i;
  263. startParallelTask(pname, pi);
  264. }
  265. });
  266. };
  267. /**
  268. * Stops a running task.
  269. */
  270. Task.prototype.stop = function() {
  271. this.state = sStateTable[this.state][STOP];
  272. };
  273. /**
  274. * Starts running a task.
  275. */
  276. Task.prototype.start = function() {
  277. this.error = false;
  278. this.state = sStateTable[this.state][START];
  279. // try to restart
  280. if(this.state === RUNNING) {
  281. this.start = new Date();
  282. this.run(this);
  283. runNext(this, 0);
  284. }
  285. };
  286. /**
  287. * Blocks a task until it one or more permits have been released. The
  288. * task will not resume until the requested number of permits have
  289. * been released with call(s) to unblock().
  290. *
  291. * @param n number of permits to wait for(default: 1).
  292. */
  293. Task.prototype.block = function(n) {
  294. n = typeof(n) === 'undefined' ? 1 : n;
  295. this.blocks += n;
  296. if(this.blocks > 0) {
  297. this.state = sStateTable[this.state][BLOCK];
  298. }
  299. };
  300. /**
  301. * Releases a permit to unblock a task. If a task was blocked by
  302. * requesting N permits via block(), then it will only continue
  303. * running once enough permits have been released via unblock() calls.
  304. *
  305. * If multiple processes need to synchronize with a single task then
  306. * use a condition variable (see forge.task.createCondition). It is
  307. * an error to unblock a task more times than it has been blocked.
  308. *
  309. * @param n number of permits to release (default: 1).
  310. *
  311. * @return the current block count (task is unblocked when count is 0)
  312. */
  313. Task.prototype.unblock = function(n) {
  314. n = typeof(n) === 'undefined' ? 1 : n;
  315. this.blocks -= n;
  316. if(this.blocks === 0 && this.state !== DONE) {
  317. this.state = RUNNING;
  318. runNext(this, 0);
  319. }
  320. return this.blocks;
  321. };
  322. /**
  323. * Sleep for a period of time before resuming tasks.
  324. *
  325. * @param n number of milliseconds to sleep (default: 0).
  326. */
  327. Task.prototype.sleep = function(n) {
  328. n = typeof(n) === 'undefined' ? 0 : n;
  329. this.state = sStateTable[this.state][SLEEP];
  330. var self = this;
  331. this.timeoutId = setTimeout(function() {
  332. self.timeoutId = null;
  333. self.state = RUNNING;
  334. runNext(self, 0);
  335. }, n);
  336. };
  337. /**
  338. * Waits on a condition variable until notified. The next task will
  339. * not be scheduled until notification. A condition variable can be
  340. * created with forge.task.createCondition().
  341. *
  342. * Once cond.notify() is called, the task will continue.
  343. *
  344. * @param cond the condition variable to wait on.
  345. */
  346. Task.prototype.wait = function(cond) {
  347. cond.wait(this);
  348. };
  349. /**
  350. * If sleeping, wakeup and continue running tasks.
  351. */
  352. Task.prototype.wakeup = function() {
  353. if(this.state === SLEEPING) {
  354. cancelTimeout(this.timeoutId);
  355. this.timeoutId = null;
  356. this.state = RUNNING;
  357. runNext(this, 0);
  358. }
  359. };
  360. /**
  361. * Cancel all remaining subtasks of this task.
  362. */
  363. Task.prototype.cancel = function() {
  364. this.state = sStateTable[this.state][CANCEL];
  365. // remove permits needed
  366. this.permitsNeeded = 0;
  367. // cancel timeouts
  368. if(this.timeoutId !== null) {
  369. cancelTimeout(this.timeoutId);
  370. this.timeoutId = null;
  371. }
  372. // remove subtasks
  373. this.subtasks = [];
  374. };
  375. /**
  376. * Finishes this task with failure and sets error flag. The entire
  377. * task will be aborted unless the next task that should execute
  378. * is passed as a parameter. This allows levels of subtasks to be
  379. * skipped. For instance, to abort only this tasks's subtasks, then
  380. * call fail(task.parent). To abort this task's subtasks and its
  381. * parent's subtasks, call fail(task.parent.parent). To abort
  382. * all tasks and simply call the task callback, call fail() or
  383. * fail(null).
  384. *
  385. * The task callback (success or failure) will always, eventually, be
  386. * called.
  387. *
  388. * @param next the task to continue at, or null to abort entirely.
  389. */
  390. Task.prototype.fail = function(next) {
  391. // set error flag
  392. this.error = true;
  393. // finish task
  394. finish(this, true);
  395. if(next) {
  396. // propagate task info
  397. next.error = this.error;
  398. next.swapTime = this.swapTime;
  399. next.userData = this.userData;
  400. // do next task as specified
  401. runNext(next, 0);
  402. } else {
  403. if(this.parent !== null) {
  404. // finish root task (ensures it is removed from task queue)
  405. var parent = this.parent;
  406. while(parent.parent !== null) {
  407. // propagate task info
  408. parent.error = this.error;
  409. parent.swapTime = this.swapTime;
  410. parent.userData = this.userData;
  411. parent = parent.parent;
  412. }
  413. finish(parent, true);
  414. }
  415. // call failure callback if one exists
  416. if(this.failureCallback) {
  417. this.failureCallback(this);
  418. }
  419. }
  420. };
  421. /**
  422. * Asynchronously start a task.
  423. *
  424. * @param task the task to start.
  425. */
  426. var start = function(task) {
  427. task.error = false;
  428. task.state = sStateTable[task.state][START];
  429. setTimeout(function() {
  430. if(task.state === RUNNING) {
  431. task.swapTime = +new Date();
  432. task.run(task);
  433. runNext(task, 0);
  434. }
  435. }, 0);
  436. };
  437. /**
  438. * Run the next subtask or finish this task.
  439. *
  440. * @param task the task to process.
  441. * @param recurse the recursion count.
  442. */
  443. var runNext = function(task, recurse) {
  444. // get time since last context swap (ms), if enough time has passed set
  445. // swap to true to indicate that doNext was performed asynchronously
  446. // also, if recurse is too high do asynchronously
  447. var swap =
  448. (recurse > sMaxRecursions) ||
  449. (+new Date() - task.swapTime) > sTimeSlice;
  450. var doNext = function(recurse) {
  451. recurse++;
  452. if(task.state === RUNNING) {
  453. if(swap) {
  454. // update swap time
  455. task.swapTime = +new Date();
  456. }
  457. if(task.subtasks.length > 0) {
  458. // run next subtask
  459. var subtask = task.subtasks.shift();
  460. subtask.error = task.error;
  461. subtask.swapTime = task.swapTime;
  462. subtask.userData = task.userData;
  463. subtask.run(subtask);
  464. if(!subtask.error) {
  465. runNext(subtask, recurse);
  466. }
  467. } else {
  468. finish(task);
  469. if(!task.error) {
  470. // chain back up and run parent
  471. if(task.parent !== null) {
  472. // propagate task info
  473. task.parent.error = task.error;
  474. task.parent.swapTime = task.swapTime;
  475. task.parent.userData = task.userData;
  476. // no subtasks left, call run next subtask on parent
  477. runNext(task.parent, recurse);
  478. }
  479. }
  480. }
  481. }
  482. };
  483. if(swap) {
  484. // we're swapping, so run asynchronously
  485. setTimeout(doNext, 0);
  486. } else {
  487. // not swapping, so run synchronously
  488. doNext(recurse);
  489. }
  490. };
  491. /**
  492. * Finishes a task and looks for the next task in the queue to start.
  493. *
  494. * @param task the task to finish.
  495. * @param suppressCallbacks true to suppress callbacks.
  496. */
  497. var finish = function(task, suppressCallbacks) {
  498. // subtask is now done
  499. task.state = DONE;
  500. delete sTasks[task.id];
  501. if(sVL >= 1) {
  502. forge.log.verbose(cat, '[%s][%s] finish',
  503. task.id, task.name, task);
  504. }
  505. // only do queue processing for root tasks
  506. if(task.parent === null) {
  507. // report error if queue is missing
  508. if(!(task.type in sTaskQueues)) {
  509. forge.log.error(cat,
  510. '[%s][%s] task queue missing [%s]',
  511. task.id, task.name, task.type);
  512. } else if(sTaskQueues[task.type].length === 0) {
  513. // report error if queue is empty
  514. forge.log.error(cat,
  515. '[%s][%s] task queue empty [%s]',
  516. task.id, task.name, task.type);
  517. } else if(sTaskQueues[task.type][0] !== task) {
  518. // report error if this task isn't the first in the queue
  519. forge.log.error(cat,
  520. '[%s][%s] task not first in queue [%s]',
  521. task.id, task.name, task.type);
  522. } else {
  523. // remove ourselves from the queue
  524. sTaskQueues[task.type].shift();
  525. // clean up queue if it is empty
  526. if(sTaskQueues[task.type].length === 0) {
  527. if(sVL >= 1) {
  528. forge.log.verbose(cat, '[%s][%s] delete queue [%s]',
  529. task.id, task.name, task.type);
  530. }
  531. /* Note: Only a task can delete a queue of its own type. This
  532. is used as a way to synchronize tasks. If a queue for a certain
  533. task type exists, then a task of that type is running.
  534. */
  535. delete sTaskQueues[task.type];
  536. } else {
  537. // dequeue the next task and start it
  538. if(sVL >= 1) {
  539. forge.log.verbose(cat,
  540. '[%s][%s] queue start next [%s] remain:%s',
  541. task.id, task.name, task.type,
  542. sTaskQueues[task.type].length);
  543. }
  544. sTaskQueues[task.type][0].start();
  545. }
  546. }
  547. if(!suppressCallbacks) {
  548. // call final callback if one exists
  549. if(task.error && task.failureCallback) {
  550. task.failureCallback(task);
  551. } else if(!task.error && task.successCallback) {
  552. task.successCallback(task);
  553. }
  554. }
  555. }
  556. };
  557. /* Tasks API */
  558. module.exports = forge.task = forge.task || {};
  559. /**
  560. * Starts a new task that will run the passed function asynchronously.
  561. *
  562. * In order to finish the task, either task.doNext() or task.fail()
  563. * *must* be called.
  564. *
  565. * The task must have a type (a string identifier) that can be used to
  566. * synchronize it with other tasks of the same type. That type can also
  567. * be used to cancel tasks that haven't started yet.
  568. *
  569. * To start a task, the following object must be provided as a parameter
  570. * (each function takes a task object as its first parameter):
  571. *
  572. * {
  573. * type: the type of task.
  574. * run: the function to run to execute the task.
  575. * success: a callback to call when the task succeeds (optional).
  576. * failure: a callback to call when the task fails (optional).
  577. * }
  578. *
  579. * @param options the object as described above.
  580. */
  581. forge.task.start = function(options) {
  582. // create a new task
  583. var task = new Task({
  584. run: options.run,
  585. name: options.name || sNoTaskName
  586. });
  587. task.type = options.type;
  588. task.successCallback = options.success || null;
  589. task.failureCallback = options.failure || null;
  590. // append the task onto the appropriate queue
  591. if(!(task.type in sTaskQueues)) {
  592. if(sVL >= 1) {
  593. forge.log.verbose(cat, '[%s][%s] create queue [%s]',
  594. task.id, task.name, task.type);
  595. }
  596. // create the queue with the new task
  597. sTaskQueues[task.type] = [task];
  598. start(task);
  599. } else {
  600. // push the task onto the queue, it will be run after a task
  601. // with the same type completes
  602. sTaskQueues[options.type].push(task);
  603. }
  604. };
  605. /**
  606. * Cancels all tasks of the given type that haven't started yet.
  607. *
  608. * @param type the type of task to cancel.
  609. */
  610. forge.task.cancel = function(type) {
  611. // find the task queue
  612. if(type in sTaskQueues) {
  613. // empty all but the current task from the queue
  614. sTaskQueues[type] = [sTaskQueues[type][0]];
  615. }
  616. };
  617. /**
  618. * Creates a condition variable to synchronize tasks. To make a task wait
  619. * on the condition variable, call task.wait(condition). To notify all
  620. * tasks that are waiting, call condition.notify().
  621. *
  622. * @return the condition variable.
  623. */
  624. forge.task.createCondition = function() {
  625. var cond = {
  626. // all tasks that are blocked
  627. tasks: {}
  628. };
  629. /**
  630. * Causes the given task to block until notify is called. If the task
  631. * is already waiting on this condition then this is a no-op.
  632. *
  633. * @param task the task to cause to wait.
  634. */
  635. cond.wait = function(task) {
  636. // only block once
  637. if(!(task.id in cond.tasks)) {
  638. task.block();
  639. cond.tasks[task.id] = task;
  640. }
  641. };
  642. /**
  643. * Notifies all waiting tasks to wake up.
  644. */
  645. cond.notify = function() {
  646. // since unblock() will run the next task from here, make sure to
  647. // clear the condition's blocked task list before unblocking
  648. var tmp = cond.tasks;
  649. cond.tasks = {};
  650. for(var id in tmp) {
  651. tmp[id].unblock();
  652. }
  653. };
  654. return cond;
  655. };