diff --git a/src/androidTest/java/tests/integration/init/InitializationTest.java b/src/androidTest/java/tests/integration/init/InitializationTest.java new file mode 100644 index 000000000..819a0cfce --- /dev/null +++ b/src/androidTest/java/tests/integration/init/InitializationTest.java @@ -0,0 +1,196 @@ +package tests.integration.init; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.buildFactory; +import static helper.IntegrationHelper.emptyAllSegments; +import static helper.IntegrationHelper.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.api.Key; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class InitializationTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch mRequestCountdownLatch; + private MockWebServer mWebServer; + + private AtomicBoolean mEventSent; + + @Before + public void setUp() { + setupServer(); + mRequestCountdownLatch = new CountDownLatch(1); + mEventSent = new AtomicBoolean(false); + } + + @Test + public void immediateClientRecreation() throws InterruptedException { + SplitFactory factory = getFactory(false); + SplitClient client = factory.client(); + client.track("some_event"); + + CountDownLatch latch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(latch)); + + // Wait for the client to be ready + boolean readyAwait = latch.await(5, TimeUnit.SECONDS); + + // Destroy it + client.destroy(); + + // Create a new client; it should be ready since it was created immediately + CountDownLatch secondReadyLatch = new CountDownLatch(1); + factory.client(new Key("new_key")).on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(secondReadyLatch)); + boolean awaitReady2 = secondReadyLatch.await(5, TimeUnit.SECONDS); + + // Wait for events to be posted + Thread.sleep(500); + + assertTrue(readyAwait); + assertTrue(awaitReady2); + assertFalse(mEventSent.get()); + } + + @Test + public void destroyOnFactoryCallsDestroyWithActiveClients() throws InterruptedException { + SplitFactory factory = getFactory(false); + SplitClient client = factory.client(); + client.track("some_event"); + + CountDownLatch latch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(latch)); + + // Wait for the client to be ready + boolean readyAwait = latch.await(5, TimeUnit.SECONDS); + + CountDownLatch secondReadyLatch = new CountDownLatch(1); + factory.client(new Key("new_key")).on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(secondReadyLatch)); + // Wait for second client to be ready + boolean awaitReady2 = secondReadyLatch.await(5, TimeUnit.SECONDS); + + // Destroy the factory + factory.destroy(); + // Wait for events to be posted + Thread.sleep(500); + + // Verify event was posted to indirectly verify that the factory was destroyed + boolean factoryWasDestroyed = mEventSent.get(); + + assertTrue(readyAwait); + assertTrue(awaitReady2); + assertTrue(factoryWasDestroyed); + } + + private SplitFactory getFactory(boolean ready) throws InterruptedException { + SplitFactory splitFactory = getSplitFactory(); + CountDownLatch latch = new CountDownLatch(1); + mRequestCountdownLatch.countDown(); + + splitFactory.client().on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(latch)); + if (ready) { + boolean await = latch.await(5, TimeUnit.SECONDS); + + if (!await) { + fail("Client was not ready"); + } + } + + return splitFactory; + } + + private SplitFactory getSplitFactory() { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url) + .eventsEndpoint(url) + .telemetryServiceEndpoint(url) + .build(); + SplitClientConfig config = new SplitClientConfig.Builder() + .trafficType("user") + .serviceEndpoints(endpoints) + .streamingEnabled(false) + .featuresRefreshRate(9999) + .segmentsRefreshRate(9999) + .impressionsRefreshRate(9999) + .logLevel(SplitLogLevel.VERBOSE) + .streamingEnabled(false) + .build(); + + return buildFactory(IntegrationHelper.dummyApiKey(), IntegrationHelper.dummyUserKey(), config, + mContext, null, DatabaseHelper.getTestDatabase(mContext)); + } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + mRequestCountdownLatch.await(); + Thread.sleep(200); + Logger.e("Path is: " + request.getPath()); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(emptyAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + String sinceFromUri = getSinceFromUri(request.getRequestUrl().uri()); + if (sinceFromUri.equals("-1")) { + return new MockResponse().setResponseCode(200).setBody(loadSplitChanges()); + } else { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptySplitChanges(1506703262916L, 1506703262916L)); + } + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.EVENTS)) { + mEventSent.set(true); + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.COUNT)) { + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.IMPRESSIONS)) { + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.UNIQUE_KEYS)) { + return new MockResponse().setResponseCode(200); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } + + private String loadSplitChanges() { + FileHelper fileHelper = new FileHelper(); + String change = fileHelper.loadFileContent(mContext, "split_changes_1.json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + return Json.toJson(parsedChange); + } +} diff --git a/src/main/java/io/split/android/client/SplitClientImpl.java b/src/main/java/io/split/android/client/SplitClientImpl.java index 10dbc756c..7b4c097f6 100644 --- a/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/src/main/java/io/split/android/client/SplitClientImpl.java @@ -77,6 +77,13 @@ public void destroy() { if (splitClientContainer.getAll().isEmpty()) { SplitFactory splitFactory = mSplitFactory.get(); if (splitFactory != null) { + if (splitFactory instanceof SplitFactoryImpl) { + try { + ((SplitFactoryImpl) splitFactory).checkClients(); + } catch (ClassCastException ignored) { + + } + } splitFactory.destroy(); } } diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index a0649aba1..be33b61c3 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -20,6 +20,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.events.EventsManagerCoordinator; @@ -485,6 +486,7 @@ static class Initializer implements Runnable { private final RolloutCacheManager mRolloutCacheManager; private final SplitTaskExecutionListener mListener; + private final ReentrantLock mInitLock; Initializer( String apiToken, @@ -497,23 +499,28 @@ static class Initializer implements Runnable { SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor, SplitStorageContainer storageContainer, SyncManager syncManager, - SplitLifecycleManager lifecycleManager) { + SplitLifecycleManager lifecycleManager, + ReentrantLock initLock) { this(new RolloutCacheManagerImpl(config, storageContainer, splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000), splitTaskFactory.createEncryptionMigrationTask(apiToken, splitDatabase, config.encryptionEnabled(), splitCipher)), - new Listener(eventsManagerCoordinator, splitTaskExecutor, splitSingleThreadTaskExecutor, syncManager, lifecycleManager)); + new Listener(eventsManagerCoordinator, splitTaskExecutor, splitSingleThreadTaskExecutor, syncManager, lifecycleManager, initLock), + initLock); } @VisibleForTesting - Initializer(RolloutCacheManager rolloutCacheManager, SplitTaskExecutionListener listener) { + Initializer(RolloutCacheManager rolloutCacheManager, SplitTaskExecutionListener listener, ReentrantLock initLock) { mRolloutCacheManager = rolloutCacheManager; mListener = listener; + mInitLock = initLock; } @Override public void run() { + Logger.v("Running SDK initializer"); + mInitLock.lock(); mRolloutCacheManager.validateCache(mListener); } @@ -524,30 +531,38 @@ static class Listener implements SplitTaskExecutionListener { private final SplitSingleThreadTaskExecutor mSplitSingleThreadTaskExecutor; private final SyncManager mSyncManager; private final SplitLifecycleManager mLifecycleManager; + private final ReentrantLock mInitLock; Listener(EventsManagerCoordinator eventsManagerCoordinator, SplitTaskExecutor splitTaskExecutor, SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor, SyncManager syncManager, - SplitLifecycleManager lifecycleManager) { + SplitLifecycleManager lifecycleManager, + ReentrantLock initLock) { mEventsManagerCoordinator = eventsManagerCoordinator; mSplitTaskExecutor = splitTaskExecutor; mSplitSingleThreadTaskExecutor = splitSingleThreadTaskExecutor; mSyncManager = syncManager; mLifecycleManager = lifecycleManager; + mInitLock = initLock; } @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { - mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - - mSplitTaskExecutor.resume(); - mSplitSingleThreadTaskExecutor.resume(); - - mSyncManager.start(); - mLifecycleManager.register(mSyncManager); - - Logger.i("Android SDK initialized!"); + try { + mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); + + mSplitTaskExecutor.resume(); + mSplitSingleThreadTaskExecutor.resume(); + + mSyncManager.start(); + mLifecycleManager.register(mSyncManager); + Logger.i("Android SDK initialized!"); + } catch (Exception e) { + Logger.e("Error initializing Android SDK", e); + } finally { + mInitLock.unlock(); + } } } } diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index 32ea3963c..ea65577f1 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -11,8 +11,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; import io.split.android.android_client.BuildConfig; import io.split.android.client.api.Key; @@ -74,6 +79,7 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitManager mManager; private final Runnable mDestroyer; private boolean mIsTerminated = false; + private final AtomicBoolean mCheckClients = new AtomicBoolean(false); private final String mApiKey; private final FactoryMonitor mFactoryMonitor = FactoryMonitorImpl.getSharedInstance(); @@ -83,6 +89,7 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitStorageContainer mStorageContainer; private final SplitClientContainer mClientContainer; private final UserConsentManager mUserConsentManager; + private final ReentrantLock mInitLock = new ReentrantLock(); public SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull SplitClientConfig config, @NonNull Context context) throws URISyntaxException { @@ -273,8 +280,13 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp eventsTracker, flagSetsFilter); mDestroyer = new Runnable() { public void run() { - Logger.w("Shutdown called for split"); + mInitLock.lock(); try { + if (mCheckClients.get() && !mClientContainer.getAll().isEmpty()) { + Logger.d("Avoiding shutdown due to active clients"); + return; + } + Logger.w("Shutdown called for split"); mStorageContainer.getTelemetryStorage().recordSessionLength(System.currentTimeMillis() - initializationStartTime); telemetrySynchronizer.flush(); telemetrySynchronizer.destroy(); @@ -285,6 +297,7 @@ public void run() { mSyncManager.stop(); Logger.d("Flushing impressions and events"); mLifecycleManager.destroy(); + mClientContainer.destroy(); Logger.d("Successful shutdown of lifecycle manager"); mFactoryMonitor.remove(mApiKey); Logger.d("Successful shutdown of segment fetchers"); @@ -299,10 +312,13 @@ public void run() { Logger.d("Successful shutdown of task executor"); mStorageContainer.getAttributesStorageContainer().destroy(); Logger.d("Successful shutdown of attributes storage"); + mIsTerminated = true; + Logger.d("SplitFactory has been destroyed"); } catch (Exception e) { Logger.e(e, "We could not shutdown split"); } finally { - mIsTerminated = true; + mCheckClients.set(false); + mInitLock.unlock(); } } }; @@ -325,7 +341,8 @@ public void run() { splitSingleThreadTaskExecutor, mStorageContainer, mSyncManager, - mLifecycleManager); + mLifecycleManager, + mInitLock); if (config.shouldRecordTelemetry()) { int activeFactoriesCount = mFactoryMonitor.count(mApiKey); @@ -382,7 +399,22 @@ public SplitManager manager() { public void destroy() { synchronized (SplitFactoryImpl.class) { if (!mIsTerminated) { - new Thread(mDestroyer).start(); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.schedule(mDestroyer, 100, TimeUnit.MILLISECONDS); + executor.schedule(new Runnable() { + @Override + public void run() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + }, 500, TimeUnit.MILLISECONDS); } } } @@ -407,6 +439,10 @@ public UserConsent getUserConsent() { return mUserConsentManager.getStatus(); } + void checkClients() { + mCheckClients.set(true); + } + private void setupValidations(SplitClientConfig splitClientConfig) { ValidationConfig.getInstance().setMaximumKeyLength(splitClientConfig.maximumKeyLength()); diff --git a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java index b0da1451f..2fd5cbded 100644 --- a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java @@ -83,4 +83,9 @@ protected void createNewClient(Key key) { mEventsManagerCoordinator.registerEventsManager(key, eventsManager); } + + @Override + public void destroy() { + // No-op + } } diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java b/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java index 5fb649a39..786e87835 100644 --- a/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java +++ b/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java @@ -38,17 +38,20 @@ public void stop() { } private void waitForNotifications() { - mExecutorService.execute(new Runnable() { - @Override - public void run() { - try { - while (true) { - onWaitForNotificationLoop(); + if (!mExecutorService.isShutdown()) { + mExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + while (true) { + onWaitForNotificationLoop(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - } catch (InterruptedException e) { } - } - }); + }); + } } protected abstract void onWaitForNotificationLoop() throws InterruptedException; diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainer.java b/src/main/java/io/split/android/client/shared/SplitClientContainer.java index d64916d11..a5779b124 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainer.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainer.java @@ -12,4 +12,6 @@ public interface SplitClientContainer { void remove(Key key); Set getAll(); + + void destroy(); } diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index a49788bf5..43b91a593 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -56,6 +56,8 @@ public final class SplitClientContainerImpl extends BaseSplitClientContainer { private final SplitTaskExecutionListener mSchedulingBackgroundSyncExecutionListener; private final MySegmentsWorkManagerWrapper mWorkManagerWrapper; private final SplitTaskExecutor mSplitClientEventTaskExecutor; + private String mStreamingTaskId = null; + private String mBackgroundSyncTaskId = null; public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @NonNull SplitFactoryImpl splitFactory, @@ -159,6 +161,17 @@ public void createNewClient(Key key) { } } + @Override + public void destroy() { + if (mStreamingTaskId != null) { + mSplitTaskExecutor.stopTask(mStreamingTaskId); + } + + if (mBackgroundSyncTaskId != null) { + mSplitTaskExecutor.stopTask(mBackgroundSyncTaskId); + } + } + @NonNull private MySegmentsTaskFactory getMySegmentsTaskFactory(Key key, SplitEventsManager eventsManager) { return mMySegmentsTaskFactoryProvider.getFactory( @@ -174,7 +187,7 @@ private void connectToStreaming() { return; } if (!mConnecting.getAndSet(true)) { - mSplitTaskExecutor.schedule(new PushNotificationManagerDeferredStartTask(mPushNotificationManager), + mStreamingTaskId = mSplitTaskExecutor.schedule(new PushNotificationManagerDeferredStartTask(mPushNotificationManager), ServiceConstants.MIN_INITIAL_DELAY, mStreamingConnectionExecutionListener); } @@ -185,7 +198,7 @@ private void scheduleMySegmentsWork() { return; } if (!mSchedulingBackgroundSync.getAndSet(true)) { - mSplitTaskExecutor.schedule(new MySegmentsBackgroundSyncScheduleTask(mWorkManagerWrapper, getKeySet()), + mBackgroundSyncTaskId = mSplitTaskExecutor.schedule(new MySegmentsBackgroundSyncScheduleTask(mWorkManagerWrapper, getKeySet()), ServiceConstants.MIN_INITIAL_DELAY, mSchedulingBackgroundSyncExecutionListener); } diff --git a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt index 7ff5e004b..335173c5d 100644 --- a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt +++ b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt @@ -25,6 +25,7 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.io.File import java.lang.IllegalArgumentException +import java.util.concurrent.locks.ReentrantLock class SplitFactoryHelperTest { @@ -141,15 +142,18 @@ class SplitFactoryHelperTest { fun `Initializer test`() { val rolloutCacheManager = mock(RolloutCacheManager::class.java) val splitTaskExecutionListener = mock(SplitTaskExecutionListener::class.java) + val initLock = mock(ReentrantLock::class.java) val initializer = SplitFactoryHelper.Initializer( rolloutCacheManager, - splitTaskExecutionListener + splitTaskExecutionListener, + initLock ) initializer.run() verify(rolloutCacheManager).validateCache(splitTaskExecutionListener) + verify(initLock).lock() } @Test @@ -159,13 +163,15 @@ class SplitFactoryHelperTest { val singleThreadTaskExecutor = mock(SplitSingleThreadTaskExecutor::class.java) val syncManager = mock(SyncManager::class.java) val lifecycleManager = mock(SplitLifecycleManager::class.java) + val initLock = mock(ReentrantLock::class.java) val listener = Listener( eventsManagerCoordinator, taskExecutor, singleThreadTaskExecutor, syncManager, - lifecycleManager + lifecycleManager, + initLock ) listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)) @@ -175,5 +181,6 @@ class SplitFactoryHelperTest { verify(singleThreadTaskExecutor).resume() verify(syncManager).start() verify(lifecycleManager).register(syncManager) + verify(initLock).unlock() } } diff --git a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java index e33e50b1f..20801d90b 100644 --- a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java +++ b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -287,6 +288,24 @@ public void differentBucketingKeyDeliversNewClient() { assertNotEquals(client, client2); } + @Test + public void destroyCancelsAllScheduledTasks() { + when(mSplitTaskExecutor.schedule(any(), anyLong(), any())) + .thenReturn("taskId_1") + .thenReturn("taskId_2"); + + Key key = new Key("matching_key"); + SplitClient clientMock = mock(SplitClient.class); + when(mSplitClientFactory.getClient(eq(key), any(), any(), anyBoolean())).thenReturn(clientMock); + when(mConfig.synchronizeInBackground()).thenReturn(true); + + mClientContainer.getClient(key); + mClientContainer.destroy(); + + verify(mSplitTaskExecutor).stopTask("taskId_1"); + verify(mSplitTaskExecutor).stopTask("taskId_2"); + } + @NonNull private SplitClientContainerImpl getSplitClientContainer(String mDefaultMatchingKey, boolean streamingEnabled) {