Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fbc65a9
feat: added new method to countly store
arifBurakDemiray May 2, 2025
eb516bd
feat: inital back off mechanism
arifBurakDemiray May 2, 2025
d9a8c84
fix: queue reset
arifBurakDemiray May 12, 2025
00daada
fix: for last two
arifBurakDemiray May 13, 2025
c7a6d2b
fix: for last two
arifBurakDemiray May 13, 2025
178859d
fix: connect timeout
arifBurakDemiray May 13, 2025
32bd270
feat: reduce timouts to 10 secs
arifBurakDemiray May 13, 2025
3fc04fc
feat: add config option to disable back off
arifBurakDemiray May 13, 2025
5f2e09d
feat: back off, reduce timeouts, disable config CHANGELOG
arifBurakDemiray May 13, 2025
6ebb749
refactor: backoff mechanism
arifBurakDemiray May 13, 2025
3d48d21
feat: add server config to backoff
arifBurakDemiray May 13, 2025
0209ebc
fix: tests of conn processor
arifBurakDemiray May 13, 2025
88ed55c
feat: rename to backoff
arifBurakDemiray May 14, 2025
ec21c87
fix: remove unused param
arifBurakDemiray May 14, 2025
58ea1bd
fix: internal timeout seconds
arifBurakDemiray May 15, 2025
8775ddf
feat: health tracker for the backoff
arifBurakDemiray May 15, 2025
55dc8b0
feat: convert the previous response time rather than last two
arifBurakDemiray May 22, 2025
27d968f
feat: more proper way
arifBurakDemiray May 29, 2025
bb32c2b
feat: convert to single req
arifBurakDemiray May 29, 2025
917413a
feat: single request and 60 seconds backoff
arifBurakDemiray May 30, 2025
ce7337b
refactor: back off callback
arifBurakDemiray May 30, 2025
6222a33
fix: the param issue
arifBurakDemiray May 30, 2025
f40314b
fix: warning log to info log
arifBurakDemiray May 30, 2025
2bfe68b
feat: RC3
arifBurakDemiray May 30, 2025
481250e
fix: tests
arifBurakDemiray May 30, 2025
0644134
Merge branch 'staging' into back_of_mech
arifBurakDemiray May 30, 2025
e9a9c41
fix: after merge
arifBurakDemiray Jun 2, 2025
7db7e65
feat: BOM sc
arifBurakDemiray Jun 2, 2025
7f83b83
feat: BOM sc impl
arifBurakDemiray Jun 2, 2025
bacb47a
feat: conscutive bom
arifBurakDemiray Jun 2, 2025
51cce8d
feat: cbom hc
arifBurakDemiray Jun 2, 2025
bda062c
Merge branch 'staging' into back_of_mech
arifBurakDemiray Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
## XX.XX.XX
* The feedback widgets now have transparent backgrounds for a cleaner look.
* Extended the notification button URL handler to allow custom handling of URLs when notification buttons are clicked in the background.
* Improved request queue handling, added a backoff mechanism to the SDK to better handle cases where the server responds slowly, enabled by default.
* Added a config method to disable backoff mechanism "disableBackoffMechanism()"
* Added a config method to disable server config updates in the initialization "disableSDKBehaviorSettingsUpdates()".
* The feedback widgets now have transparent backgrounds and fullscreen for a cleaner look.
* Extended the notification button URL handler to allow custom handling of URLs when notification buttons are clicked in the background.

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

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ org.gradle.configureondemand=true
android.useAndroidX=true
android.enableJetifier=true
# RELEASE FIELD SECTION
VERSION_NAME=25.4.1-RC2
VERSION_NAME=25.4.1-RC3
GROUP=ly.count.android
POM_URL=https://github.com/Countly/countly-sdk-android
POM_SCM_URL=https://github.com/Countly/countly-sdk-android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ of this software and associated documentation files (the "Software"), to deal
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import static ly.count.android.sdk.UtilsNetworking.sha256Hash;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -105,6 +106,26 @@ public void setUp() {
@Override public boolean getRefreshContentZoneEnabled() {
return true;
}

@Override public boolean getBOMEnabled() {
return true;
}

@Override public int getBOMAcceptedTimeoutSeconds() {
return 10;
}

@Override public double getBOMRQPercentage() {
return 0.5;
}

@Override public int getBOMRequestAge() {
return 24;
}

@Override public int getBOMDuration() {
return 60;
}
};

Countly.sharedInstance().setLoggingEnabled(true);
Expand Down Expand Up @@ -136,7 +157,7 @@ public void setUp() {
}
};

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

