github-actions[bot] commented on code in PR #67582:
URL: https://github.com/apache/doris/pull/67582#discussion_r3950212563
##########
fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java:
##########
@@ -1198,6 +1199,95 @@ static void validateRequiredQuerySelection(boolean
skipMissingVersion, int useFi
}
}
+ /** Immutable routing result for one expanded multi-key lookup tuple. */
+ public static final class PointQueryRoute {
+ private final long tabletId;
+ private final List<Long> candidateBackendIds;
+ private final boolean backendOrderBySelection;
+
+ private PointQueryRoute(long tabletId, List<Long> candidateBackendIds,
+ boolean backendOrderBySelection) {
+ this.tabletId = tabletId;
+ this.candidateBackendIds = candidateBackendIds;
+ this.backendOrderBySelection = backendOrderBySelection;
+ }
+
+ public long getTabletId() {
+ return tabletId;
+ }
+
+ public List<Long> getCandidateBackendIds() {
+ return candidateBackendIds;
+ }
+
+ public boolean isBackendOrderBySelection() {
+ return backendOrderBySelection;
+ }
+ }
+
+ /**
+ * Resolve one complete primary-key tuple for the multi-key point-query
coordinator. Routing is
+ * serial because the reusable scan node owns mutable pruning state. The
original predicates are
+ * restored before returning; only the copied route escapes this method.
+ *
+ * @return null when the tuple does not map to an existing non-empty
partition or selected bucket
+ */
+ public PointQueryRoute routePointQueryKeyTuple(List<LiteralExpr>
keyValues) throws UserException {
+ List<Column> keyColumns = olapTable.getBaseSchemaKeyColumns();
+ Preconditions.checkState(keyValues.size() == keyColumns.size(),
+ "Point-query key tuple has %s values but table requires %s",
+ keyValues.size(), keyColumns.size());
+
+ List<Expr> originalConjuncts = conjuncts;
+ List<Expr> routingConjuncts = new ArrayList<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ LiteralExpr value = keyValues.get(i);
+ Preconditions.checkState(value != null && !(value instanceof
NullLiteral),
+ "Multi-key point query cannot route a NULL key value");
+ SlotRef slot = Preconditions.checkNotNull(
+ findPointQueryKeySlot(originalConjuncts,
keyColumns.get(i).getName()),
+ "Missing point-query predicate for key column %s",
keyColumns.get(i).getName());
+ routingConjuncts.add(new
BinaryPredicate(BinaryPredicate.Operator.EQ, slot, value));
+ }
+
+ try {
+ conjuncts = routingConjuncts;
+ // Unlike the ordinary point-query path, multi-get evaluates this
mutable scan node
+ // repeatedly with different key tuples.
+ columnFilters.clear();
+ columnNameToRange.clear();
+ totalTabletsNum = 0;
+ selectedSplitNum = 0;
+ totalBytes = 0;
+ tabletBytes.clear();
+ lazyEvaluateRangeLocations();
+ if (scanTabletIds.isEmpty()) {
+ return null;
+ }
+ Preconditions.checkState(scanTabletIds.size() == 1,
+ "Multi-key point query must route to exactly one tablet");
+ if (scanBackendIds.isEmpty()) {
Review Comment:
**[P2] Preserve `skip_bad_tablet` for each routed key.**
`computeTabletInfo()` records the tablet before replica selection; when no
replica is queryable and `skip_bad_tablet=true`, `addScanRangeLocations()`
deliberately skips its range, leaving this method with a tablet ID but no
backend. Throwing here makes one bad tablet abort the whole multi-get, whereas
the existing single-key path returns an empty batch. Please return no route for
this skipped key while retaining the error when skipping is disabled, and add
an unavailable-tablet case.
##########
fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java:
##########
@@ -1198,6 +1199,95 @@ static void validateRequiredQuerySelection(boolean
skipMissingVersion, int useFi
}
}
+ /** Immutable routing result for one expanded multi-key lookup tuple. */
+ public static final class PointQueryRoute {
+ private final long tabletId;
+ private final List<Long> candidateBackendIds;
+ private final boolean backendOrderBySelection;
+
+ private PointQueryRoute(long tabletId, List<Long> candidateBackendIds,
+ boolean backendOrderBySelection) {
+ this.tabletId = tabletId;
+ this.candidateBackendIds = candidateBackendIds;
+ this.backendOrderBySelection = backendOrderBySelection;
+ }
+
+ public long getTabletId() {
+ return tabletId;
+ }
+
+ public List<Long> getCandidateBackendIds() {
+ return candidateBackendIds;
+ }
+
+ public boolean isBackendOrderBySelection() {
+ return backendOrderBySelection;
+ }
+ }
+
+ /**
+ * Resolve one complete primary-key tuple for the multi-key point-query
coordinator. Routing is
+ * serial because the reusable scan node owns mutable pruning state. The
original predicates are
+ * restored before returning; only the copied route escapes this method.
+ *
+ * @return null when the tuple does not map to an existing non-empty
partition or selected bucket
+ */
+ public PointQueryRoute routePointQueryKeyTuple(List<LiteralExpr>
keyValues) throws UserException {
+ List<Column> keyColumns = olapTable.getBaseSchemaKeyColumns();
+ Preconditions.checkState(keyValues.size() == keyColumns.size(),
+ "Point-query key tuple has %s values but table requires %s",
+ keyValues.size(), keyColumns.size());
+
+ List<Expr> originalConjuncts = conjuncts;
+ List<Expr> routingConjuncts = new ArrayList<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ LiteralExpr value = keyValues.get(i);
+ Preconditions.checkState(value != null && !(value instanceof
NullLiteral),
+ "Multi-key point query cannot route a NULL key value");
+ SlotRef slot = Preconditions.checkNotNull(
+ findPointQueryKeySlot(originalConjuncts,
keyColumns.get(i).getName()),
+ "Missing point-query predicate for key column %s",
keyColumns.get(i).getName());
+ routingConjuncts.add(new
BinaryPredicate(BinaryPredicate.Operator.EQ, slot, value));
+ }
+
+ try {
+ conjuncts = routingConjuncts;
+ // Unlike the ordinary point-query path, multi-get evaluates this
mutable scan node
+ // repeatedly with different key tuples.
+ columnFilters.clear();
+ columnNameToRange.clear();
+ totalTabletsNum = 0;
+ selectedSplitNum = 0;
+ totalBytes = 0;
+ tabletBytes.clear();
+ lazyEvaluateRangeLocations();
Review Comment:
**[P1] Apply TABLESAMPLE only once for the statement.** This per-tuple route
calls `lazyEvaluateRangeLocations()`, which clears `sampleTabletIds` and
chooses fresh random seeks for a non-REPEATABLE sample. An `IN` lookup can
consequently return the union of several independent tablet samples, unlike
normal execution's single sample. Please exclude sampled scans from multi-get
eligibility or retain one sampled tablet set across all tuples, and add a
multi-bucket TABLESAMPLE case.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/PointQueryMultiExecutor.java:
##########
@@ -0,0 +1,506 @@
+// 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.doris.qe;
+
+import org.apache.doris.analysis.ExprToThriftVisitor;
+import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.analysis.LiteralExprUtils;
+import org.apache.doris.analysis.NullLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.Status;
+import org.apache.doris.common.UserException;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.plans.PlaceholderId;
+import org.apache.doris.planner.OlapScanNode.PointQueryRoute;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.rpc.TCustomProtocolFactory;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExpr;
+import org.apache.doris.thrift.TExprNode;
+import org.apache.doris.thrift.TResultBatch;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Query-level coordinator for the supported single-column IN point query. Key
tuples routed to
+ * the same tablet are merged into one ordinary tablet_fetch_data request.
RPCs are concurrent across
+ * BEs but serial on each BE, because requests with the same UUID share a
reusable execution context
+ * there. Execution uses lightweight requests when enabled and resends a full
request on a cold cache.
+ * Cached executions require parameters bound with types compatible with their
key columns;
+ * parameter type changes requiring different comparison semantics are not
supported.
+ */
+public class PointQueryMultiExecutor extends PointQueryExecutor {
+ private final ShortCircuitQueryContext context;
+ private final StatementContext statementContext;
+ private final int maxMessageSize;
+ private final Set<Future<?>> currentRpcFutures =
Collections.synchronizedSet(new HashSet<>());
+ private final Set<Long> failedBackends = new HashSet<>();
+ private long timeoutMs = Config.point_query_timeout_ms;
+ private volatile boolean cancelled;
+
+ private static final class TabletTask {
+ private final long tabletId;
+ private final List<Backend> candidateBackends;
+ private final List<InternalService.KeyTuple> keyTuples = new
ArrayList<>();
+ private Backend backend;
+ private InternalService.PTabletKeyLookupRequest request;
+ private Future<InternalService.PTabletKeyLookupResponse> future;
+ private int attemptCount;
+ private String lastFailure;
+
+ private TabletTask(long tabletId, List<Backend> candidateBackends) {
+ this.tabletId = tabletId;
+ this.candidateBackends = candidateBackends;
+ }
+
+ private Backend nextBackend(Set<Long> failedBackends) {
+ int maxAttempts = Math.max(1,
+ Math.min(Config.max_point_query_retry_time,
candidateBackends.size()));
+ while (attemptCount < maxAttempts) {
+ Backend backend = candidateBackends.get(attemptCount++);
+ if (!failedBackends.contains(backend.getId()) &&
SimpleScheduler.isAvailable(backend)) {
+ return backend;
+ }
+ }
+ return null;
+ }
+ }
+
+ private final class ResultAccumulator {
+ private final List<ByteBuffer> rows = new ArrayList<>();
+ private final TDeserializer deserializer;
+ private long resultBytes;
+
+ private ResultAccumulator() throws TException {
+ deserializer = new TDeserializer(new
TCustomProtocolFactory(maxMessageSize));
+ }
+
+ private void add(InternalService.PTabletKeyLookupResponse response)
throws TException {
+ if (response.hasEmptyBatch() && response.getEmptyBatch()) {
+ return;
+ }
+ if (!response.hasRowBatch() || response.getRowBatch().isEmpty()) {
+ throw new TException("No row batch or empty batch found in
point-query response");
+ }
+
+ TResultBatch batch = new TResultBatch();
+ try {
+ deserializer.deserialize(batch,
response.getRowBatch().toByteArray());
+ } catch (TException e) {
+ if (ResultReceiver.isMessageSizeExceeded(e)) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ throw e;
+ }
+ for (ByteBuffer row : batch.getRows()) {
+ resultBytes += row.remaining();
+ if (resultBytes > maxMessageSize) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ rows.add(row);
+ }
+ }
+
+ private RowBatch finish() {
+ RowBatch rowBatch = new RowBatch();
+ if (rows.isEmpty()) {
+ return rowBatch;
+ }
+ TResultBatch resultBatch = new TResultBatch();
+ resultBatch.setRows(rows);
+ resultBatch.setIsCompressed(false);
+ resultBatch.setPacketSeq(0);
+ rowBatch.setBatch(resultBatch);
+ return rowBatch;
+ }
+ }
+
+ public PointQueryMultiExecutor(ShortCircuitQueryContext context,
+ StatementContext statementContext, int maxMessageSize) {
+ super(context, maxMessageSize);
+ this.context = context;
+ this.statementContext = statementContext;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public static void directExecuteShortCircuitQuery(StmtExecutor executor,
+ PreparedStatementContext preparedStmtCtx) throws Exception {
+ // Multi-get reads current bindings without mutating the cached IN
predicate.
+ executor.executeAndSendResult(false, false,
+ preparedStmtCtx.shortCircuitQueryContext.get().analzyedQuery,
+ executor.getContext().getMysqlChannel(), null, null);
+ }
+
+ @Override
+ public void setTimeout(long timeoutMs) {
+ this.timeoutMs = timeoutMs;
+ }
+
+ @Override
+ public RowBatch getNext() throws Exception {
+ try {
+ return getNextInternal();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ invalidateCache();
+ throw e;
+ } finally {
+ cancelInFlightRpcs();
+ }
+ }
+
+ private RowBatch getNextInternal() throws Exception {
+ long deadlineNanos = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+ Map<Long, TabletTask> tasksByTablet = buildTabletTasks(deadlineNanos);
+ ResultAccumulator accumulator = new ResultAccumulator();
+ List<TabletTask> pending = new ArrayList<>(tasksByTablet.values());
+ while (!pending.isEmpty()) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ pending = executeRound(pending, accumulator, deadlineNanos);
+ }
+ return accumulator.finish();
+ }
+
+ private Map<Long, TabletTask> buildTabletTasks(long deadlineNanos) throws
TException, UserException {
+ List<Column> keyColumns =
context.scanNode.getOlapTable().getBaseSchemaKeyColumns();
+ Map<String, Integer> keyIndexes = new HashMap<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ keyIndexes.put(normalizeColumnName(keyColumns.get(i).getName()),
i);
+ }
+
+ List<PlaceholderId> inPlaceholderIds =
statementContext.getPointQueryInPlaceholderIds();
+ SlotReference inSlot =
statementContext.getIdToComparisonSlot().get(inPlaceholderIds.get(0));
+ int inKeyIndex = keyIndexes.get(normalizeColumnName(
+ inSlot.getOriginalColumn().get().getName()));
+ LiteralExpr[] keyValues = new LiteralExpr[keyColumns.size()];
+ for (Map.Entry<PlaceholderId, SlotReference> entry
+ : statementContext.getIdToComparisonSlot().entrySet()) {
+ SlotReference slot = entry.getValue();
+ int keyIndex = keyIndexes.get(normalizeColumnName(
+ slot.getOriginalColumn().get().getName()));
+ // Eligibility checking guarantees exactly one placeholder for
each equality key.
+ if (keyIndex != inKeyIndex) {
+ keyValues[keyIndex] = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(entry.getKey())).toLegacyLiteral();
+ }
+ }
+
+ TSerializer serializer = new TSerializer();
+ ByteString[] serializedKeyValues = new ByteString[keyColumns.size()];
+ for (int i = 0; i < keyValues.length; ++i) {
+ if (i == inKeyIndex) {
+ continue;
+ }
+ if (keyValues[i] instanceof NullLiteral) {
+ return Collections.emptyMap();
+ }
+ keyValues[i] = normalizeKeyLiteral(keyColumns.get(i),
keyValues[i]);
+ serializedKeyValues[i] = serializeKeyLiteral(keyValues[i],
serializer);
+ }
+
+ Set<InternalService.KeyTuple> seenTuples = new HashSet<>();
+ Map<Long, TabletTask> tasksByTablet = new LinkedHashMap<>();
+ for (PlaceholderId placeholderId : inPlaceholderIds) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ LiteralExpr inValue = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(placeholderId)).toLegacyLiteral();
+ if (inValue instanceof NullLiteral) {
+ continue;
+ }
+ keyValues[inKeyIndex] =
normalizeKeyLiteral(keyColumns.get(inKeyIndex), inValue);
+ serializedKeyValues[inKeyIndex] =
serializeKeyLiteral(keyValues[inKeyIndex], serializer);
+ InternalService.KeyTuple.Builder tupleBuilder =
InternalService.KeyTuple.newBuilder();
+ for (ByteString serializedValue : serializedKeyValues) {
+ tupleBuilder.addKeyColumnLiterals(serializedValue);
+ }
+ InternalService.KeyTuple keyTuple = tupleBuilder.build();
+ if (!seenTuples.add(keyTuple)) {
+ continue;
+ }
+ PointQueryRoute route =
context.scanNode.routePointQueryKeyTuple(Arrays.asList(keyValues));
+ if (route == null) {
+ continue;
+ }
+ TabletTask task = tasksByTablet.get(route.getTabletId());
+ if (task == null) {
+ List<Backend> candidates = selectPointQueryBackends(route);
+ if (candidates.isEmpty()) {
+ throw new UserException("Tablet " + route.getTabletId()
+ + " has no available backend for multi-key point
query");
+ }
+ task = new TabletTask(route.getTabletId(), candidates);
+ tasksByTablet.put(route.getTabletId(), task);
+ }
+ task.keyTuples.add(keyTuple);
+ }
+ return tasksByTablet;
+ }
+
+ private static LiteralExpr normalizeKeyLiteral(Column column, LiteralExpr
literalExpr)
+ throws TException {
+ Type columnType = column.getType();
+ if (columnType.equals(literalExpr.getType())
+ || columnType.matchesType(literalExpr.getType())) {
+ return literalExpr;
+ }
+ try {
+ return
LiteralExprUtils.createLiteral(literalExpr.getStringValue(), columnType);
+ } catch (org.apache.doris.common.AnalysisException e) {
+ throw new TException("Failed to re-type literal for key column "
+ + column.getName() + ": " + e.getMessage(), e);
+ }
+ }
+
+ private static ByteString serializeKeyLiteral(LiteralExpr literalExpr,
TSerializer serializer)
+ throws TException {
+ TExpr thriftExpr = ExprToThriftVisitor.treeToThrift(literalExpr);
+ Preconditions.checkState(thriftExpr.getNodesSize() == 1,
+ "Expected a single TExprNode for point-query key literal, got
%s",
+ thriftExpr.getNodesSize());
+ TExprNode exprNode = thriftExpr.getNodes().get(0);
+ return ByteString.copyFrom(serializer.serialize(exprNode));
+ }
+
+ private static String normalizeColumnName(String columnName) {
+ return columnName.toLowerCase(Locale.ROOT);
+ }
+
+ private static List<Backend> selectPointQueryBackends(PointQueryRoute
route) {
+ List<Backend> candidates = new
ArrayList<>(route.getCandidateBackendIds().size());
+ for (Long backendId : route.getCandidateBackendIds()) {
+ Backend backend = Env.getCurrentSystemInfo().getBackend(backendId);
+ if (SimpleScheduler.isAvailable(backend)) {
+ candidates.add(backend);
+ }
+ }
+ if (!route.isBackendOrderBySelection()) {
+ Collections.shuffle(candidates);
+ }
+ return candidates;
+ }
+
+ private InternalService.PTabletKeyLookupRequest buildLookupRequest(
+ TabletTask task, boolean includeQueryContext) {
+ InternalService.PTabletKeyLookupRequest.Builder builder
+ = InternalService.PTabletKeyLookupRequest.newBuilder()
+ .setTabletId(task.tabletId)
+ .setIsBinaryRow(true)
+ .addAllKeyTuples(task.keyTuples);
Review Comment:
**[P1] Keep multi-get compatible with old BEs.** During a rolling upgrade
this request can reach an old replica because backend selection has no
capability gate. The base BE accepts repeated tuples but returns `NotSupported`
when the result mixes live and deleted rows, so a valid `IN` lookup fails after
retrying old replicas. Please gate batching on BE capability or fall back to
single-key/normal execution, and cover the mixed live/deleted upgrade case.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/PointQueryMultiExecutor.java:
##########
@@ -0,0 +1,506 @@
+// 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.doris.qe;
+
+import org.apache.doris.analysis.ExprToThriftVisitor;
+import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.analysis.LiteralExprUtils;
+import org.apache.doris.analysis.NullLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.Status;
+import org.apache.doris.common.UserException;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.plans.PlaceholderId;
+import org.apache.doris.planner.OlapScanNode.PointQueryRoute;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.rpc.TCustomProtocolFactory;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExpr;
+import org.apache.doris.thrift.TExprNode;
+import org.apache.doris.thrift.TResultBatch;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Query-level coordinator for the supported single-column IN point query. Key
tuples routed to
+ * the same tablet are merged into one ordinary tablet_fetch_data request.
RPCs are concurrent across
+ * BEs but serial on each BE, because requests with the same UUID share a
reusable execution context
+ * there. Execution uses lightweight requests when enabled and resends a full
request on a cold cache.
+ * Cached executions require parameters bound with types compatible with their
key columns;
+ * parameter type changes requiring different comparison semantics are not
supported.
+ */
+public class PointQueryMultiExecutor extends PointQueryExecutor {
+ private final ShortCircuitQueryContext context;
+ private final StatementContext statementContext;
+ private final int maxMessageSize;
+ private final Set<Future<?>> currentRpcFutures =
Collections.synchronizedSet(new HashSet<>());
+ private final Set<Long> failedBackends = new HashSet<>();
+ private long timeoutMs = Config.point_query_timeout_ms;
+ private volatile boolean cancelled;
+
+ private static final class TabletTask {
+ private final long tabletId;
+ private final List<Backend> candidateBackends;
+ private final List<InternalService.KeyTuple> keyTuples = new
ArrayList<>();
+ private Backend backend;
+ private InternalService.PTabletKeyLookupRequest request;
+ private Future<InternalService.PTabletKeyLookupResponse> future;
+ private int attemptCount;
+ private String lastFailure;
+
+ private TabletTask(long tabletId, List<Backend> candidateBackends) {
+ this.tabletId = tabletId;
+ this.candidateBackends = candidateBackends;
+ }
+
+ private Backend nextBackend(Set<Long> failedBackends) {
+ int maxAttempts = Math.max(1,
+ Math.min(Config.max_point_query_retry_time,
candidateBackends.size()));
+ while (attemptCount < maxAttempts) {
+ Backend backend = candidateBackends.get(attemptCount++);
+ if (!failedBackends.contains(backend.getId()) &&
SimpleScheduler.isAvailable(backend)) {
+ return backend;
+ }
+ }
+ return null;
+ }
+ }
+
+ private final class ResultAccumulator {
+ private final List<ByteBuffer> rows = new ArrayList<>();
+ private final TDeserializer deserializer;
+ private long resultBytes;
+
+ private ResultAccumulator() throws TException {
+ deserializer = new TDeserializer(new
TCustomProtocolFactory(maxMessageSize));
+ }
+
+ private void add(InternalService.PTabletKeyLookupResponse response)
throws TException {
+ if (response.hasEmptyBatch() && response.getEmptyBatch()) {
+ return;
+ }
+ if (!response.hasRowBatch() || response.getRowBatch().isEmpty()) {
+ throw new TException("No row batch or empty batch found in
point-query response");
+ }
+
+ TResultBatch batch = new TResultBatch();
+ try {
+ deserializer.deserialize(batch,
response.getRowBatch().toByteArray());
+ } catch (TException e) {
+ if (ResultReceiver.isMessageSizeExceeded(e)) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ throw e;
+ }
+ for (ByteBuffer row : batch.getRows()) {
+ resultBytes += row.remaining();
+ if (resultBytes > maxMessageSize) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ rows.add(row);
+ }
+ }
+
+ private RowBatch finish() {
+ RowBatch rowBatch = new RowBatch();
+ if (rows.isEmpty()) {
+ return rowBatch;
+ }
+ TResultBatch resultBatch = new TResultBatch();
+ resultBatch.setRows(rows);
+ resultBatch.setIsCompressed(false);
+ resultBatch.setPacketSeq(0);
+ rowBatch.setBatch(resultBatch);
+ return rowBatch;
+ }
+ }
+
+ public PointQueryMultiExecutor(ShortCircuitQueryContext context,
+ StatementContext statementContext, int maxMessageSize) {
+ super(context, maxMessageSize);
+ this.context = context;
+ this.statementContext = statementContext;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public static void directExecuteShortCircuitQuery(StmtExecutor executor,
+ PreparedStatementContext preparedStmtCtx) throws Exception {
+ // Multi-get reads current bindings without mutating the cached IN
predicate.
+ executor.executeAndSendResult(false, false,
+ preparedStmtCtx.shortCircuitQueryContext.get().analzyedQuery,
+ executor.getContext().getMysqlChannel(), null, null);
+ }
+
+ @Override
+ public void setTimeout(long timeoutMs) {
+ this.timeoutMs = timeoutMs;
+ }
+
+ @Override
+ public RowBatch getNext() throws Exception {
+ try {
+ return getNextInternal();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ invalidateCache();
+ throw e;
+ } finally {
+ cancelInFlightRpcs();
+ }
+ }
+
+ private RowBatch getNextInternal() throws Exception {
+ long deadlineNanos = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+ Map<Long, TabletTask> tasksByTablet = buildTabletTasks(deadlineNanos);
+ ResultAccumulator accumulator = new ResultAccumulator();
+ List<TabletTask> pending = new ArrayList<>(tasksByTablet.values());
+ while (!pending.isEmpty()) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ pending = executeRound(pending, accumulator, deadlineNanos);
+ }
+ return accumulator.finish();
+ }
+
+ private Map<Long, TabletTask> buildTabletTasks(long deadlineNanos) throws
TException, UserException {
+ List<Column> keyColumns =
context.scanNode.getOlapTable().getBaseSchemaKeyColumns();
+ Map<String, Integer> keyIndexes = new HashMap<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ keyIndexes.put(normalizeColumnName(keyColumns.get(i).getName()),
i);
+ }
+
+ List<PlaceholderId> inPlaceholderIds =
statementContext.getPointQueryInPlaceholderIds();
+ SlotReference inSlot =
statementContext.getIdToComparisonSlot().get(inPlaceholderIds.get(0));
+ int inKeyIndex = keyIndexes.get(normalizeColumnName(
+ inSlot.getOriginalColumn().get().getName()));
+ LiteralExpr[] keyValues = new LiteralExpr[keyColumns.size()];
+ for (Map.Entry<PlaceholderId, SlotReference> entry
+ : statementContext.getIdToComparisonSlot().entrySet()) {
+ SlotReference slot = entry.getValue();
+ int keyIndex = keyIndexes.get(normalizeColumnName(
+ slot.getOriginalColumn().get().getName()));
+ // Eligibility checking guarantees exactly one placeholder for
each equality key.
+ if (keyIndex != inKeyIndex) {
+ keyValues[keyIndex] = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(entry.getKey())).toLegacyLiteral();
+ }
+ }
+
+ TSerializer serializer = new TSerializer();
+ ByteString[] serializedKeyValues = new ByteString[keyColumns.size()];
+ for (int i = 0; i < keyValues.length; ++i) {
+ if (i == inKeyIndex) {
+ continue;
+ }
+ if (keyValues[i] instanceof NullLiteral) {
+ return Collections.emptyMap();
+ }
+ keyValues[i] = normalizeKeyLiteral(keyColumns.get(i),
keyValues[i]);
+ serializedKeyValues[i] = serializeKeyLiteral(keyValues[i],
serializer);
+ }
+
+ Set<InternalService.KeyTuple> seenTuples = new HashSet<>();
+ Map<Long, TabletTask> tasksByTablet = new LinkedHashMap<>();
+ for (PlaceholderId placeholderId : inPlaceholderIds) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ LiteralExpr inValue = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(placeholderId)).toLegacyLiteral();
+ if (inValue instanceof NullLiteral) {
+ continue;
+ }
+ keyValues[inKeyIndex] =
normalizeKeyLiteral(keyColumns.get(inKeyIndex), inValue);
+ serializedKeyValues[inKeyIndex] =
serializeKeyLiteral(keyValues[inKeyIndex], serializer);
+ InternalService.KeyTuple.Builder tupleBuilder =
InternalService.KeyTuple.newBuilder();
+ for (ByteString serializedValue : serializedKeyValues) {
+ tupleBuilder.addKeyColumnLiterals(serializedValue);
+ }
+ InternalService.KeyTuple keyTuple = tupleBuilder.build();
+ if (!seenTuples.add(keyTuple)) {
+ continue;
+ }
+ PointQueryRoute route =
context.scanNode.routePointQueryKeyTuple(Arrays.asList(keyValues));
+ if (route == null) {
+ continue;
+ }
+ TabletTask task = tasksByTablet.get(route.getTabletId());
+ if (task == null) {
+ List<Backend> candidates = selectPointQueryBackends(route);
+ if (candidates.isEmpty()) {
+ throw new UserException("Tablet " + route.getTabletId()
+ + " has no available backend for multi-key point
query");
+ }
+ task = new TabletTask(route.getTabletId(), candidates);
+ tasksByTablet.put(route.getTabletId(), task);
+ }
+ task.keyTuples.add(keyTuple);
+ }
+ return tasksByTablet;
+ }
+
+ private static LiteralExpr normalizeKeyLiteral(Column column, LiteralExpr
literalExpr)
+ throws TException {
+ Type columnType = column.getType();
+ if (columnType.equals(literalExpr.getType())
+ || columnType.matchesType(literalExpr.getType())) {
+ return literalExpr;
+ }
+ try {
+ return
LiteralExprUtils.createLiteral(literalExpr.getStringValue(), columnType);
+ } catch (org.apache.doris.common.AnalysisException e) {
+ throw new TException("Failed to re-type literal for key column "
+ + column.getName() + ": " + e.getMessage(), e);
+ }
+ }
+
+ private static ByteString serializeKeyLiteral(LiteralExpr literalExpr,
TSerializer serializer)
+ throws TException {
+ TExpr thriftExpr = ExprToThriftVisitor.treeToThrift(literalExpr);
+ Preconditions.checkState(thriftExpr.getNodesSize() == 1,
+ "Expected a single TExprNode for point-query key literal, got
%s",
+ thriftExpr.getNodesSize());
+ TExprNode exprNode = thriftExpr.getNodes().get(0);
+ return ByteString.copyFrom(serializer.serialize(exprNode));
+ }
+
+ private static String normalizeColumnName(String columnName) {
+ return columnName.toLowerCase(Locale.ROOT);
+ }
+
+ private static List<Backend> selectPointQueryBackends(PointQueryRoute
route) {
+ List<Backend> candidates = new
ArrayList<>(route.getCandidateBackendIds().size());
+ for (Long backendId : route.getCandidateBackendIds()) {
+ Backend backend = Env.getCurrentSystemInfo().getBackend(backendId);
+ if (SimpleScheduler.isAvailable(backend)) {
+ candidates.add(backend);
+ }
+ }
+ if (!route.isBackendOrderBySelection()) {
+ Collections.shuffle(candidates);
+ }
+ return candidates;
+ }
+
+ private InternalService.PTabletKeyLookupRequest buildLookupRequest(
+ TabletTask task, boolean includeQueryContext) {
+ InternalService.PTabletKeyLookupRequest.Builder builder
Review Comment:
**[P1] Preserve one FE-visible version for every tablet task.** Normal scan
ranges carry the partition visible version, but this request drops it; the BE
then uses `_version == -1`, reads every installed rowset, and probes at
`INT32_MAX`. A publish can therefore become visible on only some tasks (or
before FE exposes it), producing a fractured statement result. Please retain
the routed partition version and cap each lookup to it, or disable multi-tablet
short circuit until that snapshot can be enforced.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/PointQueryMultiExecutor.java:
##########
@@ -0,0 +1,506 @@
+// 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.doris.qe;
+
+import org.apache.doris.analysis.ExprToThriftVisitor;
+import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.analysis.LiteralExprUtils;
+import org.apache.doris.analysis.NullLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.Status;
+import org.apache.doris.common.UserException;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.plans.PlaceholderId;
+import org.apache.doris.planner.OlapScanNode.PointQueryRoute;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.rpc.TCustomProtocolFactory;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExpr;
+import org.apache.doris.thrift.TExprNode;
+import org.apache.doris.thrift.TResultBatch;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Query-level coordinator for the supported single-column IN point query. Key
tuples routed to
+ * the same tablet are merged into one ordinary tablet_fetch_data request.
RPCs are concurrent across
+ * BEs but serial on each BE, because requests with the same UUID share a
reusable execution context
+ * there. Execution uses lightweight requests when enabled and resends a full
request on a cold cache.
+ * Cached executions require parameters bound with types compatible with their
key columns;
+ * parameter type changes requiring different comparison semantics are not
supported.
+ */
+public class PointQueryMultiExecutor extends PointQueryExecutor {
+ private final ShortCircuitQueryContext context;
+ private final StatementContext statementContext;
+ private final int maxMessageSize;
+ private final Set<Future<?>> currentRpcFutures =
Collections.synchronizedSet(new HashSet<>());
+ private final Set<Long> failedBackends = new HashSet<>();
+ private long timeoutMs = Config.point_query_timeout_ms;
+ private volatile boolean cancelled;
+
+ private static final class TabletTask {
+ private final long tabletId;
+ private final List<Backend> candidateBackends;
+ private final List<InternalService.KeyTuple> keyTuples = new
ArrayList<>();
+ private Backend backend;
+ private InternalService.PTabletKeyLookupRequest request;
+ private Future<InternalService.PTabletKeyLookupResponse> future;
+ private int attemptCount;
+ private String lastFailure;
+
+ private TabletTask(long tabletId, List<Backend> candidateBackends) {
+ this.tabletId = tabletId;
+ this.candidateBackends = candidateBackends;
+ }
+
+ private Backend nextBackend(Set<Long> failedBackends) {
+ int maxAttempts = Math.max(1,
+ Math.min(Config.max_point_query_retry_time,
candidateBackends.size()));
+ while (attemptCount < maxAttempts) {
+ Backend backend = candidateBackends.get(attemptCount++);
+ if (!failedBackends.contains(backend.getId()) &&
SimpleScheduler.isAvailable(backend)) {
+ return backend;
+ }
+ }
+ return null;
+ }
+ }
+
+ private final class ResultAccumulator {
+ private final List<ByteBuffer> rows = new ArrayList<>();
+ private final TDeserializer deserializer;
+ private long resultBytes;
+
+ private ResultAccumulator() throws TException {
+ deserializer = new TDeserializer(new
TCustomProtocolFactory(maxMessageSize));
+ }
+
+ private void add(InternalService.PTabletKeyLookupResponse response)
throws TException {
+ if (response.hasEmptyBatch() && response.getEmptyBatch()) {
+ return;
+ }
+ if (!response.hasRowBatch() || response.getRowBatch().isEmpty()) {
+ throw new TException("No row batch or empty batch found in
point-query response");
+ }
+
+ TResultBatch batch = new TResultBatch();
+ try {
+ deserializer.deserialize(batch,
response.getRowBatch().toByteArray());
+ } catch (TException e) {
+ if (ResultReceiver.isMessageSizeExceeded(e)) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ throw e;
+ }
+ for (ByteBuffer row : batch.getRows()) {
+ resultBytes += row.remaining();
Review Comment:
**[P2] Keep the receiver limit per response.**
`max_msg_size_of_result_receiver` is a per-message deserialization limit, and
each tablet response is already checked under it. Summing every response's row
bytes here applies the same limit to the whole multi-tablet result, so several
individually legal responses can fail only because their aggregate crosses the
threshold. Please remove this query-wide sum or use a separately named and
documented total-result bound, and cover multiple responses whose aggregate
exceeds the per-message limit.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1545,8 +1545,13 @@ public void executeAndSendResult(boolean isOutfileQuery,
boolean isSendFields,
// ExecuteCommand publishes this same context after a
successful first prepared execution.
statementContext.setShortCircuitQueryContext(shortCircuitQueryContext);
}
- coordBase = new PointQueryExecutor(shortCircuitQueryContext,
-
context.getSessionVariable().getMaxMsgSizeOfResultReceiver());
+ if (statementContext.isMultiKeyPointQuery()) {
+ coordBase = new
PointQueryMultiExecutor(shortCircuitQueryContext, statementContext,
Review Comment:
**[P2] Publish the multi executor to the cancellation path.** This instance
only lives in local `coordBase`, while `StmtExecutor.cancel()` reaches the
delegate, a `CancelableCommand`, or the `coord` field. Consequently client
disconnect, `KILL QUERY`, and the connection timeout checker cannot call this
executor's `cancel()`, leaving all fan-out RPCs alive until the point-query
deadline. Please register and clear a race-safe cancellation target around this
execution.
##########
be/src/service/point_query_executor.cpp:
##########
@@ -627,7 +627,7 @@ Status PointQueryExecutor::_lookup_row_data() {
}
}
// filter rows by delete sign
- if (_row_hits > 0 && _reusable->delete_sign_idx() != -1) {
+ if ((_row_hits > 0 || _result_block->rows() > 0) &&
_reusable->delete_sign_idx() != -1) {
Review Comment:
**[P1] Do not clear cached partial-row-store hits with no delete-sign
data.** When row cache is enabled for a table with partial `row_store_columns`,
a hit deserializes the stored user columns but skips missing-column completion,
leaving the delete-sign vector empty and `_row_hits == 0`. This new `rows() >
0` path then gets `filtered == total == 0` and clears every warmed live row;
two identical multi-gets return rows and then empty. Please populate the
missing sign for cached rows or bypass partial-row caching, and add a
cache-warm partial-row-store test.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/PointQueryMultiExecutor.java:
##########
@@ -0,0 +1,506 @@
+// 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.doris.qe;
+
+import org.apache.doris.analysis.ExprToThriftVisitor;
+import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.analysis.LiteralExprUtils;
+import org.apache.doris.analysis.NullLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.Status;
+import org.apache.doris.common.UserException;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.plans.PlaceholderId;
+import org.apache.doris.planner.OlapScanNode.PointQueryRoute;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.rpc.TCustomProtocolFactory;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExpr;
+import org.apache.doris.thrift.TExprNode;
+import org.apache.doris.thrift.TResultBatch;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Query-level coordinator for the supported single-column IN point query. Key
tuples routed to
+ * the same tablet are merged into one ordinary tablet_fetch_data request.
RPCs are concurrent across
+ * BEs but serial on each BE, because requests with the same UUID share a
reusable execution context
+ * there. Execution uses lightweight requests when enabled and resends a full
request on a cold cache.
+ * Cached executions require parameters bound with types compatible with their
key columns;
+ * parameter type changes requiring different comparison semantics are not
supported.
+ */
+public class PointQueryMultiExecutor extends PointQueryExecutor {
+ private final ShortCircuitQueryContext context;
+ private final StatementContext statementContext;
+ private final int maxMessageSize;
+ private final Set<Future<?>> currentRpcFutures =
Collections.synchronizedSet(new HashSet<>());
+ private final Set<Long> failedBackends = new HashSet<>();
+ private long timeoutMs = Config.point_query_timeout_ms;
+ private volatile boolean cancelled;
+
+ private static final class TabletTask {
+ private final long tabletId;
+ private final List<Backend> candidateBackends;
+ private final List<InternalService.KeyTuple> keyTuples = new
ArrayList<>();
+ private Backend backend;
+ private InternalService.PTabletKeyLookupRequest request;
+ private Future<InternalService.PTabletKeyLookupResponse> future;
+ private int attemptCount;
+ private String lastFailure;
+
+ private TabletTask(long tabletId, List<Backend> candidateBackends) {
+ this.tabletId = tabletId;
+ this.candidateBackends = candidateBackends;
+ }
+
+ private Backend nextBackend(Set<Long> failedBackends) {
+ int maxAttempts = Math.max(1,
+ Math.min(Config.max_point_query_retry_time,
candidateBackends.size()));
+ while (attemptCount < maxAttempts) {
+ Backend backend = candidateBackends.get(attemptCount++);
+ if (!failedBackends.contains(backend.getId()) &&
SimpleScheduler.isAvailable(backend)) {
+ return backend;
+ }
+ }
+ return null;
+ }
+ }
+
+ private final class ResultAccumulator {
+ private final List<ByteBuffer> rows = new ArrayList<>();
+ private final TDeserializer deserializer;
+ private long resultBytes;
+
+ private ResultAccumulator() throws TException {
+ deserializer = new TDeserializer(new
TCustomProtocolFactory(maxMessageSize));
+ }
+
+ private void add(InternalService.PTabletKeyLookupResponse response)
throws TException {
+ if (response.hasEmptyBatch() && response.getEmptyBatch()) {
+ return;
+ }
+ if (!response.hasRowBatch() || response.getRowBatch().isEmpty()) {
+ throw new TException("No row batch or empty batch found in
point-query response");
+ }
+
+ TResultBatch batch = new TResultBatch();
+ try {
+ deserializer.deserialize(batch,
response.getRowBatch().toByteArray());
+ } catch (TException e) {
+ if (ResultReceiver.isMessageSizeExceeded(e)) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ throw e;
+ }
+ for (ByteBuffer row : batch.getRows()) {
+ resultBytes += row.remaining();
+ if (resultBytes > maxMessageSize) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ rows.add(row);
+ }
+ }
+
+ private RowBatch finish() {
+ RowBatch rowBatch = new RowBatch();
+ if (rows.isEmpty()) {
+ return rowBatch;
+ }
+ TResultBatch resultBatch = new TResultBatch();
+ resultBatch.setRows(rows);
+ resultBatch.setIsCompressed(false);
+ resultBatch.setPacketSeq(0);
+ rowBatch.setBatch(resultBatch);
+ return rowBatch;
+ }
+ }
+
+ public PointQueryMultiExecutor(ShortCircuitQueryContext context,
+ StatementContext statementContext, int maxMessageSize) {
+ super(context, maxMessageSize);
+ this.context = context;
+ this.statementContext = statementContext;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public static void directExecuteShortCircuitQuery(StmtExecutor executor,
+ PreparedStatementContext preparedStmtCtx) throws Exception {
+ // Multi-get reads current bindings without mutating the cached IN
predicate.
+ executor.executeAndSendResult(false, false,
+ preparedStmtCtx.shortCircuitQueryContext.get().analzyedQuery,
+ executor.getContext().getMysqlChannel(), null, null);
+ }
+
+ @Override
+ public void setTimeout(long timeoutMs) {
+ this.timeoutMs = timeoutMs;
+ }
+
+ @Override
+ public RowBatch getNext() throws Exception {
+ try {
+ return getNextInternal();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ invalidateCache();
+ throw e;
+ } finally {
+ cancelInFlightRpcs();
+ }
+ }
+
+ private RowBatch getNextInternal() throws Exception {
+ long deadlineNanos = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+ Map<Long, TabletTask> tasksByTablet = buildTabletTasks(deadlineNanos);
+ ResultAccumulator accumulator = new ResultAccumulator();
+ List<TabletTask> pending = new ArrayList<>(tasksByTablet.values());
+ while (!pending.isEmpty()) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ pending = executeRound(pending, accumulator, deadlineNanos);
+ }
+ return accumulator.finish();
+ }
+
+ private Map<Long, TabletTask> buildTabletTasks(long deadlineNanos) throws
TException, UserException {
+ List<Column> keyColumns =
context.scanNode.getOlapTable().getBaseSchemaKeyColumns();
+ Map<String, Integer> keyIndexes = new HashMap<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ keyIndexes.put(normalizeColumnName(keyColumns.get(i).getName()),
i);
+ }
+
+ List<PlaceholderId> inPlaceholderIds =
statementContext.getPointQueryInPlaceholderIds();
+ SlotReference inSlot =
statementContext.getIdToComparisonSlot().get(inPlaceholderIds.get(0));
+ int inKeyIndex = keyIndexes.get(normalizeColumnName(
+ inSlot.getOriginalColumn().get().getName()));
+ LiteralExpr[] keyValues = new LiteralExpr[keyColumns.size()];
+ for (Map.Entry<PlaceholderId, SlotReference> entry
+ : statementContext.getIdToComparisonSlot().entrySet()) {
+ SlotReference slot = entry.getValue();
+ int keyIndex = keyIndexes.get(normalizeColumnName(
+ slot.getOriginalColumn().get().getName()));
+ // Eligibility checking guarantees exactly one placeholder for
each equality key.
+ if (keyIndex != inKeyIndex) {
+ keyValues[keyIndex] = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(entry.getKey())).toLegacyLiteral();
+ }
+ }
+
+ TSerializer serializer = new TSerializer();
+ ByteString[] serializedKeyValues = new ByteString[keyColumns.size()];
+ for (int i = 0; i < keyValues.length; ++i) {
+ if (i == inKeyIndex) {
+ continue;
+ }
+ if (keyValues[i] instanceof NullLiteral) {
+ return Collections.emptyMap();
+ }
+ keyValues[i] = normalizeKeyLiteral(keyColumns.get(i),
keyValues[i]);
+ serializedKeyValues[i] = serializeKeyLiteral(keyValues[i],
serializer);
+ }
+
+ Set<InternalService.KeyTuple> seenTuples = new HashSet<>();
+ Map<Long, TabletTask> tasksByTablet = new LinkedHashMap<>();
+ for (PlaceholderId placeholderId : inPlaceholderIds) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ LiteralExpr inValue = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(placeholderId)).toLegacyLiteral();
+ if (inValue instanceof NullLiteral) {
+ continue;
+ }
+ keyValues[inKeyIndex] =
normalizeKeyLiteral(keyColumns.get(inKeyIndex), inValue);
+ serializedKeyValues[inKeyIndex] =
serializeKeyLiteral(keyValues[inKeyIndex], serializer);
+ InternalService.KeyTuple.Builder tupleBuilder =
InternalService.KeyTuple.newBuilder();
+ for (ByteString serializedValue : serializedKeyValues) {
+ tupleBuilder.addKeyColumnLiterals(serializedValue);
+ }
+ InternalService.KeyTuple keyTuple = tupleBuilder.build();
+ if (!seenTuples.add(keyTuple)) {
+ continue;
+ }
+ PointQueryRoute route =
context.scanNode.routePointQueryKeyTuple(Arrays.asList(keyValues));
+ if (route == null) {
+ continue;
+ }
+ TabletTask task = tasksByTablet.get(route.getTabletId());
+ if (task == null) {
+ List<Backend> candidates = selectPointQueryBackends(route);
+ if (candidates.isEmpty()) {
+ throw new UserException("Tablet " + route.getTabletId()
+ + " has no available backend for multi-key point
query");
+ }
+ task = new TabletTask(route.getTabletId(), candidates);
+ tasksByTablet.put(route.getTabletId(), task);
+ }
+ task.keyTuples.add(keyTuple);
+ }
+ return tasksByTablet;
+ }
+
+ private static LiteralExpr normalizeKeyLiteral(Column column, LiteralExpr
literalExpr)
+ throws TException {
+ Type columnType = column.getType();
+ if (columnType.equals(literalExpr.getType())
+ || columnType.matchesType(literalExpr.getType())) {
+ return literalExpr;
+ }
+ try {
+ return
LiteralExprUtils.createLiteral(literalExpr.getStringValue(), columnType);
Review Comment:
**[P1] Replan when a cached binding needs different comparison coercion.**
COM_STMT_EXECUTE can supply new parameter types on a later call, but cache
reuse bypasses analysis and this code force-parses the value as the key type.
For an INT key, a cached statement rebound with DOUBLE `1.5` is a valid wider
comparison that should return no row; here it fails strict integer parsing
instead. Please retain the cache's binding/coercion contract and fall back to
normal analysis when it changes, with a second-execution type-change test.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/PointQueryMultiExecutor.java:
##########
@@ -0,0 +1,506 @@
+// 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.doris.qe;
+
+import org.apache.doris.analysis.ExprToThriftVisitor;
+import org.apache.doris.analysis.LiteralExpr;
+import org.apache.doris.analysis.LiteralExprUtils;
+import org.apache.doris.analysis.NullLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.Status;
+import org.apache.doris.common.UserException;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.plans.PlaceholderId;
+import org.apache.doris.planner.OlapScanNode.PointQueryRoute;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.rpc.RpcException;
+import org.apache.doris.rpc.TCustomProtocolFactory;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExpr;
+import org.apache.doris.thrift.TExprNode;
+import org.apache.doris.thrift.TResultBatch;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TException;
+import org.apache.thrift.TSerializer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Query-level coordinator for the supported single-column IN point query. Key
tuples routed to
+ * the same tablet are merged into one ordinary tablet_fetch_data request.
RPCs are concurrent across
+ * BEs but serial on each BE, because requests with the same UUID share a
reusable execution context
+ * there. Execution uses lightweight requests when enabled and resends a full
request on a cold cache.
+ * Cached executions require parameters bound with types compatible with their
key columns;
+ * parameter type changes requiring different comparison semantics are not
supported.
+ */
+public class PointQueryMultiExecutor extends PointQueryExecutor {
+ private final ShortCircuitQueryContext context;
+ private final StatementContext statementContext;
+ private final int maxMessageSize;
+ private final Set<Future<?>> currentRpcFutures =
Collections.synchronizedSet(new HashSet<>());
+ private final Set<Long> failedBackends = new HashSet<>();
+ private long timeoutMs = Config.point_query_timeout_ms;
+ private volatile boolean cancelled;
+
+ private static final class TabletTask {
+ private final long tabletId;
+ private final List<Backend> candidateBackends;
+ private final List<InternalService.KeyTuple> keyTuples = new
ArrayList<>();
+ private Backend backend;
+ private InternalService.PTabletKeyLookupRequest request;
+ private Future<InternalService.PTabletKeyLookupResponse> future;
+ private int attemptCount;
+ private String lastFailure;
+
+ private TabletTask(long tabletId, List<Backend> candidateBackends) {
+ this.tabletId = tabletId;
+ this.candidateBackends = candidateBackends;
+ }
+
+ private Backend nextBackend(Set<Long> failedBackends) {
+ int maxAttempts = Math.max(1,
+ Math.min(Config.max_point_query_retry_time,
candidateBackends.size()));
+ while (attemptCount < maxAttempts) {
+ Backend backend = candidateBackends.get(attemptCount++);
+ if (!failedBackends.contains(backend.getId()) &&
SimpleScheduler.isAvailable(backend)) {
+ return backend;
+ }
+ }
+ return null;
+ }
+ }
+
+ private final class ResultAccumulator {
+ private final List<ByteBuffer> rows = new ArrayList<>();
+ private final TDeserializer deserializer;
+ private long resultBytes;
+
+ private ResultAccumulator() throws TException {
+ deserializer = new TDeserializer(new
TCustomProtocolFactory(maxMessageSize));
+ }
+
+ private void add(InternalService.PTabletKeyLookupResponse response)
throws TException {
+ if (response.hasEmptyBatch() && response.getEmptyBatch()) {
+ return;
+ }
+ if (!response.hasRowBatch() || response.getRowBatch().isEmpty()) {
+ throw new TException("No row batch or empty batch found in
point-query response");
+ }
+
+ TResultBatch batch = new TResultBatch();
+ try {
+ deserializer.deserialize(batch,
response.getRowBatch().toByteArray());
+ } catch (TException e) {
+ if (ResultReceiver.isMessageSizeExceeded(e)) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ throw e;
+ }
+ for (ByteBuffer row : batch.getRows()) {
+ resultBytes += row.remaining();
+ if (resultBytes > maxMessageSize) {
+ throw new TException(
+ "MaxMessageSize reached, try increase
max_msg_size_of_result_receiver");
+ }
+ rows.add(row);
+ }
+ }
+
+ private RowBatch finish() {
+ RowBatch rowBatch = new RowBatch();
+ if (rows.isEmpty()) {
+ return rowBatch;
+ }
+ TResultBatch resultBatch = new TResultBatch();
+ resultBatch.setRows(rows);
+ resultBatch.setIsCompressed(false);
+ resultBatch.setPacketSeq(0);
+ rowBatch.setBatch(resultBatch);
+ return rowBatch;
+ }
+ }
+
+ public PointQueryMultiExecutor(ShortCircuitQueryContext context,
+ StatementContext statementContext, int maxMessageSize) {
+ super(context, maxMessageSize);
+ this.context = context;
+ this.statementContext = statementContext;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public static void directExecuteShortCircuitQuery(StmtExecutor executor,
+ PreparedStatementContext preparedStmtCtx) throws Exception {
+ // Multi-get reads current bindings without mutating the cached IN
predicate.
+ executor.executeAndSendResult(false, false,
+ preparedStmtCtx.shortCircuitQueryContext.get().analzyedQuery,
+ executor.getContext().getMysqlChannel(), null, null);
+ }
+
+ @Override
+ public void setTimeout(long timeoutMs) {
+ this.timeoutMs = timeoutMs;
+ }
+
+ @Override
+ public RowBatch getNext() throws Exception {
+ try {
+ return getNextInternal();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ invalidateCache();
+ throw e;
+ } finally {
+ cancelInFlightRpcs();
+ }
+ }
+
+ private RowBatch getNextInternal() throws Exception {
+ long deadlineNanos = System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+ Map<Long, TabletTask> tasksByTablet = buildTabletTasks(deadlineNanos);
+ ResultAccumulator accumulator = new ResultAccumulator();
+ List<TabletTask> pending = new ArrayList<>(tasksByTablet.values());
+ while (!pending.isEmpty()) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ pending = executeRound(pending, accumulator, deadlineNanos);
+ }
+ return accumulator.finish();
+ }
+
+ private Map<Long, TabletTask> buildTabletTasks(long deadlineNanos) throws
TException, UserException {
+ List<Column> keyColumns =
context.scanNode.getOlapTable().getBaseSchemaKeyColumns();
+ Map<String, Integer> keyIndexes = new HashMap<>(keyColumns.size());
+ for (int i = 0; i < keyColumns.size(); ++i) {
+ keyIndexes.put(normalizeColumnName(keyColumns.get(i).getName()),
i);
+ }
+
+ List<PlaceholderId> inPlaceholderIds =
statementContext.getPointQueryInPlaceholderIds();
+ SlotReference inSlot =
statementContext.getIdToComparisonSlot().get(inPlaceholderIds.get(0));
+ int inKeyIndex = keyIndexes.get(normalizeColumnName(
+ inSlot.getOriginalColumn().get().getName()));
+ LiteralExpr[] keyValues = new LiteralExpr[keyColumns.size()];
+ for (Map.Entry<PlaceholderId, SlotReference> entry
+ : statementContext.getIdToComparisonSlot().entrySet()) {
+ SlotReference slot = entry.getValue();
+ int keyIndex = keyIndexes.get(normalizeColumnName(
+ slot.getOriginalColumn().get().getName()));
+ // Eligibility checking guarantees exactly one placeholder for
each equality key.
+ if (keyIndex != inKeyIndex) {
+ keyValues[keyIndex] = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(entry.getKey())).toLegacyLiteral();
+ }
+ }
+
+ TSerializer serializer = new TSerializer();
+ ByteString[] serializedKeyValues = new ByteString[keyColumns.size()];
+ for (int i = 0; i < keyValues.length; ++i) {
+ if (i == inKeyIndex) {
+ continue;
+ }
+ if (keyValues[i] instanceof NullLiteral) {
+ return Collections.emptyMap();
+ }
+ keyValues[i] = normalizeKeyLiteral(keyColumns.get(i),
keyValues[i]);
+ serializedKeyValues[i] = serializeKeyLiteral(keyValues[i],
serializer);
+ }
+
+ Set<InternalService.KeyTuple> seenTuples = new HashSet<>();
+ Map<Long, TabletTask> tasksByTablet = new LinkedHashMap<>();
+ for (PlaceholderId placeholderId : inPlaceholderIds) {
+ checkCancelledOrTimedOut(deadlineNanos);
+ LiteralExpr inValue = ((Literal)
statementContext.getIdToPlaceholderRealExpr()
+ .get(placeholderId)).toLegacyLiteral();
+ if (inValue instanceof NullLiteral) {
+ continue;
+ }
+ keyValues[inKeyIndex] =
normalizeKeyLiteral(keyColumns.get(inKeyIndex), inValue);
+ serializedKeyValues[inKeyIndex] =
serializeKeyLiteral(keyValues[inKeyIndex], serializer);
+ InternalService.KeyTuple.Builder tupleBuilder =
InternalService.KeyTuple.newBuilder();
+ for (ByteString serializedValue : serializedKeyValues) {
+ tupleBuilder.addKeyColumnLiterals(serializedValue);
+ }
+ InternalService.KeyTuple keyTuple = tupleBuilder.build();
+ if (!seenTuples.add(keyTuple)) {
Review Comment:
**[P1] Deduplicate canonical storage keys, not serialized literals.** Equal
DECIMAL bindings such as `1.0` and `1.00` can serialize to different
`KeyTuple`s, so both pass this set; BE then parses both at the column scale,
encodes the same primary key twice, and emits the unique-key row twice. Please
canonicalize to the exact key-column representation before hashing (or
deduplicate encoded keys on BE) and cover equal BigDecimal values with
different scales.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]