[CALCITE-1808] JaninoRelMetadataProvider loading cache might cause OutOfMemoryError
Limit the size of the cache, controlled by property "saffron.metadata.handler.cache.maximum.size" (default 1,000). Project: http://git-wip-us.apache.org/repos/asf/calcite/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/3763abfa Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/3763abfa Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/3763abfa Branch: refs/heads/master Commit: 3763abfab2675faeabd2bca9803e86d52aa50cca Parents: 189aad1 Author: Julian Hyde <[email protected]> Authored: Thu Oct 5 13:37:30 2017 -0700 Committer: Julian Hyde <[email protected]> Committed: Fri Dec 1 18:05:17 2017 -0800 ---------------------------------------------------------------------- .../rel/metadata/JaninoRelMetadataProvider.java | 28 ++++++++++++----- .../apache/calcite/util/SaffronProperties.java | 13 ++++++++ .../java/org/apache/calcite/test/Matchers.java | 6 ++-- .../apache/calcite/test/RelMetadataTest.java | 33 +++++++++++++++++--- 4 files changed, 65 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite/blob/3763abfa/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java index e3c89fb..634da05 100644 --- a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java +++ b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java @@ -54,6 +54,7 @@ import org.apache.calcite.rel.stream.LogicalDelta; import org.apache.calcite.rex.RexNode; import org.apache.calcite.util.ControlFlowException; import org.apache.calcite.util.Pair; +import org.apache.calcite.util.SaffronProperties; import org.apache.calcite.util.Util; import com.google.common.cache.CacheBuilder; @@ -106,14 +107,16 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider { * For the cache to be effective, providers should implement identity * correctly. */ private static final LoadingCache<Key, MetadataHandler> HANDLERS = - CacheBuilder.newBuilder().build( - new CacheLoader<Key, MetadataHandler>() { - public MetadataHandler load(@Nonnull Key key) { - //noinspection unchecked - return load3(key.def, key.provider.handlers(key.def), - key.relClasses); - } - }); + maxSize(CacheBuilder.newBuilder(), + SaffronProperties.INSTANCE.metadataHandlerCacheMaximumSize().get()) + .build( + new CacheLoader<Key, MetadataHandler>() { + public MetadataHandler load(@Nonnull Key key) { + //noinspection unchecked + return load3(key.def, key.provider.handlers(key.def), + key.relClasses); + } + }); // Pre-register the most common relational operators, to reduce the number of // times we re-generate. @@ -168,6 +171,15 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider { return new JaninoRelMetadataProvider(provider); } + // helper for initialization + private static <K, V> CacheBuilder<K, V> maxSize(CacheBuilder<K, V> builder, + int size) { + if (size >= 0) { + builder.maximumSize(size); + } + return builder; + } + @Override public boolean equals(Object obj) { return obj == this || obj instanceof JaninoRelMetadataProvider http://git-wip-us.apache.org/repos/asf/calcite/blob/3763abfa/core/src/main/java/org/apache/calcite/util/SaffronProperties.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/SaffronProperties.java b/core/src/main/java/org/apache/calcite/util/SaffronProperties.java index 1397871..7ae8682 100644 --- a/core/src/main/java/org/apache/calcite/util/SaffronProperties.java +++ b/core/src/main/java/org/apache/calcite/util/SaffronProperties.java @@ -19,6 +19,7 @@ package org.apache.calcite.util; import org.apache.calcite.runtime.Resources; import org.apache.calcite.runtime.Resources.BooleanProp; import org.apache.calcite.runtime.Resources.Default; +import org.apache.calcite.runtime.Resources.IntProp; import org.apache.calcite.runtime.Resources.Resource; import org.apache.calcite.runtime.Resources.StringProp; @@ -96,6 +97,18 @@ public interface SaffronProperties { @Default("primary") StringProp defaultCollationStrength(); + /** + * The string property "saffron.metadata.handler.cache.maximum.size" is the + * maximum size of the cache of metadata handlers. A typical value is + * the number of queries being concurrently prepared multiplied by the number + * of types of metadata. + * + * <p>If the value is less than 0, there is no limit. The default is 1,000. + */ + @Resource("saffron.metadata.handler.cache.maximum.size") + @Default("1000") + IntProp metadataHandlerCacheMaximumSize(); + SaffronProperties INSTANCE = Helper.instance(); /** Helper class. */ http://git-wip-us.apache.org/repos/asf/calcite/blob/3763abfa/core/src/test/java/org/apache/calcite/test/Matchers.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/Matchers.java b/core/src/test/java/org/apache/calcite/test/Matchers.java index 08733a2..76d994f 100644 --- a/core/src/test/java/org/apache/calcite/test/Matchers.java +++ b/core/src/test/java/org/apache/calcite/test/Matchers.java @@ -19,6 +19,7 @@ package org.apache.calcite.test; import org.apache.calcite.util.Util; import com.google.common.base.Functions; +import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -134,19 +135,20 @@ public class Matchers { private final double epsilon; public IsWithin(T expectedValue, double epsilon) { + Preconditions.checkArgument(epsilon >= 0D); this.expectedValue = expectedValue; this.epsilon = epsilon; } public boolean matches(Object actualValue) { - return areEqual(actualValue, expectedValue, epsilon); + return isWithin(actualValue, expectedValue, epsilon); } public void describeTo(Description description) { description.appendValue(expectedValue + " +/-" + epsilon); } - private static boolean areEqual(Object actual, Number expected, + private static boolean isWithin(Object actual, Number expected, double epsilon) { if (actual == null) { return expected == null; http://git-wip-us.apache.org/repos/asf/calcite/blob/3763abfa/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java index 09c8a50..d6b1568 100644 --- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java @@ -88,6 +88,7 @@ import org.apache.calcite.tools.Frameworks; import org.apache.calcite.tools.RelBuilder; import org.apache.calcite.util.ImmutableBitSet; import org.apache.calcite.util.ImmutableIntList; +import org.apache.calcite.util.SaffronProperties; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; @@ -120,6 +121,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import static org.apache.calcite.test.Matchers.within; + import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -166,10 +169,6 @@ public class RelMetadataTest extends SqlToRelTestBase { //~ Methods ---------------------------------------------------------------- - private static Matcher<? super Number> nearTo(Number v, Number epsilon) { - return equalTo(v); // TODO: use epsilon - } - // ---------------------------------------------------------------------- // Tests for getPercentageOriginalRows // ---------------------------------------------------------------------- @@ -780,7 +779,31 @@ public class RelMetadataTest extends SqlToRelTestBase { final RelMetadataQuery mq = RelMetadataQuery.instance(); Double result = mq.getSelectivity(rel, null); assertThat(result, - nearTo(DEFAULT_COMP_SELECTIVITY * DEFAULT_EQUAL_SELECTIVITY, EPSILON)); + within(DEFAULT_COMP_SELECTIVITY * DEFAULT_EQUAL_SELECTIVITY, EPSILON)); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1808">[CALCITE-1808] + * JaninoRelMetadataProvider loading cache might cause + * OutOfMemoryError</a>. */ + @Test public void testMetadataHandlerCacheLimit() { + Assume.assumeTrue("If cache size is too large, this test may fail and the " + + "test won't be to blame", + SaffronProperties.INSTANCE.metadataHandlerCacheMaximumSize().get() + < 10_000); + final int iterationCount = 2_000; + final RelNode rel = convertSql("select * from emp"); + final RelMetadataProvider metadataProvider = + rel.getCluster().getMetadataProvider(); + final RelOptPlanner planner = rel.getCluster().getPlanner(); + for (int i = 0; i < iterationCount; i++) { + RelMetadataQuery.THREAD_PROVIDERS.set( + JaninoRelMetadataProvider.of( + new CachingRelMetadataProvider(metadataProvider, planner))); + final RelMetadataQuery mq = RelMetadataQuery.instance(); + final Double result = mq.getRowCount(rel); + assertThat(result, within(14d, 0.1d)); + } } @Test public void testDistinctRowCountTable() {
