Split Apply Ops Into Chuncks
To prevent TransactionTooLargeException errors. If it still fails, retry with
a minimal chunk to apply one message at a time.
Bug: 10402517
Change-Id: Ia121652da6da56587939dc813abb801d055c24ca
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index 7f9997f..396ec29 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -88,6 +88,8 @@
public static final String MIME_BODY_PREFERENCE_TEXT = "0";
public static final String MIME_BODY_PREFERENCE_MIME = "2";
+ // These limits must never exceed about 500k which is half the max size of a Binder IPC buffer.
+
// For EAS 12, we use HTML, so we want a larger size than in EAS 2.5
public static final String EAS12_TRUNCATION_SIZE = "200000";
// For EAS 2.5, truncation is a code; the largest is "7", which is 100k
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index 118c704..a3a0633 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -20,12 +20,15 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.exchange.CommandStatusException;
import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -37,6 +40,8 @@
*
*/
public abstract class AbstractSyncParser extends Parser {
+ private static final String TAG = "AbstractSyncParser";
+
protected Mailbox mMailbox;
protected Account mAccount;
protected Context mContext;
@@ -90,7 +95,8 @@
* Commit any changes found during parsing
* @throws IOException
*/
- public abstract void commit() throws IOException;
+ public abstract void commit() throws IOException, RemoteException,
+ OperationApplicationException;
public boolean isLooping() {
return mLooping;
@@ -194,30 +200,16 @@
}
// Commit any changes
- commit();
-
-
- // TODO: I don't think this is still relevant. Syncing should not trigger changes in the
- // sync interval.
- /*
- // If the sync interval has changed, we need to save it
- if (mMailbox.mSyncInterval != interval) {
- cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
- mailboxUpdated = true;
- // If there are changes, and we were bounced from push/ping, try again
- } else if (mAdapter.mChangeCount > 0 &&
- mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
- mMailbox.mSyncInterval > 0) {
- userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
- cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
- mailboxUpdated = true;
- abortSyncs = true;
+ try {
+ commit();
+ if (mailboxUpdated) {
+ mMailbox.update(mContext, cv);
+ }
+ } catch (RemoteException e) {
+ LogUtils.e(TAG, "Failed to commit changes", e);
+ } catch (OperationApplicationException e) {
+ LogUtils.e(TAG, "Failed to commit changes", e);
}
- */
- if (mailboxUpdated) {
- mMailbox.update(mContext, cv);
- }
-
// Let the caller know that there's more to do
if (moreAvailable) {
userLog("MoreAvailable");
diff --git a/src/com/android/exchange/adapter/EmailSyncParser.java b/src/com/android/exchange/adapter/EmailSyncParser.java
index 8024b98..76dd163 100644
--- a/src/com/android/exchange/adapter/EmailSyncParser.java
+++ b/src/com/android/exchange/adapter/EmailSyncParser.java
@@ -7,6 +7,7 @@
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.os.Parcel;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.provider.CalendarContract;
@@ -14,6 +15,7 @@
import android.text.SpannedString;
import android.text.TextUtils;
import android.util.Base64;
+import android.util.Log;
import android.webkit.MimeTypeMap;
import com.android.emailcommon.internet.MimeMessage;
@@ -76,6 +78,14 @@
static final int LAST_VERB_FORWARD = 3;
private final Policy mPolicy;
+
+ // Max times to retry when we get a TransactionTooLargeException exception
+ private static final int MAX_RETRIES = 10;
+
+ // Max number of ops per batch. It could end up more than this but once we detect we are at or
+ // above this number, we flush.
+ private static final int MAX_OPS_PER_BATCH = 50;
+
private boolean mFetchNeeded = false;
private final Map<String, Integer> mMessageUpdateStatus = new HashMap();
@@ -719,25 +729,45 @@
}
}
+ /**
+ * Commit all changes. This results in a Binder IPC call which has constraint on the size of
+ * the data, the docs say it currently 1MB. We set a limit to the size of the message we fetch
+ * with {@link Eas#EAS12_TRUNCATION_SIZE} & {@link Eas#EAS12_TRUNCATION_SIZE} which are at 200k
+ * or bellow. As long as these limits are bellow 500k, we should be able to apply a single
+ * message (the transaction size is about double the message size because Java strings are 16
+ * bit.
+ * <b/>
+ * We first try to apply the changes in normal chunk size {@link #MAX_OPS_PER_BATCH}. If we get
+ * a {@link TransactionTooLargeException} we try again with but this time, we apply each change
+ * immediately.
+ */
@Override
- public void commit() {
- commitImpl(0);
+ public void commit() throws RemoteException, OperationApplicationException {
+ try {
+ commitImpl(MAX_OPS_PER_BATCH);
+ } catch (TransactionTooLargeException e) {
+ // Try again but apply batch after every message. The max message size defined in
+ // Eas.EAS12_TRUNCATION_SIZE or Eas.EAS2_5_TRUNCATION_SIZE is small enough to fit
+ // in a single Binder call.
+ LogUtils.w(TAG, "Transaction too large, retrying in single mode", e);
+ commitImpl(1);
+ }
}
- public void commitImpl(int tryCount) {
+ public void commitImpl(int maxOpsPerBatch)
+ throws RemoteException, OperationApplicationException {
// Use a batch operation to handle the changes
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// Maximum size of message text per fetch
int numFetched = fetchedEmails.size();
- int maxPerFetch = 0;
- if (numFetched > 0 && tryCount > 0) {
- // Educated guess that 450000 chars (900k) is ok; 600k is a killer
- // Remember that when fetching, we're not getting any other data
- // We'll keep trying, reducing the maximum each time
- // Realistically, this will rarely exceed 1, and probably never 2
- maxPerFetch = 450000 / numFetched / tryCount;
- }
+ LogUtils.d(TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d "
+ + "numDeleted=%d numChanged=%d",
+ maxOpsPerBatch,
+ numFetched,
+ newEmails.size(),
+ deletedEmails.size(),
+ changedEmails.size());
for (EmailContent.Message msg: fetchedEmails) {
// Find the original message's id (by serverId and mailbox)
Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
@@ -762,10 +792,6 @@
if (id != null) {
userLog("Fetched body successfully for ", id);
final String[] bindArgument = new String[] {id};
- if ((maxPerFetch > 0) && (msg.mText.length() > maxPerFetch)) {
- userLog("Truncating message to " + maxPerFetch);
- msg.mText = msg.mText.substring(0, maxPerFetch) + "...";
- }
ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
.withSelection(EmailContent.Body.MESSAGE_KEY + "=?", bindArgument)
.withValue(EmailContent.Body.TEXT_CONTENT, msg.mText)
@@ -776,16 +802,19 @@
EmailContent.Message.FLAG_LOADED_COMPLETE)
.build());
}
+ applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
for (EmailContent.Message msg: newEmails) {
msg.addSaveOps(ops);
+ applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
for (Long id : deletedEmails) {
ops.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
+ applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
if (!changedEmails.isEmpty()) {
@@ -806,6 +835,7 @@
.withValues(cv)
.build());
}
+ applyBatchIfNeeded(ops, maxOpsPerBatch, false);
}
// We only want to update the sync key here
@@ -815,16 +845,28 @@
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
.withValues(mailboxValues).build());
- try {
+ applyBatchIfNeeded(ops, maxOpsPerBatch, true);
+ userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
+ }
+
+ // Check if there at least MAX_OPS_PER_BATCH ops in queue and flush if there are.
+ // If force is true, flush regardless of size.
+ private void applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch,
+ boolean force)
+ throws RemoteException, OperationApplicationException {
+ if (force || ops.size() >= maxOpsPerBatch) {
+ // STOPSHIP Remove calculating size of data before ship
+ if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
+ final Parcel parcel = Parcel.obtain();
+ for (ContentProviderOperation op : ops) {
+ op.writeToParcel(parcel, 0);
+ }
+ Log.d(TAG, String.format("Committing %d ops total size=%d",
+ ops.size(), parcel.dataSize()));
+ parcel.recycle();
+ }
mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
- userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
- } catch (TransactionTooLargeException e) {
- LogUtils.w(TAG, "Transaction failed on fetched message; retrying...");
- commitImpl(++tryCount);
- } catch (RemoteException e) {
- // There is nothing to be done here; fail by returning null
- } catch (OperationApplicationException e) {
- // There is nothing to be done here; fail by returning null
+ ops.clear();
}
}
}
\ No newline at end of file