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

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

mercurial