Commit e6798f6c authored by Ricki Hirner's avatar Ricki Hirner

Allow splitting of large transactions

parent 3a6692f0
......@@ -26,6 +26,7 @@ import ezvcard.Ezvcard;
import ezvcard.VCardVersion;
import ezvcard.io.text.VCardWriter;
import ezvcard.property.Address;
import ezvcard.property.Email;
import lombok.Cleanup;
public class AndroidContactTest extends InstrumentationTestCase {
......@@ -61,10 +62,10 @@ public class AndroidContactTest extends InstrumentationTestCase {
vcard.phoneticMiddleName = "Mittelerde";
vcard.phoneticFamilyName = "Fämilie";
@Cleanup("delete") AndroidContact contact = new AndroidContact(addressBook, vcard, null, null);
AndroidContact contact = new AndroidContact(addressBook, vcard, null, null);
contact.create();
AndroidContact contact2 = new AndroidContact(addressBook, contact.id, null, null);
@Cleanup("delete") AndroidContact contact2 = new AndroidContact(addressBook, contact.id, null, null);
Contact vcard2 = contact2.getContact();
assertEquals(vcard2.displayName, vcard.displayName);
assertEquals(vcard2.prefix, vcard.prefix);
......@@ -76,6 +77,20 @@ public class AndroidContactTest extends InstrumentationTestCase {
assertEquals(vcard2.phoneticFamilyName, vcard.phoneticFamilyName);
}
public void testLargeTransaction() throws FileNotFoundException, ContactsStorageException {
Contact vcard = new Contact();
vcard.displayName = "Large Transaction";
for (int i = 0; i < 4000; i++)
vcard.emails.add(new LabeledProperty<Email>(new Email("test" + i + "@example.com")));
AndroidContact contact = new AndroidContact(addressBook, vcard, null, null);
contact.create();
@Cleanup("delete") AndroidContact contact2 = new AndroidContact(addressBook, contact.id, null, null);
Contact vcard2 = contact2.getContact();
assertEquals(4000, vcard2.emails.size());
}
public void testAddressCaretEncoding() throws IOException {
Address address = new Address();
......
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
* Copyright (c) 2013 – 2015 Ricki Hirner (bitfire web engineering).
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*/
package at.bitfire.vcard4android;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.OperationApplicationException;
import android.os.Build;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import org.apache.commons.lang3.math.NumberUtils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import lombok.NonNull;
public class BatchOperation {
private final ContentProviderClient providerClient;
private final ArrayList<ContentProviderOperation> queue = new ArrayList<>();
ContentProviderResult[] results;
public BatchOperation(@NonNull ContentProviderClient providerClient) {
this.providerClient = providerClient;
}
public int nextBackrefIdx() {
return queue.size();
}
public void enqueue(ContentProviderOperation operation) {
queue.add(operation);
}
public int commit() throws ContactsStorageException {
int affected = 0;
if (!queue.isEmpty())
try {
Constants.log.fine("Committing " + queue.size() + " operations …");
results = providerClient.applyBatch(queue);
for (ContentProviderResult result : results)
if (result != null) // will have either .uri or .count set
if (result.count != null)
affected += result.count;
else if (result.uri != null)
affected += 1;
Constants.log.fine("… " + affected + " record(s) affected");
} catch(OperationApplicationException|RemoteException e) {
throw new ContactsStorageException("Couldn't apply batch operation", e);
}
queue.clear();
return affected;
}
public ContentProviderResult getResult(int idx) {
return results[idx];
}
private final ContentProviderClient providerClient;
private final List<Operation> queue = new LinkedList<>();
private ContentProviderResult[] results;
public BatchOperation(@NonNull ContentProviderClient providerClient) {
this.providerClient = providerClient;
}
public int nextBackrefIdx() {
return queue.size();
}
public void enqueue(Operation operation) {
queue.add(operation);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
public int commit() throws ContactsStorageException {
int affected = 0;
if (!queue.isEmpty())
try {
Constants.log.fine("Committing " + queue.size() + " operations …");
results = new ContentProviderResult[queue.size()];
runBatch(0, queue.size());
for (ContentProviderResult result : results)
if (result != null) // will have either .uri or .count set
if (result.count != null)
affected += result.count;
else if (result.uri != null)
affected += 1;
Constants.log.fine("… " + affected + " record(s) affected");
} catch(OperationApplicationException|RemoteException e) {
throw new ContactsStorageException("Couldn't apply batch operation", e);
}
queue.clear();
return affected;
}
public ContentProviderResult getResult(int idx) {
return results[idx];
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
private void runBatch(int start, int end) throws RemoteException, OperationApplicationException, ContactsStorageException {
try {
Constants.log.fine("Running operations " + start + " to " + (end - 1));
ContentProviderResult partResults[] = providerClient.applyBatch(toCPO(start, end));
int n = end - start;
if (partResults.length != n)
throw new ContactsStorageException("Batch operation failed partially (only " + partResults.length + " of " + n + " operations done)");
System.arraycopy(partResults, 0, results, start, n);
} catch(TransactionTooLargeException e) {
Constants.log.warning("Transaction too large, splitting (losing atomicity)");
int mid = start + (end - start)/2;
runBatch(start, mid);
runBatch(mid, end);
}
}
private ArrayList<ContentProviderOperation> toCPO(int start, int end) {
ArrayList<ContentProviderOperation> cpo = new ArrayList<>(end - start);
int i = 0;
for (Operation op : queue.subList(start, end)) {
ContentProviderOperation.Builder builder = op.builder;
// fill in back references
if (op.backrefKey != null) {
if (op.backrefIdx < start) {
// back reference is outside of the current batch
builder .withValueBackReferences(null)
.withValue(op.backrefKey, ContentUris.parseId(results[op.backrefIdx].uri));
} else
// back reference is in current batch, apply offset
builder.withValueBackReference(op.backrefKey, op.backrefIdx - start);
}
// set a yield point at least every 450 operations
if (i++ % 450 == 0)
builder.withYieldAllowed(true);
cpo.add(builder.build());
}
return cpo;
}
public static class Operation {
final ContentProviderOperation.Builder builder;
final String backrefKey;
final int backrefIdx;
public Operation(ContentProviderOperation.Builder builder) {
this.builder = builder;
backrefKey = null;
backrefIdx = -1;
}
public Operation(ContentProviderOperation.Builder builder, String backrefKey, int backrefIdx) {
this.builder = builder;
this.backrefKey = backrefKey;
this.backrefIdx = backrefIdx;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment