Commit d53fb76a authored by kenobi's avatar kenobi Committed by Commit bot

Add a TaskQueue.

The TaskQueue handles the basics of running a set of queued up tasks, and providing updates and various other callbacks for clients to be notified about queue and task state.

BUG=

Review URL: https://codereview.chromium.org/789603004

Cr-Commit-Position: refs/heads/master@{#308444}
parent 184ab4ad
...@@ -93,3 +93,8 @@ IN_PROC_BROWSER_TEST_F(FileManagerJsTest, MediaImportHandlerTest) { ...@@ -93,3 +93,8 @@ IN_PROC_BROWSER_TEST_F(FileManagerJsTest, MediaImportHandlerTest) {
RunTest(base::FilePath( RunTest(base::FilePath(
FILE_PATH_LITERAL("background/js/media_import_handler_unittest.html"))); FILE_PATH_LITERAL("background/js/media_import_handler_unittest.html")));
} }
IN_PROC_BROWSER_TEST_F(FileManagerJsTest, TaskQueueTest) {
RunTest(base::FilePath(
FILE_PATH_LITERAL("background/js/task_queue_unittest.html")));
}
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
'media_import_handler.js', 'media_import_handler.js',
'media_scanner.js', 'media_scanner.js',
'progress_center.js', 'progress_center.js',
'task_queue.js',
'volume_manager.js', 'volume_manager.js',
'background_base.js', 'background_base.js',
], ],
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Namespace
var importer = importer || {};
/**
* A queue of tasks. Tasks (subclasses of TaskQueue.Task) can be pushed onto
* the queue. The queue becomes active whenever it is not empty, and it will
* begin executing tasks one at a time. The tasks are executed in a separate
* asynchronous context. As each task runs, it can send update notifications
* which are relayed back to clients via callbacks. When the queue runs of of
* tasks, it goes back into an idle state. Clients can set callbacks which will
* be triggered whenever the queue transitions between the active and idle
* states.
*
* @constructor
* @struct
*/
importer.TaskQueue = function() {
/** @private {!Array<!importer.TaskQueue.Task>} */
this.tasks_ = [];
/** @private {!Array<!function(string, !importer.TaskQueue.Task)>} */
this.updateCallbacks_ = [];
/** @private {?function()} */
this.activeCallback_ = null;
/** @private {?function()} */
this.idleCallback_ = null;
/** @private {boolean} */
this.active_ = false;
};
/**
* @enum {string}
*/
importer.TaskQueue.UpdateType = {
PROGRESS: 'PROGRESS',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR'
};
/**
* @param {!importer.TaskQueue.Task} task
*/
importer.TaskQueue.prototype.queueTask = function(task) {
// The Tasks that are pushed onto the queue aren't required to be inherently
// asynchronous. This code force task execution to occur asynchronously.
Promise.resolve().then(function() {
task.setOwner(this);
this.tasks_.push(task);
// If more than one task is queued, then the queue is already running.
if (this.tasks_.length === 1) {
this.runPending_();
}
}.bind(this));
};
/**
* Sets a callback to be triggered when a task updates.
* @param {function(string, !importer.TaskQueue.Task)} callback
*/
importer.TaskQueue.prototype.addUpdateCallback = function(callback) {
this.updateCallbacks_.push(callback);
};
/**
* Sets a callback that is triggered each time the queue goes from an idle
* (i.e. empty with no running tasks) to an active (i.e. having a running task)
* state.
* @param {function()} callback
*/
importer.TaskQueue.prototype.setActiveCallback = function(callback) {
this.activeCallback_ = callback;
};
/**
* Sets a callback that is triggered each time the queue goes from an active to
* an idle state. Also see #onActive.
* @param {function()} callback
*/
importer.TaskQueue.prototype.setIdleCallback = function(callback) {
this.idleCallback_ = callback;
};
/**
* Sends out notifications when a task updates. This is meant to be called by
* the running tasks owned by this queue.
* @param {!importer.TaskQueue.UpdateType} updateType
* @param {!importer.TaskQueue.Task} task
*/
importer.TaskQueue.prototype.taskUpdate = function(updateType, task) {
this.updateCallbacks_.forEach(function(callback) {
callback.call(null, updateType, task);
});
};
/**
* Sends out notifications when a task stops. Stopping could be due to
* successful completion, cancellation, or errors. This is meant to be called
* by the running tasks owned by this queue.
* @param {!importer.TaskQueue.UpdateType} reason
* @param {!importer.TaskQueue.Task} task
*/
importer.TaskQueue.prototype.taskDone = function(reason, task) {
// Assumption: the currently running task is at the head of the queue.
assert(this.tasks_[0] === task);
// Remove the completed task from the queue.
this.tasks_.shift();
// Send updates to clients.
this.taskUpdate(reason, task);
// Run the next thing in the queue.
this.runPending_();
};
/**
* Wakes the task queue up and runs the next pending task, or makes the queue go
* back to sleep if no tasks are pending.
* @private
*/
importer.TaskQueue.prototype.runPending_ = function() {
if (this.tasks_.length === 0) {
// All done - go back to idle.
this.active_ = false;
if (this.idleCallback_)
this.idleCallback_();
return;
}
if (!this.active_) {
// If the queue is currently idle, transition to active state.
this.active_ = true;
if (this.activeCallback_)
this.activeCallback_();
}
var nextTask = this.tasks_[0];
nextTask.run();
};
/**
* Interface for any Task that is to run on the TaskQueue.
* @interface
*/
importer.TaskQueue.Task = function() {};
/**
* Sets the TaskQueue that will own this task. The TaskQueue must call this
* prior to enqueuing a Task.
* @param {!importer.TaskQueue} owner
*/
importer.TaskQueue.Task.prototype.setOwner;
/**
* Performs the actual work of the Task. Child classes should implement this.
*/
importer.TaskQueue.Task.prototype.run;
/**
* Base class for importer tasks.
* @constructor
* @implements {importer.TaskQueue.Task}
*
* @param {string} taskId
*/
importer.TaskQueue.BaseTask = function(taskId) {
/** @private {string} */
this.taskId_ = taskId;
/** @private {importer.TaskQueue} */
this.owner_ = null;
};
/**
* Sets the TaskQueue that will own this task. The TaskQueue must call this
* prior to enqueuing a Task.
* @param {!importer.TaskQueue} owner
*/
importer.TaskQueue.BaseTask.prototype.setOwner = function(owner) {
this.owner_ = owner;
};
/** @override */
importer.TaskQueue.BaseTask.prototype.run = function() {};
/**
* Sends progress notifications. Task subclasses should call this to report
* progress.
* @protected
*/
importer.TaskQueue.BaseTask.prototype.notifyProgress = function() {
this.owner_.taskUpdate(importer.TaskQueue.UpdateType.PROGRESS, this);
};
/**
* Sends success notifications. Task subclasses should call this to indicate
* when they successfully complete. Calling this results in the Task instance
* being dequeued.
* @protected
*/
importer.TaskQueue.BaseTask.prototype.notifySuccess = function() {
this.owner_.taskDone(importer.TaskQueue.UpdateType.SUCCESS, this);
};
/**
* Sends error notifications. Task subclasses should call this to indicate when
* an error occurs. Tasks are assumed to stop execution once they call
* notifyError (i.e. they will be dequeued).
* @protected
*/
importer.TaskQueue.BaseTask.prototype.notifyError = function() {
this.owner_.taskDone(importer.TaskQueue.UpdateType.ERROR, this);
};
<!DOCTYPE html>
<!-- Copyright 2014 The Chromium Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
-->
<html>
<body>
<script src="../../../../../ui/webui/resources/js/cr.js"></script>
<script src="../../../../../ui/webui/resources/js/assert.js"></script>
<script src="../../../../../ui/webui/resources/js/cr/event_target.js"></script>
<script src="../../common/js/unittest_util.js"></script>
<script src="task_queue.js"></script>
<script src="task_queue_unittest.js"></script>
</body>
</html>
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @type {!TaskQueue} */
var queue;
/** @type {!Object<importer.TaskQueue.UpdateType, number>} */
var updates = {};
function setUp() {
queue = new importer.TaskQueue();
// Set up a callback to log updates from running tasks.
for (var updateType in importer.TaskQueue.UpdateType) {
// Reset counts for all update types.
updates[importer.TaskQueue.UpdateType[updateType]] = 0;
}
// Counts the number of updates of each type that have been received.
var updateCallback = function(type, updatedTask) {
updates[type]++;
};
queue.addUpdateCallback(updateCallback);
}
/**
* A Task subclass for testing.
* @constructor
* @extends {importer.TaskQueue.BaseTask}
*/
var TestTask = function() {
importer.TaskQueue.Task.call(this);
/** @type {boolean} */
this.wasRun = false;
/**
* @private {Function}
*/
this.runResolver_ = null;
this.runPromise_ = new Promise(function(resolve) {
this.runResolver_ = resolve;
}.bind(this));
};
TestTask.prototype.__proto__ = importer.TaskQueue.BaseTask.prototype;
/** @override */
TestTask.prototype.run = function() {
this.wasRun = true;
this.runResolver_(this);
};
/** @return {!Promise} A promise that settles once #run is called. */
TestTask.prototype.whenRun = function() {
return this.runPromise_;
};
// Verifies that a queued task gets run.
function testRunsTask(callback) {
var task = new TestTask();
queue.queueTask(task);
reportPromise(task.whenRun(), callback);
}
// Verifies that multiple queued tasks get run.
function testRunsTasks(callback) {
var task0 = new TestTask();
var task1 = new TestTask();
// Make the tasks call Task#notifySuccess when they are run.
task0.whenRun().then(function(task) { task.notifySuccess(); });
task1.whenRun().then(function(task) { task.notifySuccess(); });
// Enqueue both tasks, and then verify that they were run.
queue.queueTask(task0);
queue.queueTask(task1);
reportPromise(
Promise.all([task0.whenRun(), task1.whenRun()]),
callback);
}
// Verifies that the active callback triggers when the queue starts doing work
function testOnActiveCalled(callback) {
var task = new TestTask();
// Make a promise that resolves when the active callback is triggered.
var whenActive = new Promise(function(resolve) {
queue.setActiveCallback(
function() {
// Verify that the active callback is called before the task runs.
assertFalse(task.wasRun);
resolve();
});
});
// Queue a task, and then check that the active callback was triggered.
queue.queueTask(task);
reportPromise(whenActive, callback);
}
// Verifies that the idle callback triggers when the queue is empty.
function testOnIdleCalled(callback) {
var task = new TestTask();
task.whenRun().then(function(task) { task.notifySuccess(); });
// Make a promise that resolves when the idle callback is triggered
// (i.e. after all queued tasks have finished running).
var whenDone = new Promise(function(resolve) {
queue.setIdleCallback(
function() {
// Verify that the idle callback is called after the task runs.
assertTrue(task.wasRun);
resolve();
});
});
// Queue a task, then check that the idle callback was triggered.
queue.queueTask(task);
reportPromise(whenDone, callback);
}
// Verifies that the update callback is called when a task reports progress.
function testProgressUpdate(callback) {
var task = new TestTask();
// Get the task to report some progress, then success, when it's run.
task.whenRun()
.then(
function(task) {
task.notifyProgress();
return task;
})
.then(
function(task) {
task.notifySuccess();
return task;
});
// Make a promise that resolves after the task runs.
var whenDone = new Promise(function(resolve) {
queue.setIdleCallback(
function() {
// Verify that progress was recorded.
assertEquals(1, updates[importer.TaskQueue.UpdateType.PROGRESS]);
resolve();
});
});
queue.queueTask(task);
reportPromise(whenDone, callback);
}
// Verifies that the update callback is called to report successful task
// completion.
function testSuccessUpdate(callback) {
var task = new TestTask();
// Get the task to report success when it's run.
task.whenRun().then(function(task) { task.notifySuccess(); });
queue.queueTask(task);
var whenDone = new Promise(function(resolve) {
queue.setIdleCallback(
function() {
// Verify that the done callback was called.
assertEquals(1, updates[importer.TaskQueue.UpdateType.SUCCESS]);
resolve();
});
});
reportPromise(whenDone, callback);
}
// Verifies that the update callback is called to report task errors.
function testErrorUpdate(callback) {
var task = new TestTask();
// Get the task to report an error when it's run.
task.whenRun().then(function(task) { task.notifyError(); });
queue.queueTask(task);
var whenDone = new Promise(function(resolve) {
queue.setIdleCallback(
function() {
// Verify that the done callback was called.
assertEquals(1, updates[importer.TaskQueue.UpdateType.ERROR]);
resolve();
});
});
reportPromise(whenDone, callback);
}
...@@ -58,6 +58,7 @@ ...@@ -58,6 +58,7 @@
'../../background/js/import_history.js', '../../background/js/import_history.js',
'../../background/js/media_import_handler.js', '../../background/js/media_import_handler.js',
'../../background/js/media_scanner.js', '../../background/js/media_scanner.js',
'../../background/js/task_queue.js',
'../../background/js/background_base.js', '../../background/js/background_base.js',
'../../background/js/background.js', '../../background/js/background.js',
'../../../image_loader/image_loader_client.js', '../../../image_loader/image_loader_client.js',
......
...@@ -186,6 +186,7 @@ ...@@ -186,6 +186,7 @@
"background/js/media_import_handler.js", "background/js/media_import_handler.js",
"background/js/media_scanner.js", "background/js/media_scanner.js",
"background/js/progress_center.js", "background/js/progress_center.js",
"background/js/task_queue.js",
"background/js/test_util.js", "background/js/test_util.js",
"background/js/volume_manager.js", "background/js/volume_manager.js",
"background/js/background.js" "background/js/background.js"
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
<include name="IDR_FILE_MANAGER_MEDIA_IMPORT_HANDLER_JS" file="file_manager/background/js/media_import_handler.js" flattenhtml="false" type="BINDATA" /> <include name="IDR_FILE_MANAGER_MEDIA_IMPORT_HANDLER_JS" file="file_manager/background/js/media_import_handler.js" flattenhtml="false" type="BINDATA" />
<include name="IDR_FILE_MANAGER_MEDIA_SCANNER_JS" file="file_manager/background/js/media_scanner.js" flattenhtml="false" type="BINDATA" /> <include name="IDR_FILE_MANAGER_MEDIA_SCANNER_JS" file="file_manager/background/js/media_scanner.js" flattenhtml="false" type="BINDATA" />
<include name="IDR_FILE_MANAGER_PROGRESS_CENTER_JS" file="file_manager/background/js/progress_center.js" flattenhtml="false" type="BINDATA" /> <include name="IDR_FILE_MANAGER_PROGRESS_CENTER_JS" file="file_manager/background/js/progress_center.js" flattenhtml="false" type="BINDATA" />
<include name="IDR_FILE_MANAGER_TASK_QUEUE_JS" file="file_manager/background/js/task_queue.js" flattenhtml="false" type="BINDATA" />
<include name="IDR_FILE_MANAGER_TEST_UTIL_JS" file="file_manager/background/js/test_util.js" flattenhtml="false" type="BINDATA" /> <include name="IDR_FILE_MANAGER_TEST_UTIL_JS" file="file_manager/background/js/test_util.js" flattenhtml="false" type="BINDATA" />
<include name="IDR_FILE_MANAGER_VOLUME_MANAGER_JS" file="file_manager/background/js/volume_manager.js" flattenhtml="false" type="BINDATA" /> <include name="IDR_FILE_MANAGER_VOLUME_MANAGER_JS" file="file_manager/background/js/volume_manager.js" flattenhtml="false" type="BINDATA" />
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment