Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4719)

Unified Diff: chrome/browser/resources/google_now/background.js

Issue 12316075: Preventing race conditions in Google Now extension (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/browser/resources/google_now/background.js
diff --git a/chrome/browser/resources/google_now/background.js b/chrome/browser/resources/google_now/background.js
index a65618cea0d01c4655826286adba2907229bd212..33bfcee128a96c809bc946211fe1c93a2943263d 100644
--- a/chrome/browser/resources/google_now/background.js
+++ b/chrome/browser/resources/google_now/background.js
@@ -25,9 +25,10 @@
// TODO(vadimt): Report errors to the user.
// TODO(vadimt): Figure out the server name. Use it in the manifest and for
-// TODO(vadimt): Consider processing errors for all storage.set calls.
// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
// edit NOTIFICATION_CARDS_URL before building Chrome.
+// TODO(vadimt): Consider processing errors for all storage.set calls.
+
/**
* URL to retrieve notification cards.
*/
@@ -51,9 +52,179 @@ var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes
*/
var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
+/**
+ * Names for tasks that can be created by the extension.
+ */
+var UPDATE_CARDS_TASK_NAME = 'update-cards';
skare_ 2013/02/22 20:53:39 enum? /** @enum {string} */ var Task = { UPDATE
vadimt 2013/02/28 02:23:22 Done.
+var DISMISS_CARD_TASK_NAME = 'dismiss-card';
+var CARD_CLICKED_TASK_NAME = 'card-clicked';
+
+/**
+ * Name of the alarm that triggers updating the cards.
skare_ 2013/02/22 20:53:39 probably don't need this comment
vadimt 2013/02/28 02:23:22 Done.
+ */
+var UPDATE_ALARM_NAME = 'UPDATE';
skare_ 2013/02/22 20:53:39 any advantage to having the two alarm names in the
vadimt 2013/02/28 02:23:22 We are speaking about UPDATE_NOTIFICATIONS_ALARM_N
skare_ 2013/03/02 00:11:39 [ok] yeah, figured this was the case; phrased as
vadimt 2013/03/06 21:04:38 Done.
+
var storage = chrome.storage.local;
/**
+ * Check for internal errors.
skare_ 2013/02/22 20:53:39 split into another file? Do we have access to a mo
vadimt 2013/02/28 02:23:22 This won't be an issue; I've added a comment to do
+ * @param {boolean} condition Condition that must be true.
+ * @param {string} message Diagnostic message for the case when the condition is
+ * false.
+ */
+function assert(condition, message) {
+ // TODO(vadimt): Send UMAs instead of showing alert.
+ if (!condition) {
+ var errorText = 'ASSERT: ' + message;
+ console.error(errorText);
+ alert(errorText);
+ }
+}
+
+/**
+ * Build the closure that manages tasks (mutually exclusive chains of events).
+ * @return {Object} Set of methods to control tasks.
+ */
+function getTaskManager() {
skare_ 2013/02/22 20:53:39 you might want to just call this function TaskMana
vadimt 2013/02/28 02:23:22 Done.
+ /**
+ * Name of the alarm that triggers the error saying that the event page cannot
+ * unload.
+ */
+ var CANNOT_UNLOAD_ALARM_NAME = 'CANNOT-UNLOAD';
+
+ /**
+ * Maximal time we expect the event page to stay loaded after starting a task.
+ */
+ var MAXIMUM_LOADED_TIME_MINUTES = 5;
+
+ /**
+ * Queue of scheduled tasks. The first element, if present, corresponds to the
+ * currently running task.
+ */
+ var queue = [];
+
+ /**
+ * Name of the current step of the currently running task if present,
+ * otherwise, null. For diagnostics only.
+ * It's set when the task is started and before each asynchronous operation.
+ */
+ var stepName = null;
+
+ /**
+ * Start the first queued task.
+ */
+ function startFirst() {
+ // Set alarm to verify that the event page will unload in a reasonable time.
+ chrome.alarms.create(CANNOT_UNLOAD_ALARM_NAME,
+ {delayInMinutes: MAXIMUM_LOADED_TIME_MINUTES});
+
+ // Start the oldest queued task, but not remove it from the queue.
+ assert(stepName == null, 'tasks.startFirst: stepName is not null');
+ var entry = queue[0];
+ stepName = entry.name + '-initial';
+ entry.task();
+ }
+
+ /**
+ * Check if a new task can be added to a queue that contains an existing task.
+ * @param {string} newTaskName Name of the new task.
+ * @param {string} queuedTaskName Name of the task in the queue.
+ * @return {boolean} Whether the new task doesn't conflict with the existing
+ task.
skare_ 2013/02/22 20:53:39 this line lost an asterisk
vadimt 2013/02/28 02:23:22 Done.
+ */
+ function isCompatible(newTaskName, queuedTaskName) {
+ if (newTaskName == UPDATE_CARDS_TASK_NAME &&
+ queuedTaskName == UPDATE_CARDS_TASK_NAME) {
skare_ 2013/02/22 20:53:39 one more space
vadimt 2013/02/28 02:23:22 Done.
+ // If a card update is requested while an old update is still in the
+ // queue, we don't need the new update.
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a new task can be added to the task queue.
+ * @param {string} taskName Name of the new task.
+ * @return {boolean} Whether the new task can be added.
+ */
+ function canQueue(taskName) {
+ for (var i = 0; i < queue.length; ++i)
+ if (!isCompatible(taskName, queue[i].name))
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Add a new task. If another task is not running, run the task immediately.
+ * If any task in the queue is not compatible with the task, ignore the new
+ * task. Otherwise, store the task for future execution.
+ * @param {string} taskName Name of the task.
+ * @param {function()} task Code of the task.
+ */
+ function submit(taskName, task) {
+ if (!canQueue(taskName))
+ return;
+
+ queue.push({name: taskName, task: task});
+
+ if (queue.length == 1) {
+ startFirst();
+ }
+ }
+
+ /**
+ * Complete the current task and start the next queued task if available.
+ */
+ function finish() {
+ assert(queue.length >= 1, 'tasks.finish: The task queue is empty.');
+ queue.shift();
+ stepName = null;
+
+ if (queue.length >= 1)
+ startFirst();
+ }
+
+ /**
+ * Associate a name with the current step of the task. Used for diagnostics
+ * only. A task is a chain of asynchronous events; setStep should be called
+ * before starting any asynchronous operation.
+ * @param {string} step Name of new step.
+ */
+ function setStep(step) {
+ stepName = step;
+ }
+
+ chrome.alarms.onAlarm.addListener(function(alarm) {
+ if (alarm.name == CANNOT_UNLOAD_ALARM_NAME) {
+ // Error if the event page wasn't unloaded after a reasonable timeout
+ // since starting the last task.
+ // TODO(vadimt): Uncomment the assert once this bug is fixed:
+ // crbug.com/177563
+ // assert(false, 'Event page didn\'t unload, queue = ' +
+ // JSON.stringify(tasks) + ', step = ' + stepName + ' (ignore this assert
+ // if devtools is attached).');
+ }
+ });
+
+ chrome.runtime.onSuspend.addListener(function() {
+ chrome.alarms.clear(CANNOT_UNLOAD_ALARM_NAME);
+ assert(queue.length == 0 && stepName == null,
+ 'Incomplete task when unloading event page, queue = ' +
+ JSON.stringify(queue) + ', step = ' + stepName);
+ });
+
+ return {
+ submit: submit,
+ setStep: setStep,
+ finish: finish
+ };
+}
+
+var tasks = getTaskManager();
+
+/**
* Show a notification and remember information associated with it.
* @param {Object} card Google Now card represented as a set of parameters for
* showing a Chrome notification.
@@ -113,6 +284,7 @@ function parseAndShowNotificationCards(response) {
return;
}
+ tasks.setStep('parseAndShowNotificationCards-get-active-notifications');
skare_ 2013/02/22 20:53:39 so setStep is purely diagnostic information to kno
vadimt 2013/02/28 02:23:22 Yep. If a task quietly ends without calling finish
storage.get('activeNotifications', function(items) {
// Mark existing notifications that received an update in this server
// response.
@@ -148,6 +320,7 @@ function parseAndShowNotificationCards(response) {
// to the initial value. This retry period will be used the next time we
// fail to get the server-provided period.
storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
+ tasks.finish();
});
}
@@ -163,12 +336,19 @@ function requestNotificationCards(requestParameters) {
request.onload = function(event) {
if (request.status == HTTP_OK)
parseAndShowNotificationCards(request.response);
+ else
+ tasks.finish();
+ }
+
+ request.onerror = function(event) {
+ tasks.finish();
}
request.open(
'GET',
NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
true);
+ tasks.setStep('requestNotificationCards-send-request');
request.send();
}
@@ -199,23 +379,27 @@ function requestNotificationCardsWithoutLocation(positionError) {
* location.
*/
function updateNotificationsCards() {
- storage.get('retryDelaySeconds', function(items) {
- // Immediately schedule the update after the current retry period. Then,
- // we'll use update time from the server if available.
- scheduleNextUpdate(items.retryDelaySeconds);
-
- // TODO(vadimt): Consider interrupting waiting for the next update if we
- // detect that the network conditions have changed. Also, decide whether the
- // exponential backoff is needed both when we are offline and when there are
- // failures on the server side.
- var newRetryDelaySeconds =
- Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
- MAXIMUM_POLLING_PERIOD_SECONDS);
- storage.set({retryDelaySeconds: newRetryDelaySeconds});
-
- navigator.geolocation.getCurrentPosition(
- requestNotificationCardsWithLocation,
- requestNotificationCardsWithoutLocation);
+ tasks.submit(UPDATE_CARDS_TASK_NAME, function() {
+ tasks.setStep('updateNotificationsCards-get-retryDelaySeconds');
+ storage.get('retryDelaySeconds', function(items) {
+ // Immediately schedule the update after the current retry period. Then,
+ // we'll use update time from the server if available.
+ scheduleNextUpdate(items.retryDelaySeconds);
+
+ // TODO(vadimt): Consider interrupting waiting for the next update if we
+ // detect that the network conditions have changed. Also, decide whether
+ // the exponential backoff is needed both when we are offline and when
+ // there are failures on the server side.
+ var newRetryDelaySeconds =
+ Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
+ MAXIMUM_POLLING_PERIOD_SECONDS);
+ storage.set({retryDelaySeconds: newRetryDelaySeconds});
+
+ tasks.setStep('updateNotificationsCards-get-location');
+ navigator.geolocation.getCurrentPosition(
+ requestNotificationCardsWithLocation,
+ requestNotificationCardsWithoutLocation);
+ });
});
}
@@ -225,13 +409,17 @@ function updateNotificationsCards() {
* @param {string} area Name of the notification's clicked area.
*/
function onNotificationClicked(notificationId, area) {
- storage.get('activeNotifications', function(items) {
- var notificationActions = items.activeNotifications[notificationId] || {};
- if (area in notificationActions) {
- // Open URL specified for the clicked area.
- // TODO(vadimt): Figure out whether to open link in a new tab etc.
- chrome.windows.create({url: notificationActions[area]});
- }
+ tasks.submit(CARD_CLICKED_TASK_NAME, function() {
+ tasks.setStep('onNotificationClicked-get-activeNotifications');
+ storage.get('activeNotifications', function(items) {
+ var notificationActions = items.activeNotifications[notificationId] || {};
+ if (area in notificationActions) {
+ // Open URL specified for the clicked area.
+ // TODO(vadimt): Figure out whether to open link in a new tab etc.
+ chrome.windows.create({url: notificationActions[area]});
+ }
+ tasks.finish();
+ });
});
}
@@ -242,19 +430,29 @@ function onNotificationClicked(notificationId, area) {
*/
function onNotificationClosed(notificationId, byUser) {
if (byUser) {
- // TODO(vadimt): Analyze possible race conditions between request for cards
- // and dismissal.
- // Send a dismiss request to the server.
- var requestParameters = '?id=' + notificationId;
- var request = new XMLHttpRequest();
- request.responseType = 'text';
- // TODO(vadimt): If the request fails, for example, because there is no
- // internet connection, do retry with exponential backoff.
- request.open(
- 'GET',
- NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
- true);
- request.send();
+ tasks.submit(DISMISS_CARD_TASK_NAME, function() {
+ // Deleting the notification in case it was re-added while this task was
+ // waiting in the queue.
+ chrome.experimental.notification.delete(
+ notificationId,
+ function(wasDeleted) {});
skare_ 2013/02/22 20:53:39 unneeded param?
vadimt 2013/02/28 02:23:22 Done.
+
+ // Send a dismiss request to the server.
+ var requestParameters = '?id=' + notificationId;
+ var request = new XMLHttpRequest();
+ request.responseType = 'text';
+ request.onloadend = function(event) {
+ tasks.finish();
+ }
+ // TODO(vadimt): If the request fails, for example, because there is no
+ // internet connection, do retry with exponential backoff.
+ request.open(
+ 'GET',
+ NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
+ true);
+ tasks.setStep('onNotificationClosed-send-request');
+ request.send();
+ });
}
}
@@ -266,10 +464,12 @@ function onNotificationClosed(notificationId, byUser) {
function scheduleNextUpdate(delaySeconds) {
// Schedule an alarm after the specified delay. 'periodInMinutes' is for the
// case when we fail to re-register the alarm.
- chrome.alarms.create({
+ var alarmInfo = {
delayInMinutes: delaySeconds / 60,
periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
- });
+ };
+
+ chrome.alarms.create(UPDATE_ALARM_NAME, alarmInfo);
}
/**
@@ -280,7 +480,8 @@ function initialize() {
activeNotifications: {},
retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
};
- storage.set(initialStorage, updateNotificationsCards);
+ storage.set(initialStorage);
+ updateNotificationsCards();
}
chrome.runtime.onInstalled.addListener(function(details) {
@@ -293,7 +494,8 @@ chrome.runtime.onStartup.addListener(function() {
});
chrome.alarms.onAlarm.addListener(function(alarm) {
- updateNotificationsCards();
+ if (alarm.name == UPDATE_ALARM_NAME)
skare_ 2013/02/22 20:53:39 though I don't think it's in the style guide, you
vadimt 2013/02/28 02:23:22 As far as I recall, if the condition or the statem
skare_ 2013/03/02 00:11:39 yes that's correct. And I was a member of Team Omi
rgustafson 2013/03/04 22:31:27 "Do not use braces for single-line logic blocks."
vadimt 2013/03/06 21:04:38 See rgustafson's note.
vadimt 2013/03/06 21:04:38 Thanks rgustafson@!
+ updateNotificationsCards();
});
chrome.experimental.notification.onClicked.addListener(
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698