Skip to content

Commit a17becd

Browse files
update: implement asynchronous decision-making methods in Optimizely and OptimizelyUserContext with corresponding tests
1 parent 9c8dd8f commit a17becd

File tree

4 files changed

+158
-246
lines changed

4 files changed

+158
-246
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,7 @@
6464
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
6565
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
6666
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
67-
import com.optimizely.ab.optimizelydecision.DecisionMessage;
68-
import com.optimizely.ab.optimizelydecision.DecisionReasons;
69-
import com.optimizely.ab.optimizelydecision.DecisionResponse;
70-
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
71-
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
72-
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
67+
import com.optimizely.ab.optimizelydecision.*;
7368
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
7469

7570
import org.slf4j.Logger;
@@ -1525,8 +1520,8 @@ Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
15251520
* @return A decision result using traditional A/B testing logic only.
15261521
*/
15271522
OptimizelyDecision decideSync(@Nonnull OptimizelyUserContext user,
1528-
@Nonnull String key,
1529-
@Nonnull List<OptimizelyDecideOption> options) {
1523+
@Nonnull String key,
1524+
@Nonnull List<OptimizelyDecideOption> options) {
15301525
ProjectConfig projectConfig = getProjectConfig();
15311526
if (projectConfig == null) {
15321527
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
@@ -1541,28 +1536,28 @@ OptimizelyDecision decideSync(@Nonnull OptimizelyUserContext user,
15411536
/**
15421537
* Returns decision results for multiple flag keys, skipping CMAB logic and using only traditional A/B testing.
15431538
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
1544-
*
1539+
*
15451540
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
15461541
* @param keys A list of flag keys for which decisions will be made.
15471542
* @param options A list of options for decision-making.
15481543
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
15491544
*/
15501545
Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull OptimizelyUserContext user,
1551-
@Nonnull List<String> keys,
1552-
@Nonnull List<OptimizelyDecideOption> options) {
1546+
@Nonnull List<String> keys,
1547+
@Nonnull List<OptimizelyDecideOption> options) {
15531548
return decideForKeysSync(user, keys, options, false);
15541549
}
15551550

15561551
/**
15571552
* Returns decision results for all active flag keys, skipping CMAB logic and using only traditional A/B testing.
15581553
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
1559-
*
1554+
*
15601555
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
15611556
* @param options A list of options for decision-making.
15621557
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
15631558
*/
15641559
Map<String, OptimizelyDecision> decideAllSync(@Nonnull OptimizelyUserContext user,
1565-
@Nonnull List<OptimizelyDecideOption> options) {
1560+
@Nonnull List<OptimizelyDecideOption> options) {
15661561
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
15671562

15681563
ProjectConfig projectConfig = getProjectConfig();
@@ -1579,12 +1574,59 @@ Map<String, OptimizelyDecision> decideAllSync(@Nonnull OptimizelyUserContext use
15791574
}
15801575

15811576
private Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull OptimizelyUserContext user,
1582-
@Nonnull List<String> keys,
1583-
@Nonnull List<OptimizelyDecideOption> options,
1584-
boolean ignoreDefaultOptions) {
1577+
@Nonnull List<String> keys,
1578+
@Nonnull List<OptimizelyDecideOption> options,
1579+
boolean ignoreDefaultOptions) {
15851580
return decideForKeys(user, keys, options, ignoreDefaultOptions, DecisionPath.WITHOUT_CMAB);
15861581
}
15871582

1583+
//============ decide async ============//
1584+
1585+
/**
1586+
* Returns a decision result asynchronously for a given flag key and a user context.
1587+
*
1588+
* @param userContext The user context to make decisions for
1589+
* @param key A flag key for which a decision will be made
1590+
* @param callback A callback to invoke when the decision is available
1591+
* @param options A list of options for decision-making
1592+
*/
1593+
public void decideAsync(@Nonnull OptimizelyUserContext userContext,
1594+
@Nonnull String key,
1595+
@Nonnull OptimizelyDecisionCallback callback,
1596+
@Nonnull List<OptimizelyDecideOption> options) {
1597+
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, key, options, callback);
1598+
fetcher.start();
1599+
}
1600+
1601+
/**
1602+
* Returns decision results asynchronously for multiple flag keys.
1603+
*
1604+
* @param userContext The user context to make decisions for
1605+
* @param keys A list of flag keys for which decisions will be made
1606+
* @param callback A callback to invoke when decisions are available
1607+
* @param options A list of options for decision-making
1608+
*/
1609+
public void decideForKeysAsync(@Nonnull OptimizelyUserContext userContext,
1610+
@Nonnull List<String> keys,
1611+
@Nonnull OptimizelyDecisionsCallback callback,
1612+
@Nonnull List<OptimizelyDecideOption> options) {
1613+
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, keys, options, callback);
1614+
fetcher.start();
1615+
}
1616+
1617+
/**
1618+
* Returns decision results asynchronously for all active flag keys.
1619+
*
1620+
* @param userContext The user context to make decisions for
1621+
* @param callback A callback to invoke when decisions are available
1622+
* @param options A list of options for decision-making
1623+
*/
1624+
public void decideAllAsync(@Nonnull OptimizelyUserContext userContext,
1625+
@Nonnull OptimizelyDecisionsCallback callback,
1626+
@Nonnull List<OptimizelyDecideOption> options) {
1627+
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, options, callback);
1628+
fetcher.start();
1629+
}
15881630

15891631
private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
15901632
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);

core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@
3131

3232
import com.optimizely.ab.odp.ODPSegmentCallback;
3333
import com.optimizely.ab.odp.ODPSegmentOption;
34-
import com.optimizely.ab.optimizelydecision.AsyncDecisionFetcher;
3534
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
3635
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
37-
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionCallback;
38-
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionsCallback;
3936

4037
public class OptimizelyUserContext {
4138
// OptimizelyForcedDecisionsKey mapped to variationKeys
@@ -205,48 +202,6 @@ public Map<String, OptimizelyDecision> decideAll() {
205202
return decideAll(Collections.emptyList());
206203
}
207204

208-
209-
/**
210-
* Returns a decision result asynchronously for a given flag key and a user context.
211-
* support for android async decisions
212-
* @param key A flag key for which a decision will be made.
213-
* @param callback A callback to invoke when the decision is available.
214-
* @param options A list of options for decision-making.
215-
*/
216-
public void decideAsync(@Nonnull String key,
217-
@Nonnull OptimizelyDecisionCallback callback,
218-
@Nonnull List<OptimizelyDecideOption> options) {
219-
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, key, options, callback);
220-
fetcher.start();
221-
}
222-
223-
224-
/**
225-
* Returns decision results asynchronously for multiple flag keys.
226-
* support for android async decisions
227-
* @param keys A list of flag keys for which decisions will be made.
228-
* @param callback A callback to invoke when decisions are available.
229-
* @param options A list of options for decision-making.
230-
*/
231-
public void decideForKeysAsync(@Nonnull List<String> keys,
232-
@Nonnull OptimizelyDecisionsCallback callback,
233-
@Nonnull List<OptimizelyDecideOption> options) {
234-
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, keys, options, callback);
235-
fetcher.start();
236-
}
237-
238-
/**
239-
* Returns decision results asynchronously for all active flag keys.
240-
* support for android async decisions
241-
* @param callback A callback to invoke when decisions are available.
242-
* @param options A list of options for decision-making.
243-
*/
244-
public void decideAllAsync(@Nonnull OptimizelyDecisionsCallback callback,
245-
@Nonnull List<OptimizelyDecideOption> options) {
246-
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, options, callback);
247-
fetcher.start();
248-
}
249-
250205
/**
251206
* Track an event.
252207
*

core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.concurrent.CountDownLatch;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicReference;
2629
import java.util.function.Function;
2730

2831
import static org.hamcrest.CoreMatchers.is;
@@ -5135,4 +5138,101 @@ public void testDecideReturnsErrorDecisionWhenDecisionServiceFails() throws Exce
51355138
assertTrue(decision.getReasons().contains("Failed to fetch CMAB data for experiment exp-cmab."));
51365139
}
51375140

5141+
@Test
5142+
public void decideAsyncReturnsDecision() throws Exception {
5143+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
5144+
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
5145+
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
5146+
Optimizely optimizely = Optimizely.builder()
5147+
.withConfigManager(mockProjectConfigManager)
5148+
.build();
5149+
OptimizelyUserContext userContext = optimizely.createUserContext(testUserId);
5150+
5151+
final CountDownLatch latch = new CountDownLatch(1);
5152+
final AtomicReference<OptimizelyDecision> decisionRef = new AtomicReference<>();
5153+
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
5154+
5155+
optimizely.decideAsync(userContext,
5156+
FEATURE_MULTI_VARIATE_FEATURE_KEY, (OptimizelyDecision decision) -> {
5157+
try {
5158+
decisionRef.set(decision);
5159+
} catch (Throwable t) {
5160+
errorRef.set(t);
5161+
} finally {
5162+
latch.countDown();
5163+
}
5164+
},
5165+
Collections.emptyList()
5166+
);
5167+
5168+
boolean completed = latch.await(5, TimeUnit.SECONDS);
5169+
5170+
if (errorRef.get() != null) {
5171+
throw new AssertionError("Error in callback", errorRef.get());
5172+
}
5173+
5174+
assertTrue("Callback should be called within timeout", completed);
5175+
5176+
OptimizelyDecision decision = decisionRef.get();
5177+
assertNotNull("Decision should not be null", decision);
5178+
assertEquals("Flag key should match", FEATURE_MULTI_VARIATE_FEATURE_KEY, decision.getFlagKey());
5179+
}
5180+
5181+
@Test
5182+
public void decideForKeysAsyncReturnsDecisions() throws Exception {
5183+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
5184+
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
5185+
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
5186+
Optimizely optimizely = Optimizely.builder()
5187+
.withConfigManager(mockProjectConfigManager)
5188+
.build();
5189+
OptimizelyUserContext userContext = optimizely.createUserContext(testUserId);
5190+
5191+
List<String> flagKeys = Arrays.asList(
5192+
FEATURE_MULTI_VARIATE_FEATURE_KEY,
5193+
FEATURE_SINGLE_VARIABLE_STRING_KEY
5194+
);
5195+
5196+
final CountDownLatch latch = new CountDownLatch(1);
5197+
final AtomicReference<Map<String, OptimizelyDecision>> decisionsRef = new AtomicReference<>();
5198+
5199+
optimizely.decideForKeysAsync(userContext,
5200+
flagKeys, (Map<String, OptimizelyDecision> decisions) -> {
5201+
decisionsRef.set(decisions);
5202+
latch.countDown();
5203+
},
5204+
Collections.emptyList()
5205+
);
5206+
5207+
assertTrue("Callback should be called within timeout", latch.await(5, TimeUnit.SECONDS));
5208+
assertNotNull("Decisions should not be null", decisionsRef.get());
5209+
assertEquals("Should return decisions for 2 keys", 2, decisionsRef.get().size());
5210+
assertTrue("Should contain first flag key", decisionsRef.get().containsKey(FEATURE_MULTI_VARIATE_FEATURE_KEY));
5211+
assertTrue("Should contain second flag key", decisionsRef.get().containsKey(FEATURE_SINGLE_VARIABLE_STRING_KEY));
5212+
}
5213+
5214+
@Test
5215+
public void decideAllAsyncReturnsAllDecisions() throws Exception {
5216+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
5217+
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
5218+
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
5219+
Optimizely optimizely = Optimizely.builder()
5220+
.withConfigManager(mockProjectConfigManager)
5221+
.build();
5222+
OptimizelyUserContext userContext = optimizely.createUserContext(testUserId);
5223+
5224+
final CountDownLatch latch = new CountDownLatch(1);
5225+
final AtomicReference<Map<String, OptimizelyDecision>> decisionsRef = new AtomicReference<>();
5226+
5227+
optimizely.decideAllAsync(userContext, (Map<String, OptimizelyDecision> decisions) -> {
5228+
decisionsRef.set(decisions);
5229+
latch.countDown();
5230+
},
5231+
Collections.emptyList()
5232+
);
5233+
5234+
assertTrue("Callback should be called within timeout", latch.await(5, TimeUnit.SECONDS));
5235+
assertNotNull("Decisions should not be null", decisionsRef.get());
5236+
assertFalse("Decisions should not be empty", decisionsRef.get().isEmpty());
5237+
}
51385238
}

0 commit comments

Comments
 (0)