Skip to content

Commit e75693d

Browse files
update: add asynchronous decision-making methods in OptimizelyUserContext and related fetcher classes
1 parent e4fe788 commit e75693d

File tree

5 files changed

+313
-0
lines changed

5 files changed

+313
-0
lines changed

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

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

3232
import com.optimizely.ab.odp.ODPSegmentCallback;
3333
import com.optimizely.ab.odp.ODPSegmentOption;
34+
import com.optimizely.ab.optimizelydecision.AsyncDecisionFetcher;
35+
import com.optimizely.ab.optimizelydecision.AsyncDecisionsFetcher;
3436
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
3537
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
38+
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionCallback;
39+
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionsCallback;
3640

3741
public class OptimizelyUserContext {
3842
// OptimizelyForcedDecisionsKey mapped to variationKeys
@@ -269,6 +273,67 @@ public Map<String, OptimizelyDecision> decideAllWithoutCmab() {
269273
return decideAllWithoutCmab(Collections.emptyList());
270274
}
271275

276+
/**
277+
* Returns a decision result asynchronously for a given flag key and a user context.
278+
*
279+
* @param key A flag key for which a decision will be made.
280+
* @param callback A callback to invoke when the decision is available.
281+
* @param options A list of options for decision-making.
282+
*/
283+
public void decideAsync(@Nonnull String key,
284+
@Nonnull OptimizelyDecisionCallback callback,
285+
@Nonnull List<OptimizelyDecideOption> options) {
286+
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(this, key, options, callback);
287+
fetcher.start();
288+
}
289+
290+
/**
291+
* Returns a decision result asynchronously for a given flag key and a user context.
292+
*
293+
* @param key A flag key for which a decision will be made.
294+
* @param callback A callback to invoke when the decision is available.
295+
*/
296+
public void decideAsync(@Nonnull String key, @Nonnull OptimizelyDecisionCallback callback) {
297+
decideAsync(key, callback, Collections.emptyList());
298+
}
299+
300+
/**
301+
* Returns decision results asynchronously for multiple flag keys.
302+
*
303+
* @param keys A list of flag keys for which decisions will be made.
304+
* @param callback A callback to invoke when decisions are available.
305+
* @param options A list of options for decision-making.
306+
*/
307+
public void decideForKeysAsync(@Nonnull List<String> keys,
308+
@Nonnull OptimizelyDecisionsCallback callback,
309+
@Nonnull List<OptimizelyDecideOption> options) {
310+
AsyncDecisionsFetcher fetcher = new AsyncDecisionsFetcher(this, keys, options, callback);
311+
fetcher.start();
312+
}
313+
314+
/**
315+
* Returns decision results asynchronously for multiple flag keys.
316+
*/
317+
public void decideForKeysAsync(@Nonnull List<String> keys, @Nonnull OptimizelyDecisionsCallback callback) {
318+
decideForKeysAsync(keys, callback, Collections.emptyList());
319+
}
320+
321+
/**
322+
* Returns decision results asynchronously for all active flag keys.
323+
*/
324+
public void decideAllAsync(@Nonnull OptimizelyDecisionsCallback callback,
325+
@Nonnull List<OptimizelyDecideOption> options) {
326+
AsyncDecisionsFetcher fetcher = new AsyncDecisionsFetcher(this, null, options, callback, true);
327+
fetcher.start();
328+
}
329+
330+
/**
331+
* Returns decision results asynchronously for all active flag keys.
332+
*/
333+
public void decideAllAsync(@Nonnull OptimizelyDecisionsCallback callback) {
334+
decideAllAsync(callback, Collections.emptyList());
335+
}
336+
272337
/**
273338
* Track an event.
274339
*
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright 2025, Optimizely and contributors
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.optimizelydecision;
17+
18+
import com.optimizely.ab.OptimizelyUserContext;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import javax.annotation.Nonnull;
23+
import java.util.List;
24+
25+
/**
26+
* AsyncDecisionFetcher handles asynchronous decision fetching for a single flag key.
27+
* This class follows the same pattern as ODP's async segment fetching.
28+
*/
29+
public class AsyncDecisionFetcher extends Thread {
30+
private static final Logger logger = LoggerFactory.getLogger(AsyncDecisionFetcher.class);
31+
32+
private final String key;
33+
private final List<OptimizelyDecideOption> options;
34+
private final OptimizelyDecisionCallback callback;
35+
private final OptimizelyUserContext userContext;
36+
37+
/**
38+
* Constructor for async decision fetching.
39+
*
40+
* @param userContext The user context to make decisions for
41+
* @param key The flag key to decide on
42+
* @param options Decision options
43+
* @param callback Callback to invoke when decision is ready
44+
*/
45+
public AsyncDecisionFetcher(@Nonnull OptimizelyUserContext userContext,
46+
@Nonnull String key,
47+
@Nonnull List<OptimizelyDecideOption> options,
48+
@Nonnull OptimizelyDecisionCallback callback) {
49+
this.userContext = userContext;
50+
this.key = key;
51+
this.options = options;
52+
this.callback = callback;
53+
54+
// Set thread name for debugging
55+
setName("AsyncDecisionFetcher-" + key);
56+
57+
// Set as daemon thread so it doesn't prevent JVM shutdown
58+
setDaemon(true);
59+
}
60+
61+
@Override
62+
public void run() {
63+
try {
64+
OptimizelyDecision decision = userContext.decide(key, options);
65+
callback.onCompleted(decision);
66+
} catch (Exception e) {
67+
logger.error("Error in async decision fetching for key: " + key, e);
68+
// Create an error decision and pass it to the callback
69+
OptimizelyDecision errorDecision = createErrorDecision(key, e.getMessage());
70+
callback.onCompleted(errorDecision);
71+
}
72+
}
73+
74+
/**
75+
* Creates an error decision when async operation fails.
76+
* This follows the same pattern as sync methods - return a decision with error info.
77+
*
78+
* @param key The flag key that failed
79+
* @param errorMessage The error message
80+
* @return An OptimizelyDecision with error information
81+
*/
82+
private OptimizelyDecision createErrorDecision(String key, String errorMessage) {
83+
// We'll create a decision with null variation and include the error in reasons
84+
// This mirrors how the sync methods handle errors
85+
return OptimizelyDecision.newErrorDecision(key, userContext, "Async decision error: " + errorMessage);
86+
}
87+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright 2025, Optimizely and contributors
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.optimizelydecision;
17+
18+
import com.optimizely.ab.OptimizelyUserContext;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import javax.annotation.Nonnull;
23+
import javax.annotation.Nullable;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
/**
29+
* AsyncDecisionsFetcher handles asynchronous decision fetching for multiple flag keys.
30+
* This class follows the same pattern as ODP's async segment fetching.
31+
*/
32+
public class AsyncDecisionsFetcher extends Thread {
33+
private static final Logger logger = LoggerFactory.getLogger(AsyncDecisionsFetcher.class);
34+
35+
private final List<String> keys;
36+
private final List<OptimizelyDecideOption> options;
37+
private final OptimizelyDecisionsCallback callback;
38+
private final OptimizelyUserContext userContext;
39+
private final boolean decideAll;
40+
41+
/**
42+
* Constructor for deciding on specific keys.
43+
*
44+
* @param userContext The user context to make decisions for
45+
* @param keys List of flag keys to decide on
46+
* @param options Decision options
47+
* @param callback Callback to invoke when decisions are ready
48+
*/
49+
public AsyncDecisionsFetcher(@Nonnull OptimizelyUserContext userContext,
50+
@Nonnull List<String> keys,
51+
@Nonnull List<OptimizelyDecideOption> options,
52+
@Nonnull OptimizelyDecisionsCallback callback) {
53+
this(userContext, keys, options, callback, false);
54+
}
55+
56+
/**
57+
* Constructor for deciding on all flags or specific keys.
58+
*
59+
* @param userContext The user context to make decisions for
60+
* @param keys List of flag keys to decide on (null for decideAll)
61+
* @param options Decision options
62+
* @param callback Callback to invoke when decisions are ready
63+
* @param decideAll Whether to decide for all active flags
64+
*/
65+
public AsyncDecisionsFetcher(@Nonnull OptimizelyUserContext userContext,
66+
@Nullable List<String> keys,
67+
@Nonnull List<OptimizelyDecideOption> options,
68+
@Nonnull OptimizelyDecisionsCallback callback,
69+
boolean decideAll) {
70+
this.userContext = userContext;
71+
this.keys = keys;
72+
this.options = options;
73+
this.callback = callback;
74+
this.decideAll = decideAll;
75+
76+
// Set thread name for debugging
77+
String threadName = decideAll ? "AsyncDecisionsFetcher-all" : "AsyncDecisionsFetcher-keys";
78+
setName(threadName);
79+
80+
// Set as daemon thread so it doesn't prevent JVM shutdown
81+
setDaemon(true);
82+
}
83+
84+
@Override
85+
public void run() {
86+
try {
87+
Map<String, OptimizelyDecision> decisions;
88+
if (decideAll) {
89+
decisions = userContext.decideAll(options);
90+
} else {
91+
decisions = userContext.decideForKeys(keys, options);
92+
}
93+
callback.onCompleted(decisions);
94+
} catch (Exception e) {
95+
logger.error("Error in async decisions fetching", e);
96+
// Return empty map on error - this follows the pattern of sync methods
97+
callback.onCompleted(Collections.emptyMap());
98+
}
99+
}
100+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
*
3+
* Copyright 2025, Optimizely and contributors
4+
* <p>
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
* <p>
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
* <p>
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.optimizelydecision;
18+
19+
import javax.annotation.Nonnull;
20+
21+
@FunctionalInterface
22+
public interface OptimizelyDecisionCallback {
23+
/**
24+
* Called when an async decision operation completes.
25+
*
26+
* @param decision The decision result
27+
*/
28+
void onCompleted(@Nonnull OptimizelyDecision decision);
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright 2024, Optimizely and contributors
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.optimizely.ab.optimizelydecision;
17+
18+
import javax.annotation.Nonnull;
19+
import java.util.Map;
20+
21+
/**
22+
* Callback interface for async multiple decisions operations.
23+
*/
24+
@FunctionalInterface
25+
public interface OptimizelyDecisionsCallback {
26+
/**
27+
* Called when an async multiple decisions operation completes.
28+
*
29+
* @param decisions Map of flag keys to decision results
30+
*/
31+
void onCompleted(@Nonnull Map<String, OptimizelyDecision> decisions);
32+
}

0 commit comments

Comments
 (0)