diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar index ad082fe..43ca520 100644 Binary files a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar and b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar differ diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.md5 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.md5 index 30b98c0..0ffaf4b 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.md5 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.md5 @@ -1 +1 @@ -80a907f51c04fad026bc287a04d8843b \ No newline at end of file +94657cb12ae33b495c6c8229ca50e20b \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.sha1 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.sha1 index fe55b7d..b180dd4 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.sha1 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-javadoc.jar.sha1 @@ -1 +1 @@ -897c0424aa3b3b1812f1a9faea79e2a4b89d7320 \ No newline at end of file +717de0333e701c57eae4ae412a58a7a33ed16545 \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar index 8d0b455..3f438cf 100644 Binary files a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar and b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar differ diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.md5 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.md5 index 60e9740..2c33e1e 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.md5 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.md5 @@ -1 +1 @@ -7d778cbb25a26f4fb8fd39bad0f6884c \ No newline at end of file +53e557e40f5ee576a3bc77fda82a6b4c \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.sha1 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.sha1 index a5c7e65..0196db9 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.sha1 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0-sources.jar.sha1 @@ -1 +1 @@ -72d61374b26b0f05005a5c526a8100a1449c9eaa \ No newline at end of file +bbe55c8e5fdb6c8dd07c0c633ca0eb60e8d9e5bc \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar index c94b53d..12fcde9 100644 Binary files a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar and b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar differ diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.md5 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.md5 index 41c55dd..206ecef 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.md5 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.md5 @@ -1 +1 @@ -f48f73e8ac2edf3691a4dba10b07bf66 \ No newline at end of file +9f2712ae50c72a8e34d3c529b331b4b4 \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.sha1 b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.sha1 index 11050df..93df8a2 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.sha1 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/1.0/ffc-java-server-sdk-1.0.jar.sha1 @@ -1 +1 @@ -ae0fc021dbe3ee0c9c44b7eb3f4c0d3a7022c4de \ No newline at end of file +48df912e5f6310f4207d9604257fb56cd6cfbb51 \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml index 1694ba7..64eeef8 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml @@ -7,6 +7,6 @@ 1.0 - 20220302211136 + 20220322132121 diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.md5 b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.md5 index 0a4f9b8..e6ef7f6 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.md5 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.md5 @@ -1 +1 @@ -e126334975ad39d60d558403e5d1e059 \ No newline at end of file +2ae6b9273632b0afb5378a2952833b6e \ No newline at end of file diff --git a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.sha1 b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.sha1 index cd800b4..8229663 100644 --- a/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.sha1 +++ b/maven-repo/co/featureflags/ffc-java-server-sdk/maven-metadata.xml.sha1 @@ -1 +1 @@ -6984480f2b645451bc59ce262e5c459420dbbba1 \ No newline at end of file +3332ae900214c9b0be5b19856a1131093abb10a6 \ No newline at end of file diff --git a/src/main/java/co/featureflags/server/DataModel.java b/src/main/java/co/featureflags/server/DataModel.java index 255347d..7a2e393 100644 --- a/src/main/java/co/featureflags/server/DataModel.java +++ b/src/main/java/co/featureflags/server/DataModel.java @@ -24,6 +24,7 @@ public interface TimestampData { Integer FFC_FEATURE_FLAG = 100; Integer FFC_ARCHIVED_VDATA = 200; Integer FFC_PERSISTENT_VDATA = 300; + Integer FFC_SEGMENT = 400; /** * return the unique id @@ -123,8 +124,7 @@ public String getMessageType() { } boolean isProcessData() { - return "data-sync".equalsIgnoreCase(messageType) && data != null - && ("full".equalsIgnoreCase(data.eventType) || "patch".equalsIgnoreCase(data.eventType)); + return "data-sync".equalsIgnoreCase(messageType) && data != null && ("full".equalsIgnoreCase(data.eventType) || "patch".equalsIgnoreCase(data.eventType)); } } @@ -136,23 +136,30 @@ static class Data implements JsonHelper.AfterJsonParseDeserializable { private final String eventType; private final List featureFlags; + private final List segments; private Long timestamp; - Data(String eventType, List featureFlags) { + Data(String eventType, List featureFlags, List segments) { this.eventType = eventType; this.featureFlags = featureFlags; + this.segments = segments; } @Override public void afterDeserialization() { - timestamp = (featureFlags != null) - ? featureFlags.stream().map(flag -> flag.timestamp).max(Long::compare).orElse(0L) : 0L; + Long v1 = (featureFlags != null) ? featureFlags.stream().map(flag -> flag.timestamp).max(Long::compare).orElse(0L) : 0L; + Long v2 = (segments != null) ? segments.stream().map(segment -> segment.timestamp).max(Long::compare).orElse(0L) : 0L; + timestamp = Math.max(v1, v2); } public List getFeatureFlags() { return featureFlags == null ? Collections.emptyList() : featureFlags; } + public List getSegments() { + return segments == null ? Collections.emptyList() : segments; + } + public String getEventType() { return eventType; } @@ -162,12 +169,77 @@ public Long getTimestamp() { } Map> toStorageType() { - ImmutableMap.Builder newItems = ImmutableMap.builder(); + ImmutableMap.Builder flags = ImmutableMap.builder(); for (FeatureFlag flag : getFeatureFlags()) { TimestampData data = flag.isArchived ? flag.toArchivedTimestampData() : flag; - newItems.put(data.getId(), new DataStoreTypes.Item(data)); + flags.put(data.getId(), new DataStoreTypes.Item(data)); + } + ImmutableMap.Builder segments = ImmutableMap.builder(); + for (Segment segment : getSegments()) { + TimestampData data = segment.isArchived ? segment.toArchivedTimestampData() : segment; + segments.put(data.getId(), new DataStoreTypes.Item(data)); + } + return ImmutableMap.of(DataStoreTypes.FEATURES, flags.build(), DataStoreTypes.SEGMENTS, segments.build()); + } + } + + static class Segment implements TimestampData { + + private final String id; + + private final Boolean isArchived; + + private final Long timestamp; + + private final List included; + + private final List excluded; + + Segment(String id, Boolean isArchived, Long timestamp, List included, List excluded) { + this.id = id; + this.isArchived = isArchived; + this.timestamp = timestamp; + this.included = included; + this.excluded = excluded; + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean isArchived() { + return isArchived != null && isArchived; + } + + @Override + public Long getTimestamp() { + return timestamp; + } + + @Override + public Integer getType() { + return FFC_SEGMENT; + } + + public List getIncluded() { + return included == null ? Collections.emptyList() : included; + } + + public List getExcluded() { + return excluded == null ? Collections.emptyList() : excluded; + } + + public boolean isMatchUser(String userKeyId) { + if (getExcluded().contains(userKeyId)) { + return false; } - return ImmutableMap.of(DataStoreTypes.FEATURES, newItems.build()); + return getIncluded().contains(userKeyId); + } + + public TimestampData toArchivedTimestampData() { + return new ArchivedTimestampData(this.id, this.timestamp); } } @@ -187,15 +259,7 @@ static class FeatureFlag implements TimestampData { @SerializedName("variationOptions") private final List variations; - FeatureFlag(String id, - Boolean isArchived, - Long timestamp, - Boolean exptIncludeAllRules, - FeatureFlagBasicInfo info, - List prerequisites, - List rules, - List targets, - List variations) { + FeatureFlag(String id, Boolean isArchived, Long timestamp, Boolean exptIncludeAllRules, FeatureFlagBasicInfo info, List prerequisites, List rules, List targets, List variations) { this.id = id; this.isArchived = isArchived; this.timestamp = timestamp; @@ -267,15 +331,7 @@ static class FeatureFlagBasicInfo { private final List defaultRulePercentageRollouts; private final VariationOption variationOptionWhenDisabled; - FeatureFlagBasicInfo(String id, - String name, - Integer type, - String keyName, - String status, - Boolean isDefaultRulePercentageRolloutsIncludedInExpt, - Date lastUpdatedTime, - List defaultRulePercentageRollouts, - VariationOption variationOptionWhenDisabled) { + FeatureFlagBasicInfo(String id, String name, Integer type, String keyName, String status, Boolean isDefaultRulePercentageRolloutsIncludedInExpt, Date lastUpdatedTime, List defaultRulePercentageRollouts, VariationOption variationOptionWhenDisabled) { this.id = id; this.name = name; this.type = type; @@ -308,8 +364,7 @@ public String getStatus() { } public Boolean isDefaultRulePercentageRolloutsIncludedInExpt() { - return isDefaultRulePercentageRolloutsIncludedInExpt == null - ? Boolean.FALSE : isDefaultRulePercentageRolloutsIncludedInExpt; + return isDefaultRulePercentageRolloutsIncludedInExpt == null ? Boolean.FALSE : isDefaultRulePercentageRolloutsIncludedInExpt; } public Date getLastUpdatedTime() { @@ -329,8 +384,7 @@ static class FeatureFlagPrerequisite { private final String prerequisiteFeatureFlagId; private final VariationOption ValueOptionsVariationValue; - FeatureFlagPrerequisite(String prerequisiteFeatureFlagId, - VariationOption valueOptionsVariationValue) { + FeatureFlagPrerequisite(String prerequisiteFeatureFlagId, VariationOption valueOptionsVariationValue) { this.prerequisiteFeatureFlagId = prerequisiteFeatureFlagId; ValueOptionsVariationValue = valueOptionsVariationValue; } @@ -351,11 +405,7 @@ static class FeatureFlagTargetUsersWhoMatchTheseRuleParam { private final List ruleJsonContent; private final List valueOptionsVariationRuleValues; - FeatureFlagTargetUsersWhoMatchTheseRuleParam(String ruleId, - String ruleName, - Boolean isIncludedInExpt, - List ruleJsonContent, - List valueOptionsVariationRuleValues) { + FeatureFlagTargetUsersWhoMatchTheseRuleParam(String ruleId, String ruleName, Boolean isIncludedInExpt, List ruleJsonContent, List valueOptionsVariationRuleValues) { this.ruleId = ruleId; this.ruleName = ruleName; this.isIncludedInExpt = isIncludedInExpt; @@ -388,8 +438,7 @@ static class TargetIndividualForVariationOption { private final List individuals; private final VariationOption valueOption; - TargetIndividualForVariationOption(List individuals, - VariationOption valueOption) { + TargetIndividualForVariationOption(List individuals, VariationOption valueOption) { this.individuals = individuals; this.valueOption = valueOption; } @@ -403,8 +452,7 @@ public VariationOption getValueOption() { } public boolean isTargeted(String userKeyId) { - if (individuals == null) - return false; + if (individuals == null) return false; return individuals.stream().anyMatch(i -> i.keyId.equals(userKeyId)); } } @@ -414,9 +462,7 @@ static class VariationOption { private final Integer displayOrder; private final String variationValue; - VariationOption(Integer localId, - Integer displayOrder, - String variationValue) { + VariationOption(Integer localId, Integer displayOrder, String variationValue) { this.localId = localId; this.displayOrder = displayOrder; this.variationValue = variationValue; @@ -441,9 +487,7 @@ static class VariationOptionPercentageRollout { private final List rolloutPercentage; private final VariationOption valueOption; - VariationOptionPercentageRollout(Double exptRollout, - List rolloutPercentage, - VariationOption valueOption) { + VariationOptionPercentageRollout(Double exptRollout, List rolloutPercentage, VariationOption valueOption) { this.exptRollout = exptRollout; this.rolloutPercentage = rolloutPercentage; this.valueOption = valueOption; @@ -467,9 +511,7 @@ static class FeatureFlagRuleJsonContent { private final String operation; private final String value; - FeatureFlagRuleJsonContent(String property, - String operation, - String value) { + FeatureFlagRuleJsonContent(String property, String operation, String value) { this.property = property; this.operation = operation; this.value = value; @@ -494,10 +536,7 @@ static class FeatureFlagTargetIndividualUser { private final String keyId; private final String email; - FeatureFlagTargetIndividualUser(String id, - String name, - String keyId, - String email) { + FeatureFlagTargetIndividualUser(String id, String name, String keyId, String email) { this.id = id; this.name = name; this.keyId = keyId; diff --git a/src/main/java/co/featureflags/server/Evaluator.java b/src/main/java/co/featureflags/server/Evaluator.java index 5e09a3f..ef428b7 100644 --- a/src/main/java/co/featureflags/server/Evaluator.java +++ b/src/main/java/co/featureflags/server/Evaluator.java @@ -46,6 +46,8 @@ abstract class Evaluator { protected static final String IS_FALSE_CLAUSE = "IsFalse"; protected static final String MATCH_REGEX_CLAUSE = "MatchRegex"; protected static final String NOT_MATCH_REGEX_CLAUSE = "NotMatchRegex"; + protected static final String IS_IN_SEGMENT_CLAUSE = "User is in segment"; + protected static final String NOT_IN_SEGMENT_CLAUSE = "User is not in segment"; protected static final String FLAG_DISABLE_STATS = "Disabled"; @@ -53,8 +55,12 @@ abstract class Evaluator { protected final Getter flagGetter; - Evaluator(Getter flagGetter) { + protected final Getter segmentGetter; + + Evaluator(Getter flagGetter, + Getter segmentGetter) { this.flagGetter = flagGetter; + this.segmentGetter = segmentGetter; } abstract EvalResult evaluate(DataModel.FeatureFlag flag, FFCUser user, InsightTypes.Event event); diff --git a/src/main/java/co/featureflags/server/EvaluatorImp.java b/src/main/java/co/featureflags/server/EvaluatorImp.java index f9bb2e2..2db3a71 100644 --- a/src/main/java/co/featureflags/server/EvaluatorImp.java +++ b/src/main/java/co/featureflags/server/EvaluatorImp.java @@ -14,8 +14,8 @@ final class EvaluatorImp extends Evaluator { - EvaluatorImp(Getter flagGetter) { - super(flagGetter); + public EvaluatorImp(Getter flagGetter, Getter segmentGetter) { + super(flagGetter, segmentGetter); } @Override @@ -52,11 +52,7 @@ private EvalResult matchUserVariation(DataModel.FeatureFlag flag, FFCUser user, return er; } // TODO useless code - er = EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), - REASON_FALLTHROUGH, - false, - flag.getInfo().getKeyName(), - flag.getInfo().getName()); + er = EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), REASON_FALLTHROUGH, false, flag.getInfo().getKeyName(), flag.getInfo().getName()); return er; } finally { if (er != null && event != null) { @@ -68,64 +64,36 @@ private EvalResult matchUserVariation(DataModel.FeatureFlag flag, FFCUser user, private EvalResult matchFeatureFlagDisabledUserVariation(DataModel.FeatureFlag flag, FFCUser user, InsightTypes.Event event) { // case flag is off if (FLAG_DISABLE_STATS.equals(flag.getInfo().getStatus())) { - return EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), - REASON_FLAG_OFF, - false, - flag.getInfo().getKeyName(), - flag.getInfo().getName()); + return EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), REASON_FLAG_OFF, false, flag.getInfo().getKeyName(), flag.getInfo().getName()); } // case prerequisite is set - return flag.getPrerequisites().stream() - .filter(prerequisite -> { - String preFlagId = prerequisite.getPrerequisiteFeatureFlagId(); - if (!preFlagId.equals(flag.getInfo().getId())) { - DataModel.FeatureFlag preFlag = this.flagGetter.get(preFlagId); - if (preFlag == null) { - String preFlagKey = FeatureFlagKeyExtension.unpackFeatureFlagId(preFlagId, 4); - logger.warn("prerequisite flag {} not found", preFlagKey); - return true; - } - EvalResult er = matchUserVariation(preFlag, user, event); - // even if prerequisite flag is off, check if default value of prerequisite flag matches expected value - // if prerequisite failed, return the default value of this flag - return !er.getIndex().equals(prerequisite.getValueOptionsVariationValue().getLocalId()); + return flag.getPrerequisites().stream().filter(prerequisite -> { + String preFlagId = prerequisite.getPrerequisiteFeatureFlagId(); + if (!preFlagId.equals(flag.getInfo().getId())) { + DataModel.FeatureFlag preFlag = this.flagGetter.get(preFlagId); + if (preFlag == null) { + String preFlagKey = FeatureFlagKeyExtension.unpackFeatureFlagId(preFlagId, 4); + logger.warn("prerequisite flag {} not found", preFlagKey); + return true; + } + EvalResult er = matchUserVariation(preFlag, user, event); + // even if prerequisite flag is off, check if default value of prerequisite flag matches expected value + // if prerequisite failed, return the default value of this flag + return !er.getIndex().equals(prerequisite.getValueOptionsVariationValue().getLocalId()); - } - return false; - }).findFirst() - .map(prerequisite -> EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), - REASON_PREREQUISITE_FAILED, - false, - flag.getInfo().getKeyName(), - flag.getInfo().getName())) - .orElse(null); + } + return false; + }).findFirst().map(prerequisite -> EvalResult.of(flag.getInfo().getVariationOptionWhenDisabled(), REASON_PREREQUISITE_FAILED, false, flag.getInfo().getKeyName(), flag.getInfo().getName())).orElse(null); } private EvalResult matchTargetedUserVariation(DataModel.FeatureFlag featureFlag, FFCUser user) { - return featureFlag.getTargets().stream() - .filter(target -> target.isTargeted(user.getKey())) - .findFirst() - .map(target -> EvalResult.of(target.getValueOption(), - REASON_TARGET_MATCH, - featureFlag.isExptIncludeAllRules(), - featureFlag.getInfo().getKeyName(), - featureFlag.getInfo().getName())) - .orElse(null); + return featureFlag.getTargets().stream().filter(target -> target.isTargeted(user.getKey())).findFirst().map(target -> EvalResult.of(target.getValueOption(), REASON_TARGET_MATCH, featureFlag.isExptIncludeAllRules(), featureFlag.getInfo().getKeyName(), featureFlag.getInfo().getName())).orElse(null); } private EvalResult matchConditionedUserVariation(DataModel.FeatureFlag featureFlag, FFCUser user) { - DataModel.FeatureFlagTargetUsersWhoMatchTheseRuleParam targetRule = featureFlag.getRules().stream() - .filter(rule -> ifUserMatchRule(user, rule.getRuleJsonContent())) - .findFirst() - .orElse(null); + DataModel.FeatureFlagTargetUsersWhoMatchTheseRuleParam targetRule = featureFlag.getRules().stream().filter(rule -> ifUserMatchRule(user, rule.getRuleJsonContent())).findFirst().orElse(null); // optional flatmap can't infer inner type of collection - return targetRule == null ? null : - getRollOutVariationOption(targetRule.getValueOptionsVariationRuleValues(), - user, - REASON_RULE_MATCH, - targetRule.isIncludedInExpt(), - featureFlag.getInfo().getKeyName(), - featureFlag.getInfo().getName()); + return targetRule == null ? null : getRollOutVariationOption(targetRule.getValueOptionsVariationRuleValues(), user, REASON_RULE_MATCH, targetRule.isIncludedInExpt(), featureFlag.getInfo().getKeyName(), featureFlag.getInfo().getName()); } @@ -134,6 +102,8 @@ private boolean ifUserMatchRule(FFCUser user, List { boolean isInCondition = false; String op = clause.getOperation(); + // segment hasn't any operation + op = StringUtils.isBlank(op) ? clause.getProperty() : op; if (op.contains(THAN_CLAUSE)) { isInCondition = thanClause(user, clause); } else if (op.equals(EQ_CLAUSE)) { @@ -160,11 +130,26 @@ private boolean ifUserMatchRule(FFCUser user, List clauseValues = JsonHelper.deserialize(clause.getValue(), new TypeToken>() { + }.getType()); + return clauseValues.stream().map(segmentGetter::get).anyMatch(segment -> segment != null && segment.isMatchUser(pv)); + } catch (JsonParseException e) { + return false; + } + } + private boolean falseClause(FFCUser user, DataModel.FeatureFlagRuleJsonContent clause) { String pv = user.getProperty(clause.getProperty()); //TODO add list of false keyword @@ -174,9 +159,7 @@ private boolean falseClause(FFCUser user, DataModel.FeatureFlagRuleJsonContent c private boolean matchRegExClause(FFCUser user, DataModel.FeatureFlagRuleJsonContent clause) { String pv = user.getProperty(clause.getProperty()); String clauseValue = clause.getValue(); - return pv != null && Pattern.compile(Pattern.quote(clauseValue), Pattern.CASE_INSENSITIVE) - .matcher(pv) - .find(); + return pv != null && Pattern.compile(Pattern.quote(clauseValue), Pattern.CASE_INSENSITIVE).matcher(pv).find(); } private boolean trueClause(FFCUser user, DataModel.FeatureFlagRuleJsonContent clause) { @@ -243,24 +226,11 @@ private boolean oneOfClause(FFCUser user, DataModel.FeatureFlagRuleJsonContent c } private EvalResult matchDefaultUserVariation(DataModel.FeatureFlag featureFlag, FFCUser user) { - return getRollOutVariationOption(featureFlag.getInfo().getDefaultRulePercentageRollouts(), - user, - REASON_FALLTHROUGH, - featureFlag.getInfo().isDefaultRulePercentageRolloutsIncludedInExpt(), - featureFlag.getInfo().getKeyName(), - featureFlag.getInfo().getName()); + return getRollOutVariationOption(featureFlag.getInfo().getDefaultRulePercentageRollouts(), user, REASON_FALLTHROUGH, featureFlag.getInfo().isDefaultRulePercentageRolloutsIncludedInExpt(), featureFlag.getInfo().getKeyName(), featureFlag.getInfo().getName()); } - private EvalResult getRollOutVariationOption(Collection rollouts, - FFCUser user, - String reason, - boolean sendToExperiment, - String flagKeyName, - String flagName) { - return rollouts.stream() - .filter(rollout -> VariationSplittingAlgorithm.ifKeyBelongsPercentage(user.getKey(), rollout.getRolloutPercentage())) - .findFirst().map(rollout -> EvalResult.of(rollout.getValueOption(), reason, sendToExperiment, flagKeyName, flagName)) - .orElse(null); + private EvalResult getRollOutVariationOption(Collection rollouts, FFCUser user, String reason, boolean sendToExperiment, String flagKeyName, String flagName) { + return rollouts.stream().filter(rollout -> VariationSplittingAlgorithm.ifKeyBelongsPercentage(user.getKey(), rollout.getRolloutPercentage())).findFirst().map(rollout -> EvalResult.of(rollout.getValueOption(), reason, sendToExperiment, flagKeyName, flagName)).orElse(null); } diff --git a/src/main/java/co/featureflags/server/FFCClientImp.java b/src/main/java/co/featureflags/server/FFCClientImp.java index 8790e2b..036e8ae 100644 --- a/src/main/java/co/featureflags/server/FFCClientImp.java +++ b/src/main/java/co/featureflags/server/FFCClientImp.java @@ -33,6 +33,7 @@ import static co.featureflags.server.Evaluator.REASON_USER_NOT_SPECIFIED; import static co.featureflags.server.Evaluator.REASON_WRONG_TYPE; import static co.featureflags.server.exterior.DataStoreTypes.FEATURES; +import static co.featureflags.server.exterior.DataStoreTypes.SEGMENTS; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -135,7 +136,11 @@ public FFCClientImp(String envSecret, FFCConfig config) { DataStoreTypes.Item item = this.storage.get(FEATURES, key); return item == null ? null : (DataModel.FeatureFlag) item.item(); }; - this.evaluator = new EvaluatorImp(flagGetter); + Evaluator.Getter segmentGetter = key -> { + DataStoreTypes.Item item = this.storage.get(SEGMENTS, key); + return item == null ? null : (DataModel.Segment) item.item(); + }; + this.evaluator = new EvaluatorImp(flagGetter, segmentGetter); //data updator Status.DataUpdatorImpl dataUpdatorImpl = new Status.DataUpdatorImpl(this.storage); this.dataUpdator = dataUpdatorImpl; diff --git a/src/main/java/co/featureflags/server/FactoryImp.java b/src/main/java/co/featureflags/server/FactoryImp.java index 0f086fd..dd40249 100644 --- a/src/main/java/co/featureflags/server/FactoryImp.java +++ b/src/main/java/co/featureflags/server/FactoryImp.java @@ -12,6 +12,7 @@ import co.featureflags.server.exterior.InsightProcessorFactory; import co.featureflags.server.exterior.UpdateProcessor; import co.featureflags.server.exterior.UpdateProcessorFactory; +import com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.StringUtils; import java.time.Duration; @@ -82,7 +83,7 @@ public DataStoreTypes.Item get(DataStoreTypes.Category category, String key) { @Override public Map getAll(DataStoreTypes.Category category) { - return null; + return ImmutableMap.of(); } @Override diff --git a/src/main/java/co/featureflags/server/Status.java b/src/main/java/co/featureflags/server/Status.java index 8f8f5a2..0ed608f 100644 --- a/src/main/java/co/featureflags/server/Status.java +++ b/src/main/java/co/featureflags/server/Status.java @@ -20,13 +20,14 @@ public abstract class Status { public static final String RUNTIME_ERROR = "Runtime error"; public static final String UNKNOWN_ERROR = "Unknown error"; public static final String UNKNOWN_CLOSE_CODE = "Unknown close code"; + public static final String WEBSOCKET_ERROR = "WebSocket error"; /** * possible values for {@link co.featureflags.server.exterior.UpdateProcessor} */ public enum StateType { /** - * The initial state of the data source when the SDK is being initialized. + * The initial state of the update processing when the SDK is being initialized. *

* If it encounters an error that requires it to retry initialization, the state will remain at * {@link #INITIALIZING} until it either succeeds and becomes {@link #OK}, or permanently fails and @@ -215,15 +216,14 @@ public interface DataUpdator { *

* If {@code newState} is different from the previous state, and/or {@code newError} is non-null, the * SDK will start returning the new status (adding a timestamp for the change) from - * {@link DataUpdateStatusProvider#getState()}, and will trigger status change events to any - * registered listeners. + * {@link DataUpdateStatusProvider#getState()}. *

* A special case is that if {@code newState} is {@link StateType#INTERRUPTED}, * but the previous state was {@link StateType#INITIALIZING}, the state will remain at {@link StateType#INITIALIZING} * because {@link StateType#INTERRUPTED} is only meaningful after a successful startup. * - * @param newState the data storage state - * @param message the data source state + * @param newState the new state of {@link co.featureflags.server.exterior.UpdateProcessor} + * @param message an error message or null */ void updateStatus(StateType newState, ErrorInfo message); @@ -298,14 +298,15 @@ public void updateStatus(StateType newState, ErrorInfo message) { } synchronized (lockObject) { StateType oldOne = currentState.getStateType(); + StateType newState1 = newState; // interruped state is only meaningful after initialization - if (newState == StateType.INTERRUPTED && oldOne == StateType.INITIALIZING) { - newState = StateType.INITIALIZING; + if (newState1 == StateType.INTERRUPTED && oldOne == StateType.INITIALIZING) { + newState1 = StateType.INITIALIZING; } - if (newState != oldOne || message != null) { - Instant stateSince = newState == oldOne ? currentState.getStateSince() : Instant.now(); - currentState = new State(newState, stateSince, message); + if (newState1 != oldOne || message != null) { + Instant stateSince = newState1 == oldOne ? currentState.getStateSince() : Instant.now(); + currentState = new State(newState1, stateSince, message); lockObject.notifyAll(); } } @@ -374,8 +375,7 @@ public interface DataUpdateStatusProvider { * whenever they successfully initialize, encounter an error, or recover after an error. *

* For a custom implementation, it is the responsibility of the data source to report its status via {@link DataUpdator}; - * if it does not do so, the status will always be reported as - * {@link StateType#INITIALIZING}. + * if it does not do so, the status will always be reported as {@link StateType#INITIALIZING}. * * @return the latest status; will never be null */ diff --git a/src/main/java/co/featureflags/server/Streaming.java b/src/main/java/co/featureflags/server/Streaming.java index b7d522b..7e3d270 100644 --- a/src/main/java/co/featureflags/server/Streaming.java +++ b/src/main/java/co/featureflags/server/Streaming.java @@ -21,12 +21,12 @@ import org.slf4j.Logger; import java.io.EOFException; +import java.io.IOException; import java.net.SocketException; import java.net.SocketTimeoutException; import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; @@ -42,6 +42,7 @@ import static co.featureflags.server.Status.RUNTIME_ERROR; import static co.featureflags.server.Status.UNKNOWN_CLOSE_CODE; import static co.featureflags.server.Status.UNKNOWN_ERROR; +import static co.featureflags.server.Status.WEBSOCKET_ERROR; final class Streaming implements UpdateProcessor { @@ -176,43 +177,40 @@ private OkHttpClient buildWebOkHttpClient() { return builder.build(); } - private Callable processDateAsync(final DataModel.Data data) { - return () -> { - boolean opOK = false; - String eventType = data.getEventType(); - Long version = data.getTimestamp(); - Map> updatedData = data.toStorageType(); - if (FULL_OPS.equalsIgnoreCase(eventType)) { - boolean fullOK = updator.init(updatedData, version); - opOK = fullOK; - } else if (PATCH_OPS.equalsIgnoreCase(eventType)) { - // streaming patch is a real time update - // patch data contains only one item in just one category. - // no data update is considered as a good operation - boolean patchOK = true; - for (Map.Entry> entry : updatedData.entrySet()) { - DataStoreTypes.Category category = entry.getKey(); - for (Map.Entry keyItem : entry.getValue().entrySet()) { - patchOK = updator.upsert(category, keyItem.getKey(), keyItem.getValue(), version); - } + private Boolean processDateAsync(final DataModel.Data data) { + boolean opOK = false; + String eventType = data.getEventType(); + Long version = data.getTimestamp(); + Map> updatedData = data.toStorageType(); + if (FULL_OPS.equalsIgnoreCase(eventType)) { + boolean fullOK = updator.init(updatedData, version); + opOK = fullOK; + } else if (PATCH_OPS.equalsIgnoreCase(eventType)) { + // streaming patch is a real time update + // patch data contains only one item in just one category. + // no data update is considered as a good operation + boolean patchOK = true; + for (Map.Entry> entry : updatedData.entrySet()) { + DataStoreTypes.Category category = entry.getKey(); + for (Map.Entry keyItem : entry.getValue().entrySet()) { + patchOK = updator.upsert(category, keyItem.getKey(), keyItem.getValue(), version); } - opOK = patchOK; } - if (opOK) { - if (initialized.compareAndSet(false, true)) { - initFuture.complete(true); - } - logger.info("processing data is well done"); - updator.updateStatus(Status.StateType.OK, null); - } else { - // reconnect to server to get back data after data storage failed - // the reason is gathered by DataUpdator - // close code 1001 means peer going away - webSocket.close(GOING_AWAY_CLOSE, JUST_RECONN_REASON_REGISTERED); + opOK = patchOK; + } + if (opOK) { + if (initialized.compareAndSet(false, true)) { + initFuture.complete(true); } - permits.release(); - return opOK; - }; + logger.info("processing data is well done"); + updator.updateStatus(Status.StateType.OK, null); + } else { + // reconnect to server to get back data after data storage failed + // the reason is gathered by DataUpdator + // close code 1001 means peer going away + webSocket.close(GOING_AWAY_CLOSE, JUST_RECONN_REASON_REGISTERED); + } + return opOK; } private final class DefaultWebSocketListener extends StreamingWebSocketListener { @@ -225,7 +223,9 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { if (all.isProcessData()) { try { permits.acquire(); - storageUpdateExecutor.submit(processDateAsync(all.data())); + CompletableFuture + .supplyAsync(() -> processDateAsync(all.data()), storageUpdateExecutor) + .whenComplete((res, exception) -> permits.release()); } catch (InterruptedException ignore) { } } @@ -304,7 +304,10 @@ public final void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, } } } - if (errorType == null) { + if(!isReconn && t instanceof IOException){ + errorType = WEBSOCKET_ERROR; + } + else if (errorType == null) { errorType = UNKNOWN_ERROR; } } diff --git a/src/main/java/co/featureflags/server/exterior/DataStoreTypes.java b/src/main/java/co/featureflags/server/exterior/DataStoreTypes.java index 262bd82..5b930d4 100644 --- a/src/main/java/co/featureflags/server/exterior/DataStoreTypes.java +++ b/src/main/java/co/featureflags/server/exterior/DataStoreTypes.java @@ -27,6 +27,10 @@ public abstract class DataStoreTypes { "/api/public/sdk/latest-feature-flags", "/streaming"); + public final static Category SEGMENTS = new Category("segments", + "/api/public/sdk/latest-feature-flags", + "/streaming"); + /** * An enumeration of all supported {@link Category} types. *

diff --git a/src/test/java/co/featureflags/server/Demos.java b/src/test/java/co/featureflags/server/Demos.java index 63616ad..520adce 100644 --- a/src/test/java/co/featureflags/server/Demos.java +++ b/src/test/java/co/featureflags/server/Demos.java @@ -56,7 +56,7 @@ public static void main(String[] args) throws IOException { } try { String[] words = line.split("/"); - user = new FFCUser.Builder(words[0]).build(); + user = new FFCUser.Builder(words[0]).userName(words[0]).build(); Instant start = Instant.now(); FlagState res = client.variationDetail(words[1], user, "Not Found"); @@ -109,7 +109,7 @@ public static void main(String[] args) throws InterruptedException, IOException } try { String[] words = line.split("/"); - user = new FFCUser.Builder(words[0]).build(); + user = new FFCUser.Builder(words[0]).userName(words[0]).build(); Instant start = Instant.now(); FlagState res = client.variationDetail(words[1], user, "Not Found"); Instant end = Instant.now(); @@ -147,7 +147,7 @@ public static void main(String[] args) throws IOException { } try { String[] words = line.split("/"); - user = new FFCUser.Builder(words[0]).build(); + user = new FFCUser.Builder(words[0]).userName(words[0]).build(); VariationParams params = VariationParams.of(words[1], user); String jsonBody = params.jsonfy(); System.out.println(jsonBody); @@ -182,7 +182,7 @@ public static void main(String[] args) throws IOException { break; } try { - user = new FFCUser.Builder(userkey).build(); + user = new FFCUser.Builder(userkey).userName(userkey).build(); VariationParams params = VariationParams.of(null, user); String jsonBody = params.jsonfy(); System.out.println(jsonBody);