Add objects from failed download requests to sync queue
Previously only individual objects from successful requests that couldn't be processed for some reason would be added to the queue. `Sync.APIClient.downloadObjects()` now returns clearer and more consistent results. It now returns an array of promises for objects with a `keys` array of requested keys and either a `json` array of returned API JSON objects or an `error` Error, depending on whether the request succeeded or failed. This makes it easier to detect remotely missing objects and request failures.
This commit is contained in:
parent
86965988b4
commit
644c4e5925
4 changed files with 83 additions and 32 deletions
|
@ -567,14 +567,14 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
|
||||||
|
|
||||||
// Get updated item metadata
|
// Get updated item metadata
|
||||||
let library = Zotero.Libraries.get(item.libraryID);
|
let library = Zotero.Libraries.get(item.libraryID);
|
||||||
let json = yield this.apiClient.downloadObjects(
|
let { json, error } = yield this.apiClient.downloadObjects(
|
||||||
library.libraryType,
|
library.libraryType,
|
||||||
library.libraryTypeID,
|
library.libraryTypeID,
|
||||||
'item',
|
'item',
|
||||||
[item.key]
|
[item.key]
|
||||||
)[0];
|
)[0];
|
||||||
if (!Array.isArray(json)) {
|
if (error) {
|
||||||
Zotero.logError(json);
|
Zotero.logError(error);
|
||||||
throw new Error(Zotero.Sync.Storage.defaultError);
|
throw new Error(Zotero.Sync.Storage.defaultError);
|
||||||
}
|
}
|
||||||
if (json.length > 1) {
|
if (json.length > 1) {
|
||||||
|
|
|
@ -259,8 +259,9 @@ Zotero.Sync.APIClient.prototype = {
|
||||||
* @param {Integer} libraryTypeID - userID or groupID
|
* @param {Integer} libraryTypeID - userID or groupID
|
||||||
* @param {String} objectType - 'collection', 'item', 'search'
|
* @param {String} objectType - 'collection', 'item', 'search'
|
||||||
* @param {String[]} objectKeys - Keys of objects to request
|
* @param {String[]} objectKeys - Keys of objects to request
|
||||||
* @return {Array<Promise<Object[]|Error[]>>} - An array of promises for batches of JSON objects
|
* @return {Promise<Object>[]} - An array of promises for objects with JSON data as
|
||||||
* or Errors for failures
|
* { keys: String[], json: Object[] } or objects with errors as
|
||||||
|
* { keys: String[], error: Error }
|
||||||
*/
|
*/
|
||||||
downloadObjects: function (libraryType, libraryTypeID, objectType, objectKeys) {
|
downloadObjects: function (libraryType, libraryTypeID, objectType, objectKeys) {
|
||||||
if (!objectKeys.length) {
|
if (!objectKeys.length) {
|
||||||
|
@ -309,7 +310,10 @@ Zotero.Sync.APIClient.prototype = {
|
||||||
return [
|
return [
|
||||||
this.makeRequest("GET", uri)
|
this.makeRequest("GET", uri)
|
||||||
.then(function (xmlhttp) {
|
.then(function (xmlhttp) {
|
||||||
return this._parseJSON(xmlhttp.responseText)
|
return {
|
||||||
|
keys: objectKeys,
|
||||||
|
json: this._parseJSON(xmlhttp.responseText)
|
||||||
|
};
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
// Return the error without failing the whole chain
|
// Return the error without failing the whole chain
|
||||||
.catch(function (e) {
|
.catch(function (e) {
|
||||||
|
@ -317,7 +321,10 @@ Zotero.Sync.APIClient.prototype = {
|
||||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.is4xx()) {
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.is4xx()) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return e;
|
return {
|
||||||
|
keys: objectKeys,
|
||||||
|
error: e
|
||||||
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
@ -546,21 +546,26 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
||||||
this._statusCheck();
|
this._statusCheck();
|
||||||
|
|
||||||
// Get data we've downloaded in a previous loop but failed to process
|
// Get data we've downloaded in a previous loop but failed to process
|
||||||
var json = [];
|
var results = [];
|
||||||
let keysToDownload = [];
|
let keysToDownload = [];
|
||||||
|
var keysToReprocess = [];
|
||||||
for (let key in objectData) {
|
for (let key in objectData) {
|
||||||
if (objectData[key] === null) {
|
if (objectData[key] === null) {
|
||||||
keysToDownload.push(key);
|
keysToDownload.push(key);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
json.push(objectData[key]);
|
keysToReprocess.push(key);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (json.length) {
|
if (keysToReprocess) {
|
||||||
json = [json];
|
results.push({
|
||||||
|
keys: keysToReprocess,
|
||||||
|
json: keysToReprocess.map(key => objectData[key])
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Add promises for batches of downloaded data for remaining keys
|
// Add promises for batches of downloaded data for remaining keys
|
||||||
json.push(...this.apiClient.downloadObjects(
|
results.push(...this.apiClient.downloadObjects(
|
||||||
this.library.libraryType,
|
this.library.libraryType,
|
||||||
this.libraryTypeID,
|
this.libraryTypeID,
|
||||||
objectType,
|
objectType,
|
||||||
|
@ -576,33 +581,43 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
||||||
+ " in " + this.library.name
|
+ " in " + this.library.name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var missingKeys = [];
|
||||||
var conflicts = [];
|
var conflicts = [];
|
||||||
var restored = [];
|
var restored = [];
|
||||||
var num = 0;
|
var num = 0;
|
||||||
|
|
||||||
// Process batches of object data as they're available, one at a time
|
// Process batches of object data as they're available, one at a time
|
||||||
await Zotero.Promise.map(
|
await Zotero.Promise.map(
|
||||||
json,
|
results,
|
||||||
async function (batch) {
|
async function ({ keys: batchKeys, json, error }) {
|
||||||
this._statusCheck();
|
this._statusCheck();
|
||||||
|
|
||||||
Zotero.debug(`Processing batch of downloaded ${objectTypePlural} in ${this.library.name}`);
|
Zotero.debug(`Processing batch of downloaded ${objectTypePlural} in ${this.library.name}`);
|
||||||
|
|
||||||
if (!Array.isArray(batch)) {
|
if (error) {
|
||||||
this.failed = batch;
|
this.failed = error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save downloaded JSON for later attempts
|
// Save downloaded JSON for later attempts
|
||||||
batch.forEach(obj => {
|
var seenKeys = new Set();
|
||||||
|
json.forEach(obj => {
|
||||||
objectData[obj.key] = obj;
|
objectData[obj.key] = obj;
|
||||||
|
seenKeys.add(obj.key);
|
||||||
});
|
});
|
||||||
|
// If a key we requested wasn't returned, it's missing remotely
|
||||||
|
for (let key of batchKeys) {
|
||||||
|
if (!seenKeys.has(key)) {
|
||||||
|
missingKeys.push(key);
|
||||||
|
objectData[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process objects
|
// Process objects
|
||||||
let results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
let results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
||||||
objectType,
|
objectType,
|
||||||
this.libraryID,
|
this.libraryID,
|
||||||
batch,
|
json,
|
||||||
this._getOptions({
|
this._getOptions({
|
||||||
onObjectProcessed: () => {
|
onObjectProcessed: () => {
|
||||||
num++;
|
num++;
|
||||||
|
@ -627,7 +642,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
||||||
else {
|
else {
|
||||||
size = 50;
|
size = 50;
|
||||||
}
|
}
|
||||||
return Math.min(size, batch.length);
|
return Math.min(size, json.length);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -671,20 +686,21 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
||||||
|
|
||||||
// If all requests were successful, such that we had a chance to see all keys, remove keys we
|
// If all requests were successful, such that we had a chance to see all keys, remove keys we
|
||||||
// didn't see from the sync queue so they don't keep being retried forever
|
// didn't see from the sync queue so they don't keep being retried forever
|
||||||
if (!this.failed) {
|
if (missingKeys.length) {
|
||||||
let missingKeys = keys.filter(key => objectData[key] === null);
|
Zotero.debug(`Removing ${missingKeys.length} missing `
|
||||||
if (missingKeys.length) {
|
+ Zotero.Utilities.pluralize(missingKeys.length, [objectType, objectTypePlural])
|
||||||
Zotero.debug(`Removing ${missingKeys.length} missing `
|
+ " from sync queue");
|
||||||
+ Zotero.Utilities.pluralize(missingKeys.length, [objectType, objectTypePlural])
|
await Zotero.Sync.Data.Local.removeObjectsFromSyncQueue(objectType, this.libraryID, missingKeys);
|
||||||
+ " from sync queue");
|
remainingKeys = Zotero.Utilities.arrayDiff(remainingKeys, missingKeys);
|
||||||
await Zotero.Sync.Data.Local.removeObjectsFromSyncQueue(objectType, this.libraryID, missingKeys);
|
|
||||||
remainingKeys = Zotero.Utilities.arrayDiff(remainingKeys, missingKeys);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!remainingKeys.length || remainingKeys.length == lastLength) {
|
if (!remainingKeys.length || remainingKeys.length == lastLength) {
|
||||||
// Add failed objects to sync queue
|
// Add failed objects to sync queue
|
||||||
let failedKeys = keys.filter(key => objectData[key]);
|
//
|
||||||
|
// Object failed if it's still present in the object data (since successfully processed
|
||||||
|
// objects are removed) and it's not false (meaning it was missing from the request,
|
||||||
|
// which isn't a failure)
|
||||||
|
let failedKeys = keys.filter(key => objectData[key] !== undefined && objectData[key] !== false);
|
||||||
if (failedKeys.length) {
|
if (failedKeys.length) {
|
||||||
Zotero.debug(`Queueing ${failedKeys.length} failed `
|
Zotero.debug(`Queueing ${failedKeys.length} failed `
|
||||||
+ Zotero.Utilities.pluralize(failedKeys.length, [objectType, objectTypePlural])
|
+ Zotero.Utilities.pluralize(failedKeys.length, [objectType, objectTypePlural])
|
||||||
|
@ -1464,12 +1480,12 @@ Zotero.Sync.Data.Engine.prototype._updateGroupItemUsers = async function () {
|
||||||
|
|
||||||
Zotero.debug(`Updating item users in ${this.library.name}`);
|
Zotero.debug(`Updating item users in ${this.library.name}`);
|
||||||
|
|
||||||
var jsonItems = await this.apiClient.downloadObjects(
|
var { json: jsonItems, error } = await this.apiClient.downloadObjects(
|
||||||
this.library.libraryType, this.libraryTypeID, 'item', keys
|
this.library.libraryType, this.libraryTypeID, 'item', keys
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
if (!Array.isArray(jsonItems)) {
|
if (error) {
|
||||||
Zotero.logError(e);
|
Zotero.logError(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3005,6 +3005,34 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, item.key)
|
await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, item.key)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("should add object from failed download request to sync queue", async function () {
|
||||||
|
({ engine, client, caller } = await setup({
|
||||||
|
stopOnError: false
|
||||||
|
}));
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
||||||
|
var item = await createDataObject('item');
|
||||||
|
var itemKey = item.key;
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
"Last-Modified-Version": 5
|
||||||
|
};
|
||||||
|
setResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: `users/1/items?itemKey=${itemKey}&includeTrashed=1`,
|
||||||
|
status: 0,
|
||||||
|
headers,
|
||||||
|
body: ""
|
||||||
|
});
|
||||||
|
await engine._downloadObjects('item', [itemKey]);
|
||||||
|
|
||||||
|
assert.sameMembers(
|
||||||
|
await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID),
|
||||||
|
[itemKey]
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue