Skip to content

Commit cb22bf7

Browse files
authored
Merge pull request #479 from Countly/back_of_mech
Back of mechanism
2 parents 28153e4 + bda062c commit cb22bf7

File tree

14 files changed

+213
-31
lines changed

14 files changed

+213
-31
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## XX.XX.XX
2-
* The feedback widgets now have transparent backgrounds for a cleaner look.
3-
* Extended the notification button URL handler to allow custom handling of URLs when notification buttons are clicked in the background.
2+
* Improved request queue handling, added a backoff mechanism to the SDK to better handle cases where the server responds slowly, enabled by default.
3+
* Added a config method to disable backoff mechanism "disableBackoffMechanism()"
44
* Added a config method to disable server config updates in the initialization "disableSDKBehaviorSettingsUpdates()".
5+
* The feedback widgets now have transparent backgrounds and fullscreen for a cleaner look.
6+
* Extended the notification button URL handler to allow custom handling of URLs when notification buttons are clicked in the background.
57

68
* Deprecated "presentFeedbackWidget(widgetInfo, context, closeButtonText, devCallback)", replaced with "presentFeedbackWidget(widgetInfo, context, devCallback)" in the feedbacks.
79

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ org.gradle.configureondemand=true
2222
android.useAndroidX=true
2323
android.enableJetifier=true
2424
# RELEASE FIELD SECTION
25-
VERSION_NAME=25.4.1-RC2
25+
VERSION_NAME=25.4.1-RC3
2626
GROUP=ly.count.android
2727
POM_URL=https://github.com/Countly/countly-sdk-android
2828
POM_SCM_URL=https://github.com/Countly/countly-sdk-android

sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ of this software and associated documentation files (the "Software"), to deal
3434
import org.junit.Before;
3535
import org.junit.Test;
3636
import org.junit.runner.RunWith;
37+
import org.mockito.Mockito;
3738

3839
import static ly.count.android.sdk.UtilsNetworking.sha256Hash;
3940
import static org.junit.Assert.assertEquals;
@@ -105,6 +106,26 @@ public void setUp() {
105106
@Override public boolean getRefreshContentZoneEnabled() {
106107
return true;
107108
}
109+
110+
@Override public boolean getBOMEnabled() {
111+
return true;
112+
}
113+
114+
@Override public int getBOMAcceptedTimeoutSeconds() {
115+
return 10;
116+
}
117+
118+
@Override public double getBOMRQPercentage() {
119+
return 0.5;
120+
}
121+
122+
@Override public int getBOMRequestAge() {
123+
return 24;
124+
}
125+
126+
@Override public int getBOMDuration() {
127+
return 60;
128+
}
108129
};
109130

110131
Countly.sharedInstance().setLoggingEnabled(true);
@@ -136,7 +157,7 @@ public void setUp() {
136157
}
137158
};
138159

139-
connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock);
160+
connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class));
140161
testDeviceId = "123";
141162
}
142163

@@ -145,7 +166,7 @@ public void testConstructorAndGetters() {
145166
final String serverURL = "https://secureserver";
146167
final CountlyStore mockStore = mock(CountlyStore.class);
147168
final DeviceIdProvider mockDeviceId = mock(DeviceIdProvider.class);
148-
final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock);
169+
final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class));
149170
assertEquals(serverURL, connectionProcessor1.getServerURL());
150171
assertSame(mockStore, connectionProcessor1.getCountlyStore());
151172
}
@@ -212,7 +233,7 @@ public void urlConnectionCustomHeaderValues() throws IOException {
212233
customValues.put("5", "");
213234
customValues.put("6", null);
214235

215-
ConnectionProcessor connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, rip, null, customValues, moduleLog, healthTrackerMock);
236+
ConnectionProcessor connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, rip, null, customValues, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class));
216237
final URLConnection urlConnection = connectionProcessor.urlConnectionForServerRequest("eventData", null);
217238

218239
assertEquals("bb", urlConnection.getRequestProperty("aa"));

sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class TestUtils {
4444
public final static String commonAppKey = "appkey";
4545
public final static String commonDeviceId = "1234";
4646
public final static String SDK_NAME = "java-native-android";
47-
public final static String SDK_VERSION = "25.4.1-RC2";
47+
public final static String SDK_VERSION = "25.4.1-RC3";
4848
public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50;
4949

5050
public static class Activity2 extends Activity {

sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,15 @@ interface ConfigurationProvider {
1818
boolean getLocationTrackingEnabled();
1919

2020
boolean getRefreshContentZoneEnabled();
21+
22+
// BACKOFF MECHANISM
23+
boolean getBOMEnabled();
24+
25+
int getBOMAcceptedTimeoutSeconds();
26+
27+
double getBOMRQPercentage();
28+
29+
int getBOMRequestAge();
30+
31+
int getBOMDuration();
2132
}

sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ of this software and associated documentation files (the "Software"), to deal
3434
import java.net.HttpURLConnection;
3535
import java.net.URL;
3636
import java.net.URLConnection;
37+
import java.nio.charset.StandardCharsets;
3738
import java.util.Map;
3839
import javax.net.ssl.HttpsURLConnection;
3940
import javax.net.ssl.SSLContext;
@@ -49,6 +50,7 @@ of this software and associated documentation files (the "Software"), to deal
4950
*/
5051
public class ConnectionProcessor implements Runnable {
5152
private static final int CONNECT_TIMEOUT_IN_MILLISECONDS = 30_000;
53+
// used in backoff mechanism to accept half of the CONNECT_TIMEOUT_IN_MILLISECONDS
5254
private static final int READ_TIMEOUT_IN_MILLISECONDS = 30_000;
5355

5456
private static final String CRLF = "\r\n";
@@ -65,6 +67,7 @@ public class ConnectionProcessor implements Runnable {
6567
private final SSLContext sslContext_;
6668

6769
private final Map<String, String> requestHeaderCustomValues_;
70+
private final Runnable backoffCallback_;
6871

6972
static String endPointOverrideTag = "&new_end_point=";
7073

@@ -79,14 +82,15 @@ private enum RequestResult {
7982

8083
ConnectionProcessor(final String serverURL, final StorageProvider storageProvider, final DeviceIdProvider deviceIdProvider, final ConfigurationProvider configProvider,
8184
final RequestInfoProvider requestInfoProvider, final SSLContext sslContext, final Map<String, String> requestHeaderCustomValues, ModuleLog logModule,
82-
HealthTracker healthTracker) {
85+
HealthTracker healthTracker, Runnable backoffCallback) {
8386
serverURL_ = serverURL;
8487
storageProvider_ = storageProvider;
8588
deviceIdProvider_ = deviceIdProvider;
8689
configProvider_ = configProvider;
8790
sslContext_ = sslContext;
8891
requestHeaderCustomValues_ = requestHeaderCustomValues;
8992
requestInfoProvider_ = requestInfoProvider;
93+
backoffCallback_ = backoffCallback;
9094
L = logModule;
9195
this.healthTracker = healthTracker;
9296
}
@@ -96,7 +100,6 @@ private enum RequestResult {
96100
if (customEndpoint != null) {
97101
urlEndpoint = customEndpoint;
98102
}
99-
100103
// determine whether or not request has a binary image file, if it has request will be sent as POST request
101104
boolean hasPicturePath = requestData.contains(ModuleUserProfile.PICTURE_PATH_KEY);
102105
boolean usingHttpPost = requestData.contains("&crash=") || requestData.length() >= 2048 || requestInfoProvider_.isHttpPostForced() || hasPicturePath;
@@ -229,7 +232,7 @@ private enum RequestResult {
229232
break;
230233
}
231234
String value = conn.getHeaderField(headerIndex++);
232-
approximateDateSize += key.getBytes("US-ASCII").length + value.getBytes("US-ASCII").length + 2L;
235+
approximateDateSize += key.getBytes(StandardCharsets.US_ASCII).length + value.getBytes(StandardCharsets.US_ASCII).length + 2L;
233236
}
234237
} catch (Exception e) {
235238
L.e("[Connection Processor] urlConnectionForServerRequest, exception while calculating header field size: " + e);
@@ -434,6 +437,7 @@ public void run() {
434437
conn = urlConnectionForServerRequest(requestData, customEndpoint);
435438
long setupServerRequestTime = UtilsTime.getNanoTime() - pccTsStartGetURLConnection;
436439
L.d("[ConnectionProcessor] run, TIMING Setup server request took:[" + setupServerRequestTime / 1000000.0d + "] ms");
440+
437441
if (pcc != null) {
438442
pcc.TrackCounterTimeNs("ConnectionProcessorRun_07_SetupServerRequest", setupServerRequestTime);
439443
pccTsStartOnlyInternet = UtilsTime.getNanoTime();
@@ -472,7 +476,7 @@ public void run() {
472476
}
473477

474478
final RequestResult rRes;
475-
479+
476480
if (responseCode >= 200 && responseCode < 300) {
477481

478482
if (responseString.isEmpty()) {
@@ -525,6 +529,11 @@ public void run() {
525529
// successfully submitted event data to Count.ly server, so remove
526530
// this one from the stored events collection
527531
storageProvider_.removeRequest(originalRequest);
532+
533+
if (configProvider_.getBOMEnabled() && backoff(setupServerRequestTime, storedRequestCount, requestData)) {
534+
backoffCallback_.run();
535+
break;
536+
}
528537
} else {
529538
// will retry later
530539
// warning was logged above, stop processing, let next tick take care of retrying
@@ -582,6 +591,41 @@ public void run() {
582591
L.v("[ConnectionProcessor] run, TIMING Whole queue took:[" + wholeQueueTime / 1000000.0d + "] ms");
583592
}
584593

594+
/**
595+
* Backoff mechanism to prevent flooding the server with requests when server is not able to respond
596+
* Needs 3 conditions to met:
597+
* - Request has a timestamp younger than 12 hrs
598+
* - The number of requests inside the queue is less than 10% of the max queue size
599+
* - The response time from the server is greater than or equal to ACCEPTED_TIMEOUT_SECONDS
600+
*
601+
* @param responseTimeMillis response time in milliseconds
602+
* @param storedRequestCount number of requests in the queue
603+
* @param requestData request data
604+
* @return true if the backoff mechanism is triggered
605+
*/
606+
private boolean backoff(long responseTimeMillis, int storedRequestCount, String requestData) {
607+
long responseTimeSeconds = responseTimeMillis / 1_000_000_000L;
608+
boolean result = false;
609+
610+
if (responseTimeSeconds >= configProvider_.getBOMAcceptedTimeoutSeconds()) {
611+
// FLAG 1
612+
if (storedRequestCount <= storageProvider_.getMaxRequestQueueSize() * configProvider_.getBOMRQPercentage()) {
613+
// FLAG 2
614+
if (!Utils.isRequestTooOld(requestData, configProvider_.getBOMRequestAge(), "[ConnectionProcessor] backoff", L)) {
615+
// FLAG 3
616+
result = true;
617+
healthTracker.logBackoffRequest();
618+
}
619+
}
620+
}
621+
622+
if (!result) {
623+
healthTracker.logConsecutiveBackoffRequest();
624+
}
625+
626+
return result;
627+
}
628+
585629
String getServerURL() {
586630
return serverURL_;
587631
}

sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ of this software and associated documentation files (the "Software"), to deal
3131
import java.util.concurrent.Future;
3232
import java.util.concurrent.ScheduledExecutorService;
3333
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.atomic.AtomicBoolean;
3435
import javax.net.ssl.SSLContext;
3536
import javax.net.ssl.TrustManager;
3637
import org.json.JSONException;
@@ -52,6 +53,9 @@ class ConnectionQueue implements RequestQueueProvider {
5253
private Future<?> connectionProcessorFuture_;
5354
private DeviceIdProvider deviceIdProvider_;
5455
private SSLContext sslContext_;
56+
private final ScheduledExecutorService backoffScheduler_ = Executors.newSingleThreadScheduledExecutor();
57+
private final AtomicBoolean backoff_ = new AtomicBoolean(false);
58+
5559
BaseInfoProvider baseInfoProvider;
5660
HealthTracker healthTracker;
5761
public PerformanceCounterCollector pcc;
@@ -874,6 +878,10 @@ void ensureExecutor() {
874878
public void tick() {
875879
//todo enable later
876880
//assert storageProvider != null;
881+
if (backoff_.get()) {
882+
L.i("[ConnectionQueue] tick, currently backed off, skipping tick");
883+
return;
884+
}
877885

878886
boolean rqEmpty = isRequestQueueEmpty(); // this is a heavy operation, do it only once. Why heavy? reading storage
879887
boolean cpDoneIfOngoing = connectionProcessorFuture_ != null && connectionProcessorFuture_.isDone();
@@ -893,7 +901,21 @@ public void tick() {
893901
}
894902

895903
public ConnectionProcessor createConnectionProcessor() {
896-
ConnectionProcessor cp = new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, configProvider, requestInfoProvider, sslContext_, requestHeaderCustomValues, L, healthTracker);
904+
905+
ConnectionProcessor cp = new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, configProvider, requestInfoProvider, sslContext_, requestHeaderCustomValues, L, healthTracker, new Runnable() {
906+
@Override
907+
public void run() {
908+
L.d("[ConnectionQueue] createConnectionProcessor:run, backed off, countdown started for " + configProvider.getBOMDuration() + " seconds");
909+
backoff_.set(true);
910+
backoffScheduler_.schedule(new Runnable() {
911+
@Override public void run() {
912+
L.d("[ConnectionQueue] createConnectionProcessor:run, countdown finished, running tick in background thread");
913+
backoff_.set(false);
914+
tick();
915+
}
916+
}, configProvider.getBOMDuration(), TimeUnit.SECONDS);
917+
}
918+
});
897919
cp.pcc = pcc;
898920
return cp;
899921
}

sdk/src/main/java/ly/count/android/sdk/Countly.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal
4747
*/
4848
public class Countly {
4949

50-
private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "25.4.1-RC2";
50+
private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "25.4.1-RC3";
5151
/**
5252
* Used as request meta data on every request
5353
*/

sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ public class CountlyConfig {
202202
// Requests older than this value in hours would be dropped (0 means this feature is disabled)
203203
int dropAgeHours = 0;
204204
String sdkBehaviorSettings;
205+
boolean backOffMechanismEnabled = true;
205206
boolean sdkBehaviorSettingsRequestsDisabled = false;
206207

207208
/**
@@ -1013,6 +1014,16 @@ public synchronized CountlyConfig setSDKBehaviorSettings(String sdkBehaviorSetti
10131014
return this;
10141015
}
10151016

1017+
/**
1018+
* Disable the back off mechanism
1019+
*
1020+
* @return Returns the same config object for convenient linking
1021+
*/
1022+
public synchronized CountlyConfig disableBackoffMechanism() {
1023+
this.backOffMechanismEnabled = false;
1024+
return this;
1025+
}
1026+
10161027
/**
10171028
* Disable the SDK behavior settings update calls to the server
10181029
*

sdk/src/main/java/ly/count/android/sdk/CountlyStore.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,4 +992,8 @@ public void setHealthCheckCounterState(@NonNull String counterState) {
992992
editor.apply();
993993
}
994994
}
995+
996+
public int getMaxRequestQueueSize() {
997+
return maxRequestQueueSize;
998+
}
995999
}

0 commit comments

Comments
 (0)