310 lines
10 KiB
Java
310 lines
10 KiB
Java
/*
|
|
* Copyright (c) 2015-present, Parse, LLC.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
package com.parse;
|
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import com.parse.http.ParseHttpBody;
|
|
import com.parse.http.ParseHttpRequest;
|
|
import com.parse.http.ParseHttpResponse;
|
|
|
|
import java.io.IOException;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
import bolts.Continuation;
|
|
import bolts.Task;
|
|
import bolts.TaskCompletionSource;
|
|
|
|
/**
|
|
* ParseRequest takes an arbitrary HttpUriRequest and retries it a number of times with
|
|
* exponential backoff.
|
|
*/
|
|
abstract class ParseRequest<Response> {
|
|
|
|
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
|
private final AtomicInteger mCount = new AtomicInteger(1);
|
|
|
|
public Thread newThread(@NonNull Runnable r) {
|
|
return new Thread(r, "ParseRequest.NETWORK_EXECUTOR-thread-" + mCount.getAndIncrement());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* We want to use more threads than default in {@code bolts.Executors} since most of the time
|
|
* the threads will be asleep waiting for data.
|
|
*/
|
|
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
|
|
private static final int CORE_POOL_SIZE = CPU_COUNT * 2 + 1;
|
|
private static final int MAX_POOL_SIZE = CPU_COUNT * 2 * 2 + 1;
|
|
private static final long KEEP_ALIVE_TIME = 1L;
|
|
private static final int MAX_QUEUE_SIZE = 128;
|
|
|
|
private static ThreadPoolExecutor newThreadPoolExecutor(int corePoolSize, int maxPoolSize,
|
|
long keepAliveTime, TimeUnit timeUnit, BlockingQueue<Runnable> workQueue,
|
|
ThreadFactory threadFactory) {
|
|
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize,
|
|
keepAliveTime, timeUnit, workQueue, threadFactory);
|
|
executor.allowCoreThreadTimeOut(true);
|
|
return executor;
|
|
}
|
|
|
|
protected static final ExecutorService NETWORK_EXECUTOR = newThreadPoolExecutor(
|
|
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
|
|
new LinkedBlockingQueue<Runnable>(MAX_QUEUE_SIZE), sThreadFactory);
|
|
|
|
protected static final int DEFAULT_MAX_RETRIES = 4;
|
|
/* package */ static final long DEFAULT_INITIAL_RETRY_DELAY = 1000L;
|
|
|
|
private static long defaultInitialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY;
|
|
|
|
public static void setDefaultInitialRetryDelay(long delay) {
|
|
defaultInitialRetryDelay = delay;
|
|
}
|
|
public static long getDefaultInitialRetryDelay() {
|
|
return defaultInitialRetryDelay;
|
|
}
|
|
|
|
private static int maxRetries() {
|
|
//typically happens just within tests
|
|
if (ParsePlugins.get() == null) {
|
|
return DEFAULT_MAX_RETRIES;
|
|
} else {
|
|
return ParsePlugins.get().configuration().maxRetries;
|
|
}
|
|
}
|
|
|
|
/* package */ ParseHttpRequest.Method method;
|
|
/* package */ String url;
|
|
|
|
public ParseRequest(String url) {
|
|
this(ParseHttpRequest.Method.GET, url);
|
|
}
|
|
|
|
public ParseRequest(ParseHttpRequest.Method method, String url) {
|
|
this.method = method;
|
|
this.url = url;
|
|
}
|
|
|
|
protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) {
|
|
// do nothing
|
|
return null;
|
|
}
|
|
|
|
protected ParseHttpRequest newRequest(
|
|
ParseHttpRequest.Method method,
|
|
String url,
|
|
ProgressCallback uploadProgressCallback) {
|
|
ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder()
|
|
.setMethod(method)
|
|
.setUrl(url);
|
|
|
|
switch (method) {
|
|
case GET:
|
|
case DELETE:
|
|
break;
|
|
case POST:
|
|
case PUT:
|
|
requestBuilder.setBody(newBody(uploadProgressCallback));
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Invalid method " + method);
|
|
}
|
|
return requestBuilder.build();
|
|
}
|
|
|
|
/*
|
|
* Runs one iteration of the request.
|
|
*/
|
|
private Task<Response> sendOneRequestAsync(
|
|
final ParseHttpClient client,
|
|
final ParseHttpRequest request,
|
|
final ProgressCallback downloadProgressCallback) {
|
|
return Task.<Void>forResult(null).onSuccessTask(new Continuation<Void, Task<Response>>() {
|
|
@Override
|
|
public Task<Response> then(Task<Void> task) throws Exception {
|
|
ParseHttpResponse response = client.execute(request);
|
|
return onResponseAsync(response, downloadProgressCallback);
|
|
}
|
|
}, NETWORK_EXECUTOR).continueWithTask(new Continuation<Response, Task<Response>>() {
|
|
@Override
|
|
public Task<Response> then(Task<Response> task) throws Exception {
|
|
if (task.isFaulted()) {
|
|
Exception error = task.getError();
|
|
if (error instanceof IOException) {
|
|
return Task.forError(newTemporaryException("i/o failure", error));
|
|
}
|
|
}
|
|
return task;
|
|
// Jump off the network executor so this task continuations won't steal network threads
|
|
}
|
|
}, Task.BACKGROUND_EXECUTOR);
|
|
}
|
|
|
|
protected abstract Task<Response> onResponseAsync(ParseHttpResponse response,
|
|
ProgressCallback downloadProgressCallback);
|
|
|
|
/*
|
|
* Starts retrying the block.
|
|
*/
|
|
public Task<Response> executeAsync(final ParseHttpClient client) {
|
|
return executeAsync(client, (ProgressCallback) null, null, null);
|
|
}
|
|
|
|
public Task<Response> executeAsync(final ParseHttpClient client, Task<Void> cancellationToken) {
|
|
return executeAsync(client, (ProgressCallback) null, null, cancellationToken);
|
|
}
|
|
|
|
public Task<Response> executeAsync(
|
|
final ParseHttpClient client,
|
|
final ProgressCallback uploadProgressCallback,
|
|
final ProgressCallback downloadProgressCallback) {
|
|
return executeAsync(client, uploadProgressCallback, downloadProgressCallback, null);
|
|
}
|
|
|
|
public Task<Response> executeAsync(
|
|
final ParseHttpClient client,
|
|
final ProgressCallback uploadProgressCallback,
|
|
final ProgressCallback downloadProgressCallback,
|
|
Task<Void> cancellationToken) {
|
|
ParseHttpRequest request = newRequest(method, url, uploadProgressCallback);
|
|
return executeAsync(
|
|
client,
|
|
request,
|
|
downloadProgressCallback,
|
|
cancellationToken);
|
|
}
|
|
|
|
// Although we can not cancel a single request, but we allow cancel between retries so we need a
|
|
// cancellationToken here.
|
|
private Task<Response> executeAsync(
|
|
final ParseHttpClient client,
|
|
final ParseHttpRequest request,
|
|
final ProgressCallback downloadProgressCallback,
|
|
final Task<Void> cancellationToken) {
|
|
long delay = defaultInitialRetryDelay + (long) (defaultInitialRetryDelay * Math.random());
|
|
|
|
return executeAsync(
|
|
client,
|
|
request,
|
|
0,
|
|
delay,
|
|
downloadProgressCallback,
|
|
cancellationToken);
|
|
}
|
|
|
|
private Task<Response> executeAsync(
|
|
final ParseHttpClient client,
|
|
final ParseHttpRequest request,
|
|
final int attemptsMade,
|
|
final long delay,
|
|
final ProgressCallback downloadProgressCallback,
|
|
final Task<Void> cancellationToken) {
|
|
if (cancellationToken != null && cancellationToken.isCancelled()) {
|
|
return Task.cancelled();
|
|
}
|
|
return sendOneRequestAsync(client, request, downloadProgressCallback).continueWithTask(new Continuation<Response, Task<Response>>() {
|
|
@Override
|
|
public Task<Response> then(Task<Response> task) throws Exception {
|
|
Exception e = task.getError();
|
|
if (task.isFaulted() && e instanceof ParseException) {
|
|
if (cancellationToken != null && cancellationToken.isCancelled()) {
|
|
return Task.cancelled();
|
|
}
|
|
|
|
if (e instanceof ParseRequestException &&
|
|
((ParseRequestException) e).isPermanentFailure) {
|
|
return task;
|
|
}
|
|
|
|
if (attemptsMade < maxRetries()) {
|
|
PLog.i("com.parse.ParseRequest", "Request failed. Waiting " + delay
|
|
+ " milliseconds before attempt #" + (attemptsMade + 1));
|
|
|
|
final TaskCompletionSource<Response> retryTask = new TaskCompletionSource<>();
|
|
ParseExecutors.scheduled().schedule(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
executeAsync(
|
|
client,
|
|
request,
|
|
attemptsMade + 1,
|
|
delay * 2,
|
|
downloadProgressCallback,
|
|
cancellationToken).continueWithTask(new Continuation<Response, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Response> task) throws Exception {
|
|
if (task.isCancelled()) {
|
|
retryTask.setCancelled();
|
|
} else if (task.isFaulted()) {
|
|
retryTask.setError(task.getError());
|
|
} else {
|
|
retryTask.setResult(task.getResult());
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
}, delay, TimeUnit.MILLISECONDS);
|
|
return retryTask.getTask();
|
|
}
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Constructs a permanent exception that won't be retried.
|
|
*/
|
|
protected ParseException newPermanentException(int code, String message) {
|
|
ParseRequestException e = new ParseRequestException(code, message);
|
|
e.isPermanentFailure = true;
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* Constructs a temporary exception that will be retried.
|
|
*/
|
|
protected ParseException newTemporaryException(int code, String message) {
|
|
ParseRequestException e = new ParseRequestException(code, message);
|
|
e.isPermanentFailure = false;
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* Constructs a temporary exception that will be retried with json error code 100.
|
|
*
|
|
* @see ParseException#CONNECTION_FAILED
|
|
*/
|
|
protected ParseException newTemporaryException(String message, Throwable t) {
|
|
ParseRequestException e = new ParseRequestException(
|
|
ParseException.CONNECTION_FAILED, message, t);
|
|
e.isPermanentFailure = false;
|
|
return e;
|
|
}
|
|
|
|
private static class ParseRequestException extends ParseException {
|
|
boolean isPermanentFailure = false;
|
|
|
|
public ParseRequestException(int theCode, String theMessage) {
|
|
super(theCode, theMessage);
|
|
}
|
|
|
|
public ParseRequestException(int theCode, String message, Throwable cause) {
|
|
super(theCode, message, cause);
|
|
}
|
|
}
|
|
}
|