more error handling and feedback tweaks: implement "disabled" source state; handle transactionInProgress in sources.persist; ensure statement reset where not; don't refresh busy or dead sources.

This commit is contained in:
alta88@gmail.com 2009-11-03 13:01:28 -07:00
Родитель 2c3cb4a1c9
Коммит b121fad6b1
10 изменённых файлов: 1240 добавлений и 1620 удалений

Просмотреть файл

@ -92,6 +92,12 @@
}
/* Error on collection indicator */
#sourcesViewTreeChildren::-moz-tree-image(hasError) {
#sourcesViewTreeChildren::-moz-tree-image(hasError),
#sourcesViewTreeChildren::-moz-tree-image(isDisabled) {
list-style-image: url("chrome://snowl/content/icons/exclamation.png");
}
/* Source is paused/disabled */
#sourcesViewTreeChildren::-moz-tree-cell-text(isDisabled) {
color: GrayText;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -60,11 +60,11 @@ let SubscriptionListener = {
let identity = source.name;
switch(topic) {
case "snowl:subscribe:connect:start":
case "snowl:refresh:connect:start":
code = "active";
message = this.stringBundle.getString("messageConnecting");
break;
case "snowl:subscribe:connect:end":
case "snowl:refresh:connect:end":
if (data.split(":")[0] == "duplicate") {
this.duplicate(data);
}
@ -74,6 +74,11 @@ let SubscriptionListener = {
else if (data == "logindata") {
this.logindata(data);
}
else if (data.split(":", 1)[0] == "error" &&
source.attributes["statusCode"] == "db:transactionInProgress") {
code = "error";
message = this.stringBundle.getString("messageDbBusy");
}
else if (data.split(":", 1)[0] == "error") {
code = "error";
errorMsg = data.split("error:")[1];
@ -92,14 +97,11 @@ let SubscriptionListener = {
message = this.stringBundle.getString("messageConnected");
}
break;
case "snowl:subscribe:get:start":
case "snowl:refresh:get:start":
code = "active";
message = this.stringBundle.getString("messageGettingMessages");
break;
case "snowl:subscribe:get:progress":
return;
break;
case "snowl:subscribe:get:end":
case "snowl:refresh:get:end":
code = "complete";
message = this.stringBundle.getString("messageSuccess");
break;
@ -183,19 +185,17 @@ let Subscriber = {
addObservers: function() {
// FIXME: integrate the subscription listener into this object
// as individual notification handler functions.
Observers.add("snowl:subscribe:connect:start", SubscriptionListener);
Observers.add("snowl:subscribe:connect:end", SubscriptionListener);
Observers.add("snowl:subscribe:get:start", SubscriptionListener);
Observers.add("snowl:subscribe:get:progress", SubscriptionListener);
Observers.add("snowl:subscribe:get:end", SubscriptionListener);
Observers.add("snowl:refresh:connect:start", SubscriptionListener);
Observers.add("snowl:refresh:connect:end", SubscriptionListener);
Observers.add("snowl:refresh:get:start", SubscriptionListener);
Observers.add("snowl:refresh:get:end", SubscriptionListener);
},
removeObservers: function() {
Observers.remove("snowl:subscribe:connect:start", SubscriptionListener);
Observers.remove("snowl:subscribe:connect:end", SubscriptionListener);
Observers.remove("snowl:subscribe:get:start", SubscriptionListener);
Observers.remove("snowl:subscribe:get:progress", SubscriptionListener);
Observers.remove("snowl:subscribe:get:end", SubscriptionListener);
Observers.remove("snowl:refresh:connect:start", SubscriptionListener);
Observers.remove("snowl:refresh:connect:end", SubscriptionListener);
Observers.remove("snowl:refresh:get:start", SubscriptionListener);
Observers.remove("snowl:refresh:get:end", SubscriptionListener);
},
@ -305,28 +305,7 @@ let Subscriber = {
this.account.username = aCredentials.username;
this.account.refresh(null);
// If error on connect, do not persist.
if (this.account.error) {
Observers.notify("snowl:subscribe:connect:end", this.account, "error:" + this.account.lastStatus);
this.account = null;
return;
}
this.account.persist();
// If error on db, don't show success.
if (this.account.error) {
Observers.notify("snowl:subscribe:connect:end", this.account, "error:" + this.account.lastStatus);
this.account = null;
return;
}
this.account = null;
if (aCallback)
aCallback();
this.doSubscribe();
},
subscribeFeed: function(aName, aMachineURI, aCallback) {
@ -347,12 +326,16 @@ let Subscriber = {
// FIXME: fix the API so I don't have to pass a bunch of null values.
this.account = new SnowlFeed(null, aName, aMachineURI, null, null);
this.doSubscribe();
},
doSubscribe: function() {
this.account.refresh(null);
// If error on connect, or error due to null result.doc (not a feed) despite
// a successful connect (filtered bad domain or not found url), do not persist.
if (this.account.error) {
Observers.notify("snowl:subscribe:connect:end", this.account, "error:" + this.account.lastStatus);
Observers.notify("snowl:refresh:connect:end", this.account, "error:" + this.account.lastStatus);
this.account = null;
return;
}
@ -361,7 +344,7 @@ let Subscriber = {
// If error on db, don't show success.
if (this.account.error) {
Observers.notify("snowl:subscribe:connect:end", this.account, "error:" + this.account.lastStatus);
Observers.notify("snowl:refresh:connect:end", this.account, "error:" + this.account.lastStatus);
this.account = null;
return;
}

Просмотреть файл

@ -5,6 +5,7 @@ messageInvalid = The location you entered is not recognizable.
messageInvalidLoginData = You have to enter a username and password to subscribe to this message source.
messageConnectionError = There was an error connecting to this message source. Please check the location and try again.
messagePassword = Your credentials were not accepted. Please check your username and password and try again.
messageDbBusy = The Database is temporarily busy. Please try again after all sources have finished refreshing.
messageConnected = Connected.
messageGettingMessages = Getting messages...
messageSuccess = You have successfully subscribed to this message source.

Просмотреть файл

@ -920,14 +920,19 @@ let SnowlDatastore = {
* @returns {integer} the ID of the newly-created record
*/
insertMessage: function(aSourceID, aExternalID, aSubject, aAuthorID, aTimestamp, aReceived, aLink) {
this._insertMessageStatement.params.sourceID = aSourceID;
this._insertMessageStatement.params.externalID = aExternalID;
this._insertMessageStatement.params.subject = aSubject;
this._insertMessageStatement.params.authorID = aAuthorID;
this._insertMessageStatement.params.timestamp = aTimestamp;
this._insertMessageStatement.params.received = aReceived;
this._insertMessageStatement.params.link = aLink;
this._insertMessageStatement.execute();
try {
this._insertMessageStatement.params.sourceID = aSourceID;
this._insertMessageStatement.params.externalID = aExternalID;
this._insertMessageStatement.params.subject = aSubject;
this._insertMessageStatement.params.authorID = aAuthorID;
this._insertMessageStatement.params.timestamp = aTimestamp;
this._insertMessageStatement.params.received = aReceived;
this._insertMessageStatement.params.link = aLink;
this._insertMessageStatement.execute();
}
finally {
this._insertMessageStatement.reset();
}
return this.dbConnection.lastInsertRowID;
},

Просмотреть файл

@ -46,7 +46,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ISO8601DateUtils.jsm");
// modules that are generic
Cu.import("resource://snowl/modules/log4moz.js");
Cu.import("resource://snowl/modules/Mixins.js");
Cu.import("resource://snowl/modules/Observers.js");
Cu.import("resource://snowl/modules/request.js");
@ -90,11 +89,8 @@ SnowlFeed.prototype = {
// need to check it to find out what kind of object an instance is.
constructor: SnowlFeed,
// XXX Move this to SnowlSource?
get _log() {
let logger = Log4Moz.repository.getLogger("Snowl.Feed " + this.name);
this.__defineGetter__("_log", function() logger);
return this._log;
get _logName() {
return "Snowl.Feed " + (this.name ? this.name : "<new feed>");
},
// If we prompt the user to authenticate, and the user asks us to remember
@ -282,8 +278,7 @@ SnowlFeed.prototype = {
if (typeof time == "undefined" || time == null)
time = new Date();
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:start", this);
Observers.notify("snowl:refresh:connect:start", this);
let request = new Request({
// The feed processor is going to parse the response, so we override
@ -295,9 +290,9 @@ SnowlFeed.prototype = {
});
this._log.info("refresh request finished, status: " + request.status);
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:end", this, request.status);
Observers.notify("snowl:refresh:connect:end", this, request.status);
this.attributes["statusCode"] = request.status;
this.lastStatus = request.status + " (" + request.statusText + ")";
if (request.status < 200 || request.status > 299 || request.responseText.length == 0) {
this._log.trace("refresh request failed");
@ -312,6 +307,8 @@ SnowlFeed.prototype = {
if (this._authInfo)
this._saveLogin();
Observers.notify("snowl:refresh:get:start", this);
// Parse the response.
// Note: this happens synchronously, even though it uses a listener
// callback, which makes it look like it happens asynchronously.
@ -368,11 +365,9 @@ SnowlFeed.prototype = {
if (!this.humanURI)
this.humanURI = feed.link;
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:get:start", this);
this.messages = this._processFeed(feed, time);
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:get:end", this);
Observers.notify("snowl:refresh:get:end", this);
},

Просмотреть файл

@ -331,20 +331,25 @@ SnowlMessage.prototype = {
statement.params.received = SnowlDateUtils.jsToJulianDate(this.received);
}
// Set params that are common to both types of queries.
statement.params.sourceID = this.source.id;
statement.params.externalID = this.externalID;
statement.params.subject = this.subject;
statement.params.authorID = this.author ? this.author.id : null;
statement.params.timestamp = SnowlDateUtils.jsToJulianDate(this.timestamp);
statement.params.link = this.link ? this.link.spec : null;
// FIXME: persist message.current.
//statement.params.current = this.current;
statement.params.read = this.read;
statement.params.headers = JSON.stringify(this.headers);
statement.params.attributes = JSON.stringify(this.attributes);
statement.execute();
try {
// Set params that are common to both types of queries.
statement.params.sourceID = this.source.id;
statement.params.externalID = this.externalID;
statement.params.subject = this.subject;
statement.params.authorID = this.author ? this.author.id : null;
statement.params.timestamp = SnowlDateUtils.jsToJulianDate(this.timestamp);
statement.params.link = this.link ? this.link.spec : null;
// FIXME: persist message.current.
//statement.params.current = this.current;
statement.params.read = this.read;
statement.params.headers = JSON.stringify(this.headers);
statement.params.attributes = JSON.stringify(this.attributes);
statement.execute();
}
finally {
statement.reset();
}
if (this.id) {
// FIXME: update the message parts (content, summary).
@ -495,33 +500,39 @@ SnowlMessagePart.prototype = {
// FIXME: update the existing record as appropriate.
}
else {
this._stmtInsertPart.params.messageID = message.id;
this._stmtInsertPart.params.partType = this.partType;
this._stmtInsertPart.params.content = this.content;
this._stmtInsertPart.params.mediaType = this.mediaType;
this._stmtInsertPart.params.baseURI = (this.baseURI ? this.baseURI.spec : null);
this._stmtInsertPart.params.languageTag = this.languageTag;
this._stmtInsertPart.execute();
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// Insert a plaintext version of the content into the partsText fulltext
// table, converting it to plaintext first if necessary (and possible).
switch (this.mediaType) {
case "text/html":
case "application/xhtml+xml":
case "text/plain":
// Give the fulltext record the same doc ID as the row ID of the parts
// record so we can join them together to get the part (and thence the
// message) when doing a fulltext search.
this._stmtInsertPartText.params.docid = this.id;
this._stmtInsertPartText.params.content = this.plainText();
this._stmtInsertPartText.execute();
break;
default:
// It isn't a type we understand, so don't do anything with it.
// XXX If it's text/*, shouldn't we fulltext index it anyway?
try {
this._stmtInsertPart.params.messageID = message.id;
this._stmtInsertPart.params.partType = this.partType;
this._stmtInsertPart.params.content = this.content;
this._stmtInsertPart.params.mediaType = this.mediaType;
this._stmtInsertPart.params.baseURI = (this.baseURI ? this.baseURI.spec : null);
this._stmtInsertPart.params.languageTag = this.languageTag;
this._stmtInsertPart.execute();
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// Insert a plaintext version of the content into the partsText fulltext
// table, converting it to plaintext first if necessary (and possible).
switch (this.mediaType) {
case "text/html":
case "application/xhtml+xml":
case "text/plain":
// Give the fulltext record the same doc ID as the row ID of the parts
// record so we can join them together to get the part (and thence the
// message) when doing a fulltext search.
this._stmtInsertPartText.params.docid = this.id;
this._stmtInsertPartText.params.content = this.plainText();
this._stmtInsertPartText.execute();
break;
default:
// It isn't a type we understand, so don't do anything with it.
// XXX If it's text/*, shouldn't we fulltext index it anyway?
}
}
finally {
this._stmtInsertPart.reset();
this._stmtInsertPartText.reset();
}
}
}

Просмотреть файл

@ -285,28 +285,41 @@ let SnowlService = {
let now = new Date();
let staleSources = [];
for each (let source in this.sources)
if (now - source.lastRefreshed > source.refreshInterval)
if (now - source.lastRefreshed > source.refreshInterval &&
!this.sourcesByID[source.id].busy &&
source.attributes["refreshStatus"] != "disabled")
// Do not autorefresh (as opposed to user initiated refresh) if a source
// is permanently disabled (404 error eg); do not refresh busy source.
staleSources.push(source);
this.refreshAllSources(staleSources);
},
get refreshingCount() {
return this._refreshingCount ? this._refreshingCount : this._refreshingCount = 0;
},
set refreshingCount(val) {
return this._refreshingCount = val;
},
refreshAllSources: function(sources) {
let cachedsource;
let allSources = sources ? sources : this.sources;
// Set busy property, notify observer to invalidate tree.
// Set busy property.
for each (let source in allSources) {
this.sourcesByID[source.id].busy = true;
this.sourcesByID[source.id].error = false;
cachedsource = this.sourcesByID[source.id];
if (cachedsource) {
cachedsource.busy = true;
cachedsource.error = false;
cachedsource.attributes["refreshStatus"] = "active";
}
this.refreshingCount = ++this.refreshingCount;
}
if (allSources.length > 0) {
// TODO: Don't set busy on 'all' until we know when the last one is done
// so it can be unset.
// this._collectionStatsByCollectionID["all"].busy = true;
// Invalidate tree to show new state.
if (allSources.length > 0)
// Invalidate collections tree to show new state.
Observers.notify("snowl:messages:completed", "refresh");
}
// We specify the same refresh time when refreshing sources so that all
// new messages have the same received time, which makes messages sorted by
@ -315,10 +328,8 @@ let SnowlService = {
// when retrieved in the same refresh (f.e. when the user starts their
// browser in the morning after leaving it off overnight).
let refreshTime = new Date();
for each (let source in allSources) {
this._log.info("refreshStaleSources: refreshInterval - "+source.refreshInterval);
for each (let source in allSources)
this.refreshSourceTimer(source, refreshTime);
}
},
refreshSourceTimer: function(aSource, aRefreshTime) {
@ -328,12 +339,18 @@ this._log.info("refreshStaleSources: refreshInterval - "+source.refreshInterval)
aSource.name + " - " + aSource.machineURI.spec);
try {
aSource.refresh(aRefreshTime);
aSource.persist(true);
}
catch(ex) {
aSource.lastStatus = ex;
aSource.onRefreshError();
}
try {
aSource.persist(true);
}
catch(ex) {
aSource.lastStatus = ex;
aSource.onDbError();
}
} };
timer.initWithCallback(callback, 10, Ci.nsITimer.TYPE_ONE_SHOT);

Просмотреть файл

@ -42,6 +42,7 @@ const Cr = Components.results;
const Cu = Components.utils;
// modules that are generic
Cu.import("resource://snowl/modules/log4moz.js");
Cu.import("resource://snowl/modules/Observers.js");
Cu.import("resource://snowl/modules/Sync.js");
Cu.import("resource://snowl/modules/URI.js");
@ -192,7 +193,13 @@ SnowlSource.prototype = {
// specified in order for its non-set value to remain null.
this.importance = aImportance || null;
this.placeID = aPlaceID;
this.attributes = aAttributes;
this.attributes = aAttributes || new Object;
},
get _log() {
let logger = Log4Moz.repository.getLogger(this._logName);
this.__defineGetter__("_log", function() logger);
return this._log;
},
// For adding isBusy property to collections tree.
@ -314,12 +321,35 @@ SnowlSource.prototype = {
onRefreshError: function() {
this.error = true;
if (this.attributes["statusCode"] = 404) {
this.attributes["refreshStatus"] = "disabled";
SnowlService.sourcesByID[this.id].attributes["refreshStatus"] = "disabled";
}
this._log.error("Refresh error: " + this.lastStatus);
},
onDbCompleted: function() {
// Database source record updated, set notifications and states.
if (this.id) {
// Only for existing stored sources; notify refreshes collections tree state.
SnowlService.sourcesByID[this.id].error = true;
SnowlService.sourcesByID[this.id].busy = false;
SnowlService.sourcesByID[this.id].error = this.error;
SnowlService.refreshingCount = --SnowlService.refreshingCount;
}
Observers.notify("snowl:messages:completed", this.id);
},
onDbError: function() {
this.error = true;
if (this.id) {
// Only for existing stored sources; notify refreshes collections tree state.
SnowlService.sourcesByID[this.id].busy = false;
SnowlService.sourcesByID[this.id].error = this.error;
SnowlService.refreshingCount = --SnowlService.refreshingCount;
Observers.notify("snowl:messages:completed", this.id);
}
this._log.error("Refresh error: " + this.lastStatus);
this._log.error("Database error: " + this.lastStatus);
},
retrieveMessages: function() {
@ -362,17 +392,40 @@ SnowlSource.prototype = {
" humanURI = :humanURI, " +
" username = :username, " +
"lastRefreshed = :lastRefreshed, " +
" importance = :importance " +
"WHERE id = :id"
" importance = :importance, " +
" attributes = :attributes " +
"WHERE id = :id"
);
}
else {
statement = SnowlDatastore.createStatement(
"INSERT INTO sources ( name, type, machineURI, humanURI, username, lastRefreshed, importance) " +
"VALUES (:name, :type, :machineURI, :humanURI, :username, :lastRefreshed, :importance)"
"INSERT INTO sources ( name, type, machineURI, humanURI, username, " +
" lastRefreshed, importance, attributes) " +
"VALUES ( :name, :type, :machineURI, :humanURI, :username, " +
" :lastRefreshed, :importance, :attributes)"
);
}
// Need to get a transaction lock.
if (SnowlDatastore.dbConnection.transactionInProgress) {
this.attributes["statusCode"] = "db:transactionInProgress";
this.lastStatus = "Database temporarily busy, could not get transaction lock";
if (this.id) {
// Only for existing stored sources; notify refreshes collections tree state.
SnowlService.sourcesByID[this.id].busy = false;
SnowlService.sourcesByID[this.id].error = this.error;
SnowlService.refreshingCount = --SnowlService.refreshingCount;
// Observers.notify("snowl:messages:completed", this.id);
}
else {
// New subscriptions need to return feedback.
this.error = true;
this._log.info("persist: " + this.lastStatus);
}
return;
}
SnowlDatastore.dbConnection.beginTransaction();
try {
statement.params.name = this.name;
@ -382,12 +435,15 @@ SnowlSource.prototype = {
statement.params.username = this.username;
statement.params.lastRefreshed = this.lastRefreshed ? SnowlDateUtils.jsToJulianDate(this.lastRefreshed) : null;
statement.params.importance = this.importance;
statement.params.attributes = JSON.stringify(this.attributes);
if (this.id)
statement.params.id = this.id;
statement.step();
if (!this.id) {
// Extract the ID of the source from the newly-created database record.
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// New source, bump refreshing count.
SnowlService.refreshingCount = ++SnowlService.refreshingCount;
// Update message authors to include the source ID.
// FIXME: make SnowlIdentity records have a source property
@ -465,18 +521,17 @@ this._log.info("persist placeID:sources.id - " + this.placeID + " : " + this.id)
if (messagesChanged)
// Invalidate stats cache on completion of refresh with new messages.
SnowlService._collectionStatsByCollectionID = null;
// Notify collections view on completion of refresh.
SnowlService.sourcesByID[this.id].busy = false;
Observers.notify("snowl:messages:completed", this.id);
}
SnowlDatastore.dbConnection.commitTransaction();
// Source successfully stored/updated.
this.onDbCompleted();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
this.lastStatus = ex;
this.onRefreshError();
this.onDbError();
}
finally {
statement.reset();

Просмотреть файл

@ -46,7 +46,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ISO8601DateUtils.jsm");
// modules that are generic
Cu.import("resource://snowl/modules/log4moz.js");
Cu.import("resource://snowl/modules/Mixins.js");
Cu.import("resource://snowl/modules/Observers.js");
Cu.import("resource://snowl/modules/request.js");
@ -142,10 +141,8 @@ SnowlTwitter.prototype = {
// need to check it to find out what kind of object an instance is.
constructor: SnowlTwitter,
get _log() {
let logger = Log4Moz.repository.getLogger("Snowl.Twitter." + this.username);
this.__defineGetter__("_log", function() logger);
return this._log;
get _logName() {
return "Snowl.Twitter." + this.username;
},
@ -341,7 +338,7 @@ SnowlTwitter.prototype = {
time = new Date();
// this._log.info("start refresh " + this.username + " at " + time);
Observers.notify("snowl:subscribe:get:start", this);
Observers.notify("snowl:refresh:connect:start", this);
// URL parameters that modify the return value of the request.
let params = [];
@ -376,9 +373,9 @@ SnowlTwitter.prototype = {
requestHeaders: requestHeaders
});
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:end", this, request.status);
Observers.notify("snowl:refresh:connect:end", this, request.status);
this.attributes["statusCode"] = request.status;
this.lastStatus = request.status + " (" + request.statusText + ")";
if (request.status < 200 || request.status > 299 || request.responseText.length == 0) {
this.onRefreshError();
@ -393,12 +390,14 @@ SnowlTwitter.prototype = {
this._authInfo = null;
}
Observers.notify("snowl:refresh:get:start", this);
let items = JSON.parse(request.responseText);
this.messages = this._processItems(items, time);
this.lastRefreshed = time;
Observers.notify("snowl:subscribe:get:end", this);
Observers.notify("snowl:refresh:get:end", this);
},