1
0
mirror of https://github.com/etesync/android synced 2024-11-23 00:18:19 +00:00

Fetch remote records in chunks of max. 50 to avoid server and memory problems

* performance optimisation
* refactoring
This commit is contained in:
rfc2822 2013-12-24 12:49:23 +01:00
parent 385e7b5e13
commit 4505a5958d
9 changed files with 88 additions and 71 deletions

View File

@ -97,7 +97,7 @@ public class Contact extends Resource {
} }
@Override @Override
public void initialize() { public void initRemoteFields() {
uid = UUID.randomUUID().toString(); uid = UUID.randomUUID().toString();
name = uid + ".vcf"; name = uid + ".vcf";
} }
@ -113,7 +113,7 @@ public class Contact extends Resource {
Uid uid = vcard.getUid(); Uid uid = vcard.getUid();
if (uid == null) { if (uid == null) {
Log.w(TAG, "Received VCONTACT without UID, generating new one"); Log.w(TAG, "Received VCard without UID, generating new one");
uid = new Uid(UUID.randomUUID().toString()); uid = new Uid(UUID.randomUUID().toString());
} }
this.uid = uid.getValue(); this.uid = uid.getValue();

View File

@ -103,7 +103,7 @@ public class Event extends Resource {
} }
@Override @Override
public void initialize() { public void initRemoteFields() {
uid = DavSyncAdapter.generateUID(); uid = DavSyncAdapter.generateUID();
name = uid.replace("@", "_") + ".ics"; name = uid.replace("@", "_") + ".ics";
} }

View File

