/**
* Copyright (c) 2016 Couchbase, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
package com.couchbase.lite.replicator;
import com.couchbase.lite.LiteTestCaseWithDB;
import com.couchbase.lite.util.Log;
import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class ChangeTrackerTest extends LiteTestCaseWithDB {
public static final String TAG = "ChangeTracker";
/**
* Test case for CBL Java Core #317 (CBL Java #39)
* https://github.com/couchbase/couchbase-lite-java-core/issues/317
*/
public void testChangeTrackerNullPointerException() throws Throwable {
// Null pointer issue is not 100% reproducible. so try 10 times...
for (int i = 0; i < 10; i++)
changeTrackerTestWithMode(ChangeTracker.ChangeTrackerMode.OneShot, true, true);
}
public void testChangeTrackerOneShot() throws Throwable {
changeTrackerTestWithMode(ChangeTracker.ChangeTrackerMode.OneShot, true, false);
}
public void testChangeTrackerLongPoll() throws Throwable {
changeTrackerTestWithMode(ChangeTracker.ChangeTrackerMode.LongPoll, true, false);
}
public void changeTrackerTestWithMode(ChangeTracker.ChangeTrackerMode mode,
final boolean useMockReplicator,
final boolean checkLastException) throws Throwable {
final CountDownLatch changeTrackerFinishedSignal = new CountDownLatch(1);
final CountDownLatch changeReceivedSignal = new CountDownLatch(1);
ChangeTrackerClient client = new DefaultChangeTrackerClient() {
@Override
public void changeTrackerStopped(ChangeTracker tracker) {
changeTrackerFinishedSignal.countDown();
}
@Override
public void changeTrackerReceivedChange(Map<String, Object> change) {
Object seq = change.get("seq");
if (useMockReplicator) {
assertEquals("1", seq.toString());
}
changeReceivedSignal.countDown();
}
public OkHttpClient getOkHttpClient() {
Interceptor interceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if ("_changes".equals(request.url().pathSegments().get(1))) {
String json = "{\"results\":[\n" +
"{\"seq\":\"1\",\"id\":\"doc1-138\",\"changes\":[{\"rev\":\"1-82d\"}]}],\n" +
"\"last_seq\":\"*:50\"}";
Response.Builder builder = new Response.Builder()
.request(request)
.code(200)
.protocol(Protocol.HTTP_1_1)
.body(ResponseBody.create(OkHttpUtils.JSON, json));
return builder.build();
}
return chain.proceed(request);
}
};
return new OkHttpClient.Builder().addInterceptor(interceptor).build();
}
};
final ChangeTracker changeTracker =
new ChangeTracker(getReplicationURL(), mode, false, 0, client);
changeTracker.setUsePOST(isTestingAgainstSyncGateway());
changeTracker.start();
try {
boolean success = changeReceivedSignal.await(300, TimeUnit.SECONDS);
assertTrue(success);
} catch (InterruptedException e) {
e.printStackTrace();
}
changeTracker.stop();
try {
boolean success = changeTrackerFinishedSignal.await(300, TimeUnit.SECONDS);
assertTrue(success);
} catch (InterruptedException e) {
e.printStackTrace();
}
// check for NullPointer Exception or any Exception
if (checkLastException) {
// CBL Java Core #317
// This check does not work without fixing Java Core #317 because ChangeTracker status
// becomes stopped before NullPointer Exception is thrown.
if (changeTracker.getLastError() != null) {
Log.e(TAG, changeTracker.getLastError().toString());
assertFalse(changeTracker.getLastError() instanceof NullPointerException);
}
}
}
public void testChangeTrackerWithConflictsIncluded() throws Throwable {
ChangeTracker changeTracker = new ChangeTracker(getReplicationURL(),
ChangeTracker.ChangeTrackerMode.LongPoll, true, 0L, null);
changeTracker.setUsePOST(false);
assertEquals("_changes?feed=longpoll&heartbeat=30000&style=all_docs&since=0",
changeTracker.getChangesFeedPath());
changeTracker.setUsePOST(true);
assertEquals("_changes?feed=longpoll&heartbeat=30000&style=all_docs&since=0",
changeTracker.getChangesFeedPath());
Map<String, Object> postBodyMap = changeTracker.changesFeedPOSTBodyMap();
assertEquals("longpoll", postBodyMap.get("feed"));
assertEquals(30000L, ((Long) postBodyMap.get("heartbeat")).longValue());
assertEquals("all_docs", postBodyMap.get("style"));
assertEquals(0L, ((Long) postBodyMap.get("since")).longValue());
}
public void testChangeTrackerWithCompoundLastSequence() throws Throwable {
ChangeTracker changeTracker = new ChangeTracker(getReplicationURL(),
ChangeTracker.ChangeTrackerMode.LongPoll, true, "1234:56", null);
changeTracker.setUsePOST(false);
assertEquals(
"_changes?feed=longpoll&heartbeat=30000&style=all_docs&since=1234%3A56",
changeTracker.getChangesFeedPath());
changeTracker.setUsePOST(true);
assertEquals(
"_changes?feed=longpoll&heartbeat=30000&style=all_docs&since=1234%3A56",
changeTracker.getChangesFeedPath());
Map<String, Object> postBodyMap = changeTracker.changesFeedPOSTBodyMap();
assertEquals("longpoll", postBodyMap.get("feed"));
assertEquals(30000L, ((Long) postBodyMap.get("heartbeat")).longValue());
assertEquals("all_docs", postBodyMap.get("style"));
assertEquals("1234:56", postBodyMap.get("since"));
}
public void testChangeTrackerWithFilterURL() throws Throwable {
ChangeTracker changeTracker = new ChangeTracker(getReplicationURL(),
ChangeTracker.ChangeTrackerMode.LongPoll, false, 0L, null);
// set filter
changeTracker.setFilterName("filter");
// build filter map
Map<String, Object> filterMap = new HashMap<String, Object>();
filterMap.put("param", "value");
// set filter map
changeTracker.setFilterParams(filterMap);
changeTracker.setUsePOST(false);
assertEquals(
"_changes?feed=longpoll&heartbeat=30000&since=0&filter=filter¶m=value",
changeTracker.getChangesFeedPath());
changeTracker.setUsePOST(true);
assertEquals("_changes?feed=longpoll&heartbeat=30000&since=0&filter=filter",
changeTracker.getChangesFeedPath());
Map<String, Object> body = changeTracker.changesFeedPOSTBodyMap();
assertTrue(body.containsKey("filter"));
assertEquals("filter", body.get("filter"));
assertTrue(body.containsKey("param"));
assertEquals("value", body.get("param"));
}
public void testChangeTrackerWithDocsIds() throws Exception {
URL testURL = getReplicationURL();
ChangeTracker changeTracker = new ChangeTracker(testURL,
ChangeTracker.ChangeTrackerMode.LongPoll, false, 0L, null);
changeTracker.setUsePOST(false);
List<String> docIds = new ArrayList<String>();
docIds.add("doc1");
docIds.add("doc2");
changeTracker.setDocIDs(docIds);
String docIdsUnencoded = "[\"doc1\",\"doc2\"]";
String docIdsEncoded = URLEncoder.encode(docIdsUnencoded);
String expectedFeedPath = String.format(Locale.ENGLISH,
"_changes?feed=longpoll&heartbeat=30000&since=0&filter=_doc_ids&doc_ids=%s",
docIdsEncoded);
final String changesFeedPath = changeTracker.getChangesFeedPath();
assertEquals(expectedFeedPath, changesFeedPath);
changeTracker.setUsePOST(true);
assertEquals("_changes?feed=longpoll&heartbeat=30000&since=0&filter=_doc_ids",
changeTracker.getChangesFeedPath());
Map<String, Object> postBodyMap = changeTracker.changesFeedPOSTBodyMap();
assertEquals("_doc_ids", postBodyMap.get("filter"));
assertEquals(docIds, postBodyMap.get("doc_ids"));
String postBody = changeTracker.changesFeedPOSTBody();
assertTrue(postBody.contains(docIdsUnencoded));
}
public void testChangeTrackerBackoffExceptions() throws Throwable {
CustomizableMockInterceptor interceptor = new CustomizableMockInterceptor();
interceptor.addResponderThrowExceptionAllRequests();
testChangeTrackerBackoff(interceptor);
}
public void testChangeTrackerBackoffInvalidJson() throws Throwable {
CustomizableMockInterceptor interceptor = new CustomizableMockInterceptor();
interceptor.addResponderReturnInvalidChangesFeedJson();
testChangeTrackerBackoff(interceptor);
}
public void testChangeTrackerRecoverableError() throws Exception {
int errorCode = 503;
String statusMessage = "Transient Error";
int numExpectedChangeCallbacks = 2;
runChangeTrackerTransientError(ChangeTracker.ChangeTrackerMode.LongPoll,
errorCode, statusMessage, numExpectedChangeCallbacks);
}
public void testChangeTrackerRecoverableIOException() throws Exception {
int errorCode = -1; // special code to tell it to throw an IOException
String statusMessage = null;
int numExpectedChangeCallbacks = 2;
runChangeTrackerTransientError(ChangeTracker.ChangeTrackerMode.LongPoll,
errorCode, statusMessage, numExpectedChangeCallbacks);
}
public void testChangeTrackerNonRecoverableError() throws Exception {
int errorCode = 404;
String statusMessage = "NOT FOUND";
int numExpectedChangeCallbacks = 1;
runChangeTrackerTransientError(ChangeTracker.ChangeTrackerMode.LongPoll,
errorCode, statusMessage, numExpectedChangeCallbacks);
}
private Interceptor defaultInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if ("_changes".equals(request.url().pathSegments().get(1))) {
String json = "{\"results\":[\n" +
"{\"seq\":\"1\",\"id\":\"doc1-138\",\"changes\":[{\"rev\":\"1-82d\"}]}],\n" +
"\"last_seq\":\"*:50\"}";
Response.Builder builder = new Response.Builder()
.request(request)
.code(200)
.protocol(Protocol.HTTP_1_1)
.body(ResponseBody.create(OkHttpUtils.JSON, json));
return builder.build();
}
return chain.proceed(request);
}
};
}
private Interceptor createInterceptor(final int code, final String message) {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response.Builder builder = new Response.Builder()
.request(request)
.code(code)
.protocol(Protocol.HTTP_1_1)
.body(ResponseBody.create(OkHttpUtils.TEXT, message));
return builder.build();
}
};
}
private void runChangeTrackerTransientError(ChangeTracker.ChangeTrackerMode mode,
final int errorCode,
final String statusMessage,
int numExpectedChangeCallbacks) throws Exception {
final CountDownLatch finishedSignal = new CountDownLatch(1);
final CountDownLatch receivedSignal = new CountDownLatch(numExpectedChangeCallbacks);
final List<Interceptor> interceptors = new ArrayList<Interceptor>();
interceptors.add(defaultInterceptor());
interceptors.add(createInterceptor(errorCode, statusMessage));
ChangeTrackerClient client = new DefaultChangeTrackerClient() {
@Override
public void changeTrackerStopped(ChangeTracker tracker) {
finishedSignal.countDown();
}
@Override
public void changeTrackerReceivedChange(Map<String, Object> change) {
receivedSignal.countDown();
}
@Override
public OkHttpClient getOkHttpClient() {
Interceptor interceptor = interceptors.remove(0);
if (interceptor != null) {
return new OkHttpClient.Builder().addInterceptor(interceptor).build();
}
throw new RuntimeException("no more response");
}
};
final ChangeTracker changeTracker = new ChangeTracker(getReplicationURL(),
mode, false, 0L, client);
changeTracker.setUsePOST(isTestingAgainstSyncGateway());
changeTracker.start();
assertTrue(receivedSignal.await(30, TimeUnit.SECONDS));
changeTracker.stop();
assertTrue(finishedSignal.await(30, TimeUnit.SECONDS));
}
private void testChangeTrackerBackoff(final CustomizableMockInterceptor interceptor)
throws Throwable {
URL testURL = getReplicationURL();
final CountDownLatch changeTrackerFinishedSignal = new CountDownLatch(1);
ChangeTrackerClient client = new DefaultChangeTrackerClient() {
@Override
public void changeTrackerStopped(ChangeTracker tracker) {
changeTrackerFinishedSignal.countDown();
}
@Override
public OkHttpClient getOkHttpClient() {
return new OkHttpClient.Builder().addInterceptor(interceptor).build();
}
};
final ChangeTracker changeTracker = new ChangeTracker(testURL,
ChangeTracker.ChangeTrackerMode.LongPoll, false, 0L, client);
changeTracker.setUsePOST(isTestingAgainstSyncGateway());
changeTracker.start();
// sleep for a few seconds
Thread.sleep(5 * 1000);
// make sure we got less than 10 requests in those 10 seconds (if it was hammering, we'd get a lot more)
assertTrue(interceptor.getCapturedRequests().size() < 25);
interceptor.clearResponders();
interceptor.addResponseReturnEmptyChangesFeed();
// at this point, the change tracker backoff should cause it to sleep for about 3 seconds
// and so lets wait 3 seconds until it wakes up and starts getting valid responses
Thread.sleep(3 * 1000);
// now find the delta in requests received in a 2s period
int before = interceptor.getCapturedRequests().size();
Thread.sleep(2 * 1000);
int after = interceptor.getCapturedRequests().size();
// the backoff numAttempts should have been reset to 0
assertTrue(changeTracker.backoff.getNumAttempts() == 0);
changeTracker.stop();
try {
boolean success = changeTrackerFinishedSignal.await(300, TimeUnit.SECONDS);
assertTrue(success);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}