Expand All @@ -145,7 +166,7 @@ public void testConstructorAndGetters() {
final String serverURL = "https://secureserver";
final CountlyStore mockStore = mock(CountlyStore.class);
final DeviceIdProvider mockDeviceId = mock(DeviceIdProvider.class);
final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock);
final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class));
assertEquals(serverURL, connectionProcessor1.getServerURL());
assertSame(mockStore, connectionProcessor1.getCountlyStore());
}
Expand Down Expand Up @@ -212,7 +233,7 @@ public void urlConnectionCustomHeaderValues() throws IOException {
customValues.put("5", "");
customValues.put("6", null);

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

assertEquals("bb", urlConnection.getRequestProperty("aa"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class TestUtils {
public final static String commonAppKey = "appkey";
public final static String commonDeviceId = "1234";
public final static String SDK_NAME = "java-native-android";
public final static String SDK_VERSION = "25.4.1-RC2";
public final static String SDK_VERSION = "25.4.1-RC3";
public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50;

public static class Activity2 extends Activity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ interface ConfigurationProvider {
boolean getLocationTrackingEnabled();

boolean getRefreshContentZoneEnabled();

// BACKOFF MECHANISM
boolean getBOMEnabled();

int getBOMAcceptedTimeoutSeconds();

double getBOMRQPercentage();

int getBOMRequestAge();

int getBOMDuration();
}
52 changes: 48 additions & 4 deletions sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
Expand All @@ -49,6 +50,7 @@ of this software and associated documentation files (the "Software"), to deal
*/
public class ConnectionProcessor implements Runnable {
private static final int CONNECT_TIMEOUT_IN_MILLISECONDS = 30_000;
// used in backoff mechanism to accept half of the CONNECT_TIMEOUT_IN_MILLISECONDS
private static final int READ_TIMEOUT_IN_MILLISECONDS = 30_000;

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

private final Map<String, String> requestHeaderCustomValues_;
private final Runnable backoffCallback_;

static String endPointOverrideTag = "&new_end_point=";

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

ConnectionProcessor(final String serverURL, final StorageProvider storageProvider, final DeviceIdProvider deviceIdProvider, final ConfigurationProvider configProvider,
final RequestInfoProvider requestInfoProvider, final SSLContext sslContext, final Map<String, String> requestHeaderCustomValues, ModuleLog logModule,
HealthTracker healthTracker) {
HealthTracker healthTracker, Runnable backoffCallback) {
serverURL_ = serverURL;
storageProvider_ = storageProvider;
deviceIdProvider_ = deviceIdProvider;
configProvider_ = configProvider;
sslContext_ = sslContext;
requestHeaderCustomValues_ = requestHeaderCustomValues;
requestInfoProvider_ = requestInfoProvider;
backoffCallback_ = backoffCallback;
L = logModule;
this.healthTracker = healthTracker;
}
Expand All @@ -96,7 +100,6 @@ private enum RequestResult {
if (customEndpoint != null) {
urlEndpoint = customEndpoint;
}

// determine whether or not request has a binary image file, if it has request will be sent as POST request
boolean hasPicturePath = requestData.contains(ModuleUserProfile.PICTURE_PATH_KEY);
boolean usingHttpPost = requestData.contains("&crash=") || requestData.length() >= 2048 || requestInfoProvider_.isHttpPostForced() || hasPicturePath;
Expand Down Expand Up @@ -229,7 +232,7 @@ private enum RequestResult {
break;
}
String value = conn.getHeaderField(headerIndex++);
approximateDateSize += key.getBytes("US-ASCII").length + value.getBytes("US-ASCII").length + 2L;
approximateDateSize += key.getBytes(StandardCharsets.US_ASCII).length + value.getBytes(StandardCharsets.US_ASCII).length + 2L;
}
} catch (Exception e) {
L.e("[Connection Processor] urlConnectionForServerRequest, exception while calculating header field size: " + e);
Expand Down Expand Up @@ -434,6 +437,7 @@ public void run() {
conn = urlConnectionForServerRequest(requestData, customEndpoint);
long setupServerRequestTime = UtilsTime.getNanoTime() - pccTsStartGetURLConnection;
L.d("[ConnectionProcessor] run, TIMING Setup server request took:[" + setupServerRequestTime / 1000000.0d + "] ms");

if (pcc != null) {
pcc.TrackCounterTimeNs("ConnectionProcessorRun_07_SetupServerRequest", setupServerRequestTime);
pccTsStartOnlyInternet = UtilsTime.getNanoTime();
Expand Down Expand Up @@ -472,7 +476,7 @@ public void run() {
}

final RequestResult rRes;

if (responseCode >= 200 && responseCode < 300) {

if (responseString.isEmpty()) {
Expand Down Expand Up @@ -525,6 +529,11 @@ public void run() {
// successfully submitted event data to Count.ly server, so remove
// this one from the stored events collection
storageProvider_.removeRequest(originalRequest);

if (configProvider_.getBOMEnabled() && backoff(setupServerRequestTime, storedRequestCount, requestData)) {
backoffCallback_.run();
break;
}
} else {
// will retry later
// warning was logged above, stop processing, let next tick take care of retrying
Expand Down Expand Up @@ -582,6 +591,41 @@ public void run() {
L.v("[ConnectionProcessor] run, TIMING Whole queue took:[" + wholeQueueTime / 1000000.0d + "] ms");
}

/**
* Backoff mechanism to prevent flooding the server with requests when server is not able to respond
* Needs 3 conditions to met:
* - Request has a timestamp younger than 12 hrs
* - The number of requests inside the queue is less than 10% of the max queue size
* - The response time from the server is greater than or equal to ACCEPTED_TIMEOUT_SECONDS
*
* @param responseTimeMillis response time in milliseconds
* @param storedRequestCount number of requests in the queue
* @param requestData request data
* @return true if the backoff mechanism is triggered
*/
private boolean backoff(long responseTimeMillis, int storedRequestCount, String requestData) {
long responseTimeSeconds = responseTimeMillis / 1_000_000_000L;
boolean result = false;

if (responseTimeSeconds >= configProvider_.getBOMAcceptedTimeoutSeconds()) {
// FLAG 1
if (storedRequestCount <= storageProvider_.getMaxRequestQueueSize() * configProvider_.getBOMRQPercentage()) {
// FLAG 2
if (!Utils.isRequestTooOld(requestData, configProvider_.getBOMRequestAge(), "[ConnectionProcessor] backoff", L)) {
// FLAG 3
result = true;
healthTracker.logBackoffRequest();
}
}
}

if (!result) {
healthTracker.logConsecutiveBackoffRequest();
}

return result;
}

String getServerURL() {
return serverURL_;
}
Expand Down
24 changes: 23 additions & 1 deletion sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.json.JSONException;
Expand All @@ -52,6 +53,9 @@ class ConnectionQueue implements RequestQueueProvider {
private Future<?> connectionProcessorFuture_;
private DeviceIdProvider deviceIdProvider_;
private SSLContext sslContext_;
private final ScheduledExecutorService backoffScheduler_ = Executors.newSingleThreadScheduledExecutor();
private final AtomicBoolean backoff_ = new AtomicBoolean(false);

BaseInfoProvider baseInfoProvider;
HealthTracker healthTracker;
public PerformanceCounterCollector pcc;
Expand Down Expand Up @@ -874,6 +878,10 @@ void ensureExecutor() {
public void tick() {
//todo enable later
//assert storageProvider != null;
if (backoff_.get()) {
L.i("[ConnectionQueue] tick, currently backed off, skipping tick");
return;
}

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

public ConnectionProcessor createConnectionProcessor() {
ConnectionProcessor cp = new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, configProvider, requestInfoProvider, sslContext_, requestHeaderCustomValues, L, healthTracker);

ConnectionProcessor cp = new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, configProvider, requestInfoProvider, sslContext_, requestHeaderCustomValues, L, healthTracker, new Runnable() {
@Override
public void run() {
L.d("[ConnectionQueue] createConnectionProcessor:run, backed off, countdown started for " + configProvider.getBOMDuration() + " seconds");
backoff_.set(true);
backoffScheduler_.schedule(new Runnable() {
@Override public void run() {
L.d("[ConnectionQueue] createConnectionProcessor:run, countdown finished, running tick in background thread");
backoff_.set(false);
tick();
}
}, configProvider.getBOMDuration(), TimeUnit.SECONDS);
}
});
cp.pcc = pcc;
return cp;
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/main/java/ly/count/android/sdk/Countly.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal
*/
public class Countly {

private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "25.4.1-RC2";
private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "25.4.1-RC3";
/**
* Used as request meta data on every request
*/
Expand Down
11 changes: 11 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public class CountlyConfig {
// Requests older than this value in hours would be dropped (0 means this feature is disabled)
int dropAgeHours = 0;
String sdkBehaviorSettings;
boolean backOffMechanismEnabled = true;
boolean sdkBehaviorSettingsRequestsDisabled = false;

/**
Expand Down Expand Up @@ -1013,6 +1014,16 @@ public synchronized CountlyConfig setSDKBehaviorSettings(String sdkBehaviorSetti
return this;
}

/**
* Disable the back off mechanism
*
* @return Returns the same config object for convenient linking
*/
public synchronized CountlyConfig disableBackoffMechanism() {
this.backOffMechanismEnabled = false;
return this;
}

/**
* Disable the SDK behavior settings update calls to the server
*
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/CountlyStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,8 @@ public void setHealthCheckCounterState(@NonNull String counterState) {
editor.apply();
}
}

public int getMaxRequestQueueSize() {
return maxRequestQueueSize;
}
}
Loading
Loading