toolkit/modules/DeferredTask.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 "use strict";
     9 this.EXPORTED_SYMBOLS = [
    10   "DeferredTask",
    11 ];
    13 /**
    14  * Sets up a function or an asynchronous task whose execution can be triggered
    15  * after a defined delay.  Multiple attempts to run the task before the delay
    16  * has passed are coalesced.  The task cannot be re-entered while running, but
    17  * can be executed again after a previous run finished.
    18  *
    19  * A common use case occurs when a data structure should be saved into a file
    20  * every time the data changes, using asynchronous calls, and multiple changes
    21  * to the data may happen within a short time:
    22  *
    23  *   let saveDeferredTask = new DeferredTask(function* () {
    24  *     yield OS.File.writeAtomic(...);
    25  *     // Any uncaught exception will be reported.
    26  *   }, 2000);
    27  *
    28  *   // The task is ready, but will not be executed until requested.
    29  *
    30  * The "arm" method can be used to start the internal timer that will result in
    31  * the eventual execution of the task.  Multiple attempts to arm the timer don't
    32  * introduce further delays:
    33  *
    34  *   saveDeferredTask.arm();
    35  *
    36  *   // The task will be executed in 2 seconds from now.
    37  *
    38  *   yield waitOneSecond();
    39  *   saveDeferredTask.arm();
    40  *
    41  *   // The task will be executed in 1 second from now.
    42  *
    43  * The timer can be disarmed to reset the delay, or just to cancel execution:
    44  *
    45  *   saveDeferredTask.disarm();
    46  *   saveDeferredTask.arm();
    47  *
    48  *   // The task will be executed in 2 seconds from now.
    49  *
    50  * When the internal timer fires and the execution of the task starts, the task
    51  * cannot be canceled anymore.  It is however possible to arm the timer again
    52  * during the execution of the task, in which case the task will need to finish
    53  * before the timer is started again, thus guaranteeing a time of inactivity
    54  * between executions that is at least equal to the provided delay.
    55  *
    56  * The "finalize" method can be used to ensure that the task terminates
    57  * properly.  The promise it returns is resolved only after the last execution
    58  * of the task is finished.  To guarantee that the task is executed for the
    59  * last time, the method prevents any attempt to arm the timer again.
    60  *
    61  * If the timer is already armed when the "finalize" method is called, then the
    62  * task is executed immediately.  If the task was already running at this point,
    63  * then one last execution from start to finish will happen again, immediately
    64  * after the current execution terminates.  If the timer is not armed, the
    65  * "finalize" method only ensures that any running task terminates.
    66  *
    67  * For example, during shutdown, you may want to ensure that any pending write
    68  * is processed, using the latest version of the data if the timer is armed:
    69  *
    70  *   AsyncShutdown.profileBeforeChange.addBlocker(
    71  *     "Example service: shutting down",
    72  *     () => saveDeferredTask.finalize()
    73  *   );
    74  *
    75  * Instead, if you are going to delete the saved data from disk anyways, you
    76  * might as well prevent any pending write from starting, while still ensuring
    77  * that any write that is currently in progress terminates, so that the file is
    78  * not in use anymore:
    79  *
    80  *   saveDeferredTask.disarm();
    81  *   saveDeferredTask.finalize().then(() => OS.File.remove(...))
    82  *                              .then(null, Components.utils.reportError);
    83  */
    85 ////////////////////////////////////////////////////////////////////////////////
    86 //// Globals
    88 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
    90 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    92 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    93                                   "resource://gre/modules/Promise.jsm");
    94 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    95                                   "resource://gre/modules/Task.jsm");
    97 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
    98                                      "initWithCallback");
   100 ////////////////////////////////////////////////////////////////////////////////
   101 //// DeferredTask
   103 /**
   104  * Sets up a task whose execution can be triggered after a delay.
   105  *
   106  * @param aTaskFn
   107  *        Function or generator function to execute.  This argument is passed to
   108  *        the "Task.spawn" method every time the task should be executed.  This
   109  *        task is never re-entered while running.
   110  * @param aDelayMs
   111  *        Time between executions, in milliseconds.  Multiple attempts to run
   112  *        the task before the delay has passed are coalesced.  This time of
   113  *        inactivity is guaranteed to pass between multiple executions of the
   114  *        task, except on finalization, when the task may restart immediately
   115  *        after the previous execution finished.
   116  */
   117 this.DeferredTask = function (aTaskFn, aDelayMs) {
   118   this._taskFn = aTaskFn;
   119   this._delayMs = aDelayMs;
   120 }
   122 this.DeferredTask.prototype = {
   123   /**
   124    * Function or generator function to execute.
   125    */
   126   _taskFn: null,
   128   /**
   129    * Time between executions, in milliseconds.
   130    */
   131   _delayMs: null,
   133   /**
   134    * Indicates whether the task is currently requested to start again later,
   135    * regardless of whether it is currently running.
   136    */
   137   get isArmed() this._armed,
   138   _armed: false,
   140   /**
   141    * Indicates whether the task is currently running.  This is always true when
   142    * read from code inside the task function, but can also be true when read
   143    * from external code, in case the task is an asynchronous generator function.
   144    */
   145   get isRunning() !!this._runningPromise,
   147   /**
   148    * Promise resolved when the current execution of the task terminates, or null
   149    * if the task is not currently running.
   150    */
   151   _runningPromise: null,
   153   /**
   154    * nsITimer used for triggering the task after a delay, or null in case the
   155    * task is running or there is no task scheduled for execution.
   156    */
   157   _timer: null,
   159   /**
   160    * Actually starts the timer with the delay specified on construction.
   161    */
   162   _startTimer: function ()
   163   {
   164     this._timer = new Timer(this._timerCallback.bind(this), this._delayMs,
   165                             Ci.nsITimer.TYPE_ONE_SHOT);
   166   },
   168   /**
   169    * Requests the execution of the task after the delay specified on
   170    * construction.  Multiple calls don't introduce further delays.  If the task
   171    * is running, the delay will start when the current execution finishes.
   172    *
   173    * The task will always be executed on a different tick of the event loop,
   174    * even if the delay specified on construction is zero.  Multiple "arm" calls
   175    * within the same tick of the event loop are guaranteed to result in a single
   176    * execution of the task.
   177    *
   178    * @note By design, this method doesn't provide a way for the caller to detect
   179    *       when the next execution terminates, or collect a result.  In fact,
   180    *       doing that would often result in duplicate processing or logging.  If
   181    *       a special operation or error logging is needed on completion, it can
   182    *       be better handled from within the task itself, for example using a
   183    *       try/catch/finally clause in the task.  The "finalize" method can be
   184    *       used in the common case of waiting for completion on shutdown.
   185    */
   186   arm: function ()
   187   {
   188     if (this._finalized) {
   189       throw new Error("Unable to arm timer, the object has been finalized.");
   190     }
   192     this._armed = true;
   194     // In case the timer callback is running, do not create the timer now,
   195     // because this will be handled by the timer callback itself.  Also, the
   196     // timer is not restarted in case it is already running.
   197     if (!this._runningPromise && !this._timer) {
   198       this._startTimer();
   199     }
   200   },
   202   /**
   203    * Cancels any request for a delayed the execution of the task, though the
   204    * task itself cannot be canceled in case it is already running.
   205    *
   206    * This method stops any currently running timer, thus the delay will restart
   207    * from its original value in case the "arm" method is called again.
   208    */
   209   disarm: function () {
   210     this._armed = false;
   211     if (this._timer) {
   212       // Calling the "cancel" method and discarding the timer reference makes
   213       // sure that the timer callback will not be called later, even if the
   214       // timer thread has already posted the timer event on the main thread.
   215       this._timer.cancel();
   216       this._timer = null;
   217     }
   218   },
   220   /**
   221    * Ensures that any pending task is executed from start to finish, while
   222    * preventing any attempt to arm the timer again.
   223    *
   224    * - If the task is running and the timer is armed, then one last execution
   225    *   from start to finish will happen again, immediately after the current
   226    *   execution terminates, then the returned promise will be resolved.
   227    * - If the task is running and the timer is not armed, the returned promise
   228    *   will be resolved when the current execution terminates.
   229    * - If the task is not running and the timer is armed, then the task is
   230    *   started immediately, and the returned promise resolves when the new
   231    *   execution terminates.
   232    * - If the task is not running and the timer is not armed, the method returns
   233    *   a resolved promise.
   234    *
   235    * @return {Promise}
   236    * @resolves After the last execution of the task is finished.
   237    * @rejects Never.
   238    */
   239   finalize: function () {
   240     if (this._finalized) {
   241       throw new Error("The object has been already finalized.");
   242     }
   243     this._finalized = true;
   245     // If the timer is armed, it means that the task is not running but it is
   246     // scheduled for execution.  Cancel the timer and run the task immediately.
   247     if (this._timer) {
   248       this.disarm();
   249       this._timerCallback();
   250     }
   252     // Wait for the operation to be completed, or resolve immediately.
   253     if (this._runningPromise) {
   254       return this._runningPromise;
   255     }
   256     return Promise.resolve();
   257   },
   258   _finalized: false,
   260   /**
   261    * Timer callback used to run the delayed task.
   262    */
   263   _timerCallback: function ()
   264   {
   265     let runningDeferred = Promise.defer();
   267     // All these state changes must occur at the same time directly inside the
   268     // timer callback, to prevent race conditions and to ensure that all the
   269     // methods behave consistently even if called from inside the task.  This
   270     // means that the assignment of "this._runningPromise" must complete before
   271     // the task gets a chance to start.
   272     this._timer = null;
   273     this._armed = false;
   274     this._runningPromise = runningDeferred.promise;
   276     runningDeferred.resolve(Task.spawn(function () {
   277       // Execute the provided function asynchronously.
   278       yield Task.spawn(this._taskFn).then(null, Cu.reportError);
   280       // Now that the task has finished, we check the state of the object to
   281       // determine if we should restart the task again.
   282       if (this._armed) {
   283         if (!this._finalized) {
   284           this._startTimer();
   285         } else {
   286           // Execute the task again immediately, for the last time.  The isArmed
   287           // property should return false while the task is running, and should
   288           // remain false after the last execution terminates.
   289           this._armed = false;
   290           yield Task.spawn(this._taskFn).then(null, Cu.reportError);
   291         }
   292       }
   294       // Indicate that the execution of the task has finished.  This happens
   295       // synchronously with the previous state changes in the function.
   296       this._runningPromise = null;
   297     }.bind(this)).then(null, Cu.reportError));
   298   },
   299 };

mercurial