Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

### Fixed
- Push token registration and disable operations, including their API requests and retries, now run on a dedicated SDK-owned serial executor, while Iterable deep-link redirects use a separate SDK-owned serial executor. This removes their dependency on Android's process-wide `AsyncTask` queue, preserves ordering within each operation type, prevents slow redirects from delaying push work, and keeps client callbacks and attribution updates on the main thread.
- Push token registration, disable operations, Iterable deep-link redirects, and API requests now use dedicated SDK-owned executors instead of Android's process-wide `AsyncTask` queues. Push, deep-link, and offline operations preserve ordering in isolated serial lanes, while ordinary online API requests retain concurrent execution. Slow work in one lane no longer delays unrelated SDK operations, and client callbacks and attribution updates continue on the main thread.

## [3.11.0]
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void onSuccess(@NonNull JSONObject data) {
signal.countDown();
}
}, null);
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onSuccess is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -103,7 +103,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -122,7 +122,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -141,7 +141,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS));
Expand All @@ -162,7 +162,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -181,7 +181,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -200,7 +200,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
Expand All @@ -222,7 +222,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
"}");
stubAnyRequestReturningStatusCode(200, responseData);

new IterableRequestTask().execute(new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, new IterableHelper.SuccessHandler() {
dispatchRequest(new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, new IterableHelper.SuccessHandler() {
@Override
public void onSuccess(@NonNull JSONObject successData) {
try {
Expand All @@ -246,7 +246,7 @@ public void onSuccess(@NonNull JSONObject successData) {
}
});

new IterableRequestTask().execute(request);
dispatchRequest(request);
server.takeRequest(5, TimeUnit.SECONDS);

// Await for the background tasks to complete
Expand All @@ -260,8 +260,7 @@ public void testMaxRetriesOnMultipleInvalidJwtPayloads() throws Exception {
}

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null);
IterableRequestTask task = new IterableRequestTask();
task.execute(request);
dispatchRequest(request);

RecordedRequest request1 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS);
Expand All @@ -279,8 +278,7 @@ public void testResponseCode500() throws Exception {
}

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null);
IterableRequestTask task = new IterableRequestTask();
task.execute(request);
dispatchRequest(request);

RecordedRequest request1 = server.takeRequest(1, TimeUnit.SECONDS);
RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS);
Expand All @@ -300,7 +298,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(1, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS));
Expand All @@ -319,9 +317,13 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
dispatchRequest(request);

server.takeRequest(1, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
}

