Skip to content

Commit dc44c7e

Browse files
Add holdout config
1 parent 52da860 commit dc44c7e

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public class DatafileProjectConfig implements ProjectConfig {
7070
private final List<Audience> typedAudiences;
7171
private final List<EventType> events;
7272
private final List<Experiment> experiments;
73+
private final List<Holdout> holdouts;
7374
private final List<FeatureFlag> featureFlags;
7475
private final List<Group> groups;
7576
private final List<Rollout> rollouts;
@@ -95,6 +96,8 @@ public class DatafileProjectConfig implements ProjectConfig {
9596
// other mappings
9697
private final Map<String, Experiment> variationIdToExperimentMapping;
9798

99+
private final HoldoutConfig holdoutConfig;
100+
98101
private String datafile;
99102

100103
// v2 constructor
@@ -124,6 +127,34 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
124127
eventType,
125128
experiments,
126129
null,
130+
null,
131+
groups,
132+
null,
133+
null
134+
);
135+
}
136+
137+
// v3 constructor
138+
public DatafileProjectConfig(String accountId, String projectId, String version, String revision, List<Group> groups,
139+
List<Experiment> experiments, List<Holdout> holdouts, List<Attribute> attributes, List<EventType> eventType,
140+
List<Audience> audiences, boolean anonymizeIP) {
141+
this(
142+
accountId,
143+
anonymizeIP,
144+
false,
145+
null,
146+
projectId,
147+
revision,
148+
null,
149+
null,
150+
version,
151+
attributes,
152+
audiences,
153+
null,
154+
eventType,
155+
experiments,
156+
holdouts,
157+
null,
127158
groups,
128159
null,
129160
null
@@ -145,6 +176,7 @@ public DatafileProjectConfig(String accountId,
145176
List<Audience> typedAudiences,
146177
List<EventType> events,
147178
List<Experiment> experiments,
179+
List<Holdout> holdouts,
148180
List<FeatureFlag> featureFlags,
149181
List<Group> groups,
150182
List<Rollout> rollouts,
@@ -186,6 +218,12 @@ public DatafileProjectConfig(String accountId,
186218
allExperiments.addAll(experiments);
187219
allExperiments.addAll(aggregateGroupExperiments(groups));
188220
this.experiments = Collections.unmodifiableList(allExperiments);
221+
if (holdouts == null) {
222+
this.holdouts = Collections.emptyList();
223+
} else {
224+
this.holdouts = Collections.unmodifiableList(holdouts);
225+
}
226+
this.holdoutConfig = new HoldoutConfig(this.holdouts);
189227

190228
String publicKeyForODP = "";
191229
String hostForODP = "";
@@ -434,6 +472,10 @@ public List<Experiment> getExperiments() {
434472
return experiments;
435473
}
436474

475+
@Override
476+
public List<Holdout> getHoldouts() {
477+
return holdoutConfig.getAllHoldouts(); }
478+
437479
@Override
438480
public Set<String> getAllSegments() {
439481
return this.allSegments;

core-api/src/main/java/com/optimizely/ab/config/Holdout.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ public List<TrafficAllocation> getTrafficAllocation() {
165165
return trafficAllocation;
166166
}
167167

168+
public List<String> getIncludedFlags() { return includedFlags; }
169+
170+
public List<String> getExcludedFlags() { return excludedFlags; }
171+
168172
public String getGroupId() {
169173
return groupId;
170174
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package com.optimizely.ab.config;
2+
3+
import javax.annotation.Nonnull;
4+
import javax.annotation.Nullable;
5+
import java.util.*;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
/**
9+
* HoldoutConfig manages collections of Holdout objects and their relationships to flags.
10+
*/
11+
public class HoldoutConfig {
12+
private List<Holdout> allHoldouts;
13+
private List<Holdout> global;
14+
private Map<String, Holdout> holdoutIdMap;
15+
private Map<String, List<Holdout>> flagHoldoutsMap;
16+
private Map<String, List<Holdout>> includedHoldouts;
17+
private Map<String, List<Holdout>> excludedHoldouts;
18+
19+
/**
20+
* Initializes a new HoldoutConfig with an empty list of holdouts.
21+
*/
22+
public HoldoutConfig() {
23+
this(Collections.emptyList());
24+
}
25+
26+
/**
27+
* Initializes a new HoldoutConfig with the specified holdouts.
28+
*
29+
* @param allHoldouts The list of holdouts to manage
30+
*/
31+
public HoldoutConfig(@Nonnull List<Holdout> allHoldouts) {
32+
this.allHoldouts = new ArrayList<>(allHoldouts);
33+
this.global = new ArrayList<>();
34+
this.holdoutIdMap = new HashMap<>();
35+
this.flagHoldoutsMap = new ConcurrentHashMap<>();
36+
this.includedHoldouts = new HashMap<>();
37+
this.excludedHoldouts = new HashMap<>();
38+
updateHoldoutMapping();
39+
}
40+
41+
/**
42+
* Updates internal mappings of holdouts including the id map, global list,
43+
* and per-flag inclusion/exclusion maps.
44+
*/
45+
public void updateHoldoutMapping() {
46+
holdoutIdMap.clear();
47+
for (Holdout holdout : allHoldouts) {
48+
holdoutIdMap.put(holdout.getId(), holdout);
49+
}
50+
51+
flagHoldoutsMap.clear();
52+
global.clear();
53+
includedHoldouts.clear();
54+
excludedHoldouts.clear();
55+
56+
for (Holdout holdout : allHoldouts) {
57+
boolean hasIncludedFlags = !holdout.getIncludedFlags().isEmpty();
58+
boolean hasExcludedFlags = !holdout.getExcludedFlags().isEmpty();
59+
60+
if (!hasIncludedFlags && !hasExcludedFlags) {
61+
// Global holdout (applies to all flags)
62+
global.add(holdout);
63+
} else if (hasIncludedFlags) {
64+
// Holdout only applies to specific included flags
65+
for (String flagId : holdout.getIncludedFlags()) {
66+
includedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout);
67+
}
68+
} else {
69+
// Global holdout with specific exclusions
70+
global.add(holdout);
71+
72+
for (String flagId : holdout.getExcludedFlags()) {
73+
excludedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout);
74+
}
75+
}
76+
}
77+
}
78+
79+
/**
80+
* Returns the applicable holdouts for the given flag ID by combining global holdouts
81+
* (excluding any specified) and included holdouts, in that order.
82+
* Caches the result for future calls.
83+
*
84+
* @param id The flag identifier
85+
* @return A list of Holdout objects relevant to the given flag
86+
*/
87+
public List<Holdout> getHoldoutForFlag(@Nonnull String id) {
88+
if (allHoldouts.isEmpty()) {
89+
return Collections.emptyList();
90+
}
91+
92+
// Check cache and return persistent holdouts
93+
if (flagHoldoutsMap.containsKey(id)) {
94+
return flagHoldoutsMap.get(id);
95+
}
96+
97+
// Prioritize global holdouts first
98+
List<Holdout> activeHoldouts = new ArrayList<>();
99+
List<Holdout> excluded = excludedHoldouts.getOrDefault(id, Collections.emptyList());
100+
101+
if (!excluded.isEmpty()) {
102+
for (Holdout holdout : global) {
103+
if (!excluded.contains(holdout)) {
104+
activeHoldouts.add(holdout);
105+
}
106+
}
107+
} else {
108+
activeHoldouts.addAll(global);
109+
}
110+
111+
// Add included holdouts
112+
activeHoldouts.addAll(includedHoldouts.getOrDefault(id, Collections.emptyList()));
113+
114+
// Cache the result
115+
flagHoldoutsMap.put(id, activeHoldouts);
116+
117+
return activeHoldouts;
118+
}
119+
120+
/**
121+
* Get a Holdout object for an Id.
122+
*
123+
* @param id The holdout identifier
124+
* @return The Holdout object if found, null otherwise
125+
*/
126+
@Nullable
127+
public Holdout getHoldout(@Nonnull String id) {
128+
return holdoutIdMap.get(id);
129+
}
130+
131+
/**
132+
* Returns all holdouts managed by this config.
133+
*
134+
* @return An unmodifiable list of all holdouts
135+
*/
136+
public List<Holdout> getAllHoldouts() {
137+
return Collections.unmodifiableList(allHoldouts);
138+
}
139+
140+
/**
141+
* Returns the global holdouts (those that apply to all flags by default).
142+
*
143+
* @return An unmodifiable list of global holdouts
144+
*/
145+
public List<Holdout> getGlobal() {
146+
return Collections.unmodifiableList(global);
147+
}
148+
149+
}

core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Experiment getExperimentForKey(@Nonnull String experimentKey,
7070

7171
List<Experiment> getExperiments();
7272

73+
List<Holdout > getHoldouts();
74+
7375
Set<String> getAllSegments();
7476

7577
List<Experiment> getExperimentsForEventKey(String eventKey);

0 commit comments

Comments
 (0)