diff options
author | Håkon Hallingstad <hakon@oath.com> | 2019-01-03 19:54:11 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@oath.com> | 2019-01-03 19:54:11 +0100 |
commit | cf70bfb260fa8731617a327b56be433128aa0450 (patch) | |
tree | ddf55127282b97088ce301c51b46737c5ca7692e /flags | |
parent | 5d9957af203d16c448ca34a9d167e3587b3c7820 (diff) |
Typed flag classes
This reintroduces the non-generic flag classes:
- a value() returns the primitive type for flags wrapping a primitive type
- easier to use in testing
- Serializer is moved to internals of typed class
Defines the flag backed by boolean BooleanFlag instead of FeatureFlag since not
all boolean flags are necessarily guarding a feature.
Diffstat (limited to 'flags')
20 files changed, 452 insertions, 157 deletions
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/BooleanFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/BooleanFlag.java new file mode 100644 index 00000000000..1245aca65b8 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/BooleanFlag.java @@ -0,0 +1,18 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class BooleanFlag extends FlagImpl<Boolean, BooleanFlag> { + public BooleanFlag(FlagId id, boolean defaultValue, FetchVector vector, FlagSerializer<Boolean> serializer, FlagSource source) { + super(id, defaultValue, vector, serializer, source, BooleanFlag::new); + } + + public boolean value() { + return boxedValue(); + } +} 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 18c1db0d756..7bf491a39c7 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java @@ -1,44 +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; -import javax.annotation.concurrent.Immutable; - /** + * Interface for flag. + * + * @param <T> The type of the flag value (boxed for primitives) + * @param <F> The concrete subclass type of the flag * @author hakonhall */ -@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 interface Flag<T, F> { + /** The flag ID. */ + FlagId id(); - public FlagId id() { - return id; - } + /** Returns the flag serializer. */ + FlagSerializer<T> serializer(); - public Flag<T> with(FetchVector.Dimension dimension, String value) { - return new Flag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value), source); - } + /** Returns an immutable clone of the current object, except with the dimension set accordingly. */ + F with(FetchVector.Dimension dimension, String dimensionValue); - public T value() { - return source.fetch(id, fetchVector).map(deserializer::deserialize).orElse(defaultValue); - } + /** Returns the value, boxed if the flag wraps a primitive type. */ + T boxedValue(); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java index a3f490e2f96..0dce61cf0bb 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java @@ -1,7 +1,8 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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 javax.annotation.concurrent.Immutable; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -9,21 +10,21 @@ import java.util.List; * @author hakonhall */ @Immutable -public class FlagDefinition<T> { - private final UnboundFlag<T> unboundFlag; +public class FlagDefinition { + private final UnboundFlag<?, ?, ?> 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) { + public FlagDefinition(UnboundFlag<?, ?, ?> unboundFlag, String description, String modificationEffect, + FetchVector.Dimension... dimensions) { this.unboundFlag = unboundFlag; this.description = description; this.modificationEffect = modificationEffect; - this.dimensions = Collections.unmodifiableList(dimensions); + this.dimensions = Collections.unmodifiableList(Arrays.asList(dimensions)); } - public UnboundFlag<T> getUnboundFlag() { + public UnboundFlag<?, ?, ?> getUnboundFlag() { return unboundFlag; } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagImpl.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagImpl.java new file mode 100644 index 00000000000..79da03e4982 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagImpl.java @@ -0,0 +1,47 @@ +// 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; + +/** + * @author hakonhall + */ +public abstract class FlagImpl<T, F extends FlagImpl<T, F>> implements Flag<T, F> { + private final FlagId id; + private final T defaultValue; + private final FlagSerializer<T> serializer; + private final FetchVector fetchVector; + private final UnboundFlagImpl.FlagFactory<T, F> factory; + private final FlagSource source; + + protected FlagImpl(FlagId id, + T defaultValue, + FetchVector fetchVector, FlagSerializer<T> serializer, + FlagSource source, + UnboundFlagImpl.FlagFactory<T, F> factory) { + this.id = id; + this.defaultValue = defaultValue; + this.serializer = serializer; + this.fetchVector = fetchVector; + this.factory = factory; + this.source = source; + } + + @Override + public FlagId id() { + return id; + } + + @Override + public F with(FetchVector.Dimension dimension, String dimensionValue) { + return factory.create(id, defaultValue, fetchVector.with(dimension, dimensionValue), serializer, source); + } + + @Override + public T boxedValue() { + return source.fetch(id, fetchVector).map(serializer::deserialize).orElse(defaultValue); + } + + @Override + public FlagSerializer<T> serializer() { + return serializer; + } +} 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 50cb5ffdf7c..50f65e5ce13 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -1,53 +1,33 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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.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 volatile ConcurrentHashMap<FlagId, FlagDefinition> flags = new ConcurrentHashMap<>(); - private static volatile ConcurrentHashMap<FlagId, FlagDefinition<?>> flags = new ConcurrentHashMap<>(); - - public static final UnboundFlag<Boolean> HEALTHMONITOR_MONITOR_INFRA = defineBoolean( + public static final UnboundBooleanFlag HEALTHMONITOR_MONITOR_INFRA = defineFeatureFlag( "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.", + "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( + public static final UnboundBooleanFlag DUPERMODEL_CONTAINS_INFRA = defineFeatureFlag( "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( + public static final UnboundBooleanFlag DUPERMODEL_USE_CONFIGSERVERCONFIG = defineFeatureFlag( "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 " + @@ -55,75 +35,86 @@ public class Flags { "Requires restart of config server/controller to take effect.", FetchVector.Dimension.HOSTNAME); - public static final UnboundFlag<Boolean> USE_CONFIG_SERVER_CACHE = defineBoolean( + public static final UnboundBooleanFlag USE_CONFIG_SERVER_CACHE = defineFeatureFlag( "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( + public static final UnboundBooleanFlag CONFIG_SERVER_BOOTSTRAP_IN_SEPARATE_THREAD = defineFeatureFlag( "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 final UnboundFlag<Boolean> PROXYHOST_USES_REAL_ORCHESTRATOR = defineBoolean( + public static final UnboundBooleanFlag PROXYHOST_USES_REAL_ORCHESTRATOR = defineFeatureFlag( "proxyhost-uses-real-orchestrator", true, "Whether proxy hosts uses the real Orchestrator when suspending/resuming, or a synthetic.", "Takes effect immediately when changed.", FetchVector.Dimension.HOSTNAME); - public static final UnboundFlag<Boolean> CONFIGHOST_USES_REAL_ORCHESTRATOR = defineBoolean( + public static final UnboundBooleanFlag CONFIGHOST_USES_REAL_ORCHESTRATOR = defineFeatureFlag( "confighost-uses-real-orchestrator", false, "Whether the config server hosts uses the real Orchestrator when suspending/resuming, or a synthetic.", "Takes effect immediately when changed.", FetchVector.Dimension.HOSTNAME); - public static final UnboundFlag<Boolean> ENABLE_DOCKER_1_13 = defineBoolean( + public static final UnboundBooleanFlag ENABLE_DOCKER_1_13 = defineFeatureFlag( "enable-docker-1.13", true, "Whether to upgrade to Docker version 1.13.", "Takes effect on next host admin tick.", FetchVector.Dimension.HOSTNAME); - public static final UnboundFlag<Boolean> ENABLE_CROWDSTRIKE = defineBoolean( + public static final UnboundBooleanFlag ENABLE_CROWDSTRIKE = defineFeatureFlag( "enable-crowdstrike", true, "Whether to enable CrowdStrike.", "Takes effect on next host admin tick"); - public static final UnboundFlag<Boolean> ENABLE_NESSUS = defineBoolean( + public static final UnboundBooleanFlag ENABLE_NESSUS = defineFeatureFlag( "enable-nessus", true, "Whether to enable Nessus.", "Takes effect on next host admin tick"); - public static UnboundFlag<Boolean> defineBoolean(String flagId, boolean defaultValue, String description, + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, String description, + String modificationEffect, FetchVector.Dimension... dimensions) { + return define(UnboundBooleanFlag::new, flagId, defaultValue, description, modificationEffect, dimensions); + } + + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static UnboundStringFlag defineStringFlag(String flagId, String defaultValue, String description, String modificationEffect, FetchVector.Dimension... dimensions) { - return define(flagId, defaultValue, BOOLEAN_SERIALIZER, description, modificationEffect, dimensions); + return define(UnboundStringFlag::new, flagId, defaultValue, 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); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static UnboundIntFlag defineIntFlag(String flagId, int defaultValue, String description, + String modificationEffect, FetchVector.Dimension... dimensions) { + return define(UnboundIntFlag::new, flagId, defaultValue, description, modificationEffect, dimensions); } - public static UnboundFlag<Integer> defineInt(String flagId, Integer defaultValue, String description, + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static UnboundLongFlag defineLongFlag(String flagId, long defaultValue, String description, String modificationEffect, FetchVector.Dimension... dimensions) { - return define(flagId, defaultValue, INT_SERIALIZER, description, modificationEffect, dimensions); + return define(UnboundLongFlag::new, flagId, defaultValue, 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); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static <T> UnboundJacksonFlag<T> defineJacksonFlag(String flagId, T defaultValue, Class<T> jacksonClass, String description, + String modificationEffect, FetchVector.Dimension... dimensions) { + return define((id2, defaultValue2, vector2) -> new UnboundJacksonFlag<>(id2, defaultValue2, vector2, jacksonClass), + flagId, defaultValue, 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); + @FunctionalInterface + private interface TypedUnboundFlagFactory<T, U extends UnboundFlag<?, ?, ?>> { + U create(FlagId id, T defaultVale, FetchVector defaultFetchVector); } /** * Defines a Flag. * + * @param factory Factory for creating unbound flag of type U * @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 @@ -132,34 +123,46 @@ public class Flags { * 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. + * @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 * 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; + private static <T, U extends UnboundFlag<?, ?, ?>> U define(TypedUnboundFlagFactory<T, U> factory, + String flagId, + T defaultValue, + String description, + String modificationEffect, + FetchVector.Dimension[] dimensions) { + FlagId id = new FlagId(flagId); + FetchVector vector = new FetchVector().with(FetchVector.Dimension.HOSTNAME, Defaults.getDefaults().vespaHostname()); + U unboundFlag = factory.create(id, defaultValue, vector); + FlagDefinition definition = new FlagDefinition(unboundFlag, description, modificationEffect, dimensions); + flags.put(id, definition); + return unboundFlag; } - public static List<FlagDefinition<?>> getAllFlags() { + public static List<FlagDefinition> getAllFlags() { return new ArrayList<>(flags.values()); } - public static Optional<FlagDefinition<?>> getFlag(FlagId flagId) { + public static Optional<FlagDefinition> getFlag(FlagId flagId) { return Optional.ofNullable(flags.get(flagId)); } + /** + * Allows the statically defined flags to be controlled in a test. + * + * <p>Returns a Replacer instance to be used with e.g. a try-with-resources block. Within the block, + * the flags starts out as cleared. Flags can be defined, etc. When leaving the block, the flags from + * before the block is reinserted. + * */ public static Replacer clearFlagsForTesting() { return new Replacer(); } public static class Replacer implements AutoCloseable { - private final ConcurrentHashMap<FlagId, FlagDefinition<?>> savedFlags; + private final ConcurrentHashMap<FlagId, FlagDefinition> savedFlags; private Replacer() { this.savedFlags = Flags.flags; diff --git a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java new file mode 100644 index 00000000000..4a59c34e8d0 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java @@ -0,0 +1,18 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class IntFlag extends FlagImpl<Integer, IntFlag> { + public IntFlag(FlagId id, int defaultValue, FetchVector vector, FlagSerializer<Integer> serializer, FlagSource source) { + super(id, defaultValue, vector, serializer, source, IntFlag::new); + } + + public int value() { + return boxedValue(); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java new file mode 100644 index 00000000000..e4afbd231a1 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java @@ -0,0 +1,18 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class JacksonFlag<T> extends FlagImpl<T, JacksonFlag<T>> { + public JacksonFlag(FlagId id, T defaultValue, FetchVector vector, FlagSerializer<T> serializer, FlagSource source) { + super(id, defaultValue, vector, serializer, source, JacksonFlag::new); + } + + public T value() { + return boxedValue(); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java new file mode 100644 index 00000000000..8a1707c9e6a --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java @@ -0,0 +1,18 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class LongFlag extends FlagImpl<Long, LongFlag> { + public LongFlag(FlagId id, Long defaultValue, FetchVector vector, FlagSerializer<Long> serializer, FlagSource source) { + super(id, defaultValue, vector, serializer, source, LongFlag::new); + } + + public long value() { + return boxedValue(); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java index 6ca74715999..1c6e113f489 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java @@ -15,7 +15,6 @@ public class OrderedFlagSource implements FlagSource { private final List<FlagSource> sources; /** - * * @param sources Flag sources in descending priority order. */ public OrderedFlagSource(FlagSource... sources) { diff --git a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java new file mode 100644 index 00000000000..925af130b95 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java @@ -0,0 +1,18 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class StringFlag extends FlagImpl<String, StringFlag> { + public StringFlag(FlagId id, String defaultValue, FetchVector vector, FlagSerializer<String> serializer, FlagSource source) { + super(id, defaultValue, vector, serializer, source, StringFlag::new); + } + + public String value() { + return boxedValue(); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundBooleanFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundBooleanFlag.java new file mode 100644 index 00000000000..c228364434c --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundBooleanFlag.java @@ -0,0 +1,27 @@ +// 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.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.BooleanNode; + +import javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class UnboundBooleanFlag extends UnboundFlagImpl<Boolean, BooleanFlag, UnboundBooleanFlag> { + public UnboundBooleanFlag(FlagId id) { + this(id, false); + } + + public UnboundBooleanFlag(FlagId id, boolean defaultValue) { + this(id, defaultValue, new FetchVector()); + } + + public UnboundBooleanFlag(FlagId id, boolean defaultValue, FetchVector defaultFetchVector) { + super(id, defaultValue, defaultFetchVector, + new SimpleFlagSerializer<>(BooleanNode::valueOf, JsonNode::isBoolean, JsonNode::asBoolean), + UnboundBooleanFlag::new, BooleanFlag::new); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java index 597e19080ab..28307536233 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java @@ -1,38 +1,27 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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 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; - } +/** + * Interface of an unbound flag. + * + * @param <T> Type of boxed value, e.g. Integer + * @param <F> Type of flag, e.g. IntFlag + * @param <U> Type of unbound flag, e.g. UnboundIntFlag + */ +public interface UnboundFlag<T, F extends Flag<T, F>, U extends UnboundFlag<T, F, U>> { + /** The flag ID. */ + FlagId id(); - public FlagId id() { - return id; - } + /** Returns the flag serializer. */ + FlagSerializer<T> serializer(); - public UnboundFlag<T> with(FetchVector.Dimension dimension, String value) { - return new UnboundFlag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value)); - } + /** Returns a clone of the unbound flag, but with the dimension set accordingly. */ + U with(FetchVector.Dimension dimension, String dimensionValue); - public Flag<T> bindTo(FlagSource source) { - return new Flag<>(id, defaultValue, deserializer, fetchVector, source); - } + /** Binds to a flag source, returning a (bound) flag. */ + F bindTo(FlagSource source); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlagImpl.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlagImpl.java new file mode 100644 index 00000000000..513712d8753 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlagImpl.java @@ -0,0 +1,58 @@ +// 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; + +/** + * @author hakonhall + */ +public abstract class UnboundFlagImpl<T, F extends Flag<T, F>, U extends UnboundFlag<T, F, U>> + implements UnboundFlag<T, F, U> { + + private final FlagId id; + private final T defaultValue; + private final FlagSerializer<T> serializer; + private final FetchVector defaultFetchVector; + private final UnboundFlagFactory<T, F, U> unboundFlagFactory; + private final FlagFactory<T, F> flagFactory; + + public interface UnboundFlagFactory<T1, F1 extends Flag<T1, F1>, U1 extends UnboundFlag<T1, F1, U1>> { + U1 create(FlagId id, T1 defaultValue, FetchVector fetchVector); + } + + public interface FlagFactory<T2, F2 extends Flag<T2, F2>> { + F2 create(FlagId id, T2 defaultValue, FetchVector fetchVector, FlagSerializer<T2> serializer, FlagSource source); + } + + protected UnboundFlagImpl(FlagId id, + T defaultValue, + FetchVector defaultFetchVector, + FlagSerializer<T> serializer, + UnboundFlagFactory<T, F, U> unboundFlagFactory, + FlagFactory<T, F> flagFactory) { + this.id = id; + this.defaultValue = defaultValue; + this.serializer = serializer; + this.defaultFetchVector = defaultFetchVector; + this.unboundFlagFactory = unboundFlagFactory; + this.flagFactory = flagFactory; + } + + @Override + public FlagId id() { + return id; + } + + @Override + public U with(FetchVector.Dimension dimension, String dimensionValue) { + return unboundFlagFactory.create(id, defaultValue, defaultFetchVector.with(dimension, dimensionValue)); + } + + @Override + public F bindTo(FlagSource source) { + return flagFactory.create(id, defaultValue, defaultFetchVector, serializer, source); + } + + @Override + public FlagSerializer<T> serializer() { + return serializer; + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundIntFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundIntFlag.java new file mode 100644 index 00000000000..01b660fe625 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundIntFlag.java @@ -0,0 +1,23 @@ +// 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.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.IntNode; + +import javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class UnboundIntFlag extends UnboundFlagImpl<Integer, IntFlag, UnboundIntFlag> { + public UnboundIntFlag(FlagId id, int defaultValue) { + this(id, defaultValue, new FetchVector()); + } + + public UnboundIntFlag(FlagId id, int defaultValue, FetchVector defaultFetchVector) { + super(id, defaultValue, defaultFetchVector, + new SimpleFlagSerializer<>(IntNode::new, JsonNode::isIntegralNumber, JsonNode::asInt), + UnboundIntFlag::new, IntFlag::new); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundJacksonFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundJacksonFlag.java new file mode 100644 index 00000000000..1cd303d1ede --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundJacksonFlag.java @@ -0,0 +1,21 @@ +// 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 javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class UnboundJacksonFlag<T> extends UnboundFlagImpl<T, JacksonFlag<T>, UnboundJacksonFlag<T>> { + public UnboundJacksonFlag(FlagId id, T defaultValue, Class<T> jacksonClass) { + this(id, defaultValue, new FetchVector(), jacksonClass); + } + + public UnboundJacksonFlag(FlagId id, T defaultValue, FetchVector defaultFetchVector, Class<T> jacksonClass) { + super(id, defaultValue, defaultFetchVector, + new JacksonSerializer<>(jacksonClass), + (id2, defaultValue2, vector2) -> new UnboundJacksonFlag<>(id2, defaultValue2, vector2, jacksonClass), + JacksonFlag::new); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundLongFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundLongFlag.java new file mode 100644 index 00000000000..732a9073fc8 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundLongFlag.java @@ -0,0 +1,23 @@ +// 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.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.LongNode; + +import javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class UnboundLongFlag extends UnboundFlagImpl<Long, LongFlag, UnboundLongFlag> { + public UnboundLongFlag(FlagId id, long defaultValue) { + this(id, defaultValue, new FetchVector()); + } + + public UnboundLongFlag(FlagId id, Long defaultValue, FetchVector defaultFetchVector) { + super(id, defaultValue, defaultFetchVector, + new SimpleFlagSerializer<>(LongNode::new, JsonNode::isIntegralNumber, JsonNode::asLong), + UnboundLongFlag::new, LongFlag::new); + } +} diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java new file mode 100644 index 00000000000..67c4bceb3e9 --- /dev/null +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java @@ -0,0 +1,23 @@ +// 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.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import javax.annotation.concurrent.Immutable; + +/** + * @author hakonhall + */ +@Immutable +public class UnboundStringFlag extends UnboundFlagImpl<String, StringFlag, UnboundStringFlag> { + public UnboundStringFlag(FlagId id, String defaultValue) { + this(id, defaultValue, new FetchVector()); + } + + public UnboundStringFlag(FlagId id, String defaultValue, FetchVector defaultFetchVector) { + super(id, defaultValue, defaultFetchVector, + new SimpleFlagSerializer<>(TextNode::new, JsonNode::isTextual, JsonNode::asText), + UnboundStringFlag::new, StringFlag::new); + } +} 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 index bfd959e82f9..b5bc2032bae 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.flags.json; import com.fasterxml.jackson.databind.JsonNode; +import com.yahoo.vespa.flags.Deserializer; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagId; import com.yahoo.vespa.flags.FlagSource; @@ -16,9 +17,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A data structure containing all data for a single flag, that can be serialized to/from JSON, @@ -100,8 +101,12 @@ public class FlagData { } private static FlagData fromWire(WireFlagData wireFlagData) { + if (wireFlagData.id == null) { + throw new IllegalArgumentException("Flag ID missing"); + } + return new FlagData( - new FlagId(Objects.requireNonNull(wireFlagData.id)), + new FlagId(wireFlagData.id), FetchVectorHelper.fromWire(wireFlagData.defaultFetchVector), rulesFromWire(wireFlagData.rules) ); @@ -111,5 +116,13 @@ public class FlagData { if (wireRules == null) return Collections.emptyList(); return wireRules.stream().map(Rule::fromWire).collect(Collectors.toList()); } + + /** E.g. verify all RawFlag can be deserialized. */ + public void validate(Deserializer<?> deserializer) { + rules.stream() + .flatMap(rule -> rule.getValueToApply().map(Stream::of).orElse(null)) + .forEach(deserializer::deserialize); + + } } 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 7d9d7868308..e19436069b5 100644 --- a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java +++ b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java @@ -24,44 +24,44 @@ public class FileFlagSourceTest { @Test public void testFeatureLikeFlags() throws IOException { - Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER); - Flag<Boolean> byDefaultTrue = new Flag<>(id, true, source, Flags.BOOLEAN_SERIALIZER); + BooleanFlag booleanFlag = new UnboundBooleanFlag(id).bindTo(source); + BooleanFlag byDefaultTrue = new UnboundBooleanFlag(id, true).bindTo(source); - assertFalse(featureFlag.value()); + assertFalse(booleanFlag.value()); assertTrue(byDefaultTrue.value()); writeFlag(id.toString(), "true\n"); - assertTrue(featureFlag.value()); + assertTrue(booleanFlag.value()); assertTrue(byDefaultTrue.value()); writeFlag(id.toString(), "false\n"); - assertFalse(featureFlag.value()); + assertFalse(booleanFlag.value()); assertFalse(byDefaultTrue.value()); } @Test public void testIntegerLikeFlags() throws IOException { - Flag<Integer> intFlag = new Flag<>(id, -1, source, Flags.INT_SERIALIZER); - Flag<Long> longFlag = new Flag<>(id, -2L, source, Flags.LONG_SERIALIZER); + IntFlag intFlag = new UnboundIntFlag(id, -1).bindTo(source); + LongFlag longFlag = new UnboundLongFlag(id, -2L).bindTo(source); assertFalse(fetch().isPresent()); assertFalse(fetch().isPresent()); - assertEquals(-1, (int) intFlag.value()); - assertEquals(-2L, (long) longFlag.value()); + assertEquals(-1, intFlag.value()); + assertEquals(-2L, longFlag.value()); writeFlag(id.toString(), "1\n"); assertTrue(fetch().isPresent()); assertTrue(fetch().isPresent()); - assertEquals(1, (int) intFlag.value()); - assertEquals(1L, (long) longFlag.value()); + assertEquals(1, intFlag.value()); + assertEquals(1L, longFlag.value()); } @Test public void testStringFlag() throws IOException { - Flag<String> stringFlag = new Flag<>(id, "default", source, Flags.STRING_SERIALIZER); + StringFlag stringFlag = new UnboundStringFlag(id, "default").bindTo(source); assertFalse(fetch().isPresent()); assertEquals("default", stringFlag.value()); @@ -71,11 +71,11 @@ public class FileFlagSourceTest { @Test public void parseFailure() throws IOException { - Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER); - writeFlag(featureFlag.id().toString(), "garbage"); + BooleanFlag booleanFlag = new UnboundBooleanFlag(id).bindTo(source); + writeFlag(booleanFlag.id().toString(), "garbage"); try { - featureFlag.value(); + booleanFlag.value(); } catch (UncheckedIOException e) { assertThat(e.getMessage(), containsString("garbage")); } diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java index 4f7d797e07d..591198f5e50 100644 --- a/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java +++ b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java @@ -31,7 +31,7 @@ public class FlagsTest { public void testBoolean() { final boolean defaultValue = false; FlagSource source = mock(FlagSource.class); - Flag<Boolean> booleanFlag = Flags.defineBoolean("id", defaultValue, "description", + BooleanFlag booleanFlag = Flags.defineFeatureFlag("id", defaultValue, "description", "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME) .with(FetchVector.Dimension.ZONE_ID, "a-zone") .bindTo(source); @@ -65,19 +65,19 @@ public class FlagsTest { @Test public void testString() { - testGeneric(Flags.defineString("string-id", "default value", "description", + testGeneric(Flags.defineStringFlag("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); + testGeneric(Flags.defineIntFlag("int-id", 2, "desc", "mod"), 2, 3); } @Test public void testLong() { - testGeneric(Flags.defineLong("long-id", 1L, "desc", "mod"), 1L, 2L); + testGeneric(Flags.defineLongFlag("long-id", 1L, "desc", "mod"), 1L, 2L); } @Test @@ -87,25 +87,24 @@ public class FlagsTest { instance.integer = -2; instance.string = "foo"; - testGeneric(Flags.defineJackson("jackson-id", ExampleJacksonClass.class, defaultInstance, + testGeneric(Flags.defineJacksonFlag("jackson-id", defaultInstance, ExampleJacksonClass.class, "description", "modification effect", FetchVector.Dimension.HOSTNAME), defaultInstance, instance); } - private <T> void testGeneric(UnboundFlag<T> unboundFlag, T defaultValue, T value) { + private <T> void testGeneric(UnboundFlag<?, ?, ?> unboundFlag, T defaultValue, T value) { FlagSource source = mock(FlagSource.class); - Flag<T> flag = unboundFlag.bindTo(source); + Flag<?, ?> flag = unboundFlag.bindTo(source); when(source.fetch(any(), any())).thenReturn(Optional.empty()); - assertThat(flag.value(), equalTo(defaultValue)); + assertThat(flag.boxedValue(), equalTo(defaultValue)); when(source.fetch(any(), any())).thenReturn(Optional.of(JsonNodeRawFlag.fromJacksonClass(value))); - assertThat(flag.value(), equalTo(value)); + assertThat(flag.boxedValue(), equalTo(value)); assertTrue(Flags.getFlag(unboundFlag.id()).isPresent()); } - @JsonIgnoreProperties(ignoreUnknown = true) private static class ExampleJacksonClass { @JsonProperty("integer") |