1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/DeferredTask.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,299 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = [ 1.13 + "DeferredTask", 1.14 +]; 1.15 + 1.16 +/** 1.17 + * Sets up a function or an asynchronous task whose execution can be triggered 1.18 + * after a defined delay. Multiple attempts to run the task before the delay 1.19 + * has passed are coalesced. The task cannot be re-entered while running, but 1.20 + * can be executed again after a previous run finished. 1.21 + * 1.22 + * A common use case occurs when a data structure should be saved into a file 1.23 + * every time the data changes, using asynchronous calls, and multiple changes 1.24 + * to the data may happen within a short time: 1.25 + * 1.26 + * let saveDeferredTask = new DeferredTask(function* () { 1.27 + * yield OS.File.writeAtomic(...); 1.28 + * // Any uncaught exception will be reported. 1.29 + * }, 2000); 1.30 + * 1.31 + * // The task is ready, but will not be executed until requested. 1.32 + * 1.33 + * The "arm" method can be used to start the internal timer that will result in 1.34 + * the eventual execution of the task. Multiple attempts to arm the timer don't 1.35 + * introduce further delays: 1.36 + * 1.37 + * saveDeferredTask.arm(); 1.38 + * 1.39 + * // The task will be executed in 2 seconds from now. 1.40 + * 1.41 + * yield waitOneSecond(); 1.42 + * saveDeferredTask.arm(); 1.43 + * 1.44 + * // The task will be executed in 1 second from now. 1.45 + * 1.46 + * The timer can be disarmed to reset the delay, or just to cancel execution: 1.47 + * 1.48 + * saveDeferredTask.disarm(); 1.49 + * saveDeferredTask.arm(); 1.50 + * 1.51 + * // The task will be executed in 2 seconds from now. 1.52 + * 1.53 + * When the internal timer fires and the execution of the task starts, the task 1.54 + * cannot be canceled anymore. It is however possible to arm the timer again 1.55 + * during the execution of the task, in which case the task will need to finish 1.56 + * before the timer is started again, thus guaranteeing a time of inactivity 1.57 + * between executions that is at least equal to the provided delay. 1.58 + * 1.59 + * The "finalize" method can be used to ensure that the task terminates 1.60 + * properly. The promise it returns is resolved only after the last execution 1.61 + * of the task is finished. To guarantee that the task is executed for the 1.62 + * last time, the method prevents any attempt to arm the timer again. 1.63 + * 1.64 + * If the timer is already armed when the "finalize" method is called, then the 1.65 + * task is executed immediately. If the task was already running at this point, 1.66 + * then one last execution from start to finish will happen again, immediately 1.67 + * after the current execution terminates. If the timer is not armed, the 1.68 + * "finalize" method only ensures that any running task terminates. 1.69 + * 1.70 + * For example, during shutdown, you may want to ensure that any pending write 1.71 + * is processed, using the latest version of the data if the timer is armed: 1.72 + * 1.73 + * AsyncShutdown.profileBeforeChange.addBlocker( 1.74 + * "Example service: shutting down", 1.75 + * () => saveDeferredTask.finalize() 1.76 + * ); 1.77 + * 1.78 + * Instead, if you are going to delete the saved data from disk anyways, you 1.79 + * might as well prevent any pending write from starting, while still ensuring 1.80 + * that any write that is currently in progress terminates, so that the file is 1.81 + * not in use anymore: 1.82 + * 1.83 + * saveDeferredTask.disarm(); 1.84 + * saveDeferredTask.finalize().then(() => OS.File.remove(...)) 1.85 + * .then(null, Components.utils.reportError); 1.86 + */ 1.87 + 1.88 +//////////////////////////////////////////////////////////////////////////////// 1.89 +//// Globals 1.90 + 1.91 +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; 1.92 + 1.93 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.94 + 1.95 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.96 + "resource://gre/modules/Promise.jsm"); 1.97 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.98 + "resource://gre/modules/Task.jsm"); 1.99 + 1.100 +const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", 1.101 + "initWithCallback"); 1.102 + 1.103 +//////////////////////////////////////////////////////////////////////////////// 1.104 +//// DeferredTask 1.105 + 1.106 +/** 1.107 + * Sets up a task whose execution can be triggered after a delay. 1.108 + * 1.109 + * @param aTaskFn 1.110 + * Function or generator function to execute. This argument is passed to 1.111 + * the "Task.spawn" method every time the task should be executed. This 1.112 + * task is never re-entered while running. 1.113 + * @param aDelayMs 1.114 + * Time between executions, in milliseconds. Multiple attempts to run 1.115 + * the task before the delay has passed are coalesced. This time of 1.116 + * inactivity is guaranteed to pass between multiple executions of the 1.117 + * task, except on finalization, when the task may restart immediately 1.118 + * after the previous execution finished. 1.119 + */ 1.120 +this.DeferredTask = function (aTaskFn, aDelayMs) { 1.121 + this._taskFn = aTaskFn; 1.122 + this._delayMs = aDelayMs; 1.123 +} 1.124 + 1.125 +this.DeferredTask.prototype = { 1.126 + /** 1.127 + * Function or generator function to execute. 1.128 + */ 1.129 + _taskFn: null, 1.130 + 1.131 + /** 1.132 + * Time between executions, in milliseconds. 1.133 + */ 1.134 + _delayMs: null, 1.135 + 1.136 + /** 1.137 + * Indicates whether the task is currently requested to start again later, 1.138 + * regardless of whether it is currently running. 1.139 + */ 1.140 + get isArmed() this._armed, 1.141 + _armed: false, 1.142 + 1.143 + /** 1.144 + * Indicates whether the task is currently running. This is always true when 1.145 + * read from code inside the task function, but can also be true when read 1.146 + * from external code, in case the task is an asynchronous generator function. 1.147 + */ 1.148 + get isRunning() !!this._runningPromise, 1.149 + 1.150 + /** 1.151 + * Promise resolved when the current execution of the task terminates, or null 1.152 + * if the task is not currently running. 1.153 + */ 1.154 + _runningPromise: null, 1.155 + 1.156 + /** 1.157 + * nsITimer used for triggering the task after a delay, or null in case the 1.158 + * task is running or there is no task scheduled for execution. 1.159 + */ 1.160 + _timer: null, 1.161 + 1.162 + /** 1.163 + * Actually starts the timer with the delay specified on construction. 1.164 + */ 1.165 + _startTimer: function () 1.166 + { 1.167 + this._timer = new Timer(this._timerCallback.bind(this), this._delayMs, 1.168 + Ci.nsITimer.TYPE_ONE_SHOT); 1.169 + }, 1.170 + 1.171 + /** 1.172 + * Requests the execution of the task after the delay specified on 1.173 + * construction. Multiple calls don't introduce further delays. If the task 1.174 + * is running, the delay will start when the current execution finishes. 1.175 + * 1.176 + * The task will always be executed on a different tick of the event loop, 1.177 + * even if the delay specified on construction is zero. Multiple "arm" calls 1.178 + * within the same tick of the event loop are guaranteed to result in a single 1.179 + * execution of the task. 1.180 + * 1.181 + * @note By design, this method doesn't provide a way for the caller to detect 1.182 + * when the next execution terminates, or collect a result. In fact, 1.183 + * doing that would often result in duplicate processing or logging. If 1.184 + * a special operation or error logging is needed on completion, it can 1.185 + * be better handled from within the task itself, for example using a 1.186 + * try/catch/finally clause in the task. The "finalize" method can be 1.187 + * used in the common case of waiting for completion on shutdown. 1.188 + */ 1.189 + arm: function () 1.190 + { 1.191 + if (this._finalized) { 1.192 + throw new Error("Unable to arm timer, the object has been finalized."); 1.193 + } 1.194 + 1.195 + this._armed = true; 1.196 + 1.197 + // In case the timer callback is running, do not create the timer now, 1.198 + // because this will be handled by the timer callback itself. Also, the 1.199 + // timer is not restarted in case it is already running. 1.200 + if (!this._runningPromise && !this._timer) { 1.201 + this._startTimer(); 1.202 + } 1.203 + }, 1.204 + 1.205 + /** 1.206 + * Cancels any request for a delayed the execution of the task, though the 1.207 + * task itself cannot be canceled in case it is already running. 1.208 + * 1.209 + * This method stops any currently running timer, thus the delay will restart 1.210 + * from its original value in case the "arm" method is called again. 1.211 + */ 1.212 + disarm: function () { 1.213 + this._armed = false; 1.214 + if (this._timer) { 1.215 + // Calling the "cancel" method and discarding the timer reference makes 1.216 + // sure that the timer callback will not be called later, even if the 1.217 + // timer thread has already posted the timer event on the main thread. 1.218 + this._timer.cancel(); 1.219 + this._timer = null; 1.220 + } 1.221 + }, 1.222 + 1.223 + /** 1.224 + * Ensures that any pending task is executed from start to finish, while 1.225 + * preventing any attempt to arm the timer again. 1.226 + * 1.227 + * - If the task is running and the timer is armed, then one last execution 1.228 + * from start to finish will happen again, immediately after the current 1.229 + * execution terminates, then the returned promise will be resolved. 1.230 + * - If the task is running and the timer is not armed, the returned promise 1.231 + * will be resolved when the current execution terminates. 1.232 + * - If the task is not running and the timer is armed, then the task is 1.233 + * started immediately, and the returned promise resolves when the new 1.234 + * execution terminates. 1.235 + * - If the task is not running and the timer is not armed, the method returns 1.236 + * a resolved promise. 1.237 + * 1.238 + * @return {Promise} 1.239 + * @resolves After the last execution of the task is finished. 1.240 + * @rejects Never. 1.241 + */ 1.242 + finalize: function () { 1.243 + if (this._finalized) { 1.244 + throw new Error("The object has been already finalized."); 1.245 + } 1.246 + this._finalized = true; 1.247 + 1.248 + // If the timer is armed, it means that the task is not running but it is 1.249 + // scheduled for execution. Cancel the timer and run the task immediately. 1.250 + if (this._timer) { 1.251 + this.disarm(); 1.252 + this._timerCallback(); 1.253 + } 1.254 + 1.255 + // Wait for the operation to be completed, or resolve immediately. 1.256 + if (this._runningPromise) { 1.257 + return this._runningPromise; 1.258 + } 1.259 + return Promise.resolve(); 1.260 + }, 1.261 + _finalized: false, 1.262 + 1.263 + /** 1.264 + * Timer callback used to run the delayed task. 1.265 + */ 1.266 + _timerCallback: function () 1.267 + { 1.268 + let runningDeferred = Promise.defer(); 1.269 + 1.270 + // All these state changes must occur at the same time directly inside the 1.271 + // timer callback, to prevent race conditions and to ensure that all the 1.272 + // methods behave consistently even if called from inside the task. This 1.273 + // means that the assignment of "this._runningPromise" must complete before 1.274 + // the task gets a chance to start. 1.275 + this._timer = null; 1.276 + this._armed = false; 1.277 + this._runningPromise = runningDeferred.promise; 1.278 + 1.279 + runningDeferred.resolve(Task.spawn(function () { 1.280 + // Execute the provided function asynchronously. 1.281 + yield Task.spawn(this._taskFn).then(null, Cu.reportError); 1.282 + 1.283 + // Now that the task has finished, we check the state of the object to 1.284 + // determine if we should restart the task again. 1.285 + if (this._armed) { 1.286 + if (!this._finalized) { 1.287 + this._startTimer(); 1.288 + } else { 1.289 + // Execute the task again immediately, for the last time. The isArmed 1.290 + // property should return false while the task is running, and should 1.291 + // remain false after the last execution terminates. 1.292 + this._armed = false; 1.293 + yield Task.spawn(this._taskFn).then(null, Cu.reportError); 1.294 + } 1.295 + } 1.296 + 1.297 + // Indicate that the execution of the task has finished. This happens 1.298 + // synchronously with the previous state changes in the function. 1.299 + this._runningPromise = null; 1.300 + }.bind(this)).then(null, Cu.reportError)); 1.301 + }, 1.302 +};