Bug 1140810 - Upload material (non-status) Reading List modifications. r=rnewman

========

575d80fddb
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 16:35:26 2015 -0700

    Bug 1140810 - Part 2: Upload material (non-status) modifications.

========

a86e734ef1
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 10:56:07 2015 -0700

    Bug 1140810 - Part 1: Add storage test for material (non-status) modifications.

========

2259378d08
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 13:33:13 2015 -0700

    Bug 1140810 - Part 0: Add and use HTTP PATCH.

========

0222d53d98
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 14:49:19 2015 -0700

    Bug 1140810 - Pre: Don't fail in status upload when there are no failures.

========

7f2feede3b
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 14:53:45 2015 -0700

    Bug 1140810 - Pre: convert 4 spaces to 2 spaces.

========

49e80d271e
Author: Nick Alexander <nalexander@mozilla.com>
Date:   Wed Mar 25 10:40:26 2015 -0700

    Bug 1140810 - Pre: Fix whitespace.
This commit is contained in:
Nick Alexander 2015-03-26 15:47:20 -07:00
Родитель cef3c3aa64
Коммит 9c2b6b157d
8 изменённых файлов: 261 добавлений и 11 удалений

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

@ -266,15 +266,17 @@ public class LocalReadingListStorage implements ReadingListStorage {
ReadingListItems.RESOLVED_TITLE,
ReadingListItems.RESOLVED_URL,
ReadingListItems.EXCERPT,
// TODO: ReadingListItems.IS_ARTICLE,
// TODO: ReadingListItems.WORD_COUNT,
};
try {
return client.query(URI_WITHOUT_DELETED, projection, selection, null, null);
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
}
@Override
public Cursor getModified() {
final String selection = ReadingListItems.SYNC_STATUS + " = " + ReadingListItems.SYNC_STATUS_MODIFIED;

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

@ -482,7 +482,7 @@ public class ReadingListClient {
if (ReadingListConstants.DEBUG) {
Logger.info(LOG_TAG, "Patching record " + guid + ": " + body.toJSONString());
}
r.post(body);
r.patch(body);
}
/**

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

@ -10,7 +10,7 @@ public class ReadingListConstants {
public static final String GLOBAL_LOG_TAG = "FxReadingList";
public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_DISPLAYNAME + ")";
public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
public static final String DEFAULT_PROD_ENDPOINT = null; // TODO
public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/";
public static final String OAUTH_ENDPOINT_PROD = "https://oauth.accounts.firefox.com/v1";

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

@ -248,11 +248,10 @@ public class ReadingListSynchronizer {
}
if (failures == 0) {
try {
next.next();
} catch (Exception e) {
}
next.next();
return;
}
next.fail();
}
}
@ -305,6 +304,149 @@ public class ReadingListSynchronizer {
}
}
private static class ModifiedUploadDelegate implements ReadingListRecordUploadDelegate {
private final ReadingListChangeAccumulator acc;
public volatile int failures = 0;
private final StageDelegate next;
ModifiedUploadDelegate(ReadingListChangeAccumulator acc, StageDelegate next) {
this.acc = acc;
this.next = next;
}
@Override
public void onInvalidUpload(ClientReadingListRecord up,
ReadingListResponse response) {
recordFailed(up);
}
@Override
public void onConflict(ClientReadingListRecord up,
ReadingListResponse response) {
// This can happen for a material change.
failures++;
}
@Override
public void onSuccess(ClientReadingListRecord up,
ReadingListRecordResponse response,
ServerReadingListRecord down) {
if (!TextUtils.equals(up.getGUID(), down.getGUID())) {
// Uh oh!
// This should never occur. We should get an onConflict instead,
// so this would imply a server bug, or something like a truncated
// over-long GUID string.
//
// Should we wish to recover from this case, probably the right approach
// is to ensure that the GUID is overwritten locally (given that we know
// the numeric ID).
}
// We could upload our material changes but get back additional status
// changes from the server. Apply them.
acc.addChangedRecord(up.givenServerRecord(down));
}
@Override
public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
recordFailed(up);
}
@Override
public void onFailure(ClientReadingListRecord up, Exception ex) {
recordFailed(up);
}
@Override
public void onFailure(ClientReadingListRecord up, MozResponse response) {
// Since we download and apply remote changes before uploading local changes, the conflict
// window is very small. We should essentially never see true conflicts here.
if (response.getStatusCode() == 404) {
// We shouldn't see a 404; we should see a record with deleted=true when
// we fetch remote changes.
Logger.warn(LOG_TAG, "Ignoring 404 response patching record with guid: " + up.getGUID());
} else if (response.getStatusCode() == 409) {
// A 409 indicates that resolved_url has collided with an existing
// record. Not much to be done here.
Logger.info(LOG_TAG, "409 response seen; deleting record with guid: " + up.getGUID());
acc.addDeletion(up);
} else {
// We should never see a 412 since we race to upload our changes (and
// accept whatever the server gives us back).
recordFailed(up);
}
}
private void recordFailed(ClientReadingListRecord up) {
++failures;
}
@Override
public void onBatchDone() {
try {
acc.finish();
} catch (Exception e) {
next.fail(e);
return;
}
if (failures == 0) {
next.next();
return;
}
next.fail();
}
}
private Queue<ClientReadingListRecord> collectModifiedFromCursor(final Cursor cursor) {
try {
final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
final int columnGUID = cursor.getColumnIndexOrThrow(ReadingListItems.GUID);
final int columnExcerpt = cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT);
final int columnResolvedURL = cursor.getColumnIndexOrThrow(ReadingListItems.RESOLVED_URL);
final int columnResolvedTitle = cursor.getColumnIndexOrThrow(ReadingListItems.RESOLVED_TITLE);
// TODO: final int columnIsArticle = cursor.getColumnIndexOrThrow(ReadingListItems.IS_ARTICLE);
// TODO: final int columnWordCount = cursor.getColumnIndexOrThrow(ReadingListItems.WORD_COUNT);
while (cursor.moveToNext()) {
final String guid = cursor.getString(columnGUID);
if (guid == null) {
// Nothing we can do here, but this should never happen: we should
// have uploaded this record as new before trying to upload a
// material modification!
continue;
}
final ExtendedJSONObject o = new ExtendedJSONObject();
o.put("id", guid);
final String excerpt = cursor.getString(columnExcerpt); // Can be NULL.
final String resolvedURL = cursor.getString(columnResolvedURL); // Can be NULL.
final String resolvedTitle = cursor.getString(columnResolvedTitle); // Can be NULL.
if (excerpt == null && resolvedURL == null && resolvedTitle == null) {
// Nothing material to upload, so skip this record.
continue;
}
o.put("excerpt", excerpt);
o.put("resolved_url", resolvedURL);
o.put("resolved_title", resolvedTitle);
// TODO: o.put("is_article", cursor.getInt(columnIsArticle) == 1);
// TODO: o.put("word_count", cursor.getInt(columnWordCount));
final ClientMetadata cm = null;
final ServerMetadata sm = new ServerMetadata(guid, -1L);
final ClientReadingListRecord record = new ClientReadingListRecord(sm, cm, o);
toUpload.add(record);
}
return toUpload;
} finally {
cursor.close();
}
}
private Queue<ClientReadingListRecord> accumulateNewItems(Cursor cursor) {
try {
final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
@ -335,8 +477,11 @@ public class ReadingListSynchronizer {
// Nothing to do.
if (toUpload.isEmpty()) {
Logger.debug(LOG_TAG, "No new unread changes to upload. Skipping.");
delegate.next();
return;
} else {
Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " new unread changes.");
}
// Upload each record. This looks like batching, but it's really chained serial requests.
@ -368,6 +513,8 @@ public class ReadingListSynchronizer {
Logger.debug(LOG_TAG, "No new items to upload. Skipping.");
delegate.next();
return;
} else {
Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " new items.");
}
final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
@ -420,9 +567,79 @@ public class ReadingListSynchronizer {
}
}
private void uploadModified(final StageDelegate delegate) {
// TODO
delegate.next();
protected void uploadModified(final StageDelegate delegate) {
try {
// This looks strange because modified includes material changes and
// status changes, but this is called after status changes have been
// uploaded and removed from local storage. So what's left should be
// material changes. Even so, it should be safe to upload status changes
// here.
final Cursor cursor = this.local.getModified();
if (cursor == null) {
delegate.fail(new RuntimeException("Unable to get modified item cursor."));
return;
}
final Queue<ClientReadingListRecord> toUpload = collectModifiedFromCursor(cursor);
// Nothing to do.
if (toUpload.isEmpty()) {
Logger.debug(LOG_TAG, "No modified items to upload. Skipping.");
delegate.next();
return;
} else {
Logger.debug(LOG_TAG, "Uploading " + toUpload.size() + " modified items.");
}
final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
final ModifiedUploadDelegate uploadDelegate = new ModifiedUploadDelegate(acc, new StageDelegate() {
private boolean tryFlushChanges() {
Logger.debug(LOG_TAG, "Flushing post-upload changes.");
try {
acc.finish();
return true;
} catch (Exception e) {
Logger.warn(LOG_TAG, "Flushing changes failed! This sync went wrong.", e);
delegate.fail(e);
return false;
}
}
@Override
public void next() {
Logger.debug(LOG_TAG, "Modified items uploaded successfully.");
if (tryFlushChanges()) {
delegate.next();
}
}
@Override
public void fail() {
Logger.warn(LOG_TAG, "Couldn't upload modified items.");
if (tryFlushChanges()) {
delegate.fail();
}
}
@Override
public void fail(Exception e) {
Logger.warn(LOG_TAG, "Couldn't upload modified items.", e);
if (tryFlushChanges()) {
delegate.fail(e);
}
}
});
// Handle 201 for success, 400 for invalid, 303 for redirect.
// TODO: 200 == "was already on the server, we didn't touch it, here it is."
// ... we need to apply it locally.
this.remote.patch(toUpload, executor, uploadDelegate);
} catch (Exception e) {
delegate.fail(e);
return;
}
}
private void downloadIncoming(final long since, final StageDelegate delegate) {

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

@ -28,4 +28,8 @@ public class ServerReadingListRecord extends ReadingListRecord {
public String getAddedBy() {
return this.fields.getString("added_by");
}
}
public String getExcerpt() {
return this.fields.getString("excerpt");
}
}

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

@ -30,6 +30,7 @@ import ch.boye.httpclientandroidlib.client.AuthCache;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
import ch.boye.httpclientandroidlib.client.methods.HttpPatch;
import ch.boye.httpclientandroidlib.client.methods.HttpPost;
import ch.boye.httpclientandroidlib.client.methods.HttpPut;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
@ -341,6 +342,14 @@ public class BaseResource implements Resource {
this.go(request);
}
@Override
public void patch(HttpEntity body) {
Logger.debug(LOG_TAG, "HTTP PATCH " + this.uri.toASCIIString());
HttpPatch request = new HttpPatch(this.uri);
request.setEntity(body);
this.go(request);
}
@Override
public void put(HttpEntity body) {
Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString());
@ -463,4 +472,16 @@ public class BaseResource implements Resource {
public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
post(jsonEntity(jsonObject));
}
public void patch(JSONArray jsonArray) throws UnsupportedEncodingException {
patch(jsonEntity(jsonArray));
}
public void patch(ExtendedJSONObject o) {
patch(jsonEntity(o));
}
public void patch(JSONObject jsonObject) throws UnsupportedEncodingException {
patch(jsonEntity(jsonObject));
}
}

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

@ -15,5 +15,6 @@ public interface Resource {
public abstract void get();
public abstract void delete();
public abstract void post(HttpEntity body);
public abstract void patch(HttpEntity body);
public abstract void put(HttpEntity body);
}

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

@ -190,6 +190,11 @@ public class SyncStorageRequest implements Resource {
this.resource.post(body);
}
@Override
public void patch(HttpEntity body) {
this.resource.patch(body);
}
@Override
public void put(HttpEntity body) {
this.resource.put(body);