Skip to content

Commit 3db0560

Browse files
authored
fix: change sync-init flow to support project auto update on cache download (#318)
1 parent c67fa20 commit 3db0560

File tree

5 files changed

+298
-7
lines changed

5 files changed

+298
-7
lines changed

android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
import com.optimizely.ab.config.DatafileProjectConfig;
4141
import com.optimizely.ab.config.ProjectConfig;
4242
import com.optimizely.ab.config.Variation;
43+
import com.optimizely.ab.config.parser.ConfigParseException;
4344
import com.optimizely.ab.event.EventHandler;
4445
import com.optimizely.ab.event.EventProcessor;
46+
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
4547

4648
import org.junit.Before;
4749
import org.junit.Test;
@@ -60,9 +62,12 @@
6062
import static junit.framework.Assert.assertTrue;
6163
import static junit.framework.Assert.fail;
6264
import static org.mockito.Matchers.any;
65+
import static org.mockito.Matchers.anyBoolean;
66+
import static org.mockito.Matchers.anyInt;
6367
import static org.mockito.Matchers.eq;
6468
import static org.mockito.Mockito.doAnswer;
6569
import static org.mockito.Mockito.mock;
70+
import static org.mockito.Mockito.spy;
6671
import static org.mockito.Mockito.verify;
6772
import static org.mockito.Mockito.when;
6873

@@ -78,6 +83,7 @@ public class OptimizelyManagerTest {
7883
private Logger logger;
7984
private OptimizelyManager optimizelyManager;
8085
private DefaultDatafileHandler defaultDatafileHandler;
86+
private String defaultDatafile;
8187

8288
private String minDatafile = "{\n" +
8389
"experiments: [ ],\n" +
@@ -106,8 +112,8 @@ public void setup() throws Exception {
106112
.withEventHandler(eventHandler)
107113
.withEventProcessor(eventProcessor)
108114
.build(InstrumentationRegistry.getTargetContext());
109-
String datafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile);
110-
ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
115+
defaultDatafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile);
116+
ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(defaultDatafile).build();
111117

112118
when(defaultDatafileHandler.getConfig()).thenReturn(config);
113119
}
@@ -213,7 +219,8 @@ public void getDatafile() {
213219
assertNotNull(datafile);
214220
assertNotNull(optimizelyManager.getDatafileHandler());
215221
}
216-
@Test
222+
223+
@Test
217224
public void initializeAsyncWithEnvironment() {
218225
Logger logger = mock(Logger.class);
219226
DatafileHandler datafileHandler = mock(DefaultDatafileHandler.class);
@@ -496,4 +503,225 @@ public void injectOptimizelyDoesNotDuplicateCallback() {
496503
verify(logger).info("Sending Optimizely instance to listener");
497504
verify(startListener).onStart(any(OptimizelyClient.class));
498505
}
506+
507+
// Init Sync Flows
508+
509+
@Test
510+
public void initializeSyncWithUpdateOnNewDatafileDisabled() {
511+
boolean downloadToCache = true;
512+
boolean updateConfigOnNewDatafiel = false;
513+
int pollingInterval = 0; // disable polling
514+
515+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
516+
Logger logger = mock(Logger.class);
517+
Context context = InstrumentationRegistry.getTargetContext();
518+
519+
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
520+
null, null, null, null);
521+
522+
doAnswer(
523+
new Answer<Object>() {
524+
public Object answer(InvocationOnMock invocation) {
525+
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
526+
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
527+
return null;
528+
}
529+
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));
530+
531+
OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);
532+
533+
try {
534+
executor.awaitTermination(1, TimeUnit.SECONDS);
535+
} catch (InterruptedException e) {
536+
//
537+
}
538+
539+
assertEquals(client.getOptimizelyConfig().getRevision(), "7");
540+
}
541+
542+
@Test
543+
public void initializeSyncWithUpdateOnNewDatafileEnabled() {
544+
boolean downloadToCache = true;
545+
boolean updateConfigOnNewDatafiel = true;
546+
int pollingInterval = 0; // disable polling
547+
548+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
549+
Logger logger = mock(Logger.class);
550+
Context context = InstrumentationRegistry.getTargetContext();
551+
552+
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
553+
null, null, null, null);
554+
555+
doAnswer(
556+
new Answer<Object>() {
557+
public Object answer(InvocationOnMock invocation) {
558+
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
559+
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
560+
return null;
561+
}
562+
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));
563+
564+
OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);
565+
566+
try {
567+
executor.awaitTermination(1, TimeUnit.SECONDS);
568+
} catch (InterruptedException e) {
569+
//
570+
}
571+
572+
assertEquals(client.getOptimizelyConfig().getRevision(), "241");
573+
}
574+
575+
@Test
576+
public void initializeSyncWithDownloadToCacheDisabled() {
577+
boolean downloadToCache = false;
578+
boolean updateConfigOnNewDatafiel = true;
579+
int pollingInterval = 0; // disable polling
580+
581+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
582+
Logger logger = mock(Logger.class);
583+
Context context = InstrumentationRegistry.getTargetContext();
584+
585+
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
586+
null, null, null, null);
587+
588+
doAnswer(
589+
new Answer<Object>() {
590+
public Object answer(InvocationOnMock invocation) {
591+
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
592+
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
593+
return null;
594+
}
595+
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));
596+
597+
OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);
598+
599+
try {
600+
executor.awaitTermination(1, TimeUnit.SECONDS);
601+
} catch (InterruptedException e) {
602+
//
603+
}
604+
605+
assertEquals(client.getOptimizelyConfig().getRevision(), "7");
606+
}
607+
608+
@Test
609+
public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingEnabled() {
610+
boolean downloadToCache = true;
611+
boolean updateConfigOnNewDatafiel = false;
612+
int pollingInterval = 30; // enable polling
613+
614+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
615+
Logger logger = mock(Logger.class);
616+
Context context = InstrumentationRegistry.getTargetContext();
617+
618+
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
619+
null, null, null, null);
620+
621+
doAnswer(
622+
new Answer<Object>() {
623+
public Object answer(InvocationOnMock invocation) {
624+
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
625+
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
626+
return null;
627+
}
628+
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));
629+
630+
OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);
631+
632+
try {
633+
executor.awaitTermination(1, TimeUnit.SECONDS);
634+
} catch (InterruptedException e) {
635+
//
636+
}
637+
638+
// when periodic polling enabled, project config always updated on cache datafile update (regardless of "updateConfigOnNewDatafile" setting)
639+
assertEquals(client.getOptimizelyConfig().getRevision(), "241");
640+
}
641+
642+
@Test
643+
public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingEnabled() {
644+
boolean downloadToCache = true;
645+
boolean updateConfigOnNewDatafiel = true;
646+
int pollingInterval = 30; // enable polling
647+
648+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
649+
Logger logger = mock(Logger.class);
650+
Context context = InstrumentationRegistry.getTargetContext();
651+
652+
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
653+
null, null, null, null);
654+
655+
doAnswer(
656+
new Answer<Object>() {
657+
public Object answer(InvocationOnMock invocation) {
658+
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
659+
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
660+
return null;
661+
}
662+
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));
663+
664+
OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);
665+
666+
try {
667+
executor.awaitTermination(1, TimeUnit.SECONDS);
668+
} catch (InterruptedException e) {
669+
//
670+
}
671+
672+
// when periodic polling enabled, project config always updated on cache datafile update (regardless of "updateConfigOnNewDatafile" setting)
673+
assertEquals(client.getOptimizelyConfig().getRevision(), "241");
674+
}
675+
676+
@Test
677+
public void initializeSyncWithResourceDatafileNoCache() {
678+
boolean downloadToCache = true;
679+
boolean updateConfigOnNewDatafiel = true;
680+
int pollingInterval = 30; // enable polling
681+
682+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
683+
Logger logger = mock(Logger.class);
684+
Context context = InstrumentationRegistry.getTargetContext();
685+
686+
OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
687+
null, null, null, null));
688+
689+
datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
690+
OptimizelyClient client = manager.initialize(context, R.raw.datafile, downloadToCache, updateConfigOnNewDatafiel);
691+
692+
verify(manager).initialize(eq(context), eq(defaultDatafile), eq(downloadToCache), eq(updateConfigOnNewDatafiel));
693+
}
694+
695+
@Test
696+
public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() {
697+
boolean downloadToCache = true;
698+
boolean updateConfigOnNewDatafiel = true;
699+
int pollingInterval = 30; // enable polling
700+
701+
DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
702+
Logger logger = mock(Logger.class);
703+
Context context = InstrumentationRegistry.getTargetContext();
704+
705+
OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
706+
null, null, null, null));
707+
708+
datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
709+
OptimizelyClient client = manager.initialize(context, R.raw.datafile);
710+
711+
verify(manager).initialize(eq(context), eq(defaultDatafile), eq(true), eq(false));
712+
}
713+
714+
715+
// Utils
716+
717+
void mockProjectConfig(DefaultDatafileHandler datafileHandler, String datafile) {
718+
ProjectConfig config = null;
719+
try {
720+
config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
721+
when(datafileHandler.getConfig()).thenReturn(config);
722+
} catch (ConfigParseException e) {
723+
e.printStackTrace();
724+
}
725+
}
726+
499727
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"4","rollouts":[],"anonymizeIP":true,"botFiltering":true,"projectId":"10431130345","variables":[],"featureFlags":[{"experimentIds":["10390977673"],"id":"4482920077","key":"feature_1","rolloutId":"","variables":[{"defaultValue":"42","id":"2687470095","key":"i_42","type":"integer"},{"defaultValue":"4.2","id":"2689280165","key":"d_4_2","type":"double"},{"defaultValue":"true","id":"2689660112","key":"b_true","type":"boolean"},{"defaultValue":"foo","id":"2696150066","key":"s_foo","type":"string"}]},{"experimentIds":["10420810910"],"id":"4482920078","key":"feature_2","rolloutId":"","variables":[]}],"experiments":[{"status":"Running","key":"exp_with_audience","layerId":"10420273888","trafficAllocation":[{"entityId":"10389729780","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"featureEnabled":true,"id":"10389729780","key":"a"},{"variables":[],"id":"10416523121","key":"b"}],"forcedVariations":{},"id":"10390977673"},{"status":"Running","key":"exp_no_audience","layerId":"10417730432","trafficAllocation":[{"entityId":"10418551353","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"id":"10418551353","key":"variation_with_traffic"},{"variables":[],"id":"10418510624","key":"variation_no_traffic"}],"forcedVariations":{},"id":"10420810910"}],"audiences":[{"id":"10413101795","conditions":"[\"and\", [\"or\", [\"or\", {\"type\": \"custom_attribute\", \"name\": \"testvar\", \"value\": \"testvalue\"}]]]","name":"testvalue_audience"}],"groups":[],"attributes":[{"id":"10401066170","key":"testvar"}],"accountId":"10367498574","events":[{"experimentIds":["10420810910"],"id":"10404198134","key":"event1"},{"experimentIds":["10420810910","10390977673"],"id":"10404198135","key":"event_multiple_running_exp_attached"}],"revision":"241"}

