com.couchbase.lite.replicator.PullerInternal Maven / Gradle / Ivy
package com.couchbase.lite.replicator;
import com.couchbase.lite.CouchbaseLiteException;
import com.couchbase.lite.Database;
import com.couchbase.lite.Document;
import com.couchbase.lite.Manager;
import com.couchbase.lite.Misc;
import com.couchbase.lite.RevisionList;
import com.couchbase.lite.Status;
import com.couchbase.lite.TransactionalTask;
import com.couchbase.lite.internal.InterfaceAudience;
import com.couchbase.lite.internal.RevisionInternal;
import com.couchbase.lite.storage.SQLException;
import com.couchbase.lite.support.BatchProcessor;
import com.couchbase.lite.support.Batcher;
import com.couchbase.lite.support.CustomFuture;
import com.couchbase.lite.support.HttpClientFactory;
import com.couchbase.lite.support.RemoteRequestCompletionBlock;
import com.couchbase.lite.support.SequenceMap;
import com.couchbase.lite.util.CollectionUtils;
import com.couchbase.lite.util.Log;
import com.couchbase.lite.util.URIUtils;
import com.couchbase.lite.util.Utils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Pull Replication
*
* @exclude
*/
@InterfaceAudience.Private
public class PullerInternal extends ReplicationInternal implements ChangeTrackerClient {
private static final String TAG = Log.TAG_SYNC;
private static final int MAX_OPEN_HTTP_CONNECTIONS = 16;
// Maximum number of revs to fetch in a single bulk request
public static final int MAX_REVS_TO_GET_IN_BULK = 50;
// Maximum number of revision IDs to pass in an "?atts_since=" query param
public static final int MAX_NUMBER_OF_ATTS_SINCE = 50;
// Tune this parameter based on application needs.
public static int CHANGE_TRACKER_RESTART_DELAY_MS = 10 * 1000;
public static final int MAX_PENDING_DOCS = 200;
private static final int INSERTION_BATCHER_DELAY = 250; // 0.25 Seconds
private ChangeTracker changeTracker;
protected SequenceMap pendingSequences;
protected Boolean canBulkGet; // Does the server support _bulk_get requests?
protected List revsToPull = Collections.synchronizedList(
new ArrayList(100));
protected List bulkRevsToPull = Collections.synchronizedList(
new ArrayList(100));
protected List deletedRevsToPull = Collections.synchronizedList(
new ArrayList(100));
protected int httpConnectionCount;
protected Batcher downloadsToInsert;
private String str = null;
public PullerInternal(Database db,
URL remote,
HttpClientFactory clientFactory,
ScheduledExecutorService workExecutor,
Replication.Lifecycle lifecycle,
Replication parentReplication) {
super(db, remote, clientFactory, workExecutor, lifecycle, parentReplication);
}
/**
* Actual work of starting the replication process.
*/
protected void beginReplicating() {
Log.v(TAG, "submit startReplicating()");
workExecutor.submit(new Runnable() {
@Override
public void run() {
if (isRunning()) {
Log.v(TAG, "start startReplicating()");
initPendingSequences();
initDownloadsToInsert();
startChangeTracker();
}
// start replicator ...
}
});
}
private void initDownloadsToInsert() {
if (downloadsToInsert == null) {
int capacity = 200;
downloadsToInsert = new Batcher(workExecutor,
capacity, INSERTION_BATCHER_DELAY, new BatchProcessor() {
@Override
public void process(List inbox) {
insertDownloads(inbox);
}
});
}
}
@Override
protected void onBeforeScheduleRetry() {
// stop change tracker
if (changeTracker != null)
changeTracker.stop();
}
public boolean isPull() {
return true;
}
protected void maybeCreateRemoteDB() {
// puller never needs to do this
}
protected void startChangeTracker() {
// make sure not start new changeTracker if pull replicator is not running or idle
if (!(stateMachine.isInState(ReplicationState.RUNNING) ||
stateMachine.isInState(ReplicationState.IDLE)))
return;
// if changeTracker is already running, not start new one.
if (changeTracker != null && changeTracker.isRunning())
return;
ChangeTracker.ChangeTrackerMode changeTrackerMode;
// it always starts out as OneShot, but if its a continuous replication
// it will switch to longpoll later.
changeTrackerMode = ChangeTracker.ChangeTrackerMode.OneShot;
Log.d(TAG, "%s: starting ChangeTracker with since=%s mode=%s",
this, lastSequence, changeTrackerMode);
changeTracker = new ChangeTracker(remote, changeTrackerMode, true, lastSequence, this);
changeTracker.setAuthenticator(getAuthenticator());
Log.d(TAG, "%s: started ChangeTracker %s", this, changeTracker);
if (filterName != null) {
changeTracker.setFilterName(filterName);
if (filterParams != null) {
changeTracker.setFilterParams(filterParams);
}
}
changeTracker.setDocIDs(documentIDs);
changeTracker.setRequestHeaders(requestHeaders);
changeTracker.setContinuous(lifecycle == Replication.Lifecycle.CONTINUOUS);
changeTracker.setUsePOST(serverIsSyncGatewayVersion("0.93"));
changeTracker.start();
}
/**
* Process a bunch of remote revisions from the _changes feed at once
*/
@Override
@InterfaceAudience.Private
protected void processInbox(RevisionList inbox) {
Log.d(TAG, "processInbox called");
if (canBulkGet == null) {
canBulkGet = serverIsSyncGatewayVersion("0.81");
}
// Ask the local database which of the revs are not known to it:
String lastInboxSequence = ((PulledRevision) inbox.get(inbox.size() - 1)).getRemoteSequenceID();
int numRevisionsRemoved = 0;
try {
// findMissingRevisions is the local equivalent of _revs_diff. it looks at the
// array of revisions in "inbox" and removes the ones that already exist.
// So whatever's left in 'inbox'
// afterwards are the revisions that need to be downloaded.
numRevisionsRemoved = db.findMissingRevisions(inbox);
} catch (SQLException e) {
Log.e(TAG, String.format("%s failed to look up local revs", this), e);
inbox = null;
}
//introducing this to java version since inbox may now be null everywhere
int inboxCount = 0;
if (inbox != null) {
inboxCount = inbox.size();
}
if (numRevisionsRemoved > 0) {
Log.v(TAG, "%s: processInbox() setting changesCount to: %s",
this, getChangesCount().get() - numRevisionsRemoved);
// May decrease the changesCount, to account for the revisions we just found out we don't need to get.
addToChangesCount(-1 * numRevisionsRemoved);
}
if (inboxCount == 0) {
// Nothing to do. Just bump the lastSequence.
Log.d(TAG,
"%s no new remote revisions to fetch. add lastInboxSequence (%s) to pendingSequences (%s)",
this, lastInboxSequence, pendingSequences);
long seq = pendingSequences.addValue(lastInboxSequence);
pendingSequences.removeSequence(seq);
setLastSequence(pendingSequences.getCheckpointedValue());
pauseOrResume();
return;
}
Log.v(TAG, "%s: fetching %s remote revisions...", this, inboxCount);
// Dump the revs into the queue of revs to pull from the remote db:
for (int i = 0; i < inbox.size(); i++) {
PulledRevision rev = (PulledRevision) inbox.get(i);
if (canBulkGet || (rev.getGeneration() == 1 && !rev.isDeleted() && !rev.isConflicted())) {
bulkRevsToPull.add(rev);
} else {
queueRemoteRevision(rev);
}
rev.setSequence(pendingSequences.addValue(rev.getRemoteSequenceID()));
}
pullRemoteRevisions();
pauseOrResume();
}
/**
* Start up some HTTP GETs, within our limit on the maximum simultaneous number
*
* The entire method is not synchronized, only the portion pulling work off the list
* Important to not hold the synchronized block while we do network access
*/
@InterfaceAudience.Private
public void pullRemoteRevisions() {
//find the work to be done in a synchronized block
List workToStartNow = new ArrayList();
List bulkWorkToStartNow = new ArrayList();
synchronized (bulkRevsToPull) {
while (httpConnectionCount + workToStartNow.size() < MAX_OPEN_HTTP_CONNECTIONS) {
int nBulk = (bulkRevsToPull.size() < MAX_REVS_TO_GET_IN_BULK) ?
bulkRevsToPull.size() : MAX_REVS_TO_GET_IN_BULK;
if (nBulk == 1) {
// Rather than pulling a single revision in 'bulk', just pull it normally:
queueRemoteRevision(bulkRevsToPull.remove(0));
nBulk = 0;
}
if (nBulk > 0) {
bulkWorkToStartNow.addAll(bulkRevsToPull.subList(0, nBulk));
bulkRevsToPull.subList(0, nBulk).clear();
} else {
// Prefer to pull an existing revision over a deleted one:
if (revsToPull.size() == 0 && deletedRevsToPull.size() == 0) {
break; // both queues are empty
} else if (revsToPull.size() > 0) {
workToStartNow.add(revsToPull.remove(0));
} else if (deletedRevsToPull.size() > 0) {
workToStartNow.add(deletedRevsToPull.remove(0));
}
}
}
}
//actually run it outside the synchronized block
if (bulkWorkToStartNow.size() > 0) {
pullBulkRevisions(bulkWorkToStartNow);
}
for (RevisionInternal work : workToStartNow) {
pullRemoteRevision(work);
}
}
// Get a bunch of revisions in one bulk request. Will use _bulk_get if possible.
protected void pullBulkRevisions(List bulkRevs) {
int nRevs = bulkRevs.size();
if (nRevs == 0) {
return;
}
Log.d(TAG, "%s bulk-fetching %d remote revisions...", this, nRevs);
Log.d(TAG, "%s bulk-fetching remote revisions: %s", this, bulkRevs);
if (!canBulkGet) {
pullBulkWithAllDocs(bulkRevs);
return;
}
Log.v(TAG, "%s: POST _bulk_get", this);
final List remainingRevs = new ArrayList(bulkRevs);
++httpConnectionCount;
final BulkDownloader dl;
try {
dl = new BulkDownloader(workExecutor,
clientFactory,
remote,
bulkRevs,
db,
this.requestHeaders,
new BulkDownloader.BulkDownloaderDocumentBlock() {
public void onDocument(Map props) {
// Got a revision!
// Find the matching revision in 'remainingRevs' and get its sequence:
RevisionInternal rev;
if (props.get("_id") != null) {
rev = new RevisionInternal(props);
} else {
rev = new RevisionInternal((String) props.get("id"),
(String) props.get("rev"), false);
}
int pos = remainingRevs.indexOf(rev);
if (pos > -1) {
rev.setSequence(remainingRevs.get(pos).getSequence());
remainingRevs.remove(pos);
} else {
Log.w(TAG, "%s : Received unexpected rev rev", this);
}
if (props.get("_id") != null) {
// Add to batcher ... eventually it will be fed to -insertRevisions:.
queueDownloadedRevision(rev);
} else {
Status status = statusFromBulkDocsResponseItem(props);
Throwable err = new CouchbaseLiteException(status);
revisionFailed(rev, err);
}
}
},
new RemoteRequestCompletionBlock() {
public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) {
// The entire _bulk_get is finished:
if (e != null) {
setError(e);
completedChangesCount.addAndGet(remainingRevs.size());
}
--httpConnectionCount;
// Start another task if there are still revisions waiting to be pulled:
pullRemoteRevisions();
}
}
);
} catch (Exception e) {
Log.e(TAG, "%s: pullBulkRevisions Exception: %s", this, e);
return;
}
dl.setAuthenticator(getAuthenticator());
// set compressed request - gzip
dl.setCompressedRequest(canSendCompressedRequests());
synchronized (remoteRequestExecutor) {
if (!remoteRequestExecutor.isShutdown()) {
Future future = remoteRequestExecutor.submit(dl);
pendingFutures.add(future);
}
}
}
private void putLocalDocument(final String docId, final Map localDoc) {
workExecutor.submit(new Runnable() {
@Override
public void run() {
try {
getLocalDatabase().putLocalDocument(docId, localDoc);
} catch (CouchbaseLiteException e) {
Log.w(TAG, "Failed to store retryCount value for docId: " + docId, e);
}
}
});
}
private void pruneFailedDownload(final String docId) {
workExecutor.submit(new Runnable() {
@Override
public void run() {
try {
getLocalDatabase().deleteLocalDocument(docId);
} catch (CouchbaseLiteException e) {
Log.w(TAG, "Failed to delete local document: " + docId, e);
}
}
});
}
// This invokes the tranformation block if one is installed and queues the resulting CBL_Revision
private void queueDownloadedRevision(RevisionInternal rev) {
if (revisionBodyTransformationBlock != null) {
// Add 'file' properties to attachments pointing to their bodies:
for (Map.Entry> entry : (
(Map>) rev.getProperties().get("_attachments")).entrySet()) {
String name = entry.getKey();
Map attachment = entry.getValue();
attachment.remove("file");
if (attachment.get("follows") != null && attachment.get("data") == null) {
String filePath = db.fileForAttachmentDict(attachment).getPath();
if (filePath != null)
attachment.put("file", filePath);
}
}
RevisionInternal xformed = transformRevision(rev);
if (xformed == null) {
Log.v(TAG, "%s: Transformer rejected revision %s", this, rev);
pendingSequences.removeSequence(rev.getSequence());
lastSequence = pendingSequences.getCheckpointedValue();
pauseOrResume();
return;
}
rev = xformed;
// Clean up afterwards
Map> attachments = (Map>) rev.getProperties().get("_attachments");
for (Map.Entry> entry : attachments.entrySet()) {
Map attachment = entry.getValue();
attachment.remove("file");
}
}
if (rev != null && rev.getBody() != null)
rev.getBody().compact();
downloadsToInsert.queueObject(rev);
}
// Get as many revisions as possible in one _all_docs request.
// This is compatible with CouchDB, but it only works for revs of generation 1 without attachments.
protected void pullBulkWithAllDocs(final List bulkRevs) {
// http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
++httpConnectionCount;
final RevisionList remainingRevs = new RevisionList(bulkRevs);
Collection keys = CollectionUtils.transform(bulkRevs,
new CollectionUtils.Functor() {
public String invoke(RevisionInternal rev) {
return rev.getDocID();
}
}
);
Map body = new HashMap();
body.put("keys", keys);
Future future = sendAsyncRequest("POST",
"/_all_docs?include_docs=true",
body,
new RemoteRequestCompletionBlock() {
public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) {
Map res = (Map) result;
if (e != null) {
setError(e);
// TODO: There is a known bug caused by the line below, which is
// TODO: causing testMockSinglePullCouchDb to fail when running on a Nexus5 device.
// TODO: (the batching behavior is different in that case)
// TODO: See https://github.com/couchbase/couchbase-lite-java-core/issues/271
// completedChangesCount.addAndGet(bulkRevs.size());
} else {
// Process the resulting rows' documents.
// We only add a document if it doesn't have attachments, and if its
// revID matches the one we asked for.
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy