Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Added a new config option `setWebviewDisplayOption(WebViewDisplayOption)` to control how Content and Feedback Widgets are displayed.
* `IMMERSIVE` mode (default): Full-screen display (except cutouts).
* `SAFE_AREA` mode: Omits status bar, navigation bar and cutouts when displaying webviews.
* Added a new init config option `disableGradualRequestCleaner()` to change request queue overflow behavior. When enabled, all overflowing requests (plus one slot) are removed at once instead of being cleaned gradually in limited batches.

* Immediate requests now will be run by parallel executor instead of serial by default.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ly.count.android.sdk;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;

/**
* Tests for the request queue cleaning behavior with and without the gradual cleaner disabled.
*/
@RunWith(AndroidJUnit4.class)
public class CountlyStoreRequestCleanerTests {
CountlyStore store;

@Before
public void setUp() {
store = new CountlyStore(TestUtils.getContext(), mock(ModuleLog.class));
store.clear();
}

private void fillRequests(int count) {
for (int i = 0; i < count; i++) {
store.addRequest("req_" + i, false);
}
}

@Test
public void testDefaultGradualCleanerRemovesUpToLoopLimit() {
store.maxRequestQueueSize = 1000;
fillRequests(200);
assertEquals(200, store.getRequests().length);

// Reduce max to force clean on next add
store.maxRequestQueueSize = 50; // new limit
store.addRequest("req_new_default", false);

// With gradual cleaner: removed Math.min(100, overflow(150)) + 1 = 101 before adding new
// Remaining after removal: 99, after adding new: 100
String[] requests = store.getRequests();
assertEquals(100, requests.length);
// First remaining element should be the original index 101 (req_101)
assertTrue("Expected first retained request to be req_101 but was " + requests[0], requests[0].equals("req_101"));
// Last element should be the newly added one
assertEquals("req_new_default", requests[requests.length - 1]);
}

@Test
public void testDisableGradualRequestCleanerRemovesAllOverflow() {
store.maxRequestQueueSize = 1000;
fillRequests(200);
assertEquals(200, store.getRequests().length);

// Enable new mode
store.setDisableGradualRequestCleaner(true);

// Reduce max to force clean on next add
store.maxRequestQueueSize = 50; // new limit
store.addRequest("req_new_disabled", false);

// With disabled gradual cleaner: removed overflow (150) + 1 = 151 before add
// Remaining after removal: 49, after adding new: 50
String[] requests = store.getRequests();
assertEquals(50, requests.length);
// First remaining element should be original index 151 (req_151)
assertTrue("Expected first retained request to be req_151 but was " + requests[0], requests[0].equals("req_151"));
// Last element should be the newly added one
assertEquals("req_new_disabled", requests[requests.length - 1]);
}
}
5 changes: 5 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/Countly.java
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ public synchronized Countly init(CountlyConfig config) {
L.d("[Init] request queue size set to [" + config.maxRequestQueueSize + "]");
countlyStore.setLimits(config.maxRequestQueueSize);

if (config.disableGradualRequestCleaner) {
L.d("[Init] Disabling gradual request queue cleaning. Overflow will be removed in one pass.");
countlyStore.setDisableGradualRequestCleaner(true);
}

if (config.storageProvider == null) {
// outside of tests this should be null
config.storageProvider = config.countlyStore;
Expand Down
17 changes: 17 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 @@ -210,6 +210,9 @@ public class CountlyConfig {
boolean useSerialExecutor = false;
WebViewDisplayOption webViewDisplayOption = WebViewDisplayOption.IMMERSIVE;

// If set to true, request queue cleaner will remove all overflow at once instead of gradually (loop limited) removing
boolean disableGradualRequestCleaner = false;

/**
* THIS VARIABLE SHOULD NOT BE USED
* IT IS ONLY FOR INTERNAL TESTING
Expand Down Expand Up @@ -1083,6 +1086,20 @@ public synchronized CountlyConfig setUseSerialExecutor(boolean useSerial) {
return this;
}

/**
* Disable the gradual request cleaner. By default when the request queue exceeds the configured
* maximum size, only a limited number of the oldest requests are removed per cleanup cycle
* (capped by an internal loop limit of 100) to gradually shrink the queue. Calling this method changes
* the behavior so that whenever the queue exceeds the maximum size, all overflowing requests
* (plus one extra slot for the new request) are removed in a single operation.
*
* @return Returns the same config object for convenient linking
*/
public synchronized CountlyConfig disableGradualRequestCleaner() {
this.disableGradualRequestCleaner = true;
return this;
}

/**
* APM configuration interface to be used with CountlyConfig
*/
Expand Down
19 changes: 17 additions & 2 deletions sdk/src/main/java/ly/count/android/sdk/CountlyStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public class CountlyStore implements StorageProvider, EventQueueProvider {
int dropAgeHours = 0;
private final static int requestRemovalLoopLimit = 100;

// If true, when cleaning request queue overflow remove all overflowing requests at once (plus one slot)
// instead of gradually removing up to 'requestRemovalLoopLimit'
boolean disableGradualRequestCleaner = false;

//explicit storage fields
boolean explicitStorageModeEnabled;
boolean esDirtyFlag = false;
Expand Down Expand Up @@ -497,15 +501,26 @@ synchronized void deleteOldestRequests(List<String> requests) {
return;
}

int requestsToRemove = Math.min(requestRemovalLoopLimit, requests.size() - maxRequestQueueSize) + 1; // +1 because it should open a new place for newcomer
L.i("[CountlyStore] deleteOldestRequests, Will remove the oldest " + requestsToRemove + " request");
int overflow = requests.size() - maxRequestQueueSize;
int requestsToRemove;
if (disableGradualRequestCleaner) {
requestsToRemove = overflow + 1;
L.i("[CountlyStore] deleteOldestRequests, Gradual cleaner disabled. Removing all overflow: " + requestsToRemove + " request");
} else {
requestsToRemove = Math.min(requestRemovalLoopLimit, overflow) + 1; // +1 because it should open a new place for newcomer
L.i("[CountlyStore] deleteOldestRequests, Will remove the oldest " + requestsToRemove + " request");
}
requests.subList(0, requestsToRemove).clear(); // sublist reflects all changes to the main list

if (pcc != null) {
pcc.TrackCounterTimeNs("CountlyStore_deleteOldestRequest", UtilsTime.getNanoTime() - tsStart);
}
}

void setDisableGradualRequestCleaner(boolean disable) {
disableGradualRequestCleaner = disable;
}

synchronized void deleteOldestRequest_reworked() {
long tsStart = 0L;
if (pcc != null) {
Expand Down
Loading