diff options
Diffstat (limited to 'flags/src/main/java')
11 files changed, 288 insertions, 47 deletions
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java index d2228c58c51..4cce7a9bc2a 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java @@ -24,27 +24,40 @@ public class FetchVector { * Note: If this enum is changed, you must also change {@link DimensionHelper}. */ public enum Dimension { - /** - * WARNING: DO NOT USE - * - * <p>ALL flags can be set differently in different zones: This dimension is ONLY useful for the controller - * that needs to handle multiple zones. - * - * <p>Value from ZoneId::value is of the form environment.region. - */ - ZONE_ID, - /** Value from ApplicationId::serializedForm of the form tenant:applicationName:instance. */ APPLICATION_ID, - /** Fully qualified hostname */ - HOSTNAME, - /** Node type from com.yahoo.config.provision.NodeType::name, e.g. tenant, host, confighost, controller, etc. */ NODE_TYPE, /** Cluster type from com.yahoo.config.provision.ClusterSpec.Type::name, e.g. content, container, admin */ - CLUSTER_TYPE + CLUSTER_TYPE, + + /** + * Fully qualified hostname. + * + * <p>NOTE: There is seldom any need to set HOSTNAME, as it is always set implicitly (in {@link Flags}) + * from {@code Defaults.getDefaults().vespaHostname()}. The hostname may e.g. be overridden when + * fetching flag value for a Docker container node. + */ + HOSTNAME, + + /** + * Vespa version from Version::toFullString of the form Major.Minor.Micro. + * + * <p>NOTE: There is seldom any need to set VESPA_VERSION, as it is always set implicitly + * (in {@link Flags}) from {@link com.yahoo.component.Vtag#currentVersion}. The version COULD e.g. + * be overridden when fetching flag value for a Docker container node. + */ + VESPA_VERSION, + + /** + * Zone from ZoneId::value of the form environment.region. + * + * <p>NOTE: There is seldom any need to set ZONE_ID, as all flags are set per zone anyways. The controller + * could PERHAPS use this where it handles multiple zones. + */ + ZONE_ID } private final Map<Dimension, String> map; diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index a27171f29a2..f4b223ead82 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -1,6 +1,7 @@ // Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.flags; +import com.yahoo.component.Vtag; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.flags.custom.PreprovisionCapacity; @@ -11,6 +12,7 @@ import java.util.TreeMap; import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; import static com.yahoo.vespa.flags.FetchVector.Dimension.HOSTNAME; import static com.yahoo.vespa.flags.FetchVector.Dimension.NODE_TYPE; +import static com.yahoo.vespa.flags.FetchVector.Dimension.VESPA_VERSION; /** * Definitions of feature flags. @@ -235,7 +237,8 @@ public class Flags { * from the FlagSource. * @param <T> The boxed type of the flag value, e.g. Boolean for flags guarding features. * @param <U> The type of the unbound flag, e.g. UnboundBooleanFlag. - * @return An unbound flag with {@link FetchVector.Dimension#HOSTNAME HOSTNAME} environment. The ZONE environment + * @return An unbound flag with {@link FetchVector.Dimension#HOSTNAME HOSTNAME} and + * {@link FetchVector.Dimension#VESPA_VERSION VESPA_VERSION} already set. The ZONE environment * is typically implicit. */ private static <T, U extends UnboundFlag<?, ?, ?>> U define(TypedUnboundFlagFactory<T, U> factory, @@ -245,7 +248,11 @@ public class Flags { String modificationEffect, FetchVector.Dimension[] dimensions) { FlagId id = new FlagId(flagId); - FetchVector vector = new FetchVector().with(HOSTNAME, Defaults.getDefaults().vespaHostname()); + FetchVector vector = new FetchVector() + .with(HOSTNAME, Defaults.getDefaults().vespaHostname()) + // Warning: In unit tests and outside official Vespa releases, the currentVersion is e.g. 7.0.0 + // (determined by the current major version). Consider not setting VESPA_VERSION if minor = micro = 0. + .with(VESPA_VERSION, Vtag.currentVersion.toFullString()); U unboundFlag = factory.create(id, defaultValue, vector); FlagDefinition definition = new FlagDefinition(unboundFlag, description, modificationEffect, dimensions); flags.put(id, definition); diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java new file mode 100644 index 00000000000..435ddbb3e19 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/BlacklistCondition.java @@ -0,0 +1,10 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +/** + * @author hakonhall + */ +public class BlacklistCondition extends ListCondition { + public static BlacklistCondition create(CreateParams params) { return new BlacklistCondition(params); } + private BlacklistCondition(CreateParams params) { super(Type.BLACKLIST, params); } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java index a0ad08fb0b3..96dbc8197c1 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java @@ -1,62 +1,72 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.flags.json; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.json.wire.WireCondition; -import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; /** * @author hakonhall */ -public class Condition implements Predicate<FetchVector> { - public enum Type { WHITELIST, BLACKLIST } +public interface Condition extends Predicate<FetchVector> { + enum Type { + WHITELIST, + BLACKLIST, + RELATIONAL; - private final Type type; - private final FetchVector.Dimension dimension; - private final List<String> values; + public String toWire() { return name().toLowerCase(); } - public Condition(Type type, FetchVector.Dimension dimension, String... values) { - this(type, dimension, Arrays.asList(values)); - } + public static Type fromWire(String typeString) { + for (Type type : values()) { + if (type.name().equalsIgnoreCase(typeString)) { + return type; + } + } - public Condition(Type type, FetchVector.Dimension dimension, List<String> values) { - this.type = type; - this.dimension = dimension; - this.values = values; + throw new IllegalArgumentException("Unknown type: '" + typeString + "'"); + } } - @Override - public boolean test(FetchVector vector) { - boolean isMember = vector.getValue(dimension).filter(values::contains).isPresent(); + class CreateParams { + private final FetchVector.Dimension dimension; + private final List<String> values; + private final Optional<String> predicate; - switch (type) { - case WHITELIST: return isMember; - case BLACKLIST: return !isMember; - default: throw new IllegalArgumentException("Unknown type " + type); + public CreateParams(FetchVector.Dimension dimension, List<String> values, Optional<String> predicate) { + this.dimension = Objects.requireNonNull(dimension); + this.values = Objects.requireNonNull(values); + this.predicate = Objects.requireNonNull(predicate); } + + public FetchVector.Dimension dimension() { return dimension; } + public List<String> values() { return values; } + public Optional<String> predicate() { return predicate; } } - public static Condition fromWire(WireCondition wireCondition) { + static Condition fromWire(WireCondition wireCondition) { Objects.requireNonNull(wireCondition.type); - Type type = Type.valueOf(wireCondition.type.toUpperCase()); + Condition.Type type = Condition.Type.fromWire(wireCondition.type); Objects.requireNonNull(wireCondition.dimension); FetchVector.Dimension dimension = DimensionHelper.fromWire(wireCondition.dimension); List<String> values = wireCondition.values == null ? List.of() : wireCondition.values; + Optional<String> predicate = Optional.ofNullable(wireCondition.predicate); - return new Condition(type, dimension, values); - } + var params = new CreateParams(dimension, values, predicate); - public WireCondition toWire() { - WireCondition wire = new WireCondition(); - wire.type = type.name().toLowerCase(); - wire.dimension = DimensionHelper.toWire(dimension); - wire.values = values.isEmpty() ? null : values; - return wire; + switch (type) { + case WHITELIST: return WhitelistCondition.create(params); + case BLACKLIST: return BlacklistCondition.create(params); + case RELATIONAL: return RelationalCondition.create(params); + } + + throw new IllegalArgumentException("Unknown type '" + type + "'"); } + + WireCondition toWire(); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java index 4fe27e81f2b..c7081ca72ab 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java @@ -18,6 +18,7 @@ public class DimensionHelper { serializedDimensions.put(FetchVector.Dimension.APPLICATION_ID, "application"); serializedDimensions.put(FetchVector.Dimension.NODE_TYPE, "node-type"); serializedDimensions.put(FetchVector.Dimension.CLUSTER_TYPE, "cluster-type"); + serializedDimensions.put(FetchVector.Dimension.VESPA_VERSION, "vespa-version"); if (serializedDimensions.size() != FetchVector.Dimension.values().length) { throw new IllegalStateException(FetchVectorHelper.class.getName() + " is not in sync with " + diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java new file mode 100644 index 00000000000..c2c76529833 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java @@ -0,0 +1,43 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.json.wire.WireCondition; + +import java.util.List; + +/** + * @author hakonhall + */ +public abstract class ListCondition implements Condition { + private final Condition.Type type; + private final FetchVector.Dimension dimension; + private final List<String> values; + private final boolean isWhitelist; + + protected ListCondition(Type type, CreateParams params) { + this.type = type; + this.dimension = params.dimension(); + this.values = List.copyOf(params.values()); + this.isWhitelist = type == Type.WHITELIST; + + if (params.predicate().isPresent()) { + throw new IllegalArgumentException(getClass().getSimpleName() + " does not support the 'predicate' field"); + } + } + + @Override + public boolean test(FetchVector fetchVector) { + boolean listContainsValue = fetchVector.getValue(dimension).map(values::contains).orElse(false); + return isWhitelist == listContainsValue; + } + + @Override + public WireCondition toWire() { + var condition = new WireCondition(); + condition.type = type.toWire(); + condition.dimension = DimensionHelper.toWire(dimension); + condition.values = values.isEmpty() ? null : values; + return condition; + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java new file mode 100644 index 00000000000..afaf94b26b6 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java @@ -0,0 +1,64 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +import com.yahoo.component.Version; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.json.wire.WireCondition; + +import java.util.List; +import java.util.function.Predicate; + +/** + * @author hakonhall + */ +public class RelationalCondition implements Condition { + private final RelationalPredicate relationalPredicate; + private final Predicate<String> predicate; + private final FetchVector.Dimension dimension; + + public static RelationalCondition create(CreateParams params) { + if (!params.values().isEmpty()) { + throw new IllegalArgumentException(RelationalCondition.class.getSimpleName() + + " does not support the 'values' field"); + } + + String predicate = params.predicate().orElseThrow(() -> + new IllegalArgumentException(RelationalCondition.class.getSimpleName() + + " requires the predicate field in the condition")); + RelationalPredicate relationalPredicate = RelationalPredicate.fromWire(predicate); + + switch (params.dimension()) { + case VESPA_VERSION: + final Version rightVersion = Version.fromString(relationalPredicate.rightOperand()); + Predicate<String> p = (String leftString) -> { + Version leftVersion = Version.fromString(leftString); + return relationalPredicate.operator().evaluate(leftVersion, rightVersion); + }; + return new RelationalCondition(relationalPredicate, p, params.dimension()); + default: + throw new IllegalArgumentException(RelationalCondition.class.getSimpleName() + + " not supported for dimension " + FetchVector.Dimension.VESPA_VERSION.name()); + } + } + + private RelationalCondition(RelationalPredicate relationalPredicate, Predicate<String> predicate, + FetchVector.Dimension dimension) { + this.relationalPredicate = relationalPredicate; + this.predicate = predicate; + this.dimension = dimension; + } + + @Override + public boolean test(FetchVector fetchVector) { + return fetchVector.getValue(dimension).map(predicate::test).orElse(false); + } + + @Override + public WireCondition toWire() { + var condition = new WireCondition(); + condition.type = Condition.Type.RELATIONAL.toWire(); + condition.dimension = DimensionHelper.toWire(dimension); + condition.predicate = relationalPredicate.toWire(); + return condition; + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java new file mode 100644 index 00000000000..ca7a997f447 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalOperator.java @@ -0,0 +1,39 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +import java.util.Objects; +import java.util.function.Function; + +/** + * @author hakonhall + */ +public enum RelationalOperator { + EQUAL ("==", compareToValue -> compareToValue == 0), + NOT_EQUAL ("!=", compareToValue -> compareToValue != 0), + LESS_EQUAL ("<=", compareToValue -> compareToValue <= 0), + LESS ("<" , compareToValue -> compareToValue < 0), + GREATER_EQUAL(">=", compareToValue -> compareToValue >= 0), + GREATER (">" , compareToValue -> compareToValue > 0); + + private String text; + private final Function<Integer, Boolean> compareToValuePredicate; + + RelationalOperator(String text, Function<Integer, Boolean> compareToValuePredicate) { + this.text = text; + this.compareToValuePredicate = compareToValuePredicate; + } + + public String toText() { return text; } + + /** Returns true if 'left op right' is true, with 'op' being the operator represented by this. */ + public <T extends Comparable<T>> boolean evaluate(T left, T right) { + Objects.requireNonNull(left); + Objects.requireNonNull(right); + return evaluate(left.compareTo(right)); + } + + public boolean evaluate(int compareToValue) { + return compareToValuePredicate.apply(compareToValue); + } +} + diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java new file mode 100644 index 00000000000..c5ad195e0d2 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalPredicate.java @@ -0,0 +1,43 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author hakonhall + */ +public class RelationalPredicate { + private final String originalPredicateString; + private final RelationalOperator operator; + private final String rightOperand; + + /** @param predicateString is e.g. "> SUFFIX" or "<=SUFFIX". The first part is {@link RelationalOperator}. */ + public static RelationalPredicate fromWire(String predicateString) { + // Make sure we try to match e.g. "<=" before "<" as the prefix of predicateString. + List<RelationalOperator> operatorsByDecendingLength = Stream.of(RelationalOperator.values()) + .sorted(Comparator.comparing(operator -> - operator.toText().length())) + .collect(Collectors.toList()); + + for (var operator : operatorsByDecendingLength) { + if (predicateString.startsWith(operator.toText())) { + String suffix = predicateString.substring(operator.toText().length()); + return new RelationalPredicate(predicateString, operator, suffix); + } + } + + throw new IllegalArgumentException("Predicate string '" + predicateString + "' does not start with a relation operator"); + } + + private RelationalPredicate(String originalPredicateString, RelationalOperator operator, String rightOperand) { + this.originalPredicateString = originalPredicateString; + this.operator = operator; + this.rightOperand = rightOperand; + } + + public RelationalOperator operator() { return operator; } + public String rightOperand() { return rightOperand; } + public String toWire() { return originalPredicateString; } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java new file mode 100644 index 00000000000..ab266fdaf1d --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/WhitelistCondition.java @@ -0,0 +1,10 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.flags.json; + +/** + * @author hakonhall + */ +public class WhitelistCondition extends ListCondition { + public static WhitelistCondition create(CreateParams params) { return new WhitelistCondition(params); } + private WhitelistCondition(CreateParams params) { super(Type.WHITELIST, params); } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java index 2020ce1e49f..1729444fcf2 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java @@ -16,4 +16,5 @@ public class WireCondition { @JsonProperty("type") public String type; @JsonProperty("dimension") public String dimension; @JsonProperty("values") public List<String> values; + @JsonProperty("predicate") public String predicate; } |