diff --git a/evcache-core/src/main/java/com/netflix/evcache/EVCacheImpl.java b/evcache-core/src/main/java/com/netflix/evcache/EVCacheImpl.java index dadc5a38..f9e9084a 100644 --- a/evcache-core/src/main/java/com/netflix/evcache/EVCacheImpl.java +++ b/evcache-core/src/main/java/com/netflix/evcache/EVCacheImpl.java @@ -6,6 +6,7 @@ import com.netflix.archaius.api.PropertyRepository; import com.netflix.evcache.EVCacheInMemoryCache.DataNotFoundException; import com.netflix.evcache.EVCacheLatch.Policy; +import com.netflix.evcache.config.EVCacheTranscoderProperties; import com.netflix.evcache.dto.KeyMapDto; import com.netflix.evcache.event.EVCacheEvent; import com.netflix.evcache.event.EVCacheEventListener; @@ -76,8 +77,7 @@ public class EVCacheImpl implements EVCache, EVCacheImplMBean { private static final Logger log = LoggerFactory.getLogger(EVCacheImpl.class); - - private static final int ENVELOPE_COMPRESSION_DISABLED = Integer.MAX_VALUE; + private static final int DEFAULT_COMPRESSION_THRESHOLD = Integer.MAX_VALUE; private final Clock clock; private final String _appName; @@ -166,12 +166,13 @@ public class EVCacheImpl implements EVCache, EVCacheImplMBean { this.maxHashLength = propertyRepository.get(appName + ".max.hash.length", Integer.class).orElse(-1); this.encoderBase = propertyRepository.get(appName + ".hash.encoder", String.class).orElse("base64"); this.autoHashKeys = propertyRepository.get(_appName + ".auto.hash.keys", Boolean.class).orElseGet("evcache.auto.hash.keys").orElse(false); - // Whether the EVCacheValue envelope (hashed keys) is written using the compact binary format - // instead of native Java serialization. - final boolean useBinarySerialization = propertyRepository.get(_appName + ".envelope.binary.serialization.enabled", Boolean.class) - .orElseGet("evcache.envelope.binary.serialization.enabled").orElse(false).get(); + // EVCacheValue envelope (hashed-key path) transcoder. The binary-vs-Java-OOS encoding + // switch is resolved through EVCacheTranscoderProperties; max size is read inline and + // compression is held at DEFAULT_COMPRESSION_THRESHOLD (Integer.MAX_VALUE) so the + // envelope's leading magic byte stays untouched on the wire. + final EVCacheTranscoderProperties evCacheTranscoderProperties = new EVCacheTranscoderProperties(_appName, propertyRepository); final int maxValueSize = propertyRepository.get("default.evcache.max.data.size", Integer.class).orElse(20 * 1024 * 1024).get(); - this.evcacheValueTranscoder = new EVCacheTranscoder(maxValueSize, ENVELOPE_COMPRESSION_DISABLED, useBinarySerialization); + this.evcacheValueTranscoder = new EVCacheTranscoder(maxValueSize, DEFAULT_COMPRESSION_THRESHOLD, evCacheTranscoderProperties); // default max key length is 200, instead of using what is defined in MemcachedClientIF.MAX_KEY_LENGTH (250). This is to accommodate // auto key prepend with appname for duet feature. diff --git a/evcache-core/src/main/java/com/netflix/evcache/EVCacheTranscoder.java b/evcache-core/src/main/java/com/netflix/evcache/EVCacheTranscoder.java index 789dd100..e5e13feb 100644 --- a/evcache-core/src/main/java/com/netflix/evcache/EVCacheTranscoder.java +++ b/evcache-core/src/main/java/com/netflix/evcache/EVCacheTranscoder.java @@ -1,5 +1,6 @@ package com.netflix.evcache; +import com.netflix.evcache.config.EVCacheTranscoderProperties; import com.netflix.evcache.pool.EVCacheValue; import com.netflix.evcache.pool.EVCacheValueSerde; import com.netflix.evcache.util.EVCacheConfig; @@ -8,7 +9,7 @@ public class EVCacheTranscoder extends EVCacheSerializingTranscoder { - private final boolean useBinarySerialization; + private final EVCacheTranscoderProperties properties; public EVCacheTranscoder() { this(EVCacheConfig.getInstance().getPropertyRepository().get("default.evcache.max.data.size", Integer.class).orElse(20 * 1024 * 1024).get()); @@ -19,13 +20,15 @@ public EVCacheTranscoder(int max) { } public EVCacheTranscoder(int max, int compressionThreshold) { - this(max, compressionThreshold, false); + this(max, compressionThreshold, new EVCacheTranscoderProperties(null, EVCacheConfig.getInstance().getPropertyRepository())); } - public EVCacheTranscoder(int max, int compressionThreshold, boolean useBinarySerialization) { + public EVCacheTranscoder(int max, int compressionThreshold, EVCacheTranscoderProperties properties) { super(max); - setCompressionThreshold(compressionThreshold); - this.useBinarySerialization = useBinarySerialization; + this.properties = properties; + this.setCompressionThreshold( + compressionThreshold + ); } @Override @@ -46,7 +49,7 @@ public CachedData encode(Object o) { @Override protected byte[] serialize(Object o) { - if (useBinarySerialization && o instanceof EVCacheValue) { + if (this.properties.isBinarySerializationEnabled() && o instanceof EVCacheValue) { return EVCacheValueSerde.serialize((EVCacheValue) o); } return super.serialize(o); diff --git a/evcache-core/src/main/java/com/netflix/evcache/config/EVCacheTranscoderProperties.java b/evcache-core/src/main/java/com/netflix/evcache/config/EVCacheTranscoderProperties.java new file mode 100644 index 00000000..b8fcfb28 --- /dev/null +++ b/evcache-core/src/main/java/com/netflix/evcache/config/EVCacheTranscoderProperties.java @@ -0,0 +1,87 @@ +package com.netflix.evcache.config; + +import com.netflix.archaius.api.Property; +import com.netflix.archaius.api.PropertyRepository; + +/** + * Typed access to the FastProperties that govern {@link com.netflix.evcache.EVCacheTranscoder} + * behavior. Today the bundle holds a single entry — {@link Key#USE_BINARY_SERIALIZATION} — but + * the {@link Key} enum is the extension point: additional transcoder properties land here and + * inherit the same resolution chain without touching call sites. + * + *

Every property resolves at construction through: + * + *

    + *
  1. Per-app override: {@code .}
  2. + *
  3. Global default: {@code }
  4. + *
  5. Static default: the value baked into this class
  6. + *
+ * + *

Properties are read once at construction and cached as primitives. A future field that + * needs runtime mutability can skip the cached primitive and call + * {@link #getProperty(Key, Class, Object)} on each access — the three-level resolution + * applies to dynamic reads too. + */ +public class EVCacheTranscoderProperties { + + + public enum Key { + USE_BINARY_SERIALIZATION("binary.serialization.enabled", "default.evcache.binary.serialization.enabled"); + + final String appKeySuffix; + final String globalKey; + + Key(String appKeySuffix, String globalKey) { + this.appKeySuffix = appKeySuffix; + this.globalKey = globalKey; + } + } + + private static final boolean DEFAULT_BINARY_SERIALIZATION_ENABLED = false; + + private final String appName; + private final PropertyRepository propertyRepository; + + private final boolean binarySerializationEnabled; + + /** + * Construct the bundle and snapshot every property via the three-level resolution chain. + * + * @param appName the EVCache app name used as the per-app override prefix + * (e.g. {@code "EVCACHE_FOO"}). When {@code null} or empty the + * per-app step is skipped and resolution starts at the global + * key — useful for the no-app transcoder constructors and for + * callers that only want fleet-wide defaults. + * @param propertyRepository the Archaius2 PropertyRepository to resolve against. Never null; + * pass {@code EVCacheConfig.getInstance().getPropertyRepository()} + * for the production wiring. + */ + public EVCacheTranscoderProperties(String appName, PropertyRepository propertyRepository) { + this.appName = appName; + this.propertyRepository = propertyRepository; + this.binarySerializationEnabled = getProperty(appName, propertyRepository, + Key.USE_BINARY_SERIALIZATION, Boolean.class, DEFAULT_BINARY_SERIALIZATION_ENABLED).get(); + } + + public boolean isBinarySerializationEnabled() { + return binarySerializationEnabled; + } + + /** + * Read a property dynamically (re-evaluates on every call), with the same per-app -> global -> + * static-default chain used for the cached fields above. + */ + public T getProperty(Key key, Class type, T defaultValue) { + return getProperty(appName, propertyRepository, key, type, defaultValue).get(); + } + + private static Property getProperty(String appName, PropertyRepository propertyRepository, + Key key, Class type, T defaultValue) { + if (appName == null || appName.isEmpty()) { + return propertyRepository.get(key.globalKey, type).orElse(defaultValue); + } + return propertyRepository.get(appName + "." + key.appKeySuffix, type) + .orElseGet(key.globalKey) + .orElse(defaultValue); + } +} diff --git a/evcache-core/src/test/java/com/netflix/evcache/config/EVCacheTranscoderPropertiesTest.java b/evcache-core/src/test/java/com/netflix/evcache/config/EVCacheTranscoderPropertiesTest.java new file mode 100644 index 00000000..ce0680a9 --- /dev/null +++ b/evcache-core/src/test/java/com/netflix/evcache/config/EVCacheTranscoderPropertiesTest.java @@ -0,0 +1,67 @@ +package com.netflix.evcache.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.netflix.archaius.DefaultPropertyFactory; +import com.netflix.archaius.api.PropertyRepository; +import com.netflix.archaius.config.DefaultSettableConfig; + +import org.testng.annotations.Test; + +/** + * Three-level resolution tests for {@link EVCacheTranscoderProperties#isBinarySerializationEnabled()}: + * per-app override → global default → static default. + */ +public class EVCacheTranscoderPropertiesTest { + + private static final String APP = "MYAPP"; + private static final String PER_APP_KEY = "MYAPP.binary.serialization.enabled"; + private static final String GLOBAL_KEY = "default.evcache.binary.serialization.enabled"; + + private static PropertyRepository repo(DefaultSettableConfig cfg) { + return DefaultPropertyFactory.from(cfg); + } + + @Test + public void binarySerialization_perAppOverrideWins() { + DefaultSettableConfig cfg = new DefaultSettableConfig(); + cfg.setProperty(PER_APP_KEY, "true"); + + EVCacheTranscoderProperties props = new EVCacheTranscoderProperties(APP, repo(cfg)); + assertThat(props.isBinarySerializationEnabled()).isTrue(); + } + + @Test + public void binarySerialization_globalFallbackWhenPerAppUnset() { + DefaultSettableConfig cfg = new DefaultSettableConfig(); + cfg.setProperty(GLOBAL_KEY, "true"); + + EVCacheTranscoderProperties props = new EVCacheTranscoderProperties(APP, repo(cfg)); + assertThat(props.isBinarySerializationEnabled()).isTrue(); + } + + @Test + public void binarySerialization_staticDefaultWhenBothUnset() { + EVCacheTranscoderProperties props = new EVCacheTranscoderProperties(APP, repo(new DefaultSettableConfig())); + assertThat(props.isBinarySerializationEnabled()).isFalse(); + } + + @Test + public void binarySerialization_perAppBeatsGlobal() { + DefaultSettableConfig cfg = new DefaultSettableConfig(); + cfg.setProperty(PER_APP_KEY, "false"); + cfg.setProperty(GLOBAL_KEY, "true"); + + EVCacheTranscoderProperties props = new EVCacheTranscoderProperties(APP, repo(cfg)); + assertThat(props.isBinarySerializationEnabled()).isFalse(); + } + + @Test + public void binarySerialization_nullAppNameUsesGlobalKey() { + DefaultSettableConfig cfg = new DefaultSettableConfig(); + cfg.setProperty(GLOBAL_KEY, "true"); + + EVCacheTranscoderProperties props = new EVCacheTranscoderProperties(null, repo(cfg)); + assertThat(props.isBinarySerializationEnabled()).isTrue(); + } +} diff --git a/evcache-core/src/test/java/com/netflix/evcache/pool/EVCacheValueSerdeTest.java b/evcache-core/src/test/java/com/netflix/evcache/pool/EVCacheValueSerdeTest.java index a0ae610f..44f92d7c 100644 --- a/evcache-core/src/test/java/com/netflix/evcache/pool/EVCacheValueSerdeTest.java +++ b/evcache-core/src/test/java/com/netflix/evcache/pool/EVCacheValueSerdeTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Arrays; +import com.netflix.evcache.config.EVCacheTranscoderProperties; import org.testng.annotations.Test; import com.netflix.evcache.EVCacheTranscoder; @@ -34,7 +35,11 @@ public class EVCacheValueSerdeTest { /** Binary-enabled transcoder, compression disabled, so encoded bytes start with our magic. */ private static EVCacheTranscoder binaryTranscoder() { - return new EVCacheTranscoder(20 * 1024 * 1024, Integer.MAX_VALUE, true); + com.netflix.archaius.config.DefaultSettableConfig cfg = new com.netflix.archaius.config.DefaultSettableConfig(); + cfg.setProperty("testApp.binary.serialization.enabled", "true"); + return new EVCacheTranscoder(20 * 1024 * 1024, Integer.MAX_VALUE, + new EVCacheTranscoderProperties("testApp", + com.netflix.archaius.DefaultPropertyFactory.from(cfg))); } /** Default transcoder (binary OFF, falls through to native Java serialization). */ diff --git a/evcache-core/src/test/java/test-suite.xml b/evcache-core/src/test/java/test-suite.xml index 4e0dffb0..e31a08d4 100644 --- a/evcache-core/src/test/java/test-suite.xml +++ b/evcache-core/src/test/java/test-suite.xml @@ -4,6 +4,7 @@ +