Skip to content

Commit 7828df1

Browse files
authored
[AI-FSSDK] [FSSDK-12368] Remove legacy flag-level holdout fields (#604)
1 parent bde8d3e commit 7828df1

12 files changed

Lines changed: 51 additions & 518 deletions

File tree

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

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ public class Holdout implements ExperimentCore {
4343
private final Condition<AudienceIdCondition> audienceConditions;
4444
private final List<Variation> variations;
4545
private final List<TrafficAllocation> trafficAllocation;
46-
private final List<String> includedFlags;
47-
private final List<String> excludedFlags;
4846

4947
private final Map<String, Variation> variationKeyToVariationMap;
5048
private final Map<String, Variation> variationIdToVariationMap;
@@ -70,7 +68,7 @@ public String toString() {
7068

7169
@VisibleForTesting
7270
public Holdout(String id, String key) {
73-
this(id, key, "Running", Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null, null);
71+
this(id, key, "Running", Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList());
7472
}
7573

7674
// Keep only this constructor and add @JsonCreator to it
@@ -81,18 +79,14 @@ public Holdout(@JsonProperty("id") @Nonnull String id,
8179
@JsonProperty("audienceIds") @Nonnull List<String> audienceIds,
8280
@JsonProperty("audienceConditions") @Nullable Condition audienceConditions,
8381
@JsonProperty("variations") @Nonnull List<Variation> variations,
84-
@JsonProperty("trafficAllocation") @Nonnull List<TrafficAllocation> trafficAllocation,
85-
@JsonProperty("includedFlags") @Nullable List<String> includedFlags,
86-
@JsonProperty("excludedFlags") @Nullable List<String> excludedFlags) {
82+
@JsonProperty("trafficAllocation") @Nonnull List<TrafficAllocation> trafficAllocation) {
8783
this.id = id;
8884
this.key = key;
8985
this.status = status;
9086
this.audienceIds = audienceIds;
9187
this.audienceConditions = audienceConditions;
9288
this.variations = variations;
9389
this.trafficAllocation = trafficAllocation;
94-
this.includedFlags = includedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(includedFlags);
95-
this.excludedFlags = excludedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(excludedFlags);
9690
this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(this.variations);
9791
this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(this.variations);
9892
}
@@ -141,14 +135,6 @@ public String getGroupId() {
141135
return "";
142136
}
143137

144-
public List<String> getIncludedFlags() {
145-
return includedFlags;
146-
}
147-
148-
public List<String> getExcludedFlags() {
149-
return excludedFlags;
150-
}
151-
152138
public boolean isActive() {
153139
return status.equals(Holdout.HoldoutStatus.RUNNING.toString());
154140
}

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

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,19 @@
2121
import java.util.ArrayList;
2222
import java.util.Collections;
2323
import java.util.HashMap;
24-
import java.util.HashSet;
2524
import java.util.List;
2625
import java.util.Map;
27-
import java.util.Set;
28-
import java.util.concurrent.ConcurrentHashMap;
2926

3027
import javax.annotation.Nonnull;
3128
import javax.annotation.Nullable;
3229

3330
/**
34-
* HoldoutConfig manages collections of Holdout objects and their relationships to flags.
31+
* HoldoutConfig manages collections of Holdout objects.
32+
* All holdouts are global and apply to all flags.
3533
*/
3634
public class HoldoutConfig {
3735
private List<Holdout> allHoldouts;
38-
private List<Holdout> global;
3936
private Map<String, Holdout> holdoutIdMap;
40-
private Map<String, List<Holdout>> flagHoldoutsMap;
41-
private Map<String, List<Holdout>> includedHoldouts;
42-
private Map<String, Set<Holdout>> excludedHoldouts;
4337

4438
/**
4539
* Initializes a new HoldoutConfig with an empty list of holdouts.
@@ -55,91 +49,29 @@ public HoldoutConfig() {
5549
*/
5650
public HoldoutConfig(@Nonnull List<Holdout> allHoldouts) {
5751
this.allHoldouts = new ArrayList<>(allHoldouts);
58-
this.global = new ArrayList<>();
5952
this.holdoutIdMap = new HashMap<>();
60-
this.flagHoldoutsMap = new ConcurrentHashMap<>();
61-
this.includedHoldouts = new HashMap<>();
62-
this.excludedHoldouts = new HashMap<>();
6353
updateHoldoutMapping();
6454
}
6555

6656
/**
67-
* Updates internal mappings of holdouts including the id map, global list,
68-
* and per-flag inclusion/exclusion maps.
57+
* Updates internal mapping of holdout IDs to holdout objects.
6958
*/
7059
private void updateHoldoutMapping() {
7160
holdoutIdMap.clear();
7261
for (Holdout holdout : allHoldouts) {
7362
holdoutIdMap.put(holdout.getId(), holdout);
7463
}
75-
76-
flagHoldoutsMap.clear();
77-
global.clear();
78-
includedHoldouts.clear();
79-
excludedHoldouts.clear();
80-
81-
for (Holdout holdout : allHoldouts) {
82-
boolean hasIncludedFlags = !holdout.getIncludedFlags().isEmpty();
83-
boolean hasExcludedFlags = !holdout.getExcludedFlags().isEmpty();
84-
85-
if (!hasIncludedFlags && !hasExcludedFlags) {
86-
// Global holdout (applies to all flags)
87-
global.add(holdout);
88-
} else if (hasIncludedFlags) {
89-
// Holdout only applies to specific included flags
90-
for (String flagId : holdout.getIncludedFlags()) {
91-
includedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout);
92-
}
93-
} else {
94-
// Global holdout with specific exclusions
95-
global.add(holdout);
96-
97-
for (String flagId : holdout.getExcludedFlags()) {
98-
excludedHoldouts.computeIfAbsent(flagId, k -> new HashSet<>()).add(holdout);
99-
}
100-
}
101-
}
10264
}
10365

10466
/**
105-
* Returns the applicable holdouts for the given flag ID by combining global holdouts
106-
* (excluding any specified) and included holdouts, in that order.
107-
* Caches the result for future calls.
67+
* Returns all holdouts for the given flag ID.
68+
* Since all holdouts are now global, this returns all holdouts.
10869
*
10970
* @param id The flag identifier
110-
* @return A list of Holdout objects relevant to the given flag
71+
* @return A list of all Holdout objects
11172
*/
11273
public List<Holdout> getHoldoutForFlag(@Nonnull String id) {
113-
if (allHoldouts.isEmpty()) {
114-
return Collections.emptyList();
115-
}
116-
117-
// Check cache and return persistent holdouts
118-
if (flagHoldoutsMap.containsKey(id)) {
119-
return flagHoldoutsMap.get(id);
120-
}
121-
122-
// Prioritize global holdouts first
123-
List<Holdout> activeHoldouts = new ArrayList<>();
124-
Set<Holdout> excluded = excludedHoldouts.getOrDefault(id, Collections.emptySet());
125-
126-
if (!excluded.isEmpty()) {
127-
for (Holdout holdout : global) {
128-
if (!excluded.contains(holdout)) {
129-
activeHoldouts.add(holdout);
130-
}
131-
}
132-
} else {
133-
activeHoldouts.addAll(global);
134-
}
135-
136-
// Add included holdouts
137-
activeHoldouts.addAll(includedHoldouts.getOrDefault(id, Collections.emptyList()));
138-
139-
// Cache the result
140-
flagHoldoutsMap.put(id, activeHoldouts);
141-
142-
return activeHoldouts;
74+
return Collections.unmodifiableList(allHoldouts);
14375
}
14476

14577
/**

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

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,23 +202,7 @@ static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext c
202202
List<TrafficAllocation> trafficAllocations =
203203
parseTrafficAllocation(holdoutJson.getAsJsonArray("trafficAllocation"));
204204

205-
List<String> includedFlags = new ArrayList<>();
206-
if (holdoutJson.has("includedFlags")) {
207-
JsonArray includedIdsJson = holdoutJson.getAsJsonArray("includedFlags");
208-
for (JsonElement hoIdObj : includedIdsJson) {
209-
includedFlags.add(hoIdObj.getAsString());
210-
}
211-
}
212-
213-
List<String> excludedFlags = new ArrayList<>();
214-
if (holdoutJson.has("excludedFlags")) {
215-
JsonArray excludedIdsJson = holdoutJson.getAsJsonArray("excludedFlags");
216-
for (JsonElement hoIdObj : excludedIdsJson) {
217-
excludedFlags.add(hoIdObj.getAsString());
218-
}
219-
}
220-
221-
return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags);
205+
return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations);
222206
}
223207

224208
static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) {

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

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -218,34 +218,8 @@ private List<Holdout> parseHoldouts(JSONArray holdoutJson) {
218218
List<TrafficAllocation> trafficAllocations =
219219
parseTrafficAllocation(holdoutObject.getJSONArray("trafficAllocation"));
220220

221-
List<String> includedFlags;
222-
if (holdoutObject.has("includedFlags")) {
223-
JSONArray includedIdsJson = holdoutObject.getJSONArray("includedFlags");
224-
includedFlags = new ArrayList<>(includedIdsJson.length());
225-
226-
for (int j = 0; j < includedIdsJson.length(); j++) {
227-
Object idObj = includedIdsJson.get(j);
228-
includedFlags.add((String) idObj);
229-
}
230-
} else {
231-
includedFlags = Collections.emptyList();
232-
}
233-
234-
List<String> excludedFlags;
235-
if (holdoutObject.has("excludedFlags")) {
236-
JSONArray excludedIdsJson = holdoutObject.getJSONArray("excludedFlags");
237-
excludedFlags = new ArrayList<>(excludedIdsJson.length());
238-
239-
for (int j = 0; j < excludedIdsJson.length(); j++) {
240-
Object idObj = excludedIdsJson.get(j);
241-
excludedFlags.add((String) idObj);
242-
}
243-
} else {
244-
excludedFlags = Collections.emptyList();
245-
}
246-
247221
holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations,
248-
trafficAllocations, includedFlags, excludedFlags));
222+
trafficAllocations));
249223
}
250224

251225
return holdouts;

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

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,22 +237,8 @@ private List<Holdout> parseHoldouts(JSONArray holdoutJson) {
237237
List<TrafficAllocation> trafficAllocations =
238238
parseTrafficAllocation((JSONArray) hoObject.get("trafficAllocation"));
239239

240-
List<String> includedFlags;
241-
if (hoObject.containsKey("includedFlags")) {
242-
includedFlags = new ArrayList<String>((JSONArray) hoObject.get("includedFlags"));
243-
} else {
244-
includedFlags = Collections.emptyList();
245-
}
246-
247-
List<String> excludedFlags;
248-
if (hoObject.containsKey("excludedFlags")) {
249-
excludedFlags = new ArrayList<String>((JSONArray) hoObject.get("excludedFlags"));
250-
} else {
251-
excludedFlags = Collections.emptyList();
252-
}
253-
254240
holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations,
255-
trafficAllocations, includedFlags, excludedFlags));
241+
trafficAllocations));
256242
}
257243

258244
return holdouts;

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

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,69 +2199,4 @@ public void decide_for_keys_with_holdout() throws Exception {
21992199
logbackVerifier.expectMessage(Level.INFO, expectedReason);
22002200
}
22012201

2202-
@Test
2203-
public void decide_all_with_holdout() throws Exception {
2204-
2205-
Optimizely optWithHoldout = createOptimizelyWithHoldouts();
2206-
String userId = "user123";
2207-
Map<String, Object> attrs = new HashMap<>();
2208-
// ppid120000 buckets user into holdout_included_flags
2209-
attrs.put("$opt_bucketing_id", "ppid120000");
2210-
OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs);
2211-
2212-
// All flag keys present in holdouts-project-config.json
2213-
List<String> allFlagKeys = Arrays.asList(
2214-
"boolean_feature",
2215-
"double_single_variable_feature",
2216-
"integer_single_variable_feature",
2217-
"boolean_single_variable_feature",
2218-
"string_single_variable_feature",
2219-
"multi_variate_feature",
2220-
"multi_variate_future_feature",
2221-
"mutex_group_feature"
2222-
);
2223-
2224-
// Flags INCLUDED in holdout_included_flags (only these should be holdout decisions)
2225-
List<String> includedInHoldout = Arrays.asList(
2226-
"boolean_feature",
2227-
"double_single_variable_feature",
2228-
"integer_single_variable_feature"
2229-
);
2230-
2231-
Map<String, OptimizelyDecision> decisions = user.decideAll(Arrays.asList(
2232-
OptimizelyDecideOption.INCLUDE_REASONS,
2233-
OptimizelyDecideOption.DISABLE_DECISION_EVENT
2234-
));
2235-
assertEquals(allFlagKeys.size(), decisions.size());
2236-
2237-
String holdoutExperimentId = "1007543323427"; // holdout_included_flags id
2238-
String variationId = "$opt_dummy_variation_id";
2239-
String variationKey = "ho_off_key";
2240-
String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags).";
2241-
2242-
int holdoutCount = 0;
2243-
for (String flagKey : allFlagKeys) {
2244-
OptimizelyDecision d = decisions.get(flagKey);
2245-
assertNotNull("Missing decision for flag " + flagKey, d);
2246-
if (includedInHoldout.contains(flagKey)) {
2247-
// Should be holdout decision
2248-
assertEquals(variationKey, d.getVariationKey());
2249-
assertFalse(d.getEnabled());
2250-
assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason));
2251-
DecisionMetadata metadata = new DecisionMetadata.Builder()
2252-
.setFlagKey(flagKey)
2253-
.setRuleKey("holdout_included_flags")
2254-
.setRuleType("holdout")
2255-
.setVariationKey(variationKey)
2256-
.setEnabled(false)
2257-
.build();
2258-
holdoutCount++;
2259-
} else {
2260-
// Should NOT be a holdout decision
2261-
assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason));
2262-
}
2263-
}
2264-
assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount);
2265-
logbackVerifier.expectMessage(Level.INFO, expectedReason);
2266-
}
22672202
}

0 commit comments

Comments
 (0)