[
https://issues.apache.org/jira/browse/DRILL-8555?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18132971#comment-18132971
]
ASF GitHub Bot commented on DRILL-8555:
---------------------------------------
letian-jiang commented on code in PR #3086:
URL: https://github.com/apache/drill/pull/3086#discussion_r4390303319
##########
exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/PlanCache.java:
##########
@@ -0,0 +1,744 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.planner.sql;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.SqlWithItem;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.util.NlsString;
+import org.apache.drill.common.expression.AnyValueExpression;
+import org.apache.drill.common.expression.BooleanOperator;
+import org.apache.drill.common.expression.CastExpression;
+import org.apache.drill.common.expression.ConvertExpression;
+import org.apache.drill.common.expression.ExpressionStringBuilder;
+import org.apache.drill.common.expression.FunctionCall;
+import org.apache.drill.common.expression.IfExpression;
+import org.apache.drill.common.expression.LiteralExpression;
+import org.apache.drill.common.expression.LogicalExpression;
+import org.apache.drill.common.expression.NullExpression;
+import org.apache.drill.common.expression.SchemaPath;
+import org.apache.drill.common.expression.TypedNullConstant;
+import org.apache.drill.common.expression.ValueExpressions;
+import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.common.parser.LogicalExpressionParser;
+import org.apache.drill.common.types.TypeProtos.MajorType;
+import org.apache.drill.common.types.TypeProtos.MinorType;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.metrics.DrillMetrics;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.physical.PhysicalPlan;
+import org.apache.drill.exec.planner.PhysicalPlanReader;
+import org.apache.drill.exec.planner.logical.DrillTable;
+import org.apache.drill.exec.planner.logical.DrillTableSelection;
+import org.apache.drill.exec.rpc.NamedThreadFactory;
+import org.apache.drill.exec.server.options.OptionValue;
+import org.apache.drill.exec.store.StoragePlugin;
+import org.apache.drill.exec.store.StoragePluginRegistry;
+import org.apache.drill.exec.store.PlanCacheTable;
+import org.apache.drill.exec.util.Pointer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Ticker;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.Ints;
+import com.codahale.metrics.Gauge;
+
+/** Drillbit-scoped, immutable JSON snapshots of query plans. */
+public final class PlanCache implements AutoCloseable {
+ private static final Logger logger =
LoggerFactory.getLogger(PlanCache.class);
+ private static final ObjectMapper JSON = new ObjectMapper();
+ private static final String MARKER =
ExpressionStringBuilder.BOUND_DYNAMIC_PARAM + "(";
+ private final Cache<String, Entry> entries;
+ private final AtomicLong successfulBinds = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+ private final AtomicLong invalidations = new AtomicLong();
+ private final Map<String, Gauge<Long>> metrics = new LinkedHashMap<>();
+ private volatile long generation;
+ private final ThreadPoolExecutor writer = new ThreadPoolExecutor(1, 1, 0L,
+ TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(64),
+ new NamedThreadFactory("plan-cache-writer-"));
+
+ /** A zero expiration duration disables that policy; a zero capacity
disables storage. */
+ public PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess) {
+ this(maxSizeBytes, expireAfterWrite, expireAfterAccess,
Ticker.systemTicker());
+ }
+
+ @VisibleForTesting
+ PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess, Ticker ticker) {
+ checkArgument(maxSizeBytes >= 0, "%s must be non-negative",
ExecConstants.PLAN_CACHE_MAX_SIZE_BYTES);
+ checkArgument(!expireAfterWrite.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_WRITE);
+ checkArgument(!expireAfterAccess.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_ACCESS);
+ CacheBuilder<String, Entry> builder = CacheBuilder.newBuilder()
+ .maximumWeight(maxSizeBytes)
+ .weigher((String key, Entry value) -> Ints.saturatedCast(
+ key.getBytes(StandardCharsets.UTF_8).length +
value.contentSizeBytes))
+ .ticker(ticker)
+ .recordStats();
+ // Passing zero to Guava would expire entries immediately, rather than
disable the policy.
+ if (!expireAfterWrite.isZero()) {
+ builder.expireAfterWrite(expireAfterWrite);
+ }
+ if (!expireAfterAccess.isZero()) {
+ builder.expireAfterAccess(expireAfterAccess);
+ }
+ entries = builder.build();
+ logger.info("Plan cache settings: max_size_bytes={},
expire_after_write={}, expire_after_access={}"
+ + " (zero expiration disables that policy)", maxSizeBytes,
expireAfterWrite, expireAfterAccess);
+ registerMetric("hits", successfulBinds::get);
+ registerMetric("misses", misses::get);
+ registerMetric("invalidations", invalidations::get);
+ registerMetric("evictions", () -> entries.stats().evictionCount());
+ registerMetric("entries", entries::size);
+ }
+
+ private void registerMetric(String suffix, Gauge<Long> gauge) {
+ String name = "drill.plan_cache." + suffix;
+ metrics.put(name, gauge);
+ DrillMetrics.register(name, gauge);
+ }
+
+ public long getHitCount() {
+ return successfulBinds.get();
+ }
+
+ public void recordBind() {
+ successfulBinds.incrementAndGet();
+ }
+
+ /** Counts an eligible lookup that could not produce a compatible, bound
plan. */
+ public void recordMiss() {
+ misses.incrementAndGet();
+ }
+
+ /** Updates optional explain output using the cached template. */
+ static void bindTextPlan(Pointer<String> textPlan, String template,
List<SqlLiteral> literals) {
+ if (textPlan != null) {
+ textPlan.value = textWithBindings(template, literals);
+ }
+ }
+
+ /** Updates optional explain output and returns its original template for
cache publication. */
+ static String bindTextPlan(Pointer<String> textPlan, List<SqlLiteral>
literals) {
+ String template = textPlan == null ? null : textPlan.value;
+ bindTextPlan(textPlan, template, literals);
+ return template;
+ }
+
+ /** Appends values from this query without changing the cached explain
template. */
+ private static String textWithBindings(String template, List<SqlLiteral>
literals) {
+ if (template == null || literals.isEmpty()) {
+ return template;
+ }
+ StringBuilder text = new StringBuilder(template);
+ if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
+ text.append('\n');
+ }
+ text.append("Parameters: ");
+ for (int i = 0; i < literals.size(); i++) {
+ if (i > 0) {
+ text.append(", ");
+ }
+ text.append('?').append(i).append(" = ")
+ .append(literals.get(i).toString().replace("\r",
"\\r").replace("\n", "\\n"));
+ }
+ return text.toString();
+ }
+
+ public Entry get(String key) {
+ return entries.getIfPresent(key);
+ }
+
+ /** Waits for writes already submitted by completed queries; intended for
tests. */
+ public void awaitWrites() throws InterruptedException, ExecutionException,
TimeoutException {
+ writer.submit(() -> { }).get(10, TimeUnit.SECONDS);
+ }
+
+ public void writeAfterSuccess(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) {
+ try {
+ writer.execute(() -> {
+ try {
+ put(key, json, textPlan, reader, context, expectedGeneration);
+ } catch (RuntimeException | IOException e) {
+ logger.debug("Plan cache entry could not be published", e);
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ logger.debug("Plan cache writer is busy; skipping cache entry", e);
+ }
+ }
+
+ public void invalidate(String key) {
+ invalidations.incrementAndGet();
+ entries.invalidate(key);
+ }
+
+ long getGeneration() {
+ return generation;
+ }
+
+ /** Clears local entries and prevents pre-clear plans from being published
later. */
+ public synchronized void clear() {
+ generation++;
+ invalidations.incrementAndGet();
+ entries.invalidateAll();
+ entries.cleanUp();
+ logger.info("Local plan cache cleared; generation={}", generation);
+ }
+
+ public boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context) throws IOException {
+ return put(key, json, textPlan, reader, context, generation);
+ }
+
+ private boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) throws IOException {
+ // The engine checks generic context and expression compatibility. Each
+ // opted-in plugin is responsible for rebuilding its scans'
value-dependent state.
+ if (context == null || expectedGeneration != generation) {
+ return false;
+ }
+ // Confirm that standard PhysicalPlan JSON can be read back before
publishing.
+ reader.readPhysicalPlan(json);
Review Comment:
Added trial binding before publication, replacing JSON-only validation.
Unbindable templates—for example, ordinary text containing the parameter
marker—are skipped without affecting the completed query.
##########
exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/PlanCache.java:
##########
@@ -0,0 +1,744 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.planner.sql;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.SqlWithItem;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.util.NlsString;
+import org.apache.drill.common.expression.AnyValueExpression;
+import org.apache.drill.common.expression.BooleanOperator;
+import org.apache.drill.common.expression.CastExpression;
+import org.apache.drill.common.expression.ConvertExpression;
+import org.apache.drill.common.expression.ExpressionStringBuilder;
+import org.apache.drill.common.expression.FunctionCall;
+import org.apache.drill.common.expression.IfExpression;
+import org.apache.drill.common.expression.LiteralExpression;
+import org.apache.drill.common.expression.LogicalExpression;
+import org.apache.drill.common.expression.NullExpression;
+import org.apache.drill.common.expression.SchemaPath;
+import org.apache.drill.common.expression.TypedNullConstant;
+import org.apache.drill.common.expression.ValueExpressions;
+import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.common.parser.LogicalExpressionParser;
+import org.apache.drill.common.types.TypeProtos.MajorType;
+import org.apache.drill.common.types.TypeProtos.MinorType;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.metrics.DrillMetrics;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.physical.PhysicalPlan;
+import org.apache.drill.exec.planner.PhysicalPlanReader;
+import org.apache.drill.exec.planner.logical.DrillTable;
+import org.apache.drill.exec.planner.logical.DrillTableSelection;
+import org.apache.drill.exec.rpc.NamedThreadFactory;
+import org.apache.drill.exec.server.options.OptionValue;
+import org.apache.drill.exec.store.StoragePlugin;
+import org.apache.drill.exec.store.StoragePluginRegistry;
+import org.apache.drill.exec.store.PlanCacheTable;
+import org.apache.drill.exec.util.Pointer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Ticker;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.Ints;
+import com.codahale.metrics.Gauge;
+
+/** Drillbit-scoped, immutable JSON snapshots of query plans. */
+public final class PlanCache implements AutoCloseable {
+ private static final Logger logger =
LoggerFactory.getLogger(PlanCache.class);
+ private static final ObjectMapper JSON = new ObjectMapper();
+ private static final String MARKER =
ExpressionStringBuilder.BOUND_DYNAMIC_PARAM + "(";
+ private final Cache<String, Entry> entries;
+ private final AtomicLong successfulBinds = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+ private final AtomicLong invalidations = new AtomicLong();
+ private final Map<String, Gauge<Long>> metrics = new LinkedHashMap<>();
+ private volatile long generation;
+ private final ThreadPoolExecutor writer = new ThreadPoolExecutor(1, 1, 0L,
+ TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(64),
+ new NamedThreadFactory("plan-cache-writer-"));
+
+ /** A zero expiration duration disables that policy; a zero capacity
disables storage. */
+ public PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess) {
+ this(maxSizeBytes, expireAfterWrite, expireAfterAccess,
Ticker.systemTicker());
+ }
+
+ @VisibleForTesting
+ PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess, Ticker ticker) {
+ checkArgument(maxSizeBytes >= 0, "%s must be non-negative",
ExecConstants.PLAN_CACHE_MAX_SIZE_BYTES);
+ checkArgument(!expireAfterWrite.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_WRITE);
+ checkArgument(!expireAfterAccess.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_ACCESS);
+ CacheBuilder<String, Entry> builder = CacheBuilder.newBuilder()
+ .maximumWeight(maxSizeBytes)
+ .weigher((String key, Entry value) -> Ints.saturatedCast(
+ key.getBytes(StandardCharsets.UTF_8).length +
value.contentSizeBytes))
+ .ticker(ticker)
+ .recordStats();
+ // Passing zero to Guava would expire entries immediately, rather than
disable the policy.
+ if (!expireAfterWrite.isZero()) {
+ builder.expireAfterWrite(expireAfterWrite);
+ }
+ if (!expireAfterAccess.isZero()) {
+ builder.expireAfterAccess(expireAfterAccess);
+ }
+ entries = builder.build();
+ logger.info("Plan cache settings: max_size_bytes={},
expire_after_write={}, expire_after_access={}"
+ + " (zero expiration disables that policy)", maxSizeBytes,
expireAfterWrite, expireAfterAccess);
+ registerMetric("hits", successfulBinds::get);
+ registerMetric("misses", misses::get);
+ registerMetric("invalidations", invalidations::get);
+ registerMetric("evictions", () -> entries.stats().evictionCount());
+ registerMetric("entries", entries::size);
+ }
+
+ private void registerMetric(String suffix, Gauge<Long> gauge) {
+ String name = "drill.plan_cache." + suffix;
+ metrics.put(name, gauge);
+ DrillMetrics.register(name, gauge);
+ }
+
+ public long getHitCount() {
+ return successfulBinds.get();
+ }
+
+ public void recordBind() {
+ successfulBinds.incrementAndGet();
+ }
+
+ /** Counts an eligible lookup that could not produce a compatible, bound
plan. */
+ public void recordMiss() {
+ misses.incrementAndGet();
+ }
+
+ /** Updates optional explain output using the cached template. */
+ static void bindTextPlan(Pointer<String> textPlan, String template,
List<SqlLiteral> literals) {
+ if (textPlan != null) {
+ textPlan.value = textWithBindings(template, literals);
+ }
+ }
+
+ /** Updates optional explain output and returns its original template for
cache publication. */
+ static String bindTextPlan(Pointer<String> textPlan, List<SqlLiteral>
literals) {
+ String template = textPlan == null ? null : textPlan.value;
+ bindTextPlan(textPlan, template, literals);
+ return template;
+ }
+
+ /** Appends values from this query without changing the cached explain
template. */
+ private static String textWithBindings(String template, List<SqlLiteral>
literals) {
+ if (template == null || literals.isEmpty()) {
+ return template;
+ }
+ StringBuilder text = new StringBuilder(template);
+ if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
+ text.append('\n');
+ }
+ text.append("Parameters: ");
+ for (int i = 0; i < literals.size(); i++) {
+ if (i > 0) {
+ text.append(", ");
+ }
+ text.append('?').append(i).append(" = ")
+ .append(literals.get(i).toString().replace("\r",
"\\r").replace("\n", "\\n"));
+ }
+ return text.toString();
+ }
+
+ public Entry get(String key) {
+ return entries.getIfPresent(key);
+ }
+
+ /** Waits for writes already submitted by completed queries; intended for
tests. */
+ public void awaitWrites() throws InterruptedException, ExecutionException,
TimeoutException {
+ writer.submit(() -> { }).get(10, TimeUnit.SECONDS);
+ }
+
+ public void writeAfterSuccess(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) {
+ try {
+ writer.execute(() -> {
+ try {
+ put(key, json, textPlan, reader, context, expectedGeneration);
+ } catch (RuntimeException | IOException e) {
+ logger.debug("Plan cache entry could not be published", e);
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ logger.debug("Plan cache writer is busy; skipping cache entry", e);
+ }
+ }
+
+ public void invalidate(String key) {
+ invalidations.incrementAndGet();
+ entries.invalidate(key);
+ }
+
+ long getGeneration() {
+ return generation;
+ }
+
+ /** Clears local entries and prevents pre-clear plans from being published
later. */
+ public synchronized void clear() {
+ generation++;
+ invalidations.incrementAndGet();
+ entries.invalidateAll();
+ entries.cleanUp();
+ logger.info("Local plan cache cleared; generation={}", generation);
+ }
+
+ public boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context) throws IOException {
+ return put(key, json, textPlan, reader, context, generation);
+ }
+
+ private boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) throws IOException {
+ // The engine checks generic context and expression compatibility. Each
+ // opted-in plugin is responsible for rebuilding its scans'
value-dependent state.
+ if (context == null || expectedGeneration != generation) {
+ return false;
+ }
+ // Confirm that standard PhysicalPlan JSON can be read back before
publishing.
+ reader.readPhysicalPlan(json);
+ // Readback may perform plugin I/O. Keep it outside the clear/publication
lock,
+ // then check again so a concurrent clear cannot be undone by that
readback.
+ synchronized (this) {
+ if (expectedGeneration != generation) {
+ logger.debug("Skipping plan cache publication after a cache clear");
+ return false;
+ }
+ entries.put(key, new Entry(json, textPlan, context));
Review Comment:
The next compatibility check rejects the stale entry. Since schema changes
are infrequent, I’m accepting the occasional extra replanning cycle for this PR.
##########
exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/PlanCache.java:
##########
@@ -0,0 +1,744 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.planner.sql;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.SqlWithItem;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.util.NlsString;
+import org.apache.drill.common.expression.AnyValueExpression;
+import org.apache.drill.common.expression.BooleanOperator;
+import org.apache.drill.common.expression.CastExpression;
+import org.apache.drill.common.expression.ConvertExpression;
+import org.apache.drill.common.expression.ExpressionStringBuilder;
+import org.apache.drill.common.expression.FunctionCall;
+import org.apache.drill.common.expression.IfExpression;
+import org.apache.drill.common.expression.LiteralExpression;
+import org.apache.drill.common.expression.LogicalExpression;
+import org.apache.drill.common.expression.NullExpression;
+import org.apache.drill.common.expression.SchemaPath;
+import org.apache.drill.common.expression.TypedNullConstant;
+import org.apache.drill.common.expression.ValueExpressions;
+import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.common.parser.LogicalExpressionParser;
+import org.apache.drill.common.types.TypeProtos.MajorType;
+import org.apache.drill.common.types.TypeProtos.MinorType;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.metrics.DrillMetrics;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.physical.PhysicalPlan;
+import org.apache.drill.exec.planner.PhysicalPlanReader;
+import org.apache.drill.exec.planner.logical.DrillTable;
+import org.apache.drill.exec.planner.logical.DrillTableSelection;
+import org.apache.drill.exec.rpc.NamedThreadFactory;
+import org.apache.drill.exec.server.options.OptionValue;
+import org.apache.drill.exec.store.StoragePlugin;
+import org.apache.drill.exec.store.StoragePluginRegistry;
+import org.apache.drill.exec.store.PlanCacheTable;
+import org.apache.drill.exec.util.Pointer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Ticker;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.Ints;
+import com.codahale.metrics.Gauge;
+
+/** Drillbit-scoped, immutable JSON snapshots of query plans. */
+public final class PlanCache implements AutoCloseable {
+ private static final Logger logger =
LoggerFactory.getLogger(PlanCache.class);
+ private static final ObjectMapper JSON = new ObjectMapper();
+ private static final String MARKER =
ExpressionStringBuilder.BOUND_DYNAMIC_PARAM + "(";
+ private final Cache<String, Entry> entries;
+ private final AtomicLong successfulBinds = new AtomicLong();
+ private final AtomicLong misses = new AtomicLong();
+ private final AtomicLong invalidations = new AtomicLong();
+ private final Map<String, Gauge<Long>> metrics = new LinkedHashMap<>();
+ private volatile long generation;
+ private final ThreadPoolExecutor writer = new ThreadPoolExecutor(1, 1, 0L,
+ TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(64),
+ new NamedThreadFactory("plan-cache-writer-"));
+
+ /** A zero expiration duration disables that policy; a zero capacity
disables storage. */
+ public PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess) {
+ this(maxSizeBytes, expireAfterWrite, expireAfterAccess,
Ticker.systemTicker());
+ }
+
+ @VisibleForTesting
+ PlanCache(long maxSizeBytes, Duration expireAfterWrite, Duration
expireAfterAccess, Ticker ticker) {
+ checkArgument(maxSizeBytes >= 0, "%s must be non-negative",
ExecConstants.PLAN_CACHE_MAX_SIZE_BYTES);
+ checkArgument(!expireAfterWrite.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_WRITE);
+ checkArgument(!expireAfterAccess.isNegative(), "%s must be non-negative",
+ ExecConstants.PLAN_CACHE_EXPIRE_AFTER_ACCESS);
+ CacheBuilder<String, Entry> builder = CacheBuilder.newBuilder()
+ .maximumWeight(maxSizeBytes)
+ .weigher((String key, Entry value) -> Ints.saturatedCast(
+ key.getBytes(StandardCharsets.UTF_8).length +
value.contentSizeBytes))
+ .ticker(ticker)
+ .recordStats();
+ // Passing zero to Guava would expire entries immediately, rather than
disable the policy.
+ if (!expireAfterWrite.isZero()) {
+ builder.expireAfterWrite(expireAfterWrite);
+ }
+ if (!expireAfterAccess.isZero()) {
+ builder.expireAfterAccess(expireAfterAccess);
+ }
+ entries = builder.build();
+ logger.info("Plan cache settings: max_size_bytes={},
expire_after_write={}, expire_after_access={}"
+ + " (zero expiration disables that policy)", maxSizeBytes,
expireAfterWrite, expireAfterAccess);
+ registerMetric("hits", successfulBinds::get);
+ registerMetric("misses", misses::get);
+ registerMetric("invalidations", invalidations::get);
+ registerMetric("evictions", () -> entries.stats().evictionCount());
+ registerMetric("entries", entries::size);
+ }
+
+ private void registerMetric(String suffix, Gauge<Long> gauge) {
+ String name = "drill.plan_cache." + suffix;
+ metrics.put(name, gauge);
+ DrillMetrics.register(name, gauge);
+ }
+
+ public long getHitCount() {
+ return successfulBinds.get();
+ }
+
+ public void recordBind() {
+ successfulBinds.incrementAndGet();
+ }
+
+ /** Counts an eligible lookup that could not produce a compatible, bound
plan. */
+ public void recordMiss() {
+ misses.incrementAndGet();
+ }
+
+ /** Updates optional explain output using the cached template. */
+ static void bindTextPlan(Pointer<String> textPlan, String template,
List<SqlLiteral> literals) {
+ if (textPlan != null) {
+ textPlan.value = textWithBindings(template, literals);
+ }
+ }
+
+ /** Updates optional explain output and returns its original template for
cache publication. */
+ static String bindTextPlan(Pointer<String> textPlan, List<SqlLiteral>
literals) {
+ String template = textPlan == null ? null : textPlan.value;
+ bindTextPlan(textPlan, template, literals);
+ return template;
+ }
+
+ /** Appends values from this query without changing the cached explain
template. */
+ private static String textWithBindings(String template, List<SqlLiteral>
literals) {
+ if (template == null || literals.isEmpty()) {
+ return template;
+ }
+ StringBuilder text = new StringBuilder(template);
+ if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
+ text.append('\n');
+ }
+ text.append("Parameters: ");
+ for (int i = 0; i < literals.size(); i++) {
+ if (i > 0) {
+ text.append(", ");
+ }
+ text.append('?').append(i).append(" = ")
+ .append(literals.get(i).toString().replace("\r",
"\\r").replace("\n", "\\n"));
+ }
+ return text.toString();
+ }
+
+ public Entry get(String key) {
+ return entries.getIfPresent(key);
+ }
+
+ /** Waits for writes already submitted by completed queries; intended for
tests. */
+ public void awaitWrites() throws InterruptedException, ExecutionException,
TimeoutException {
+ writer.submit(() -> { }).get(10, TimeUnit.SECONDS);
+ }
+
+ public void writeAfterSuccess(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) {
+ try {
+ writer.execute(() -> {
+ try {
+ put(key, json, textPlan, reader, context, expectedGeneration);
+ } catch (RuntimeException | IOException e) {
+ logger.debug("Plan cache entry could not be published", e);
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ logger.debug("Plan cache writer is busy; skipping cache entry", e);
+ }
+ }
+
+ public void invalidate(String key) {
+ invalidations.incrementAndGet();
+ entries.invalidate(key);
+ }
+
+ long getGeneration() {
+ return generation;
+ }
+
+ /** Clears local entries and prevents pre-clear plans from being published
later. */
+ public synchronized void clear() {
+ generation++;
+ invalidations.incrementAndGet();
+ entries.invalidateAll();
+ entries.cleanUp();
+ logger.info("Local plan cache cleared; generation={}", generation);
+ }
+
+ public boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context) throws IOException {
+ return put(key, json, textPlan, reader, context, generation);
+ }
+
+ private boolean put(String key, String json, String textPlan,
+ PhysicalPlanReader reader, ContextSnapshot context, long
expectedGeneration) throws IOException {
+ // The engine checks generic context and expression compatibility. Each
+ // opted-in plugin is responsible for rebuilding its scans'
value-dependent state.
+ if (context == null || expectedGeneration != generation) {
+ return false;
+ }
+ // Confirm that standard PhysicalPlan JSON can be read back before
publishing.
+ reader.readPhysicalPlan(json);
+ // Readback may perform plugin I/O. Keep it outside the clear/publication
lock,
+ // then check again so a concurrent clear cannot be undone by that
readback.
+ synchronized (this) {
+ if (expectedGeneration != generation) {
+ logger.debug("Skipping plan cache publication after a cache clear");
+ return false;
+ }
+ entries.put(key, new Entry(json, textPlan, context));
+ }
+ logger.debug("Plan cache entry published; entries={}", entries.size());
+ return true;
+ }
+
+ @Override
+ public void close() {
+ writer.shutdownNow();
+ // Test JVMs may contain several Drillbits. A closing cache must not remove
+ // gauges registered by a newer cache under the same names.
+ synchronized (DrillMetrics.class) {
+ metrics.forEach((name, gauge) -> {
+ if (DrillMetrics.getRegistry().getMetrics().get(name) == gauge) {
+ DrillMetrics.getRegistry().remove(name);
+ }
+ });
+ }
+ }
+
+ public static final class Entry {
+ private final String json;
+ private final long contentSizeBytes;
+ private final String textPlan;
+ private final ContextSnapshot context;
+
+ private Entry(String json, String textPlan, ContextSnapshot context) {
+ this.json = json;
+ this.contentSizeBytes = (long)
json.getBytes(StandardCharsets.UTF_8).length
+ + (textPlan == null ? 0 :
textPlan.getBytes(StandardCharsets.UTF_8).length);
+ this.textPlan = textPlan;
+ this.context = context;
+ }
+
+ public boolean matchesContext(ContextSnapshot current) {
+ return context.matches(current);
+ }
+
+ public String getTextPlan() {
+ return textPlan;
+ }
+
+ public PhysicalPlan bind(List<SqlLiteral> literals, PhysicalPlanReader
reader)
+ throws IOException {
+ JsonNode tree = JSON.readTree(json);
+ walk(tree, literals);
+ return reader.readPhysicalPlan(JSON.writeValueAsString(tree));
+ }
+ }
+
+ /** Options, plugin configurations, and table versions captured before
lookup and planning. */
+ public static final class ContextSnapshot {
+ private final String optionsFingerprint;
+ private final Map<TableIdentifier, String> tableVersions;
+ private final Map<String, String> pluginConfigs;
+
+ private ContextSnapshot(String optionsFingerprint, Map<TableIdentifier,
String> tableVersions,
+ Map<String, String> pluginConfigs) {
+ this.optionsFingerprint = Objects.requireNonNull(optionsFingerprint,
"optionsFingerprint");
+ this.tableVersions = Collections.unmodifiableMap(tableVersions);
+ this.pluginConfigs = Collections.unmodifiableMap(pluginConfigs);
+ }
+
+ private static final class TableIdentifier {
+ private final String storageName;
+ private final String tableId;
+
+ private TableIdentifier(String storageName, String tableId) {
+ this.storageName = storageName;
+ this.tableId = tableId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof TableIdentifier)) {
+ return false;
+ }
+ TableIdentifier that = (TableIdentifier) other;
+ return Objects.equals(storageName, that.storageName)
+ && Objects.equals(tableId, that.tableId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(storageName, tableId);
+ }
+ }
+
+ private static final class ResolvedTable {
+ private final List<String> tableNames;
+ private final String storageName;
+ private final StoragePlugin plugin;
+ private final DrillTableSelection selection;
+
+ private ResolvedTable(List<String> tableNames, String storageName,
+ StoragePlugin plugin, DrillTableSelection selection) {
+ this.tableNames = tableNames;
+ this.storageName = storageName;
+ this.plugin = plugin;
+ this.selection = selection;
+ }
+ }
+
+ /** Resolves every physical table in a query, including joins and
subqueries. */
+ public static ContextSnapshot resolve(SchemaPlus defaultSchema, SqlNode
query,
+ QueryContext context) {
+ StoragePluginRegistry plugins = context.getStorage();
+ List<ResolvedTable> dependencies = new ArrayList<>();
+ try {
+ // Check every participating plugin before reading any table version.
+ if (!collectQuery(defaultSchema, query, new HashSet<>(),
dependencies)) {
+ return null;
+ }
+ } catch (RuntimeException e) {
+ logger.debug("Query sources could not be validated for the plan
cache", e);
+ return null;
+ }
+ Map<TableIdentifier, String> versions = new LinkedHashMap<>();
+ Map<String, String> configs = new LinkedHashMap<>();
+ for (ResolvedTable dependency : dependencies) {
+ try {
+ PlanCacheTable source =
dependency.plugin.planCacheTable(dependency.selection);
+ if (source == null) {
+ return null;
+ }
+ TableIdentifier identifier = new
TableIdentifier(dependency.storageName,
+ source.getIdentifier());
+ String previous = versions.putIfAbsent(identifier,
source.getVersion());
+ if (previous != null && !previous.equals(source.getVersion())) {
+ return null;
+ }
+ String fingerprint = configFingerprint(plugins,
dependency.plugin.getConfig());
+ String previousConfig = configs.putIfAbsent(dependency.storageName,
fingerprint);
+ if (previousConfig != null && !previousConfig.equals(fingerprint)) {
+ return null;
+ }
+ } catch (IOException | RuntimeException e) {
+ logger.debug("Table context could not be captured for the plan
cache", e);
+ return null;
+ }
+ }
+ return new ContextSnapshot(optionFingerprint(context), versions,
configs);
+ }
+
+ private static boolean collectQuery(SchemaPlus schema, SqlNode node,
+ Set<String> ctes, List<ResolvedTable> dependencies) {
+ if (node == null) {
+ return true;
+ }
+ if (node instanceof SqlNodeList) {
+ for (SqlNode child : (SqlNodeList) node) {
+ if (!collectQuery(schema, child, ctes, dependencies)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (node instanceof SqlWith) {
+ SqlWith with = (SqlWith) node;
+ Set<String> local = new HashSet<>(ctes);
+ for (SqlNode itemNode : with.withList) {
+ SqlWithItem item = (SqlWithItem) itemNode;
+ if (!collectQuery(schema, item.query, local, dependencies)) {
+ return false;
+ }
+ local.add(item.name.getSimple());
+ }
+ return collectQuery(schema, with.body, local, dependencies);
+ }
+ if (node instanceof SqlSelect) {
+ SqlSelect select = (SqlSelect) node;
+ if (!collectFrom(schema, select.getFrom(), ctes, dependencies)) {
+ return false;
+ }
+ for (SqlNode operand : select.getOperandList()) {
+ if (operand != select.getFrom()
+ && !collectQuery(schema, operand, ctes, dependencies)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (node instanceof SqlCall) {
+ for (SqlNode operand : ((SqlCall) node).getOperandList()) {
+ if (!collectQuery(schema, operand, ctes, dependencies)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private static boolean collectFrom(SchemaPlus schema, SqlNode from,
+ Set<String> ctes, List<ResolvedTable> dependencies) {
+ if (from == null || from.getKind() == SqlKind.VALUES) {
+ return true;
+ }
+ if (from instanceof SqlIdentifier) {
+ List<String> names = ((SqlIdentifier) from).names;
+ if (names.size() == 1 && ctes.contains(names.get(0))) {
+ return true;
+ }
+ for (ResolvedTable dependency : dependencies) {
+ if (dependency.tableNames.equals(names)) {
+ return true;
+ }
+ }
+ ResolvedTable source = resolveTable(schema, names);
+ if (source == null) {
+ return false;
+ }
+ dependencies.add(source);
+ return true;
+ }
+ if (from instanceof SqlCall) {
+ SqlCall call = (SqlCall) from;
+ if (call.getKind() == SqlKind.AS || call.getKind() == SqlKind.LATERAL)
{
+ return collectFrom(schema, call.operand(0), ctes, dependencies);
+ }
+ if (call.getKind() == SqlKind.JOIN) {
+ return collectFrom(schema, call.operand(0), ctes, dependencies)
+ && collectFrom(schema, call.operand(3), ctes, dependencies)
+ && collectQuery(schema, call.operand(5), ctes, dependencies);
+ }
+ if (call.getKind().belongsTo(SqlKind.QUERY)) {
+ return collectQuery(schema, call, ctes, dependencies);
+ }
+ }
+ return false;
+ }
+
+ private static ResolvedTable resolveTable(SchemaPlus defaultSchema,
List<String> names) {
+ if (names.isEmpty() || names.get(names.size() - 1).contains("*")
+ || names.get(names.size() - 1).contains("?")) {
+ return null;
+ }
+ SchemaPlus schema = SchemaUtilities.findSchema(defaultSchema,
+ names.subList(0, names.size() - 1));
+ if (schema == null) {
+ return null;
+ }
+ Table table = schema.getTable(names.get(names.size() - 1));
+ if (!(table instanceof DrillTable)) {
+ return null;
+ }
+ DrillTable drillTable = (DrillTable) table;
+ Object selection = drillTable.getSelection();
+ if (!(selection instanceof DrillTableSelection)) {
+ return null;
+ }
+ StoragePlugin storagePlugin = drillTable.getPlugin();
+ if (!storagePlugin.supportPlanCache((DrillTableSelection) selection)) {
+ return null;
+ }
+ return new ResolvedTable(
+ Collections.unmodifiableList(new ArrayList<>(names)),
+ drillTable.getStorageEngineName(), storagePlugin,
(DrillTableSelection) selection);
+ }
+
+ private boolean matches(ContextSnapshot current) {
+ return current != null
+ && optionsFingerprint.equals(current.optionsFingerprint)
+ && tableVersions.equals(current.tableVersions)
+ && pluginConfigs.equals(current.pluginConfigs);
+ }
+
+ /** Options and plugin configurations identify separate reusable
templates. */
+ String keyFingerprint() {
+ StringBuilder configs = new StringBuilder();
+ new TreeMap<>(pluginConfigs).forEach((name, fingerprint) -> configs
+
.append(name.length()).append(':').append(name).append('=').append(fingerprint).append('\n'));
+ // Keep table versions outside the key so incompatible entries can be
invalidated.
+ return optionsFingerprint + '\n'
+ + Hashing.sha256().hashString(configs,
StandardCharsets.UTF_8).toString();
+ }
+
+ private static String optionFingerprint(QueryContext context) {
+ // OptionManager iteration walks system defaults, system overrides,
session
+ // overrides, and query overrides in precedence order. getOptionList()
only
+ // exposes local session/query values and misses system-level changes.
+ Map<String, OptionValue> options = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (OptionValue option : context.getOptions()) {
+ options.put(option.name, option);
+ }
+ StringBuilder fingerprint = new StringBuilder();
+ for (OptionValue option : options.values()) {
+
fingerprint.append(option.name).append(':').append(option.kind).append('=');
+ switch (option.kind) {
+ case BOOLEAN:
+ fingerprint.append(option.bool_val);
+ break;
+ case LONG:
+ fingerprint.append(option.num_val);
+ break;
+ case DOUBLE:
+ fingerprint.append(option.float_val);
+ break;
+ case STRING:
+
fingerprint.append(option.string_val.length()).append(':').append(option.string_val);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported option kind");
+ }
+ fingerprint.append('\n');
+ }
+ // Keep the cache key bounded even when the system has many registered
options.
+ return Hashing.sha256().hashString(fingerprint,
StandardCharsets.UTF_8).toString();
+ }
+
+ private static String configFingerprint(StoragePluginRegistry plugins,
+ StoragePluginConfig config) {
+ if (config == null) {
+ throw new IllegalArgumentException("Missing plugin configuration");
+ }
+ String encoded = plugins.encode(config);
+ if (encoded == null) {
+ throw new IllegalArgumentException("Plugin configuration could not be
encoded");
+ }
+ return Hashing.sha256().hashString(encoded,
StandardCharsets.UTF_8).toString();
+ }
+ }
+
+ private static LiteralExpression bindLiteral(SqlLiteral literal, MajorType
type, int index) {
+ Object value = literal.getValue();
+ LogicalExpression expression;
+ if (value instanceof BigDecimal) {
+ BigDecimal number = (BigDecimal) value;
+ if (type.getMinorType() == MinorType.VARDECIMAL) {
+ BigDecimal decimal = number.setScale(type.getScale(),
RoundingMode.UNNECESSARY);
+ if (decimal.precision() > type.getPrecision()) {
+ throw new IllegalArgumentException("Decimal parameter exceeds cached
precision");
+ }
+ expression = ValueExpressions.getVarDecimal(decimal,
+ type.getPrecision(), type.getScale());
+ } else if (type.getMinorType() == MinorType.FLOAT8) {
+ expression = ValueExpressions.getFloat8(number.doubleValue());
+ } else if (type.getMinorType() == MinorType.FLOAT4) {
+ expression = ValueExpressions.getFloat4(number.floatValue());
+ } else if (type.getMinorType() == MinorType.BIGINT) {
+ expression = ValueExpressions.getBigInt(number.longValueExact());
+ } else if (type.getMinorType() == MinorType.INT) {
+ expression = ValueExpressions.getInt(number.intValueExact());
+ } else {
+ throw new IllegalArgumentException("Unsupported numeric parameter
type");
+ }
+ } else if (value instanceof Boolean) {
+ expression = ValueExpressions.getBit((Boolean) value);
+ } else if (value instanceof NlsString) {
+ String string = ((NlsString) value).getValue();
+ expression = ValueExpressions.getChar(string, type.getPrecision());
+ } else {
+ throw new IllegalArgumentException("Unsupported parameter type");
+ }
+ LiteralExpression bound = (LiteralExpression) expression;
+ bound.setDynamicParamIndex(index);
Review Comment:
Documented that caching must remain disabled during rolling upgrades and can
only be enabled after all Drillbits are upgraded.
##########
exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/ClearPlanCacheHandler.java:
##########
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.planner.sql.handlers;
+
+import org.apache.calcite.sql.SqlNode;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.physical.PhysicalPlan;
+import org.apache.drill.exec.planner.sql.DirectPlan;
+import org.apache.drill.exec.planner.sql.parser.SqlClearPlanCache;
+import org.apache.drill.exec.proto.CoordinationProtos.DrillbitEndpoint;
+import org.apache.drill.exec.work.foreman.ForemanSetupException;
+
+/** Clears the cache on the Drillbit that plans this command. */
+public final class ClearPlanCacheHandler extends AbstractSqlHandler {
+ private final QueryContext context;
+
+ public ClearPlanCacheHandler(QueryContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PhysicalPlan getPlan(SqlNode sqlNode) throws ForemanSetupException {
+ unwrap(sqlNode, SqlClearPlanCache.class);
+ checkAdminPrivileges(context, context.getOptions(), "Not authorized to
clear the SYSTEM plan cache.");
+ context.getDrillbitContext().getPlanCache().clear();
Review Comment:
Documented that CLEAR affects only the receiving Drillbit. For example,
clearing node A does not clear node B behind the same load balancer. The
response identifies the cleared node.
##########
contrib/format-iceberg/src/main/java/org/apache/drill/exec/store/iceberg/format/IcebergFormatPlugin.java:
##########
@@ -104,6 +109,27 @@ public boolean supportsRead() {
return true;
}
+ @Override
+ public boolean supportPlanCache(FileSelection selection) {
+ return selection != null && config.getSnapshot() == null
+ && !(selection instanceof IcebergMetadataFileSelection) &&
selection.getSelectionRoot() != null;
+ }
+
+ @Override
+ public PlanCacheTable planCacheTable(FileSelection selection) throws
IOException {
+ if (!supportPlanCache(selection)) {
+ return null;
+ }
+ String identifier = selection.getSelectionRoot().toString();
+ Table table = new HadoopTables(fsConf).load(identifier);
+ if (!(table instanceof HasTableOperations)) {
+ return null;
+ }
+ TableMetadata metadata = ((HasTableOperations)
table).operations().current();
+ return metadata == null || metadata.uuid() == null ? null
+ : new PlanCacheTable(identifier, metadata.uuid() + ":" +
metadata.currentSchemaId());
Review Comment:
Provided schema contents now contribute to table compatibility. For example,
changing `id VARCHAR` to `id BIGINT` invalidates the old entry. Added tests
with and without provided schemas, including native schema changes. Tables
without an Iceberg UUID still bypass caching; this is now explicit in the
plugin guide.
> Physical plan cache for parameterized SQL queries
> -------------------------------------------------
>
> Key: DRILL-8555
> URL: https://issues.apache.org/jira/browse/DRILL-8555
> Project: Apache Drill
> Issue Type: New Feature
> Components: Query Planning & Optimization
> Reporter: Letian Jiang
> Priority: Major
>
> h2. Motivation
> Repeated queries that differ only in literals are a common workload, yet each
> query currently requires full planning. This adds unnecessary latency and
> wastes CPU time.
> h2. Brief Design
> Add a per-Drillbit, in-memory plan cache. Drill automatically parameterizes
> eligible SQL queries and reuses a cached physical plan with the current
> literal values, without changes to client queries.
> h2. Plan reuse safety
> * *Engine:* matches the SQL template and checks relevant option changes,
> plugin configuration changes, and whether base table versions still match.
> * *Storage plugin:* explicitly opts in, provides a plan-compatible table
> version, and is responsible for rebuilding its predicate-dependent scan work
> from the rebound expressions.
> h2. Expected benefit
> Short, frequently executed queries such as HBase reads can benefit
> substantially. Analytical queries with significant planning cost can benefit
> as well.
> h2. Implementation status
> A working implementation is available, including HBase, Paimon and Iceberg
> integrations. A pull request will follow.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)