summaryrefslogtreecommitdiffstats
path: root/flags
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-12-30 20:10:16 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-12-30 20:10:16 +0100
commitaf82f15b8ec3a7c19d1b9ba48b53edf9feb6de48 (patch)
treeae2e16385bea0218a7bb3232cff0d6ea0104c528 /flags
parent4e810c250f3013982a3fc935de9f083eacef1d7c (diff)
Configserver flags REST API
Adds a new ZooKeeper backed flag source. It is defined in a new module configserver-flags to allow as many as possible config server modules to depend on it by minimizing dependencies. The content of the ZK backed flag source can be viewed and modified through REST API on the config server/controller. The data stored per flag looks like { "rules": [ { "conditions": [ { "type": "whitelist", "dimension": "hostname", "values": ["host1"] } ], "value": true } ] } typical for enabling a feature flag on host1. 2 types of conditions are so far supported: whitelist and blacklist. All the conditions must match in order for the value to apply. If the value is null (or absent), the default value will be used. At the time the flag's value is retrieved, it is resolved against the conditions with the current zone, hostname, and/or application. The same data structure is used for FileFlagSource for files in /etc/vespa/flags with the ".2" extension. The FlagSource component injected in the config server is changed to: 1. Return the flag value if specified in /etc/vespa/flags, or otherwise 2. return flag value from ZooKeeper (same as REST API) The current flags (module) is also changed: - All flags must be defined in com.yahoo.vespa.flags.Flags. This allows the ZK backed flag source additional sanity checking when modifying flags. - If it makes sense to have different flag value depending on e.g. the application, then at some point before the value is retrieved, one has to bind the flag to that application (using with() to set up the fetch vector). Future changes would be to 0. make a merged FlagSource in host admin, 1. add support for viewing and modifying feature flags in dashboard, 2. in hv tool.
Diffstat (limited to 'flags')
-rw-r--r--flags/pom.xml11
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java10
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java62
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java82
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java43
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flag.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java41
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagId.java7
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java8
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java3
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java130
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java58
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java23
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java48
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java33
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java14
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Serializer.java10
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java38
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java65
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java104
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java58
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java5
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java48
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java27
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java19
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java55
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java19
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java47
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java131
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java66
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java50
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java38
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java82
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java130
38 files changed, 1427 insertions, 363 deletions
diff --git a/flags/pom.xml b/flags/pom.xml
index 5a535ad4de8..ade598556de 100644
--- a/flags/pom.xml
+++ b/flags/pom.xml
@@ -13,9 +13,10 @@
</parent>
<artifactId>flags</artifactId>
- <version>6-SNAPSHOT</version>
<packaging>container-plugin</packaging>
+ <version>6-SNAPSHOT</version>
<name>${project.artifactId}</name>
+ <description>Feature flags.</description>
<dependencies>
<dependency>
@@ -55,6 +56,14 @@
<artifactId>bundle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>8</source>
+ <target>8</target>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java b/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java
new file mode 100644
index 00000000000..7ececa5489a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java
@@ -0,0 +1,10 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+@FunctionalInterface
+public interface Deserializer<T> {
+ T deserialize(RawFlag rawFlag);
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java
deleted file mode 100644
index 7cc5529646e..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * A FeatureFlag is a boolean flag.
- *
- * @author hakonhall
- */
-public class FeatureFlag implements Flag {
- private final boolean defaultValue;
- private final FlagId id;
- private final FlagSource source;
-
- public static Function<FlagSource, FeatureFlag> createUnbound(String flagId, boolean defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, FeatureFlag> createUnbound(FlagId id, boolean defaultValue) {
- return source -> new FeatureFlag(id, defaultValue, source);
- }
-
- public FeatureFlag(String flagId, boolean defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public FeatureFlag(FlagId id, boolean defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public boolean value() {
- return source.getString(id).map(FeatureFlag::booleanFromString).orElse(defaultValue);
- }
-
- private static boolean booleanFromString(String string) {
- String canonicalString = string.trim().toLowerCase();
- if (canonicalString.equals("true")) {
- return true;
- } else if (canonicalString.equals("false")) {
- return false;
- }
-
- throw new IllegalArgumentException("Unable to convert to true or false: '" + string + "'");
- }
-
- @Override
- public String toString() {
- return "IntFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
new file mode 100644
index 00000000000..581ec599aab
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
@@ -0,0 +1,82 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Denotes which RawFlag should be retrieved from {@link FlagSource} for a given {@link FlagId},
+ * as the raw flag may depend on the hostname, application, etc.
+ *
+ * @author hakonhall
+ */
+@Immutable
+public
+class FetchVector {
+ public enum Dimension {
+ /** Value from ZoneId::value */
+ ZONE_ID,
+ /** Value from ApplicationId::serializedForm */
+ APPLICATION_ID,
+ /** Fully qualified hostname */
+ HOSTNAME
+ }
+
+ private final Map<Dimension, String> map;
+
+ public FetchVector() {
+ this.map = Collections.emptyMap();
+ }
+
+ public static FetchVector fromMap(Map<Dimension, String> map) {
+ return new FetchVector(new HashMap<>(map));
+ }
+
+ private FetchVector(Map<Dimension, String> map) {
+ this.map = Collections.unmodifiableMap(map);
+ }
+
+ public Optional<String> getValue(Dimension dimension) {
+ return Optional.ofNullable(map.get(dimension));
+ }
+
+ public Map<Dimension, String> toMap() {
+ return map;
+ }
+
+ /** Returns a new FetchVector, identical to {@code this} except for its value in {@code dimension}. */
+ public FetchVector with(Dimension dimension, String value) {
+ return makeFetchVector(merged -> merged.put(dimension, value));
+ }
+
+ /** Returns a new FetchVector, identical to {@code this} except for its values in the override's dimensions. */
+ public FetchVector with(FetchVector override) {
+ return makeFetchVector(vector -> vector.putAll(override.map));
+ }
+
+ private FetchVector makeFetchVector(Consumer<EnumMap<Dimension, String>> mapModifier) {
+ EnumMap<Dimension, String> mergedMap = new EnumMap<>(Dimension.class);
+ mergedMap.putAll(map);
+ mapModifier.accept(mergedMap);
+ return new FetchVector(mergedMap);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ FetchVector that = (FetchVector) o;
+ return Objects.equals(map, that.map);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(map);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
index 1a856fee60a..3403a15d7ff 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
@@ -1,7 +1,10 @@
// Copyright 2018 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.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
+import com.yahoo.vespa.flags.json.FlagData;
+import com.yahoo.vespa.flags.json.Rule;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -11,14 +14,15 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.Optional;
/**
- * A {@link FlagSource} backed by local files.
- *
* @author hakonhall
*/
public class FileFlagSource implements FlagSource {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
static final String FLAGS_DIRECTORY = "/etc/vespa/flags";
private final Path flagsDirectory;
@@ -37,13 +41,36 @@ public class FileFlagSource implements FlagSource {
}
@Override
- public Optional<String> getString(FlagId id) {
- return getBytes(id).map(bytes -> new String(bytes, StandardCharsets.UTF_8));
+ public Optional<RawFlag> fetch(FlagId flagId, FetchVector vector) {
+ return getResolver(flagId).resolve(vector);
+ }
+
+ private FlagData getResolver(FlagId flagId) {
+ Optional<String> v2String = getString(flagId, ".2");
+ if (v2String.isPresent()) {
+ return FlagData.deserialize(v2String.get());
+ }
+
+ Optional<String> v1String = getString(flagId, "");
+ if (v1String.isPresent()) {
+ // version 1: File contains value as a JSON
+ // version 2: File contains FileResolver as a JSON (which may contain many values, one for each rule)
+ // version 1 files should probably be discontinued
+ Rule rule = new Rule(Optional.of(JsonNodeRawFlag.fromJson(v1String.get())), Collections.emptyList());
+ return new FlagData(new FetchVector(), Collections.singletonList(rule));
+ }
+
+ // Will eventually resolve to empty RawFlag
+ return new FlagData();
+ }
+
+ private Optional<String> getString(FlagId id, String suffix) {
+ return getBytes(id, suffix).map(bytes -> new String(bytes, StandardCharsets.UTF_8));
}
- public Optional<byte[]> getBytes(FlagId id) {
+ private Optional<byte[]> getBytes(FlagId id, String suffix) {
try {
- return Optional.of(Files.readAllBytes(getPath(id)));
+ return Optional.of(Files.readAllBytes(getPath(id, suffix)));
} catch (NoSuchFileException e) {
return Optional.empty();
} catch (IOException e) {
@@ -51,8 +78,8 @@ public class FileFlagSource implements FlagSource {
}
}
- private Path getPath(FlagId id) {
- return flagsDirectory.resolve(id.toString());
+ private Path getPath(FlagId id, String suffix) {
+ return flagsDirectory.resolve(id.toString() + suffix);
}
@Override
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
index 831e0d0dab9..18c1db0d756 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
@@ -1,9 +1,44 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.flags;
+import javax.annotation.concurrent.Immutable;
+
/**
* @author hakonhall
*/
-public interface Flag {
- FlagId id();
+@Immutable
+public class Flag<T> {
+ private final FlagId id;
+ private final T defaultValue;
+ private final FlagSource source;
+ private final Deserializer<T> deserializer;
+ private final FetchVector fetchVector;
+
+ public Flag(String flagId, T defaultValue, FlagSource source, Deserializer<T> deserializer) {
+ this(new FlagId(flagId), defaultValue, source, deserializer);
+ }
+
+ public Flag(FlagId id, T defaultValue, FlagSource source, Deserializer<T> deserializer) {
+ this(id, defaultValue, deserializer, new FetchVector(), source);
+ }
+
+ public Flag(FlagId id, T defaultValue, Deserializer<T> deserializer, FetchVector fetchVector, FlagSource source) {
+ this.id = id;
+ this.defaultValue = defaultValue;
+ this.source = source;
+ this.deserializer = deserializer;
+ this.fetchVector = fetchVector;
+ }
+
+ public FlagId id() {
+ return id;
+ }
+
+ public Flag<T> with(FetchVector.Dimension dimension, String value) {
+ return new Flag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value), source);
+ }
+
+ public T value() {
+ return source.fetch(id, fetchVector).map(deserializer::deserialize).orElse(defaultValue);
+ }
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java
new file mode 100644
index 00000000000..a3f490e2f96
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java
@@ -0,0 +1,41 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class FlagDefinition<T> {
+ private final UnboundFlag<T> unboundFlag;
+ private final String description;
+ private final String modificationEffect;
+ private final List<FetchVector.Dimension> dimensions;
+
+ public FlagDefinition(UnboundFlag<T> unboundFlag, String description, String modificationEffect,
+ List<FetchVector.Dimension> dimensions) {
+ this.unboundFlag = unboundFlag;
+ this.description = description;
+ this.modificationEffect = modificationEffect;
+ this.dimensions = Collections.unmodifiableList(dimensions);
+ }
+
+ public UnboundFlag<T> getUnboundFlag() {
+ return unboundFlag;
+ }
+
+ public List<FetchVector.Dimension> getDimensions() {
+ return dimensions;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getModificationEffect() {
+ return modificationEffect;
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
index f004df063ed..ae38fbe7dc1 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
@@ -9,7 +9,7 @@ import java.util.regex.Pattern;
* @author hakonhall
*/
@Immutable
-public class FlagId {
+public class FlagId implements Comparable<FlagId> {
private static final Pattern ID_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$");
private final String id;
@@ -23,6 +23,11 @@ public class FlagId {
}
@Override
+ public int compareTo(FlagId that) {
+ return this.id.compareTo(that.id);
+ }
+
+ @Override
public String toString() {
return id;
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java
new file mode 100644
index 00000000000..697cbfcbb4a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+public interface FlagSerializer<T> extends Serializer<T>, Deserializer<T> {
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
index 509db40c4d4..da8e6b29cab 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
@@ -7,6 +7,5 @@ import java.util.Optional;
* @author hakonhall
*/
public interface FlagSource {
- /** The String value of a flag, or empty if not set by the source. */
- Optional<String> getString(FlagId id);
+ Optional<RawFlag> fetch(FlagId id, FetchVector vector);
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
new file mode 100644
index 00000000000..b66ba9fc0c9
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -0,0 +1,130 @@
+// Copyright 2018 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Definitions of most/all flags.
+ *
+ * <p>The flags are centrally defined in this module to allow 1. all code to access flags that may be used in
+ * quite different modules and processes, and 2. in particular allow the config server to access all flags
+ * so operators have a nicer UI for setting, modifying, or removing flag values.
+ *
+ * <p>This class should have been an enum, but unfortunately enums cannot be generic, which will eventually be
+ * fixed with <a href="https://openjdk.java.net/jeps/301">JEP 301: Enhanced Enums</a>.
+ *
+ * @author hakonhall
+ */
+public class Flags {
+ public static final FlagSerializer<Boolean> BOOLEAN_SERIALIZER = new SimpleFlagSerializer<>(BooleanNode::valueOf, JsonNode::isBoolean, JsonNode::asBoolean);
+ public static final FlagSerializer<String> STRING_SERIALIZER = new SimpleFlagSerializer<>(TextNode::new, JsonNode::isTextual, JsonNode::asText);
+ public static final FlagSerializer<Integer> INT_SERIALIZER = new SimpleFlagSerializer<>(IntNode::new, JsonNode::isIntegralNumber, JsonNode::asInt);
+ public static final FlagSerializer<Long> LONG_SERIALIZER = new SimpleFlagSerializer<>(LongNode::new, JsonNode::isIntegralNumber, JsonNode::asLong);
+
+ private static final ConcurrentHashMap<FlagId, FlagDefinition<?>> flags = new ConcurrentHashMap<>();
+
+ public static final UnboundFlag<Boolean> HEALTHMONITOR_MONITOR_INFRA = defineBoolean(
+ "healthmonitor-monitorinfra", true,
+ "Whether the health monitor in service monitor monitors the health of infrastructure applications.",
+ "Affects all applications activated after the value is changed.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> DUPERMODEL_CONTAINS_INFRA = defineBoolean(
+ "dupermodel-contains-infra", true,
+ "Whether the DuperModel in config server/controller includes active infrastructure applications " +
+ "(except from controller/config apps).",
+ "Requires restart of config server/controller to take effect.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> DUPERMODEL_USE_CONFIGSERVERCONFIG = defineBoolean(
+ "dupermodel-use-configserverconfig", true,
+ "For historical reasons, the ApplicationInfo in the DuperModel for controllers and config servers " +
+ "is based on the ConfigserverConfig (this flag is true). We want to transition to use the " +
+ "infrastructure application activated by the InfrastructureProvisioner once that supports health.",
+ "Requires restart of config server/controller to take effect.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> USE_CONFIG_SERVER_CACHE = defineBoolean(
+ "use-config-server-cache", true,
+ "Whether config server will use cache to answer config requests.",
+ "Takes effect immediately when changed.",
+ FetchVector.Dimension.HOSTNAME, FetchVector.Dimension.APPLICATION_ID);
+
+ public static final UnboundFlag<Boolean> CONFIG_SERVER_BOOTSTRAP_IN_SEPARATE_THREAD = defineBoolean(
+ "config-server-bootstrap-in-separate-thread", true,
+ "Whether to run config server/controller bootstrap in a separate thread.",
+ "Takes effect only at bootstrap of config server/controller",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static UnboundFlag<Boolean> defineBoolean(String flagId, boolean defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, BOOLEAN_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<String> defineString(String flagId, String defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, STRING_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<Integer> defineInt(String flagId, Integer defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, INT_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<Long> defineLong(String flagId, Long defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, LONG_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static <T> UnboundFlag<T> defineJackson(String flagId, Class<T> jacksonClass, T defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, new JacksonSerializer<>(jacksonClass), description, modificationEffect, dimensions);
+ }
+
+ /**
+ * Defines a Flag.
+ *
+ * @param flagId The globally unique FlagId.
+ * @param defaultValue The default value if none is present after resolution.
+ * @param deserializer Deserialize JSON to value type.
+ * @param description Description of how the flag is used.
+ * @param modificationEffect What is required for the flag to take effect? A restart of process? immediately? etc.
+ * @param dimensions What dimensions will be set in the {@link FetchVector} when fetching
+ * the flag value in
+ * {@link FlagSource#fetch(FlagId, FetchVector) FlagSource::fetch}.
+ * For instance, if APPLICATION is one of the dimensions here, you should make sure
+ * APPLICATION is set to the ApplicationId in the fetch vector when fetching the RawFlag
+ * from the FlagSource.
+ * @param <T> The type of the flag value, typically Boolean for flags guarding features.
+ * @return An unbound flag with {@link FetchVector.Dimension#HOSTNAME HOSTNAME} environment. The ZONE environment
+ * is typically implicit.
+ */
+ private static <T> UnboundFlag<T> define(String flagId, T defaultValue, Deserializer<T> deserializer,
+ String description, String modificationEffect,
+ FetchVector.Dimension... dimensions) {
+ UnboundFlag<T> flag = new UnboundFlag<>(flagId, defaultValue, deserializer)
+ .with(FetchVector.Dimension.HOSTNAME, Defaults.getDefaults().vespaHostname());
+ FlagDefinition<T> definition = new FlagDefinition<>(flag, description, modificationEffect, Arrays.asList(dimensions));
+ flags.put(flag.id(), definition);
+ return flag;
+ }
+
+ public static List<FlagDefinition<?>> getAllFlags() {
+ return new ArrayList<>(flags.values());
+ }
+
+ public static Optional<FlagDefinition<?>> getFlag(FlagId flagId) {
+ return Optional.ofNullable(flags.get(flagId));
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
deleted file mode 100644
index f7c9645c5db..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class IntFlag implements Flag {
- private final FlagId id;
- private final int defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, IntFlag> createUnbound(String flagId, int defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, IntFlag> createUnbound(FlagId id, int defaultValue) {
- return source -> new IntFlag(id, defaultValue, source);
- }
-
- public IntFlag(String flagId, int defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public IntFlag(FlagId id, int defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public int value() {
- return source.getString(id).map(String::trim).map(Integer::parseInt).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "IntFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java
deleted file mode 100644
index 99add358e75..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2018 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.fasterxml.jackson.databind.ObjectMapper;
-
-import java.util.function.Function;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author hakonhall
- */
-public class JacksonFlag<T> implements Flag {
- private final static ObjectMapper mapper = new ObjectMapper();
-
- private final FlagId id;
- private final Class<T> jacksonClass;
- private final T defaultValue;
- private final FlagSource source;
-
- public static <T> Function<FlagSource, JacksonFlag<T>> createUnbound(String flagId, Class<T> jacksonClass, T defaultValue) {
- return createUnbound(new FlagId(flagId), jacksonClass, defaultValue);
- }
-
- public static <T> Function<FlagSource, JacksonFlag<T>> createUnbound(FlagId id, Class<T> jacksonClass, T defaultValue) {
- return source -> new JacksonFlag<>(id, jacksonClass, defaultValue, source);
- }
-
- public JacksonFlag(String flagId, Class<T> jacksonClass, T defaultValue, FlagSource source) {
- this(new FlagId(flagId), jacksonClass, defaultValue, source);
- }
-
- public JacksonFlag(FlagId id, Class<T> jacksonClass, T defaultValue, FlagSource source) {
- this.id = id;
- this.jacksonClass = jacksonClass;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public T value() {
- return source.getString(id).map(string -> uncheck(() -> mapper.readValue(string, jacksonClass))).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "JacksonFlag{" +
- "id=" + id +
- ", jacksonClass=" + jacksonClass +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java
new file mode 100644
index 00000000000..b6a4c9b87c6
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java
@@ -0,0 +1,23 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+public class JacksonSerializer<T> implements FlagSerializer<T> {
+ private final Class<T> jacksonClass;
+
+ public JacksonSerializer(Class<T> jacksonClass) {
+ this.jacksonClass = jacksonClass;
+ }
+
+ @Override
+ public T deserialize(RawFlag rawFlag) {
+ return JsonNodeRawFlag.fromJsonNode(rawFlag.asJsonNode()).toJacksonClass(jacksonClass);
+ }
+
+ @Override
+ public RawFlag serialize(T value) {
+ return JsonNodeRawFlag.fromJacksonClass(value);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java
new file mode 100644
index 00000000000..f41dd9f9e5c
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java
@@ -0,0 +1,48 @@
+// Copyright 2018 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.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * {@link RawFlag} using Jackson's {@link JsonNode}.
+ *
+ * @author hakonhall
+ */
+public class JsonNodeRawFlag implements RawFlag {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private final JsonNode jsonNode;
+
+ private JsonNodeRawFlag(JsonNode jsonNode) {
+ this.jsonNode = jsonNode;
+ }
+
+ public static JsonNodeRawFlag fromJson(String json) {
+ return new JsonNodeRawFlag(uncheck(() -> mapper.readTree(json)));
+ }
+
+ public static JsonNodeRawFlag fromJsonNode(JsonNode jsonNode) {
+ return new JsonNodeRawFlag(jsonNode);
+ }
+
+ public static <T> JsonNodeRawFlag fromJacksonClass(T value) {
+ return new JsonNodeRawFlag(uncheck(() -> mapper.valueToTree(value)));
+ }
+
+ public <T> T toJacksonClass(Class<T> jacksonClass) {
+ return uncheck(() -> mapper.treeToValue(jsonNode, jacksonClass));
+ }
+
+ @Override
+ public JsonNode asJsonNode() {
+ return jsonNode;
+ }
+
+ @Override
+ public String asJson() {
+ return jsonNode.toString();
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
deleted file mode 100644
index d60dc7b5adc..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class LongFlag implements Flag {
- private final FlagId id;
- private final long defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, LongFlag> createUnbound(String flagId, int defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, LongFlag> createUnbound(FlagId id, int defaultValue) {
- return source -> new LongFlag(id, defaultValue, source);
- }
-
- public LongFlag(String flagId, long defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public LongFlag(FlagId id, long defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public long value() {
- return source.getString(id).map(String::trim).map(Long::parseLong).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "LongFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java
new file mode 100644
index 00000000000..6ca74715999
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java
@@ -0,0 +1,33 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A {@link FlagSource#fetch(FlagId, FetchVector) fetch} on this flag source will return the {@link RawFlag}
+ * from the first (highest priority) flag source that returns a raw flag for the fetch vector.
+ *
+ * @author hakonhall
+ */
+public class OrderedFlagSource implements FlagSource {
+ private final List<FlagSource> sources;
+
+ /**
+ *
+ * @param sources Flag sources in descending priority order.
+ */
+ public OrderedFlagSource(FlagSource... sources) {
+ this.sources = Arrays.asList(sources);
+ }
+
+ @Override
+ public Optional<RawFlag> fetch(FlagId id, FetchVector vector) {
+ return sources.stream()
+ .map(source -> source.fetch(id, vector))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java
new file mode 100644
index 00000000000..2308659470e
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java
@@ -0,0 +1,14 @@
+// Copyright 2018 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.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A {@link RawFlag} represents the typeless flag value, possibly partially deserialized.
+ *
+ * @author hakonhall
+ */
+public interface RawFlag {
+ JsonNode asJsonNode();
+ String asJson();
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java b/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java
new file mode 100644
index 00000000000..3569b10c8f4
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java
@@ -0,0 +1,10 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+@FunctionalInterface
+public interface Serializer<T> {
+ RawFlag serialize(T value);
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java
new file mode 100644
index 00000000000..2340588f54a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java
@@ -0,0 +1,39 @@
+// Copyright 2018 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.fasterxml.jackson.databind.JsonNode;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * @author hakonhall
+ */
+public class SimpleFlagSerializer<T> implements FlagSerializer<T> {
+ private final Function<T, JsonNode> serializer;
+ private final Predicate<JsonNode> isCorrectType;
+ private final Function<JsonNode, T> deserializer;
+
+ public SimpleFlagSerializer(Function<T, JsonNode> serializer,
+ Predicate<JsonNode> isCorrectType,
+ Function<JsonNode, T> deserializer) {
+ this.serializer = serializer;
+ this.isCorrectType = isCorrectType;
+ this.deserializer = deserializer;
+ }
+
+ @Override
+ public JsonNodeRawFlag serialize(T value) {
+ return JsonNodeRawFlag.fromJsonNode(serializer.apply(value));
+ }
+
+ @Override
+ public T deserialize(RawFlag rawFlag) {
+ JsonNode jsonNode = rawFlag.asJsonNode();
+ if (!isCorrectType.test(jsonNode)) {
+ throw new IllegalArgumentException("Wrong type of JsonNode: " + jsonNode.getNodeType());
+ }
+
+ return deserializer.apply(jsonNode);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
deleted file mode 100644
index 8226e999238..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class StringFlag implements Flag {
- private final FlagId id;
- private final String defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, StringFlag> createUnbound(String flagId, String defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, StringFlag> createUnbound(FlagId id, String defaultValue) {
- return source -> new StringFlag(id, defaultValue, source);
- }
-
- public StringFlag(String flagId, String defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public StringFlag(FlagId id, String defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public String value() {
- return source.getString(id).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "StringFlag{" +
- "id=" + id +
- ", defaultValue='" + defaultValue + '\'' +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java
new file mode 100644
index 00000000000..597e19080ab
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class UnboundFlag<T> {
+ private final FlagId id;
+ private final T defaultValue;
+ private final Deserializer<T> deserializer;
+ private final FetchVector fetchVector;
+
+ public UnboundFlag(String flagId, T defaultValue, Deserializer<T> deserializer) {
+ this(new FlagId(flagId), defaultValue, deserializer, new FetchVector());
+ }
+
+ public UnboundFlag(FlagId id, T defaultValue, Deserializer<T> deserializer, FetchVector fetchVector) {
+ this.id = id;
+ this.defaultValue = defaultValue;
+ this.deserializer = deserializer;
+ this.fetchVector = fetchVector;
+ }
+
+ public FlagId id() {
+ return id;
+ }
+
+ public UnboundFlag<T> with(FetchVector.Dimension dimension, String value) {
+ return new UnboundFlag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value));
+ }
+
+ public Flag<T> bindTo(FlagSource source) {
+ return new Flag<>(id, defaultValue, deserializer, fetchVector, source);
+ }
+}
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
new file mode 100644
index 00000000000..c07232bfe66
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java
@@ -0,0 +1,65 @@
+// Copyright 2018 Yahoo Holdings. 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.DimensionHelper;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * @author hakonhall
+ */
+public class Condition implements Predicate<FetchVector> {
+ public enum Type { WHITELIST, BLACKLIST }
+
+ private final Type type;
+ private final FetchVector.Dimension dimension;
+ private final Set<String> values;
+
+ public Condition(Type type, FetchVector.Dimension dimension, String... values) {
+ this(type, dimension, new HashSet<>(Arrays.asList(values)));
+ }
+
+ public Condition(Type type, FetchVector.Dimension dimension, Set<String> values) {
+ this.type = type;
+ this.dimension = dimension;
+ this.values = values;
+ }
+
+ @Override
+ public boolean test(FetchVector vector) {
+ boolean isMember = vector.getValue(dimension).filter(values::contains).isPresent();
+
+ switch (type) {
+ case WHITELIST: return isMember;
+ case BLACKLIST: return !isMember;
+ default: throw new IllegalArgumentException("Unknown type " + type);
+ }
+ }
+
+ public static Condition fromWire(WireCondition wireCondition) {
+ Objects.requireNonNull(wireCondition.type);
+ Type type = Type.valueOf(wireCondition.type.toUpperCase());
+
+ Objects.requireNonNull(wireCondition.dimension);
+ FetchVector.Dimension dimension = DimensionHelper.fromWire(wireCondition.dimension);
+
+ Set<String> values = wireCondition.values == null ? Collections.emptySet() : wireCondition.values;
+
+ return new Condition(type, dimension, values);
+ }
+
+ 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;
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java
new file mode 100644
index 00000000000..9ee3b2dcd85
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java
@@ -0,0 +1,104 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.RawFlag;
+import com.yahoo.vespa.flags.json.wire.FetchVectorHelper;
+import com.yahoo.vespa.flags.json.wire.WireFlagData;
+import com.yahoo.vespa.flags.json.wire.WireRule;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * A data structure containing all data for a single flag, that can be serialized to/from JSON,
+ * and that can be used to implement {@link FlagSource}.
+ *
+ * @author hakonhall
+ */
+@Immutable
+public class FlagData {
+ private final List<Rule> rules;
+ private final FetchVector defaultFetchVector;
+
+ public FlagData() {
+ this(new FetchVector(), Collections.emptyList());
+ }
+
+ public FlagData(FetchVector defaultFetchVector, Rule... rules) {
+ this(defaultFetchVector, Arrays.asList(rules));
+ }
+
+ public FlagData(FetchVector defaultFetchVector, List<Rule> rules) {
+ this.rules = Collections.unmodifiableList(new ArrayList<>(rules));
+ this.defaultFetchVector = defaultFetchVector;
+ }
+
+ public Optional<RawFlag> resolve(FetchVector fetchVector) {
+ return rules.stream()
+ .filter(rule -> rule.match(defaultFetchVector.with(fetchVector)))
+ .findFirst()
+ .flatMap(Rule::getValueToApply);
+ }
+
+ public String serializeToJson() {
+ return toWire().serializeToJson();
+ }
+
+ public byte[] serializeToUtf8Json() {
+ return toWire().serializeToBytes();
+ }
+
+ public void serializeToOutputStream(OutputStream outputStream) {
+ toWire().serializeToOutputStream(outputStream);
+ }
+
+ public JsonNode toJsonNode() {
+ return toWire().serializeToJsonNode();
+ }
+
+ private WireFlagData toWire() {
+ WireFlagData wireFlagData = new WireFlagData();
+
+ if (!rules.isEmpty()) {
+ wireFlagData.rules = rules.stream().map(Rule::toWire).collect(Collectors.toList());
+ }
+
+ wireFlagData.defaultFetchVector = FetchVectorHelper.toWire(defaultFetchVector);
+
+ return wireFlagData;
+ }
+
+ public static FlagData deserializeUtf8Json(byte[] bytes) {
+ return fromWire(WireFlagData.deserialize(bytes));
+ }
+
+ public static FlagData deserialize(InputStream inputStream) {
+ return fromWire(WireFlagData.deserialize(inputStream));
+ }
+
+ public static FlagData deserialize(String string) {
+ return fromWire(WireFlagData.deserialize(string));
+ }
+
+ private static FlagData fromWire(WireFlagData wireFlagData) {
+ return new FlagData(
+ FetchVectorHelper.fromWire(wireFlagData.defaultFetchVector), rulesFromWire(wireFlagData.rules)
+ );
+ }
+
+ private static List<Rule> rulesFromWire(List<WireRule> wireRules) {
+ if (wireRules == null) return Collections.emptyList();
+ return wireRules.stream().map(Rule::fromWire).collect(Collectors.toList());
+ }
+}
+
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java
new file mode 100644
index 00000000000..b7d60889419
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java
@@ -0,0 +1,58 @@
+// Copyright 2018 Yahoo Holdings. 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.JsonNodeRawFlag;
+import com.yahoo.vespa.flags.RawFlag;
+import com.yahoo.vespa.flags.json.wire.WireRule;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class Rule {
+ private final List<Condition> andConditions;
+ private final Optional<RawFlag> valueToApply;
+
+ public Rule(Optional<RawFlag> valueToApply, Condition... andConditions) {
+ this(valueToApply, Arrays.asList(andConditions));
+ }
+
+ public Rule(Optional<RawFlag> valueToApply, List<Condition> andConditions) {
+ this.andConditions = andConditions;
+ this.valueToApply = valueToApply;
+ }
+
+ public boolean match(FetchVector fetchVector) {
+ return andConditions.stream().allMatch(condition -> condition.test(fetchVector));
+ }
+
+ public Optional<RawFlag> getValueToApply() {
+ return valueToApply;
+ }
+
+ public WireRule toWire() {
+ WireRule wireRule = new WireRule();
+
+ if (!andConditions.isEmpty()) {
+ wireRule.andConditions = andConditions.stream().map(Condition::toWire).collect(Collectors.toList());
+ }
+
+ wireRule.value = valueToApply.map(RawFlag::asJsonNode).orElse(null);
+
+ return wireRule;
+ }
+
+ public static Rule fromWire(WireRule wireRule) {
+ List<Condition> conditions = wireRule.andConditions == null ?
+ Collections.emptyList() :
+ wireRule.andConditions.stream().map(Condition::fromWire).collect(Collectors.toList());
+ Optional<RawFlag> value = wireRule.value == null ? Optional.empty() : Optional.of(JsonNodeRawFlag.fromJsonNode(wireRule.value));
+ return new Rule(value, conditions);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java b/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java
new file mode 100644
index 00000000000..6fbe3d587c5
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java
new file mode 100644
index 00000000000..e2cb6dd0932
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java
@@ -0,0 +1,48 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.yahoo.vespa.flags.FetchVector;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class DimensionHelper {
+ private static Map<FetchVector.Dimension, String> serializedDimensions = new HashMap<>();
+ static {
+ serializedDimensions.put(FetchVector.Dimension.ZONE_ID, "zone");
+ serializedDimensions.put(FetchVector.Dimension.HOSTNAME, "hostname");
+ serializedDimensions.put(FetchVector.Dimension.APPLICATION_ID, "application");
+
+ if (serializedDimensions.size() != FetchVector.Dimension.values().length) {
+ throw new IllegalStateException(FetchVectorHelper.class.getName() + " is not in sync with " +
+ FetchVector.Dimension.class.getName());
+ }
+ }
+
+ private static Map<String, FetchVector.Dimension> deserializedDimensions = serializedDimensions.
+ entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
+
+ public static String toWire(FetchVector.Dimension dimension) {
+ String serializedDimension = serializedDimensions.get(dimension);
+ if (serializedDimension == null) {
+ throw new IllegalArgumentException("Unsupported dimension (please add it): '" + dimension + "'");
+ }
+
+ return serializedDimension;
+ }
+
+ public static FetchVector.Dimension fromWire(String serializedDimension) {
+ FetchVector.Dimension dimension = deserializedDimensions.get(serializedDimension);
+ if (dimension == null) {
+ throw new IllegalArgumentException("Unknown serialized dimension: '" + serializedDimension + "'");
+ }
+
+ return dimension;
+ }
+
+ private DimensionHelper() { }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java
new file mode 100644
index 00000000000..834e4024a6a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java
@@ -0,0 +1,27 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.yahoo.vespa.flags.FetchVector;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class FetchVectorHelper {
+ public static Map<String, String> toWire(FetchVector vector) {
+ Map<FetchVector.Dimension, String> map = vector.toMap();
+ if (map.isEmpty()) return null;
+ return map.entrySet().stream().collect(Collectors.toMap(
+ entry -> DimensionHelper.toWire(entry.getKey()),
+ Map.Entry::getValue));
+ }
+
+ public static FetchVector fromWire(Map<String, String> wireMap) {
+ if (wireMap == null) return new FetchVector();
+ return FetchVector.fromMap(wireMap.entrySet().stream().collect(Collectors.toMap(
+ entry -> DimensionHelper.fromWire(entry.getKey()),
+ Map.Entry::getValue)));
+ }
+}
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
new file mode 100644
index 00000000000..31aa6200fdb
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
@@ -0,0 +1,19 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Set;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireCondition {
+ @JsonProperty("type") public String type;
+ @JsonProperty("dimension") public String dimension;
+ @JsonProperty("values") public Set<String> values;
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java
new file mode 100644
index 00000000000..b4a000d7b70
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java
@@ -0,0 +1,55 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireFlagData {
+ @JsonProperty("rules") public List<WireRule> rules;
+ @JsonProperty("attributes") public Map<String, String> defaultFetchVector;
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ public byte[] serializeToBytes() {
+ return uncheck(() -> mapper.writeValueAsBytes(this));
+ }
+
+ public String serializeToJson() {
+ return uncheck(() -> mapper.writeValueAsString(this));
+ }
+
+ public JsonNode serializeToJsonNode() {
+ return uncheck(() -> mapper.valueToTree(this));
+ }
+
+ public void serializeToOutputStream(OutputStream outputStream) {
+ uncheck(() -> mapper.writeValue(outputStream, this));
+ }
+
+ public static WireFlagData deserialize(byte[] bytes) {
+ return uncheck(() -> mapper.readValue(bytes, WireFlagData.class));
+ }
+
+ public static WireFlagData deserialize(String string) {
+ return uncheck(() -> mapper.readValue(string, WireFlagData.class));
+ }
+
+ public static WireFlagData deserialize(InputStream inputStream) {
+ return uncheck(() -> mapper.readValue(inputStream, WireFlagData.class));
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java
new file mode 100644
index 00000000000..38619e87488
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java
@@ -0,0 +1,19 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireRule {
+ @JsonProperty("conditions") public List<WireCondition> andConditions;
+ @JsonProperty("value") public JsonNode value;
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
index 9e7508706ed..7d9d7868308 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
@@ -5,9 +5,11 @@ import com.yahoo.vespa.test.file.TestFileSystem;
import org.junit.Test;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Optional;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
@@ -22,18 +24,18 @@ public class FileFlagSourceTest {
@Test
public void testFeatureLikeFlags() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
- FeatureFlag byDefaultTrue = new FeatureFlag(id, true, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
+ Flag<Boolean> byDefaultTrue = new Flag<>(id, true, source, Flags.BOOLEAN_SERIALIZER);
assertFalse(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "True\n");
+ writeFlag(id.toString(), "true\n");
assertTrue(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "False\n");
+ writeFlag(id.toString(), "false\n");
assertFalse(featureFlag.value());
assertFalse(byDefaultTrue.value());
@@ -41,35 +43,48 @@ public class FileFlagSourceTest {
@Test
public void testIntegerLikeFlags() throws IOException {
- StringFlag stringFlag = new StringFlag(id, "default", source);
- IntFlag intFlag = new IntFlag(id, -1, source);
- LongFlag longFlag = new LongFlag(id, -2L, source);
+ Flag<Integer> intFlag = new Flag<>(id, -1, source, Flags.INT_SERIALIZER);
+ Flag<Long> longFlag = new Flag<>(id, -2L, source, Flags.LONG_SERIALIZER);
- assertFalse(source.getString(id).isPresent());
- assertEquals("default", stringFlag.value());
- assertEquals(-1, intFlag.value());
- assertEquals(-2L, longFlag.value());
+ assertFalse(fetch().isPresent());
+ assertFalse(fetch().isPresent());
+ assertEquals(-1, (int) intFlag.value());
+ assertEquals(-2L, (long) longFlag.value());
writeFlag(id.toString(), "1\n");
- assertTrue(source.getString(id).isPresent());
+ assertTrue(fetch().isPresent());
+ assertTrue(fetch().isPresent());
+ assertEquals(1, (int) intFlag.value());
+ assertEquals(1L, (long) longFlag.value());
+ }
+
+ @Test
+ public void testStringFlag() throws IOException {
+ Flag<String> stringFlag = new Flag<>(id, "default", source, Flags.STRING_SERIALIZER);
+ assertFalse(fetch().isPresent());
+ assertEquals("default", stringFlag.value());
+
+ writeFlag(id.toString(), "\"1\\n\"\n");
assertEquals("1\n", stringFlag.value());
- assertEquals(1, intFlag.value());
- assertEquals(1L, longFlag.value());
}
@Test
public void parseFailure() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
writeFlag(featureFlag.id().toString(), "garbage");
try {
featureFlag.value();
- } catch (IllegalArgumentException e) {
+ } catch (UncheckedIOException e) {
assertThat(e.getMessage(), containsString("garbage"));
}
}
+ private Optional<RawFlag> fetch() {
+ return source.fetch(id, new FetchVector());
+ }
+
private void writeFlag(String flagId, String value) throws IOException {
Path featurePath = fileSystem.getPath(FileFlagSource.FLAGS_DIRECTORY).resolve(flagId);
Files.createDirectories(featurePath.getParent());
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
new file mode 100644
index 00000000000..4f7d797e07d
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
@@ -0,0 +1,131 @@
+// Copyright 2018 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.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsTest {
+ @Test
+ public void testBoolean() {
+ final boolean defaultValue = false;
+ FlagSource source = mock(FlagSource.class);
+ Flag<Boolean> booleanFlag = Flags.defineBoolean("id", defaultValue, "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME)
+ .with(FetchVector.Dimension.ZONE_ID, "a-zone")
+ .bindTo(source);
+ assertThat(booleanFlag.id().toString(), equalTo("id"));
+
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.empty());
+ // default value without raw flag
+ assertThat(booleanFlag.value(), equalTo(defaultValue));
+
+ ArgumentCaptor<FetchVector> vector = ArgumentCaptor.forClass(FetchVector.class);
+ verify(source).fetch(any(), vector.capture());
+ // hostname is set by default
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).isPresent(), is(true));
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).get(), is(not(emptyOrNullString())));
+ // zone is set because it was set on the unbound flag above
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.ZONE_ID), is(Optional.of("a-zone")));
+ // application is not set
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.empty()));
+
+ RawFlag rawFlag = mock(RawFlag.class);
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.of(rawFlag));
+ when(rawFlag.asJsonNode()).thenReturn(BooleanNode.getTrue());
+
+ // raw flag deserializes to true
+ assertThat(booleanFlag.with(FetchVector.Dimension.APPLICATION_ID, "an-app").value(), equalTo(true));
+
+ verify(source, times(2)).fetch(any(), vector.capture());
+ // application was set on the (bound) flag.
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.of("an-app")));
+ }
+
+ @Test
+ public void testString() {
+ testGeneric(Flags.defineString("string-id", "default value", "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME),
+ "default value", "other value");
+ }
+
+ @Test
+ public void testInt() {
+ testGeneric(Flags.defineInt("int-id", 2, "desc", "mod"), 2, 3);
+ }
+
+ @Test
+ public void testLong() {
+ testGeneric(Flags.defineLong("long-id", 1L, "desc", "mod"), 1L, 2L);
+ }
+
+ @Test
+ public void testJacksonClass() {
+ ExampleJacksonClass defaultInstance = new ExampleJacksonClass();
+ ExampleJacksonClass instance = new ExampleJacksonClass();
+ instance.integer = -2;
+ instance.string = "foo";
+
+ testGeneric(Flags.defineJackson("jackson-id", ExampleJacksonClass.class, defaultInstance,
+ "description", "modification effect", FetchVector.Dimension.HOSTNAME),
+ defaultInstance, instance);
+ }
+
+ private <T> void testGeneric(UnboundFlag<T> unboundFlag, T defaultValue, T value) {
+ FlagSource source = mock(FlagSource.class);
+ Flag<T> flag = unboundFlag.bindTo(source);
+
+ when(source.fetch(any(), any())).thenReturn(Optional.empty());
+ assertThat(flag.value(), equalTo(defaultValue));
+
+ when(source.fetch(any(), any())).thenReturn(Optional.of(JsonNodeRawFlag.fromJacksonClass(value)));
+ assertThat(flag.value(), equalTo(value));
+
+ assertTrue(Flags.getFlag(unboundFlag.id()).isPresent());
+ }
+
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private static class ExampleJacksonClass {
+ @JsonProperty("integer")
+ public int integer = 1;
+
+ @JsonProperty("string")
+ public String string = "2";
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ExampleJacksonClass that = (ExampleJacksonClass) o;
+ return integer == that.integer &&
+ Objects.equals(string, that.string);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(integer, string);
+ }
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java b/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
deleted file mode 100644
index 8bd486f5b47..00000000000
--- a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 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.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import org.junit.Test;
-
-import java.util.Objects;
-import java.util.Optional;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class JacksonFlagTest {
- private final FlagId id = new FlagId("id");
- private final ExampleJacksonClass defaultValue = new ExampleJacksonClass();
- private final FlagSource source = mock(FlagSource.class);
- private final JacksonFlag<ExampleJacksonClass> jacksonFlag = new JacksonFlag<>(id.toString(), ExampleJacksonClass.class, defaultValue, source);
-
- @Test
- public void unsetThenSet() {
- when(source.getString(id)).thenReturn(Optional.empty());
- ExampleJacksonClass value = jacksonFlag.value();
- assertEquals(1, value.integer);
- assertEquals("2", value.string);
- assertEquals("3", value.dummy);
-
- when(source.getString(id)).thenReturn(Optional.of("{\"integer\": 4, \"string\": \"foo\", \"stray\": 6}"));
- value = jacksonFlag.value();
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
-
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- private static class ExampleJacksonClass {
- @JsonProperty("integer")
- public int integer = 1;
-
- @JsonProperty("string")
- public String string = "2";
-
- @JsonProperty("dummy")
- public String dummy = "3";
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ExampleJacksonClass that = (ExampleJacksonClass) o;
- return integer == that.integer &&
- Objects.equals(string, that.string) &&
- Objects.equals(dummy, that.dummy);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(integer, string, dummy);
- }
- }
-} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java
new file mode 100644
index 00000000000..5465f89e2eb
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java
@@ -0,0 +1,50 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class OrderedFlagSourceTest {
+ @Test
+ public void test() {
+ FlagSource source1 = mock(FlagSource.class);
+ FlagSource source2 = mock(FlagSource.class);
+ OrderedFlagSource orderedSource = new OrderedFlagSource(source1, source2);
+
+ FlagId id = new FlagId("id");
+ FetchVector vector = new FetchVector();
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertFalse(orderedSource.fetch(id, vector).isPresent());
+ verify(source1, times(1)).fetch(any(), any());
+ verify(source2, times(1)).fetch(any(), any());
+
+ RawFlag rawFlag = mock(RawFlag.class);
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(2)).fetch(any(), any());
+ verify(source2, times(2)).fetch(any(), any());
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(3)).fetch(any(), any());
+ // Not invoked as source1 provided raw flag
+ verify(source2, times(2)).fetch(any(), any());
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
new file mode 100644
index 00000000000..96cbce71fa8
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. 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 org.junit.Test;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class ConditionTest {
+ @Test
+ public void testWhitelist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertFalse(condition.test(new FetchVector()));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+
+ @Test
+ public void testBlacklist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.BLACKLIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertTrue(condition.test(new FetchVector()));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
new file mode 100644
index 00000000000..2eb12e53ddc
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
@@ -0,0 +1,82 @@
+// Copyright 2018 Yahoo Holdings. 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.RawFlag;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class FlagDataTest {
+ private final String json = "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"host1\", \"host2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"app1\", \"app2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " },\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"values\": [ \"zone1\", \"zone2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": false\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"zone1\"\n" +
+ " }\n" +
+ "}";
+
+ private final FetchVector vector = new FetchVector();
+
+ @Test
+ public void test() {
+ // Second rule matches with the default zone matching
+ verify(Optional.of("false"), vector);
+
+ // First rule matches only if both conditions match
+ verify(Optional.of("false"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app2"));
+ verify(Optional.of("true"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app3"));
+
+ // No rules apply if zone is overridden to an unknown zone
+ verify(Optional.empty(), vector.with(FetchVector.Dimension.ZONE_ID, "unknown zone"));
+ }
+
+ private void verify(Optional<String> expectedValue, FetchVector vector) {
+ FlagData data = FlagData.deserialize(json);
+ Optional<RawFlag> rawFlag = data.resolve(vector);
+
+ if (expectedValue.isPresent()) {
+ assertTrue(rawFlag.isPresent());
+ assertEquals(expectedValue.get(), rawFlag.get().asJson());
+ } else {
+ assertFalse(rawFlag.isPresent());
+ }
+
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
new file mode 100644
index 00000000000..f3f8c147212
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
@@ -0,0 +1,130 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+import com.yahoo.vespa.flags.json.wire.WireFlagData;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+/**
+ * @author hakonhall
+ */
+public class SerializationTest {
+ @Test
+ public void emptyJson() throws IOException {
+ String json = "{}";
+ WireFlagData wireData = WireFlagData.deserialize(json);
+ assertThat(wireData.defaultFetchVector, nullValue());
+ assertThat(wireData.rules, nullValue());
+ assertThat(wireData.serializeToJson(), equalTo(json));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{}"));
+ }
+
+ @Test
+ public void deserialization() throws IOException {
+ String json = "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"a1\", \"a2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"h1\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"z1\",\n" +
+ " \"application\": \"a1\",\n" +
+ " \"hostname\": \"h1\"\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ // rule
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(2));
+ assertThat(wireData.rules.get(0).value.getNodeType(), equalTo(JsonNodeType.BOOLEAN));
+ assertThat(wireData.rules.get(0).value.asBoolean(), equalTo(true));
+ // first condition
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("application"));
+ assertThat(whitelistCondition.values, equalTo(new HashSet<>(Arrays.asList("a1", "a2"))));
+ // second condition
+ WireCondition blacklistCondition = wireData.rules.get(0).andConditions.get(1);
+ assertThat(blacklistCondition.type, equalTo("blacklist"));
+ assertThat(blacklistCondition.dimension, equalTo("hostname"));
+ assertThat(blacklistCondition.values, equalTo(new HashSet<>(Arrays.asList("h1"))));
+
+ // attributes
+ assertThat(wireData.defaultFetchVector, notNullValue());
+ assertThat(wireData.defaultFetchVector.get("zone"), equalTo("z1"));
+ assertThat(wireData.defaultFetchVector.get("application"), equalTo("a1"));
+ assertThat(wireData.defaultFetchVector.get("hostname"), equalTo("h1"));
+
+ // Verify serialization of RawFlag == serialization by ObjectMapper
+ ObjectMapper mapper = new ObjectMapper();
+ String serializedWithObjectMapper = mapper.writeValueAsString(mapper.readTree(json));
+ assertThat(wireData.serializeToJson(), equalTo(serializedWithObjectMapper));
+
+ // Unfortunately the order of attributes members are different...
+ // assertThat(FlagData.deserialize(json).serializeToJson(), equalTo(serializedWithObjectMapper));
+ }
+
+ @Test
+ public void jsonWithStrayFields() {
+ String json = "{\n" +
+ " \"foo\": true,\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"bar\": \"zoo\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"other\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(1));
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("zone"));
+ assertThat(whitelistCondition.values, nullValue());
+ assertThat(wireData.rules.get(0).value, nullValue());
+ assertThat(wireData.defaultFetchVector, anEmptyMap());
+
+ assertThat(wireData.serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}],\"attributes\":{}}"));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}]}"));
+ }
+}