diff --git a/.github/workflows/instrumented.yml b/.github/workflows/instrumented.yml index 04e7d0236..ad07f29d9 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -4,10 +4,12 @@ on: pull_request: branches: - 'development' + - 'master' + - '*_baseline' jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false diff --git a/CHANGES.txt b/CHANGES.txt index 5df09b929..90d2171e6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +5.1.1 (Feb 11, 2025) +- Fixed issue when calling destroy() before the SDK has initialized. + 5.1.0 (Jan 17, 2025) - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs. diff --git a/build.gradle b/build.gradle index 37af751b0..7eb29e1da 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.1.0' + splitVersion = '5.1.1' } android { 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) {