@ -8,9 +8,6 @@
package at.bitfire.davdroid.resource; package at.bitfire.davdroid.resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import org.apache.commons.lang.ArrayUtils;
import lombok.Cleanup; import lombok.Cleanup;
import android.accounts.Account; import android.accounts.Account;
@ -75,10 +72,13 @@ public abstract class LocalCollection<T extends Resource> {
@Cleanup Cursor cursor = providerClient.query(entriesURI(), @Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null); where, null, null);
LinkedList<Long> dirty = new LinkedList<Long>(); if (cursor == null)
while (cursor != null && cursor.moveToNext()) throw new LocalStorageException("Couldn't query dirty records");
dirty.add(cursor.getLong(0));
return ArrayUtils.toPrimitive(dirty.toArray(new Long[0]), -1); long[] dirty = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++)
dirty[idx] = cursor.getLong(0);
return dirty;
} catch(RemoteException ex) { } catch(RemoteException ex) {
throw new LocalStorageException(ex); throw new LocalStorageException(ex);
} }
@ -92,10 +92,13 @@ public abstract class LocalCollection<T extends Resource> {
@Cleanup Cursor cursor = providerClient.query(entriesURI(), @Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
where, null, null); where, null, null);
LinkedList<Long> deleted = new LinkedList<Long>(); if (cursor == null)
while (cursor != null && cursor.moveToNext()) throw new LocalStorageException("Couldn't query dirty records");
deleted.add(cursor.getLong(0));
return ArrayUtils.toPrimitive(deleted.toArray(new Long[0]), -1); long deleted[] = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++)
deleted[idx] = cursor.getLong(0);
return deleted;
} catch(RemoteException ex) { } catch(RemoteException ex) {
throw new LocalStorageException(ex); throw new LocalStorageException(ex);
} }
@ -110,22 +113,25 @@ public abstract class LocalCollection<T extends Resource> {
@Cleanup Cursor cursor = providerClient.query(entriesURI(), @Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() }, new String[] { entryColumnID() },
where, null, null); where, null, null);
LinkedList<Long> fresh = new LinkedList<Long>(); if (cursor == null)
while (cursor != null && cursor.moveToNext()) { throw new LocalStorageException("Couldn't query new records");
long[] fresh = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++) {
long id = cursor.getLong(0); long id = cursor.getLong(0);
// new record: generate UID + remote file name so that we can upload // new record: generate UID + remote file name so that we can upload
T resource = findById(id, false); T resource = findById(id, false);
resource.initialize(); resource.initRemoteFields();
// write generated UID + remote file name into database // write generated UID + remote file name into database
ContentValues values = new ContentValues(2); ContentValues values = new ContentValues(2);
values.put(entryColumnUID(), resource.getUid()); values.put(entryColumnUID(), resource.getUid());
values.put(entryColumnRemoteName(), resource.getName()); values.put(entryColumnRemoteName(), resource.getName());
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null); providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
fresh.add(id); fresh[idx] = id;
} }
return ArrayUtils.toPrimitive(fresh.toArray(new Long[0]), -1); return fresh;
} catch(RemoteException ex) { } catch(RemoteException ex) {
throw new LocalStorageException(ex); throw new LocalStorageException(ex);
} }
@ -217,7 +223,7 @@ public abstract class LocalCollection<T extends Resource> {
public void commit() throws LocalStorageException { public void commit() throws LocalStorageException {
if (!pendingOperations.isEmpty()) if (!pendingOperations.isEmpty())
try { try {
Log.i(TAG, "Committing " + pendingOperations.size() + " operations"); Log.d(TAG, "Committing " + pendingOperations.size() + " operations");
providerClient.applyBatch(pendingOperations); providerClient.applyBatch(pendingOperations);
pendingOperations.clear(); pendingOperations.clear();
} catch (RemoteException ex) { } catch (RemoteException ex) {

View File

@ -76,6 +76,8 @@ public abstract class RemoteCollection<T extends Resource> {
if (resources.length == 1) if (resources.length == 1)
return (T[]) new Resource[] { get(resources[0]) }; return (T[]) new Resource[] { get(resources[0]) };
Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)");
LinkedList<String> names = new LinkedList<String>(); LinkedList<String> names = new LinkedList<String>();
for (Resource resource : resources) for (Resource resource : resources)
names.add(resource.getName()); names.add(resource.getName());

View File

@ -36,7 +36,7 @@ public abstract class Resource {
} }
// sets resource name and UID // sets resource name and UID
public abstract void initialize(); public abstract void initRemoteFields();
public abstract void parseEntity(InputStream entity) throws IOException, ParserException, VCardException; public abstract void parseEntity(InputStream entity) throws IOException, ParserException, VCardException;
public abstract ByteArrayOutputStream toEntity() throws IOException, ValidationException; public abstract ByteArrayOutputStream toEntity() throws IOException, ValidationException;

View File

@ -58,7 +58,7 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
// set class loader for iCal4j ResourceLoader // set class loader for iCal4j ResourceLoader
Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
SyncManager syncManager = new SyncManager(account, accountManager); SyncManager syncManager = new SyncManager();
Map<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider); Map<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider);
if (syncCollections == null) if (syncCollections == null)

View File

@ -15,10 +15,9 @@ import net.fortuna.ical4j.model.ValidationException;
import org.apache.http.HttpException; import org.apache.http.HttpException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.SyncResult; import android.content.SyncResult;
import android.util.Log; import android.util.Log;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.resource.LocalCollection; import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException; import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RecordNotFoundException; import at.bitfire.davdroid.resource.RecordNotFoundException;
@ -30,15 +29,9 @@ import at.bitfire.davdroid.webdav.PreconditionFailedException;
public class SyncManager { public class SyncManager {
private static final String TAG = "davdroid.SyncManager"; private static final String TAG = "davdroid.SyncManager";
protected Account account; private static final int MAX_MULTIGET_RESOURCES = 50;
protected AccountManager accountManager;
public SyncManager(Account account, AccountManager accountManager) {
this.account = account;
this.accountManager = accountManager;
}
public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, boolean manualSync, SyncResult syncResult) throws LocalStorageException, IOException, HttpException { public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, boolean manualSync, SyncResult syncResult) throws LocalStorageException, IOException, HttpException {
// PHASE 1: push local changes to server // PHASE 1: push local changes to server
int deletedRemotely = pushDeleted(local, remote), int deletedRemotely = pushDeleted(local, remote),
@ -79,8 +72,8 @@ public class SyncManager {
} }
// PHASE 3: pull remote changes from server // PHASE 3: pull remote changes from server
syncResult.stats.numInserts = pullNew(local, remote, remotelyAdded); syncResult.stats.numInserts = pullNew(local, remote, remotelyAdded.toArray(new Resource[0]));
syncResult.stats.numUpdates = pullChanged(local, remote, remotelyUpdated); syncResult.stats.numUpdates = pullChanged(local, remote, remotelyUpdated.toArray(new Resource[0]));
Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore"); Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore");
local.deleteAllExceptRemoteNames(remoteResources); local.deleteAllExceptRemoteNames(remoteResources);
@ -96,9 +89,10 @@ public class SyncManager {
private int pushDeleted(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException { private int pushDeleted(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0; int count = 0;
long[] deletedIDs = local.findDeleted(); long[] deletedIDs = local.findDeleted();
if (deletedIDs != null) {
try {
Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)"); Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)");
for (long id : deletedIDs) { for (long id : deletedIDs)
try { try {
Resource res = local.findById(id, false); Resource res = local.findById(id, false);
if (res.getName() != null) // is this resource even present remotely? if (res.getName() != null) // is this resource even present remotely?
@ -109,8 +103,10 @@ public class SyncManager {
Log.i(TAG, "Locally-deleted resource has already been removed from server"); Log.i(TAG, "Locally-deleted resource has already been removed from server");
} catch(PreconditionFailedException e) { } catch(PreconditionFailedException e) {
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile"); Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
} catch (RecordNotFoundException e) {
Log.e(TAG, "Couldn't read locally-deleted record", e);
} }
} } finally {
local.commit(); local.commit();
} }
return count; return count;
@ -119,21 +115,22 @@ public class SyncManager {
private int pushNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException { private int pushNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0; int count = 0;
long[] newIDs = local.findNew(); long[] newIDs = local.findNew();
if (newIDs != null) {
Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)"); Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)");
for (long id : newIDs) { try {
for (long id : newIDs)
try { try {
Resource res = local.findById(id, true); Resource res = local.findById(id, true);
Log.d(TAG, "Uploading " + res.toString());
remote.add(res); remote.add(res);
local.clearDirty(res); local.clearDirty(res);
count++;
} catch(PreconditionFailedException e) { } catch(PreconditionFailedException e) {
Log.i(TAG, "Didn't overwrite existing resource with other content"); Log.i(TAG, "Didn't overwrite existing resource with other content");
} catch (ValidationException e) { } catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for adding: " + e.toString()); Log.e(TAG, "Couldn't create entity for adding: " + e.toString());
} catch (RecordNotFoundException e) {
Log.e(TAG, "Couldn't read new record", e);
} }
count++; } finally {
}
local.commit(); local.commit();
} }
return count; return count;
@ -142,49 +139,59 @@ public class SyncManager {
private int pushDirty(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException { private int pushDirty(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
int count = 0; int count = 0;
long[] dirtyIDs = local.findDirty(); long[] dirtyIDs = local.findDirty();
if (dirtyIDs != null) {
Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)"); Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)");
try {
for (long id : dirtyIDs) { for (long id : dirtyIDs) {
try { try {
Resource res = local.findById(id, true); Resource res = local.findById(id, true);
remote.update(res); remote.update(res);
local.clearDirty(res); local.clearDirty(res);
count++;
} catch(PreconditionFailedException e) { } catch(PreconditionFailedException e) {
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile"); Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile");
} catch (ValidationException e) { } catch (ValidationException e) {
Log.e(TAG, "Couldn't create entity for updating: " + e.toString()); Log.e(TAG, "Couldn't create entity for updating: " + e.toString());
} catch (RecordNotFoundException e) {
Log.e(TAG, "Couldn't read dirty record", e);
} }
count++;
} }
} finally {
local.commit(); local.commit();
} }
return count; return count;
} }
private int pullNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToAdd) throws LocalStorageException, IOException, HttpException { private int pullNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Resource[] resourcesToAdd) throws LocalStorageException, IOException, HttpException {
int count = 0; int count = 0;
Log.i(TAG, "Fetching " + resourcesToAdd.size() + " new remote resource(s)"); Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)");
if (!resourcesToAdd.isEmpty()) {
for (Resource res : remote.multiGet(resourcesToAdd.toArray(new Resource[0]))) { for (Resource[] resources : ArrayUtils.partition(resourcesToAdd, MAX_MULTIGET_RESOURCES))
Log.i(TAG, "Adding " + res.getName()); try {
for (Resource res : remote.multiGet(resources)) {
Log.d(TAG, "Adding " + res.getName());
local.add(res); local.add(res);
local.commit();
count++; count++;
} }
} finally {
local.commit();
} }
return count; return count;
} }
private int pullChanged(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToUpdate) throws LocalStorageException, IOException, HttpException { private int pullChanged(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Resource[] resourcesToUpdate) throws LocalStorageException, IOException, HttpException {
int count = 0; int count = 0;
Log.i(TAG, "Fetching " + resourcesToUpdate.size() + " updated remote resource(s)"); Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)");
if (!resourcesToUpdate.isEmpty())
for (Resource res : remote.multiGet(resourcesToUpdate.toArray(new Resource[0]))) { for (Resource[] resources : ArrayUtils.partition(resourcesToUpdate, MAX_MULTIGET_RESOURCES))
try {
for (Resource res : remote.multiGet(resources)) {
Log.i(TAG, "Updating " + res.getName()); Log.i(TAG, "Updating " + res.getName());
local.updateByRemoteName(res); local.updateByRemoteName(res);
local.commit();
count++; count++;
} }
} finally {
local.commit();
}
return count; return count;
} }

