diff --git a/CHANGELOG.md b/CHANGELOG.md index f7821cfe6..6cfea34c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ #Changelog +## 8.1.2(2021-01-18) +* 区域查询采用SingleFlight模式 +* 增加网络链接状态检测 + ## 8.1.1 (2021-01-06) * 优化日志统计 diff --git a/README.md b/README.md index b967950d1..66725c7fb 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ https://github.com/qiniudemo/qiniu-lab-android | 7.0.7 | Android 2.2+ | android-async-http 1.4.8 | ### 注意 -* 推荐使用最新版:8.1.1 +* 推荐使用最新版:8.1.2 * AndroidNetwork.getMobileDbm()可以获取手机信号强度,需要如下权限(API>=18时生效) ``` diff --git a/library/src/androidTest/java/com/qiniu/android/ConnectCheckTest.java b/library/src/androidTest/java/com/qiniu/android/ConnectCheckTest.java new file mode 100644 index 000000000..bc77e6698 --- /dev/null +++ b/library/src/androidTest/java/com/qiniu/android/ConnectCheckTest.java @@ -0,0 +1,46 @@ +package com.qiniu.android; + +import com.qiniu.android.http.connectCheck.ConnectChecker; +import com.qiniu.android.storage.GlobalConfiguration; + +public class ConnectCheckTest extends BaseTest { + + public void testCheck() { + + int maxCount = 100; + int successCount = 0; + for (int i = 0; i < maxCount; i++) { + if (ConnectChecker.check()) { + successCount += 1; + } + } + + assertEquals("maxCount:" + maxCount + " successCount:" + successCount, maxCount, successCount); + } + + public void testCustomCheckHosts() { + GlobalConfiguration.getInstance().connectCheckURLStrings = new String[]{"https://www.baidu.com"}; + int maxCount = 100; + int successCount = 0; + for (int i = 0; i < maxCount; i++) { + if (ConnectChecker.check()) { + successCount += 1; + } + } + + assertEquals("maxCount:" + maxCount + " successCount:" + successCount, maxCount, successCount); + } + + public void testNotConnected() { + GlobalConfiguration.getInstance().connectCheckURLStrings = new String[]{"https://www.test1.com", "https://www.test2.com"}; + int maxCount = 100; + int successCount = 0; + for (int i = 0; i < maxCount; i++) { + if (ConnectChecker.check()) { + successCount += 1; + } + } + + assertEquals("maxCount:" + maxCount + " successCount:" + successCount, 0, successCount); + } +} diff --git a/library/src/androidTest/java/com/qiniu/android/SingleFlightTest.java b/library/src/androidTest/java/com/qiniu/android/SingleFlightTest.java new file mode 100644 index 000000000..cea74bb71 --- /dev/null +++ b/library/src/androidTest/java/com/qiniu/android/SingleFlightTest.java @@ -0,0 +1,188 @@ +package com.qiniu.android; + +import com.qiniu.android.utils.LogUtil; +import com.qiniu.android.utils.SingleFlight; + +public class SingleFlightTest extends BaseTest { + + private static final int RetryCount = 5; + + public void testSync() { + final TestStatus testStatus = new TestStatus(); + testStatus.maxCount = 1000; + testStatus.completeCount = 0; + + SingleFlight singleFlight = new SingleFlight(); + for (int i = 0; i < testStatus.maxCount; i++) { + singleFlightPerform(singleFlight, i, RetryCount, false, new CompleteHandler() { + @Override + public void complete() throws Exception { + testStatus.completeCount += 1; + LogUtil.d("== sync completeCount:" + testStatus.completeCount); + } + }); + } + + wait(new WaitConditional() { + @Override + public boolean shouldWait() { + return testStatus.maxCount != testStatus.completeCount; + } + }, 60); + } + + public void testSyncRetry() { + final TestStatus testStatus = new TestStatus(); + testStatus.maxCount = 1000; + testStatus.completeCount = 0; + + SingleFlight singleFlight = new SingleFlight(); + for (int i = 0; i < testStatus.maxCount; i++) { + singleFlightPerform(singleFlight, i, 0, false, new CompleteHandler() { + @Override + public void complete() throws Exception { + testStatus.completeCount += 1; + LogUtil.d("== sync completeCount:" + testStatus.completeCount); + } + }); + } + + wait(new WaitConditional() { + @Override + public boolean shouldWait() { + return testStatus.maxCount != testStatus.completeCount; + } + }, 60); + } + + public void testAsync() { + final TestStatus testStatus = new TestStatus(); + testStatus.maxCount = 1000; + testStatus.completeCount = 0; + + SingleFlight singleFlight = new SingleFlight(); + for (int i = 0; i < testStatus.maxCount; i++) { + singleFlightPerform(singleFlight, i, RetryCount, true, new CompleteHandler() { + @Override + public void complete() throws Exception { + synchronized (testStatus) { + testStatus.completeCount += 1; + } + LogUtil.d("== async complete Count:" + testStatus.completeCount); + } + }); + } + + wait(new WaitConditional() { + @Override + public boolean shouldWait() { + return testStatus.maxCount != testStatus.completeCount; + } + }, 60); + + assertTrue("== async" + "max Count:" + testStatus.maxCount + " complete Count:" + testStatus.completeCount, testStatus.maxCount == testStatus.completeCount); + LogUtil.d("== async" + "max Count:" + testStatus.maxCount + " complete Count:" + testStatus.completeCount); + } + + public void testAsyncRetry() { + final TestStatus testStatus = new TestStatus(); + testStatus.maxCount = 1000; + testStatus.completeCount = 0; + + SingleFlight singleFlight = new SingleFlight(); + for (int i = 0; i < testStatus.maxCount; i++) { + singleFlightPerform(singleFlight, i, 0, true, new CompleteHandler() { + @Override + public void complete() throws Exception { + synchronized (testStatus) { + testStatus.completeCount += 1; + } + LogUtil.d("== async completeCount:" + testStatus.completeCount); + } + }); + } + + wait(new WaitConditional() { + @Override + public boolean shouldWait() { + return testStatus.maxCount != testStatus.completeCount; + } + }, 60); + + LogUtil.d("== async completeCount:" + testStatus.completeCount + " end"); + } + + private void singleFlightPerform(final SingleFlight singleFlight, + final int index, + final int retryCount, + final boolean isAsync, + final CompleteHandler completeHandler) { + + try { + singleFlight.perform("key", new SingleFlight.ActionHandler() { + @Override + public void action(final SingleFlight.CompleteHandler singleFlightCompleteHandler) throws Exception { + + final CompleteHandler completeHandlerP = new CompleteHandler() { + @Override + public void complete() throws Exception { + if (retryCount < RetryCount) { + LogUtil.d("== " + (isAsync ? "async" : "sync") + " action retryCount:" + retryCount + " index:" + index + " error"); + throw new Exception("== 123 =="); + } else { + LogUtil.d("== " + (isAsync ? "async" : "sync") + " action retryCount:" + retryCount + " index:" + index + " value"); + singleFlightCompleteHandler.complete(index + ""); + } + } + }; + + if (isAsync) { + new Thread(new Runnable() { + @Override + public void run() { + try { + completeHandlerP.complete(); + } catch (Exception e) { + singleFlightCompleteHandler.complete(null); + } + } + }).start(); + } else { + completeHandlerP.complete(); + } + } + }, new SingleFlight.CompleteHandler() { + @Override + public void complete(Object value) { + if (retryCount < RetryCount) { + singleFlightPerform(singleFlight, index, retryCount + 1, isAsync, completeHandler); + } else { + LogUtil.d("== " + (isAsync ? "async" : "sync") + " action complete retryCount:" + retryCount + " value:" + value + " index:" + index); + if (!isAsync) { + assertTrue("index:" + index + "value error",(value + "").equals(index + "")); + } + try { + completeHandler.complete(); + } catch (Exception e) { + } + } + } + }); + } catch (Exception e) { + singleFlightPerform(singleFlight, index, retryCount + 1, isAsync, completeHandler); + } + } + + + + private interface CompleteHandler { + void complete() throws Exception; + } + + + protected static class TestStatus { + int maxCount; + int completeCount; + } + +} diff --git a/library/src/main/java/com/qiniu/android/common/AutoZone.java b/library/src/main/java/com/qiniu/android/common/AutoZone.java index d11ceb487..020e40a11 100644 --- a/library/src/main/java/com/qiniu/android/common/AutoZone.java +++ b/library/src/main/java/com/qiniu/android/common/AutoZone.java @@ -4,6 +4,7 @@ import com.qiniu.android.http.request.RequestTransaction; import com.qiniu.android.http.metrics.UploadRegionRequestMetrics; import com.qiniu.android.storage.UpToken; +import com.qiniu.android.utils.SingleFlight; import org.json.JSONObject; @@ -23,6 +24,8 @@ public final class AutoZone extends Zone { private Map zonesInfoMap = new ConcurrentHashMap<>(); private ArrayList transactions = new ArrayList<>(); + private static final SingleFlight SingleFlight = new SingleFlight(); + //私有云可能改变ucServer public void setUcServer(String ucServer) { this.ucServer = ucServer; @@ -72,27 +75,56 @@ public void preQuery(final UpToken token, final QueryHandler completeHandler) { return; } - final RequestTransaction transaction = createUploadRequestTransaction(token); - transaction.queryUploadHosts(true, new RequestTransaction.RequestCompleteHandler() { - @Override - public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { - if (responseInfo != null && responseInfo.isOK() && response != null) { - ZonesInfo zonesInfoP = ZonesInfo.createZonesInfo(response); - zonesInfoMap.put(cacheKey, zonesInfoP); - GlobalCache.getInstance().cache(response, cacheKey); - completeHandler.complete(0, responseInfo, requestMetrics); - } else { - if (responseInfo.isNetworkBroken()) { - completeHandler.complete(ResponseInfo.NetworkError, responseInfo, requestMetrics); - } else { - ZonesInfo zonesInfoP = FixedZone.localsZoneInfo().getZonesInfo(token); + + try { + SingleFlight.perform(cacheKey, new SingleFlight.ActionHandler() { + @Override + public void action(final com.qiniu.android.utils.SingleFlight.CompleteHandler completeHandler) throws Exception { + + final RequestTransaction transaction = createUploadRequestTransaction(token); + transaction.queryUploadHosts(true, new RequestTransaction.RequestCompleteHandler() { + @Override + public void complete(ResponseInfo responseInfo, UploadRegionRequestMetrics requestMetrics, JSONObject response) { + destroyUploadRequestTransaction(transaction); + + SingleFlightValue value = new SingleFlightValue(); + value.responseInfo = responseInfo; + value.response = response; + value.metrics = requestMetrics; + completeHandler.complete(value); + } + }); + } + + }, new SingleFlight.CompleteHandler() { + @Override + public void complete(Object value) { + SingleFlightValue singleFlightValue = (SingleFlightValue)value; + ResponseInfo responseInfo = singleFlightValue.responseInfo; + UploadRegionRequestMetrics requestMetrics = singleFlightValue.metrics; + JSONObject response = singleFlightValue.response; + + if (responseInfo != null && responseInfo.isOK() && response != null) { + ZonesInfo zonesInfoP = ZonesInfo.createZonesInfo(response); zonesInfoMap.put(cacheKey, zonesInfoP); + GlobalCache.getInstance().cache(response, cacheKey); completeHandler.complete(0, responseInfo, requestMetrics); + } else { + if (responseInfo.isNetworkBroken()) { + completeHandler.complete(ResponseInfo.NetworkError, responseInfo, requestMetrics); + } else { + ZonesInfo zonesInfoP = FixedZone.localsZoneInfo().getZonesInfo(token); + zonesInfoMap.put(cacheKey, zonesInfoP); + completeHandler.complete(0, responseInfo, requestMetrics); + } } } - destroyUploadRequestTransaction(transaction); - } - }); + }); + + } catch (Exception e) { + /// 此处永远不会执行,回调只为占位 + completeHandler.complete(ResponseInfo.NetworkError, ResponseInfo.localIOError("uc query"), null); + } } private RequestTransaction createUploadRequestTransaction(UpToken token) { @@ -107,6 +139,11 @@ private void destroyUploadRequestTransaction(RequestTransaction transaction) { transactions.remove(transaction); } + private static class SingleFlightValue { + private ResponseInfo responseInfo; + private JSONObject response; + private UploadRegionRequestMetrics metrics; + } private static class GlobalCache { private static GlobalCache globalCache = new GlobalCache(); diff --git a/library/src/main/java/com/qiniu/android/common/Constants.java b/library/src/main/java/com/qiniu/android/common/Constants.java index 51be5db93..a76e38c45 100644 --- a/library/src/main/java/com/qiniu/android/common/Constants.java +++ b/library/src/main/java/com/qiniu/android/common/Constants.java @@ -2,7 +2,7 @@ public final class Constants { - public static final String VERSION = "8.1.1"; + public static final String VERSION = "8.1.2"; public static final String UTF_8 = "utf-8"; } diff --git a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java index f32eb08f2..1e53e5207 100644 --- a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java +++ b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java @@ -279,7 +279,7 @@ public boolean canConnectToHost(){ public boolean isHostUnavailable(){ // 基本不可恢复,注:会影响下次请求,范围太大可能会造成大量的timeout - if (isTlsError() || statusCode == 502 || statusCode == 503 || statusCode == 504 || statusCode == 599) { + if (statusCode == 502 || statusCode == 503 || statusCode == 504 || statusCode == 599) { return true; } else { return false; diff --git a/library/src/main/java/com/qiniu/android/http/connectCheck/ConnectChecker.java b/library/src/main/java/com/qiniu/android/http/connectCheck/ConnectChecker.java new file mode 100644 index 000000000..9c8acd995 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/http/connectCheck/ConnectChecker.java @@ -0,0 +1,138 @@ +package com.qiniu.android.http.connectCheck; + +import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.http.metrics.UploadSingleRequestMetrics; +import com.qiniu.android.http.request.IRequestClient; +import com.qiniu.android.http.request.Request; +import com.qiniu.android.http.request.httpclient.SystemHttpClient; +import com.qiniu.android.storage.GlobalConfiguration; +import com.qiniu.android.utils.LogUtil; +import com.qiniu.android.utils.SingleFlight; +import com.qiniu.android.utils.Wait; + +import org.json.JSONObject; + +public class ConnectChecker { + + private static SingleFlight singleFlight = new SingleFlight<>(); + + public static boolean check() { + + final CheckResult result = new CheckResult(); + + final Wait wait = new Wait(); + check(new CheckCompleteHandler() { + @Override + public void complete(boolean isConnected) { + result.isConnected = isConnected; + wait.stopWait(); + } + }); + wait.startWait(); + + return result.isConnected; + } + + private static void check(final CheckCompleteHandler completeHandler) { + + try { + singleFlight.perform("connect_check", new SingleFlight.ActionHandler() { + @Override + public void action(final SingleFlight.CompleteHandler singleFlightComplete) throws Exception { + checkAllHosts(new CheckCompleteHandler() { + @Override + public void complete(boolean isConnected) { + singleFlightComplete.complete(isConnected); + } + }); + } + }, new SingleFlight.CompleteHandler() { + @Override + public void complete(Boolean value) { + completeHandler.complete(value); + } + }); + } catch (Exception e) { + completeHandler.complete(true); + } + } + + private static void checkAllHosts(final CheckCompleteHandler completeHandler) { + String[] allHosts = GlobalConfiguration.getInstance().connectCheckURLStrings; + if (allHosts == null) { + completeHandler.complete(true); + return; + } + + allHosts = allHosts.clone(); + final CheckStatus checkStatus = new CheckStatus(); + checkStatus.totalCount = allHosts.length; + checkStatus.completeCount = 0; + checkStatus.isCompleted = false; + for (String host : allHosts) { + checkHost(host, new CheckCompleteHandler() { + @Override + public void complete(boolean isHostConnected) { + + synchronized (checkStatus) { + checkStatus.completeCount += 1; + } + if (isHostConnected) { + checkStatus.isConnected = true; + } + if (isHostConnected || checkStatus.completeCount == checkStatus.totalCount) { + synchronized (checkStatus) { + if (checkStatus.isCompleted) { + LogUtil.i("== check all hosts has completed totalCount:" + checkStatus.totalCount + " completeCount:" + checkStatus.completeCount); + return; + } else { + LogUtil.i("== check all hosts completed totalCount:" + checkStatus.totalCount + " completeCount:" + checkStatus.completeCount); + checkStatus.isCompleted = true; + } + } + completeHandler.complete(checkStatus.isConnected); + } else { + LogUtil.i("== check all hosts not completed totalCount:" + checkStatus.totalCount + " completeCount:" + checkStatus.completeCount); + } + } + }); + } + + } + + private static void checkHost(final String host, final CheckCompleteHandler completeHandler) { + + Request request = new Request(host, Request.HttpMethodHEAD, null, null, GlobalConfiguration.getInstance().connectCheckTimeout); + SystemHttpClient client = new SystemHttpClient(); + + LogUtil.i("== checkHost:" + host); + client.request(request, true, null, null, new IRequestClient.RequestClientCompleteHandler() { + @Override + public void complete(ResponseInfo responseInfo, UploadSingleRequestMetrics metrics, JSONObject response) { + if (responseInfo.statusCode > 99) { + LogUtil.i("== checkHost:" + host + " result: true"); + completeHandler.complete(true); + } else { + LogUtil.i("== checkHost:" + host + " result: false"); + completeHandler.complete(false); + } + } + }); + } + + + private interface CheckCompleteHandler { + void complete(boolean isConnected); + } + + private static class CheckStatus { + private int totalCount = 0; + private int completeCount = 0; + private boolean isCompleted = false; + private boolean isConnected = false; + } + + private static class CheckResult { + private boolean isConnected = false; + } +} diff --git a/library/src/main/java/com/qiniu/android/http/metrics/UploadTaskMetrics.java b/library/src/main/java/com/qiniu/android/http/metrics/UploadTaskMetrics.java index a273c6af7..8cf029664 100644 --- a/library/src/main/java/com/qiniu/android/http/metrics/UploadTaskMetrics.java +++ b/library/src/main/java/com/qiniu/android/http/metrics/UploadTaskMetrics.java @@ -4,16 +4,17 @@ import com.qiniu.android.http.request.IUploadRegion; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class UploadTaskMetrics { public ArrayList regions; - private HashMap metricsInfo; + private Map metricsInfo; public UploadTaskMetrics(ArrayList regions) { this.regions = regions; - this.metricsInfo = new HashMap(); + this.metricsInfo = new ConcurrentHashMap<>(); } diff --git a/library/src/main/java/com/qiniu/android/http/request/HttpSingleRequest.java b/library/src/main/java/com/qiniu/android/http/request/HttpSingleRequest.java index 90388a417..d970d6a67 100644 --- a/library/src/main/java/com/qiniu/android/http/request/HttpSingleRequest.java +++ b/library/src/main/java/com/qiniu/android/http/request/HttpSingleRequest.java @@ -4,6 +4,7 @@ import com.qiniu.android.collect.ReportItem; import com.qiniu.android.collect.UploadInfoReporter; import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.http.connectCheck.ConnectChecker; import com.qiniu.android.http.dns.DnsPrefetcher; import com.qiniu.android.http.request.httpclient.SystemHttpClient; import com.qiniu.android.http.request.handler.CheckCancelHandler; @@ -106,6 +107,12 @@ public void complete(ResponseInfo responseInfo, UploadSingleRequestMetrics metri if (metrics != null){ requestMetricsList.add(metrics); } + + if (shouldCheckConnect(responseInfo) && !ConnectChecker.check()) { + String message = "check origin statusCode:" + responseInfo.statusCode + " error:" + responseInfo.error; + responseInfo = ResponseInfo.errorInfo(ResponseInfo.NetworkSlow, message); + } + LogUtil.i("key:" + StringUtils.toNonnullString(requestInfo.key) + " response:" + StringUtils.toNonnullString(responseInfo)); if (shouldRetryHandler != null && shouldRetryHandler.shouldRetry(responseInfo, response) @@ -126,6 +133,15 @@ public void complete(ResponseInfo responseInfo, UploadSingleRequestMetrics metri } + private boolean shouldCheckConnect(ResponseInfo responseInfo) { + return responseInfo != null && ( + responseInfo.statusCode == -1001 || /* timeout */ + responseInfo.statusCode == -1003 || /* unknown host */ + responseInfo.statusCode == -1004 || /* cannot connect to host */ + responseInfo.statusCode == -1005 || /* connection lost */ + responseInfo.statusCode == -1009 || /* not connected to host */ + responseInfo.isTlsError()); + } private synchronized void completeAction(Request request, ResponseInfo responseInfo, diff --git a/library/src/main/java/com/qiniu/android/http/request/Request.java b/library/src/main/java/com/qiniu/android/http/request/Request.java index 5c0dc5a20..943d31470 100644 --- a/library/src/main/java/com/qiniu/android/http/request/Request.java +++ b/library/src/main/java/com/qiniu/android/http/request/Request.java @@ -1,14 +1,12 @@ package com.qiniu.android.http.request; -import java.net.Inet4Address; -import java.net.Inet6Address; import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; public class Request { + public static final String HttpMethodHEAD = "HEAD"; public static final String HttpMethodGet = "GET"; public static final String HttpMethodPOST = "POST"; public static final String HttpMethodPUT = "PUT"; diff --git a/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java b/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java index 7cd09531d..d8a8b78fa 100644 --- a/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java +++ b/library/src/main/java/com/qiniu/android/http/request/httpclient/SystemHttpClient.java @@ -214,7 +214,8 @@ private okhttp3.Request.Builder createRequestBuilder(final RequestClientProgress Headers allHeaders = Headers.of(currentRequest.allHeaders); okhttp3.Request.Builder requestBuilder = null; - if (currentRequest.httpMethod.equals(Request.HttpMethodGet)){ + if (currentRequest.httpMethod.equals(Request.HttpMethodHEAD) || + currentRequest.httpMethod.equals(Request.HttpMethodGet)){ requestBuilder = new okhttp3.Request.Builder().get().url(currentRequest.urlString); for (String key : currentRequest.allHeaders.keySet()){ String value = currentRequest.allHeaders.get(key); diff --git a/library/src/main/java/com/qiniu/android/storage/GlobalConfiguration.java b/library/src/main/java/com/qiniu/android/storage/GlobalConfiguration.java index a5ad0a42d..2e73b8d89 100644 --- a/library/src/main/java/com/qiniu/android/storage/GlobalConfiguration.java +++ b/library/src/main/java/com/qiniu/android/storage/GlobalConfiguration.java @@ -3,6 +3,8 @@ import com.qiniu.android.http.dns.Dns; import com.qiniu.android.utils.Utils; +import java.util.List; + public class GlobalConfiguration { /** @@ -11,44 +13,58 @@ public class GlobalConfiguration { public boolean isDnsOpen = true; /** - * dns 预取失败后 会进行重新预取 dnsRepreHostNum为最多尝试次数 + * dns 预取失败后 会进行重新预取 dnsRepreHostNum为最多尝试次数 */ public int dnsRepreHostNum = 2; /** - * dns预取缓存时间 单位:秒 + * dns预取缓存时间 单位:秒 */ public int dnsCacheTime = 120; /** - * 自定义DNS解析客户端host + * 自定义DNS解析客户端host */ public Dns dns = null; /** - * 自定义DNS解析客户端host + * 自定义DNS解析客户端host */ public String dnsCacheDir = Utils.sdkDirectory() + "/dnsCache/"; /** - * Host全局冻结时间 单位:秒 默认:30 推荐范围:[10 ~ 60] - * 当某个Host的上传失败后并且可能短时间无法恢复,会冻结该Host,globalHostFrozenTime为全局冻结时间 - * Host全局冻结时间 单位:秒 默认:10 推荐范围:[5 ~ 30] - * 当某个Host的上传失败后并且可能短时间无法恢复,会冻结该Host + * Host全局冻结时间 单位:秒 默认:30 推荐范围:[10 ~ 60] + * 当某个Host的上传失败后并且可能短时间无法恢复,会冻结该Host,globalHostFrozenTime为全局冻结时间 + * Host全局冻结时间 单位:秒 默认:10 推荐范围:[5 ~ 30] + * 当某个Host的上传失败后并且可能短时间无法恢复,会冻结该Host */ public int globalHostFrozenTime = 10; /** - * Host局部冻结时间,只会影响当前上传操作 单位:秒 默认:5*60 推荐范围:[60 ~ 10*60] - * 当某个Host的上传失败后并且短时间可能会恢复,会局部冻结该Host + * Host局部冻结时间,只会影响当前上传操作 单位:秒 默认:5*60 推荐范围:[60 ~ 10*60] + * 当某个Host的上传失败后并且短时间可能会恢复,会局部冻结该Host */ - public int partialHostFrozenTime = 5*60; + public int partialHostFrozenTime = 5 * 60; + /** + * 网络连接状态检测使用的connectCheckURLStrings,网络链接状态检测可能会影响重试机制,启动网络连接状态检测有助于提高上传可用性。 + * 当请求的 Response 为网络异常时,并发对 connectCheckURLStrings 中 URLString 进行 HEAD 请求,以此检测当前网络状态的链接状态,其中任意一个 URLString 链接成功则认为当前网络状态链接良好; + * 当 connectCheckURLStrings 为 nil 或者 空数组时则弃用检测功能。 + */ + public String[] connectCheckURLStrings = new String[]{"http://www.qiniu.com", "http://www.baidu.com", "http://www.google.com"}; + + /** + * 网络连接状态检测HEAD请求超时,默认:3s + */ + public int connectCheckTimeout = 3; + private static GlobalConfiguration configuration = new GlobalConfiguration(); - private GlobalConfiguration(){ + + private GlobalConfiguration() { } - public static GlobalConfiguration getInstance(){ + + public static GlobalConfiguration getInstance() { return configuration; } } diff --git a/library/src/main/java/com/qiniu/android/utils/SingleFlight.java b/library/src/main/java/com/qiniu/android/utils/SingleFlight.java new file mode 100644 index 000000000..db6e5a632 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/utils/SingleFlight.java @@ -0,0 +1,134 @@ +package com.qiniu.android.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SingleFlight { + + private Map> callInfo = new HashMap<>(); + + /** + * 异步 SingleFlight 执行函数 + * + * @param key actionHandler 对应的 key,同一时刻同一个 key 最多只有一个对应的 actionHandler 在执行 + * @param actionHandler 执行函数,注意:actionHandler 中,【完成回调】和【抛出异常】二者有且有一个,且只能出现一次 + * @param completeHandler single flight 执行 actionHandler 后的完成回调 + */ + public void perform(String key, ActionHandler actionHandler, CompleteHandler completeHandler) throws Exception { + if (actionHandler == null) { + return; + } + + boolean isFirstTask = false; + boolean shouldComplete = false; + SingleFlightCall call = null; + synchronized (this) { + + if (key != null) { + call = callInfo.get(key); + } + + if (call == null) { + call = new SingleFlightCall<>(); + if (key != null) { + callInfo.put(key, call); + } + isFirstTask = true; + } + + synchronized (call) { + shouldComplete = call.isComplete; + if (!shouldComplete) { + SingleFlightTask task = new SingleFlightTask<>(); + task.completeHandler = completeHandler; + call.tasks.add(task); + } + } + } + + if (shouldComplete) { + if (call.exception != null) { + throw call.exception; + } else { + if (completeHandler != null) { + completeHandler.complete(call.value); + } + } + return; + } + + if (!isFirstTask) { + return; + } + + final String finalKey = key; + final SingleFlightCall finalCall = call; + try { + actionHandler.action(new CompleteHandler() { + @Override + public void complete(T value) { + List> currentTasks = null; + synchronized (finalCall) { + if (finalCall.isComplete) { + return; + } + finalCall.isComplete = true; + finalCall.value = value; + currentTasks = new ArrayList<>(finalCall.tasks); + } + if (finalKey != null) { + synchronized (this) { + callInfo.remove(finalKey); + } + } + for (SingleFlightTask task : currentTasks) { + if (task != null && task.completeHandler != null) { + task.completeHandler.complete(finalCall.value); + } + } + } + }); + } catch (Exception e) { + List> currentTasks = null; + synchronized (finalCall) { + if (finalCall.isComplete) { + return; + } + finalCall.isComplete = true; + finalCall.exception = e; + currentTasks = new ArrayList<>(call.tasks); + } + if (key != null) { + synchronized (this) { + callInfo.remove(key); + } + } + for (SingleFlightTask task : currentTasks) { + if (task != null && task.completeHandler != null) { + throw call.exception; + } + } + } + } + + private static class SingleFlightTask { + private CompleteHandler completeHandler; + } + + private static class SingleFlightCall { + private boolean isComplete = false; + private List> tasks = new ArrayList<>(); + private T value; + private Exception exception; + } + + public interface CompleteHandler { + void complete(T value); + } + + public interface ActionHandler { + void action(CompleteHandler completeHandler) throws Exception; + } +}