Commit 2a0421f3 authored by Ricki Hirner's avatar Ricki Hirner

Allow splitting of large transactions

parent ee884d35
......@@ -226,6 +226,21 @@ public class AndroidEventTest extends InstrumentationTestCase {
assertEquals(1, updatedEvent.attendees.size());
}
public void testLargeTransaction() throws ParseException, CalendarStorageException, URISyntaxException, FileNotFoundException {
Event event = new Event();
event.uid = "sample1@testLargeTransaction";
event.summary = "Large event";
event.dtStart = new DtStart("20150502T120000Z");
event.dtEnd = new DtEnd("20150502T130000Z");
for (int i = 0; i < 4000; i++)
event.attendees.add(new Attendee(new URI("mailto:att" + i + "@example.com")));
Uri uri = new TestEvent(calendar, event).add();
@Cleanup("delete") TestEvent testEvent = new TestEvent(calendar, ContentUris.parseId(uri));
assertEquals(4000, testEvent.getEvent().attendees.size());
}
public void testBuildAllDayEntry() throws ParseException, FileNotFoundException, CalendarStorageException {
// add all-day event to calendar provider
Event event = new Event();
......
......@@ -367,7 +367,7 @@ public abstract class AndroidEvent {
final int idxEvent = batch.nextBackrefIdx();
buildEvent(null, builder);
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder));
// add reminders
for (VAlarm alarm : event.alarms)
......@@ -407,12 +407,11 @@ public abstract class AndroidEvent {
Constants.log.log(Level.WARNING, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
}
}
builder .withValueBackReference(Events.ORIGINAL_ID, idxEvent)
.withValue(Events.ORIGINAL_ALL_DAY, event.isAllDay() ? 1 : 0)
builder .withValue(Events.ORIGINAL_ALL_DAY, event.isAllDay() ? 1 : 0)
.withValue(Events.ORIGINAL_INSTANCE_TIME, date.getTime());
int idxException = batch.nextBackrefIdx();
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder, Events.ORIGINAL_ID, idxEvent));
// add exception reminders
for (VAlarm alarm : exception.alarms)
......@@ -450,12 +449,11 @@ public abstract class AndroidEvent {
protected void delete(BatchOperation batch) {
// remove event
batch.enqueue(ContentProviderOperation.newDelete(eventSyncURI()).build());
batch.enqueue(new BatchOperation.Operation(ContentProviderOperation.newDelete(eventSyncURI())));
// remove exceptions of that event, too (CalendarProvider doesn't do this)
batch.enqueue(ContentProviderOperation.newDelete(eventsSyncURI())
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(id) })
.build());
batch.enqueue(new BatchOperation.Operation(ContentProviderOperation.newDelete(eventsSyncURI())
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(id) })));
}
protected void buildEvent(Event recurrence, Builder builder) {
......@@ -551,7 +549,6 @@ public abstract class AndroidEvent {
protected void insertReminder(BatchOperation batch, int idxEvent, VAlarm alarm) {
Builder builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(Reminders.CONTENT_URI));
builder.withValueBackReference(Reminders.EVENT_ID, idxEvent);
final Action action = alarm.getAction();
final int method;
......@@ -569,13 +566,12 @@ public abstract class AndroidEvent {
.withValue(Reminders.MINUTES, minutes);
Constants.log.fine("Adding alarm " + minutes + " minutes before event, method: " + method);
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder, Reminders.EVENT_ID, idxEvent));
}
@TargetApi(16)
protected void insertAttendee(BatchOperation batch, int idxEvent, Attendee attendee) {
Builder builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(Attendees.CONTENT_URI));
builder.withValueBackReference(Attendees.EVENT_ID, idxEvent);
final URI member = attendee.getCalAddress();
if ("mailto".equalsIgnoreCase(member.getScheme()))
......@@ -631,7 +627,7 @@ public abstract class AndroidEvent {
builder .withValue(Attendees.ATTENDEE_TYPE, type)
.withValue(Attendees.ATTENDEE_STATUS, status);
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder, Attendees.EVENT_ID, idxEvent));
}
......
......@@ -222,7 +222,7 @@ public abstract class AndroidTask {
BatchOperation batch = new BatchOperation(taskList.provider.client);
Builder builder = ContentProviderOperation.newInsert(taskList.syncAdapterURI(taskList.provider.tasksUri()));
buildTask(builder, false);
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder));
batch.commit();
return batch.getResult(0).uri;
}
......@@ -233,7 +233,7 @@ public abstract class AndroidTask {
BatchOperation batch = new BatchOperation(taskList.provider.client);
Builder builder = ContentProviderOperation.newUpdate(taskSyncURI());
buildTask(builder, true);
batch.enqueue(builder.build());
batch.enqueue(new BatchOperation.Operation(builder));
batch.commit();
}
......
......@@ -12,59 +12,129 @@
package at.bitfire.ical4android;
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 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 CalendarStorageException {
int affected = 0;
if (!queue.isEmpty())
try {
Constants.log.fine("Committing " + queue.size() + " operations …");
results = providerClient.applyBatch(queue);
if (results.length != queue.size())
throw new CalendarStorageException("Batch operation failed partially (only " + results.length + " of " + queue.size() + " operations done)");
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;
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 CalendarStorageException {
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 CalendarStorageException("Couldn't apply batch operation", e);
}
queue.clear();
return affected;
}
public ContentProviderResult getResult(int idx) {
return results[idx];
}
} catch(OperationApplicationException|RemoteException e) {
throw new CalendarStorageException("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, CalendarStorageException {
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 CalendarStorageException("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);
for (Operation op : queue.subList(start, end)) {
ContentProviderOperation.Builder builder = op.builder;
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);
}
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