View File

@ -236,7 +236,7 @@ public class WebDavResource {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS) if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException(); throw new DavNoMultiStatusException();
@Cleanup("consumeContent") HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity == null) if (entity == null)
throw new DavNoContentException(); throw new DavNoContentException();
@ -277,11 +277,11 @@ public class WebDavResource {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS) if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException(); throw new DavNoMultiStatusException();
@Cleanup("consumeContent") HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity == null) if (entity == null)
throw new DavNoContentException(); throw new DavNoContentException();
InputStream rawContent = entity.getContent(); @Cleanup InputStream rawContent = entity.getContent();
if (rawContent == null) if (rawContent == null)
throw new DavNoContentException(); throw new DavNoContentException();
@Cleanup LoggingInputStream content = new LoggingInputStream(TAG, rawContent); @Cleanup LoggingInputStream content = new LoggingInputStream(TAG, rawContent);
@ -303,7 +303,7 @@ public class WebDavResource {
HttpResponse response = client.execute(get); HttpResponse response = client.execute(get);
checkResponse(response); checkResponse(response);
@Cleanup("consumeContent") HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity == null) if (entity == null)
throw new DavNoContentException(); throw new DavNoContentException();

View File

@ -1,16 +1,19 @@
package at.bitfire.davdroid.webdav.test; package at.bitfire.davdroid.webdav.test;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.List; import java.util.List;
import lombok.Cleanup;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpException; import org.apache.http.HttpException;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.webdav.DAVException; import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.DavMultiget; import at.bitfire.davdroid.webdav.DavMultiget;
import at.bitfire.davdroid.webdav.HttpPropfind; import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.NotFoundException; import at.bitfire.davdroid.webdav.NotFoundException;
@ -88,7 +91,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
try { try {
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL); simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
fail(); fail();
} catch(DAVException ex) { } catch(DavException ex) {
} }
assertNull(simpleFile.getCurrentUserPrincipal()); assertNull(simpleFile.getCurrentUserPrincipal());
} }
@ -141,13 +144,12 @@ public class WebDavResourceTest extends InstrumentationTestCase {
public void testGet() throws URISyntaxException, IOException, HttpException { public void testGet() throws URISyntaxException, IOException, HttpException {
simpleFile.get(); simpleFile.get();
assertTrue(IOUtils.contentEquals( @Cleanup InputStream is = assetMgr.open("test.random", AssetManager.ACCESS_STREAMING);
assetMgr.open("test.random", AssetManager.ACCESS_STREAMING), byte[] expected = IOUtils.toByteArray(is);
simpleFile.getContent() assertEquals(expected, simpleFile.getContent());
));
} }
public void testMultiGet() throws DAVException, IOException, HttpException { public void testMultiGet() throws DavException, IOException, HttpException {
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf", true); WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf", true);
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" }); davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" });
assertEquals(2, davAddressBook.getMembers().size()); assertEquals(2, davAddressBook.getMembers().size());