private void dispatchRequest(IterableApiRequest request) {
IterableRequestDispatcher.online().execute(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,6 @@ public static boolean isSDKInitialized() {
return initializationRun && backgroundInitComplete && sdkConfigured;
}


/**
* Register a callback to be notified when SDK initialization completes.
* If the SDK is already initialized, the callback is invoked immediately.
Expand Down Expand Up @@ -992,7 +991,10 @@ static void initializeForPush(@Nullable Context context) {
this.embeddedManager = embeddedManager;
this.pushRegistration = Objects.requireNonNull(pushRegistration);
}

void setRequestDispatcher(IterableRequestDispatcher requestDispatcher) {
apiClient = new IterableApiClient(new IterableApiAuthProvider(),
IterableRequestDispatchers.same(Objects.requireNonNull(requestDispatcher)));
}
//endregion

//region SDK public functions
Expand Down Expand Up @@ -1567,7 +1569,6 @@ public void trackPurchase(double total, @NonNull List<CommerceItem> items, @Null
queueOrExecute(() -> trackPurchase(total, items, dataFields, null), "trackPurchase(" + total + ", " + items.size() + " items, dataFields)");
}


/**
* Tracks a purchase.
* @param total total purchase amount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
class IterableApiClient {
private static final String TAG = "IterableApiClient";
private final @NonNull AuthProvider authProvider;
private final IterablePushRegistrationRequestProcessor pushRegistrationRequestProcessor;
private final @NonNull IterableRequestDispatchers requestDispatchers;
private final OnlineRequestProcessor pushRegistrationRequestProcessor;
// A newer push action invalidates retries from earlier registration or disable requests.
private final AtomicLong pushRegistrationRequestGeneration = new AtomicLong();
private RequestProcessor requestProcessor;
Expand All @@ -45,14 +46,22 @@ interface AuthProvider {
}

IterableApiClient(@NonNull AuthProvider authProvider) {
this(authProvider, IterableRequestDispatchers.sdk());
}

IterableApiClient(
@NonNull AuthProvider authProvider,
@NonNull IterableRequestDispatchers requestDispatchers
) {
this.authProvider = authProvider;
this.requestDispatchers = requestDispatchers;
pushRegistrationRequestProcessor =
new IterablePushRegistrationRequestProcessor();
new OnlineRequestProcessor(requestDispatchers.push());
}

private RequestProcessor getRequestProcessor() {
if (requestProcessor == null) {
requestProcessor = new OnlineRequestProcessor();
requestProcessor = new OnlineRequestProcessor(requestDispatchers.online());
}
return requestProcessor;
}
Expand All @@ -70,8 +79,12 @@ void setOfflineProcessingEnabled(boolean offlineMode) {
}

this.requestProcessor = offlineMode
? new OfflineRequestProcessor(authProvider.getContext())
: new OnlineRequestProcessor();
? new OfflineRequestProcessor(
authProvider.getContext(),
requestDispatchers.online(),
requestDispatchers.offline()
)
: new OnlineRequestProcessor(requestDispatchers.online());
}

void getRemoteConfiguration(IterableHelper.IterableActionHandler actionHandler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@

import android.os.Handler;
import android.os.Looper;
import android.os.Process;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

final class IterableExecutors {
private static final Executor PUSH_EXECUTOR = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "IterablePushExecutor");
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
});
// HttpURLConnection is blocking I/O, so ordinary API work uses a bounded
// multi-thread pool rather than the serial executors used for ordered work.
static final int REQUEST_THREAD_COUNT = 8;
// Matches the historical AsyncTask queue bound without inheriting its
// platform-version-dependent thread-pool behavior.
static final int REQUEST_QUEUE_CAPACITY = 128;
private static final long REQUEST_THREAD_KEEP_ALIVE_SECONDS = 30;
private static final AtomicInteger REQUEST_THREAD_ID = new AtomicInteger();
private static final Executor PUSH_EXECUTOR =
newSingleThreadExecutor("IterablePushExecutor");
private static final Executor DEEP_LINK_EXECUTOR =
Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "IterableDeepLinkExecutor");
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
});
newSingleThreadExecutor("IterableDeepLinkExecutor");
private static final Executor OFFLINE_EXECUTOR =
newSingleThreadExecutor("IterableOfflineExecutor");
private static final Executor REQUEST_EXECUTOR =
newRequestExecutor(REQUEST_THREAD_COUNT, REQUEST_QUEUE_CAPACITY);
private static final Executor MAIN_EXECUTOR =
runnable -> new Handler(Looper.getMainLooper()).post(runnable);

Expand All @@ -34,7 +42,47 @@ static Executor deepLink() {
return DEEP_LINK_EXECUTOR;
}

static Executor request() {
return REQUEST_EXECUTOR;
}

static Executor offline() {
return OFFLINE_EXECUTOR;
}

static Executor main() {
return MAIN_EXECUTOR;
}

private static Executor newSingleThreadExecutor(String threadName) {
return Executors.newSingleThreadExecutor(
runnable -> newThread(runnable, threadName)
);
}

static ThreadPoolExecutor newRequestExecutor(int threadCount, int queueCapacity) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
threadCount,
threadCount,
REQUEST_THREAD_KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueCapacity),
runnable -> newThread(
runnable,
"IterableRequestExecutor-" + REQUEST_THREAD_ID.incrementAndGet()
),
new ThreadPoolExecutor.AbortPolicy()
);
executor.allowCoreThreadTimeOut(true);
return executor;
}

private static Thread newThread(Runnable runnable, String threadName) {
Thread thread = new Thread(() -> {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
runnable.run();
}, threadName);
thread.setDaemon(true);
return thread;
}
}
Loading
Loading