android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,24 @@ public OptimizelyClient initialize(@NonNull Context context, @NonNull String dat
186186
* @return an {@link OptimizelyClient} instance
187187
*/
188188
public OptimizelyClient initialize(@NonNull Context context, @Nullable String datafile, boolean downloadToCache) {
189+
return initialize(context, datafile, downloadToCache, false);
190+
}
191+
192+
/**
193+
* Initialize Optimizely Synchronously using the datafile passed in.
194+
* It should be noted that even though it initiates a download of the datafile to cache, this method does not use that cached datafile.
195+
* You can always test if a datafile exists in cache with {@link #isDatafileCached(Context)}.
196+
* <p>
197+
* Instantiates and returns an {@link OptimizelyClient} instance. It will also cache the instance
198+
* for future lookups via getClient
199+
*
200+
* @param context any {@link Context} instance
201+
* @param datafile the datafile used to initialize the OptimizelyClient.
202+
* @param downloadToCache to check if datafile should get updated in cache after initialization.
203+
* @param updateConfigOnNewDatafile When a new datafile is fetched from the server in the background thread, the SDK will be updated with the new datafile immediately if this value is set to true. When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
204+
* @return an {@link OptimizelyClient} instance
205+
*/
206+
public OptimizelyClient initialize(@NonNull Context context, @Nullable String datafile, boolean downloadToCache, boolean updateConfigOnNewDatafile) {
189207
if (!isAndroidVersionSupported()) {
190208
return optimizelyClient;
191209
}
@@ -208,8 +226,9 @@ public OptimizelyClient initialize(@NonNull Context context, @Nullable String da
208226
} catch (Error e) {
209227
logger.error("Unable to build OptimizelyClient instance", e);
210228
}
211-
if(downloadToCache){
212-
datafileHandler.downloadDatafile(context, datafileConfig, null);
229+
230+
if (downloadToCache) {
231+
datafileHandler.downloadDatafileToCache(context, datafileConfig, updateConfigOnNewDatafile);
213232
}
214233

215234
return optimizelyClient;
@@ -226,17 +245,19 @@ public OptimizelyClient initialize(@NonNull Context context, @Nullable String da
226245
*
227246
* @param context any {@link Context} instance
228247
* @param datafileRes the R id that the data file is located under.
248+
* @param downloadToCache to check if datafile should get updated in cache after initialization.
249+
* @param updateConfigOnNewDatafile When a new datafile is fetched from the server in the background thread, the SDK will be updated with the new datafile immediately if this value is set to true. When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
229250
* @return an {@link OptimizelyClient} instance
230251
*/
231252
@NonNull
232-
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes) {
253+
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes, boolean downloadToCache, boolean updateConfigOnNewDatafile) {
233254
try {
234255

235256
String datafile;
236257
Boolean datafileInCache = isDatafileCached(context);
237258
datafile = getDatafile(context, datafileRes);
238259

239-
optimizelyClient = initialize(context, datafile, true);
260+
optimizelyClient = initialize(context, datafile, downloadToCache, updateConfigOnNewDatafile);
240261
if (datafileInCache) {
241262
cleanupUserProfileCache(getUserProfileService());
242263
}
@@ -248,6 +269,24 @@ public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer dat
248269
return optimizelyClient;
249270
}
250271

272+
/**
273+
* Initialize Optimizely Synchronously by loading the resource, use it to initialize Optimizely,
274+
* and downloading the latest datafile from the CDN in the background to cache.
275+
* <p>
276+
* Instantiates and returns an {@link OptimizelyClient} instance using the datafile cached on disk
277+
* if not available then it will expect that raw data file should exist on given id.
278+
* and initialize using raw file. Will also cache the instance
279+
* for future lookups via getClient. The datafile should be stored in res/raw.
280+
*
281+
* @param context any {@link Context} instance
282+
* @param datafileRes the R id that the data file is located under.
283+
* @return an {@link OptimizelyClient} instance
284+
*/
285+
@NonNull
286+
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes) {
287+
return initialize(context, datafileRes, true, false);
288+
}
289+
251290
private void cleanupUserProfileCache(UserProfileService userProfileService) {
252291
final DefaultUserProfileService defaultUserProfileService;
253292
if (userProfileService instanceof DefaultUserProfileService) {

datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public interface DatafileHandler {
5252
*/
5353
void downloadDatafile(Context context, DatafileConfig datafileConfig, DatafileLoadedListener listener);
5454

55+
default void downloadDatafileToCache(final Context context, DatafileConfig datafileConfig, boolean updateConfigOnNewDatafile) {
56+
downloadDatafile(context, datafileConfig, null);
57+
}
58+
5559
/**
5660
* Start background updates to the project datafile .
5761
*

0 commit comments

Comments
 (0)