xiangfu0 commented on code in PR #19396:
URL: https://github.com/apache/pinot/pull/19396#discussion_r4213852934
##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java:
##########
@@ -623,6 +632,9 @@ private ImmutableQueryEnvironment.Config
getQueryEnvConf(HttpHeaders httpHeaders
.defaultDisabledPlannerRules(_defaultDisabledPlannerRules)
.defaultSortExchangeCopyLimit(sortExchangeCopyThreshold)
.defaultSealedInListThreshold(sealedInListThreshold)
+ .defaultWindowSortOnSender(windowSortOnSender)
+
.isKWayMergeSupported(!QueryOptionsUtils.isMultiClusterRoutingEnabled(queryOptions,
false)
+ && _kWayMergeSupported.getAsBoolean())
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
The option-off path now short-circuits before consulting the capability
supplier. The supplier itself only reads the shared tracker's volatile
snapshot: it does not call Helix, ZooKeeper, or isConnected(). The focused
capability tests verify repeated reads make no manager calls.
##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/KWayMergeSupportPredicate.java:
##########
@@ -0,0 +1,99 @@
+/**
+ * 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.pinot.broker.requesthandler;
+
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixManager;
+import org.apache.helix.NotificationContext;
+import org.apache.helix.api.listeners.BatchMode;
+import org.apache.helix.api.listeners.InstanceConfigChangeListener;
+import org.apache.helix.api.listeners.PreFetch;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.pinot.common.version.PinotVersion;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.InstanceTypeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Enables new merge plan nodes only after a complete homogeneous
broker/server release-version snapshot.
+/// Uses the SAFE stats policy's version comparison, but fails closed on
initialization and read failures.
+/// Snapshot builds are excluded because equal snapshot versions can contain
different protocol implementations.
+/// Helix drives updates serially; query threads read the volatile result
without locking.
+@BatchMode(enabled = false)
+@PreFetch(enabled = false)
+public class KWayMergeSupportPredicate implements
InstanceConfigChangeListener, BooleanSupplier {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KWayMergeSupportPredicate.class);
+ private final HelixManager _manager;
+ private final String _version;
+ private volatile boolean _supported;
+
+ public KWayMergeSupportPredicate(HelixManager manager) {
+ this(manager, PinotVersion.VERSION);
+ }
+
+ KWayMergeSupportPredicate(HelixManager manager, String version) {
+ _manager = manager;
+ _version = version;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return _supported && _manager.isConnected();
+ }
+
+ @Override
+ public synchronized void onInstanceConfigChange(List<InstanceConfig>
configs, NotificationContext context) {
+ // Clear first: no partial snapshot or failed refresh may leave the new
wire node enabled.
+ _supported = false;
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
The shared tracker now computes and publishes one snapshot after a
successful update, without clearing the previous result during reads. It logs
incompatible live instance IDs and reported versions, plus read failures.
Failures intentionally fail closed; config/live/INIT notifications refresh the
state and allow recovery. Tracker tests cover publication during an update,
failure/recovery and finalization.
##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/KWayMergeSupportPredicate.java:
##########
@@ -0,0 +1,99 @@
+/**
+ * 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.pinot.broker.requesthandler;
+
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixManager;
+import org.apache.helix.NotificationContext;
+import org.apache.helix.api.listeners.BatchMode;
+import org.apache.helix.api.listeners.InstanceConfigChangeListener;
+import org.apache.helix.api.listeners.PreFetch;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.pinot.common.version.PinotVersion;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.InstanceTypeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Enables new merge plan nodes only after a complete homogeneous
broker/server release-version snapshot.
+/// Uses the SAFE stats policy's version comparison, but fails closed on
initialization and read failures.
+/// Snapshot builds are excluded because equal snapshot versions can contain
different protocol implementations.
+/// Helix drives updates serially; query threads read the volatile result
without locking.
+@BatchMode(enabled = false)
+@PreFetch(enabled = false)
+public class KWayMergeSupportPredicate implements
InstanceConfigChangeListener, BooleanSupplier {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KWayMergeSupportPredicate.class);
+ private final HelixManager _manager;
+ private final String _version;
+ private volatile boolean _supported;
+
+ public KWayMergeSupportPredicate(HelixManager manager) {
+ this(manager, PinotVersion.VERSION);
+ }
+
+ KWayMergeSupportPredicate(HelixManager manager, String version) {
+ _manager = manager;
+ _version = version;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return _supported && _manager.isConnected();
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Removed isConnected() from the query path. The shared version tracker
publishes a cached result and resets it on FINALIZE/non-update notifications;
INIT refreshes it. The predicate tests cover initialization, cached reads and
finalization.
##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/KWayMergeSupportPredicate.java:
##########
@@ -0,0 +1,99 @@
+/**
+ * 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.pinot.broker.requesthandler;
+
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixManager;
+import org.apache.helix.NotificationContext;
+import org.apache.helix.api.listeners.BatchMode;
+import org.apache.helix.api.listeners.InstanceConfigChangeListener;
+import org.apache.helix.api.listeners.PreFetch;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.pinot.common.version.PinotVersion;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.InstanceTypeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Enables new merge plan nodes only after a complete homogeneous
broker/server release-version snapshot.
+/// Uses the SAFE stats policy's version comparison, but fails closed on
initialization and read failures.
+/// Snapshot builds are excluded because equal snapshot versions can contain
different protocol implementations.
+/// Helix drives updates serially; query threads read the volatile result
without locking.
+@BatchMode(enabled = false)
+@PreFetch(enabled = false)
+public class KWayMergeSupportPredicate implements
InstanceConfigChangeListener, BooleanSupplier {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KWayMergeSupportPredicate.class);
+ private final HelixManager _manager;
+ private final String _version;
+ private volatile boolean _supported;
+
+ public KWayMergeSupportPredicate(HelixManager manager) {
+ this(manager, PinotVersion.VERSION);
+ }
+
+ KWayMergeSupportPredicate(HelixManager manager, String version) {
+ _manager = manager;
+ _version = version;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return _supported && _manager.isConnected();
+ }
+
+ @Override
+ public synchronized void onInstanceConfigChange(List<InstanceConfig>
configs, NotificationContext context) {
+ // Clear first: no partial snapshot or failed refresh may leave the new
wire node enabled.
+ _supported = false;
+ if (context.getType() != NotificationContext.Type.INIT
+ && context.getType() != NotificationContext.Type.CALLBACK) {
+ return;
+ }
+ if (_version == null || _version.equals(PinotVersion.UNKNOWN) ||
_version.contains("SNAPSHOT")) {
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Added WindowKWayMergeIntegrationTest with a scoped test-only supplier
override, restored in finally; no production option bypasses the
release-version gate. It passed locally and compares windowKWayMerge=true/false
for ROWS/FOLLOWING and RANGE/peer frames with DESC NULLS FIRST, null/duplicate
keys and 16 offline + 16 realtime rows. It asserts the physical merge plan,
exact complete results and executed local/gRPC mailbox statistics.
##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/KWayMergeSupportPredicate.java:
##########
@@ -0,0 +1,99 @@
+/**
+ * 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.pinot.broker.requesthandler;
+
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixManager;
+import org.apache.helix.NotificationContext;
+import org.apache.helix.api.listeners.BatchMode;
+import org.apache.helix.api.listeners.InstanceConfigChangeListener;
+import org.apache.helix.api.listeners.PreFetch;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.pinot.common.version.PinotVersion;
+import org.apache.pinot.spi.config.instance.InstanceType;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.InstanceTypeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Enables new merge plan nodes only after a complete homogeneous
broker/server release-version snapshot.
+/// Uses the SAFE stats policy's version comparison, but fails closed on
initialization and read failures.
+/// Snapshot builds are excluded because equal snapshot versions can contain
different protocol implementations.
+/// Helix drives updates serially; query threads read the volatile result
without locking.
+@BatchMode(enabled = false)
+@PreFetch(enabled = false)
+public class KWayMergeSupportPredicate implements
InstanceConfigChangeListener, BooleanSupplier {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KWayMergeSupportPredicate.class);
+ private final HelixManager _manager;
+ private final String _version;
+ private volatile boolean _supported;
+
+ public KWayMergeSupportPredicate(HelixManager manager) {
+ this(manager, PinotVersion.VERSION);
+ }
+
+ KWayMergeSupportPredicate(HelixManager manager, String version) {
+ _manager = manager;
+ _version = version;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return _supported && _manager.isConnected();
+ }
+
+ @Override
+ public synchronized void onInstanceConfigChange(List<InstanceConfig>
configs, NotificationContext context) {
+ // Clear first: no partial snapshot or failed refresh may leave the new
wire node enabled.
+ _supported = false;
+ if (context.getType() != NotificationContext.Type.INIT
+ && context.getType() != NotificationContext.Type.CALLBACK) {
+ return;
+ }
+ if (_version == null || _version.equals(PinotVersion.UNKNOWN) ||
_version.contains("SNAPSHOT")) {
+ return;
+ }
+ try {
+ HelixAdmin admin = _manager.getClusterManagmentTool();
+ String cluster = _manager.getClusterName();
+ List<String> instances = admin.getInstancesInCluster(cluster);
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Extracted ClusterVersionTracker and reused it from the merge gate and
SendStatsPredicate.Safe. Config data changes read only the changed live
broker/server config; unrelated/non-live configs do not trigger reads or block
the gate. Membership changes read live records to detect same-name restarts but
reuse configs for unchanged sessions, refreshing only new/restarted/unreadable
participants. INIT/config child changes perform a full live refresh. The
tracker tests cover read counts, stale configs, restarts and failure recovery.
##########
pinot-query-planner/src/main/java/org/apache/pinot/query/planner/plannode/MailboxMergeReceiveNode.java:
##########
@@ -0,0 +1,87 @@
+/**
+ * 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.pinot.query.planner.plannode;
+
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.pinot.calcite.rel.logical.PinotRelExchangeType;
+import org.apache.pinot.common.utils.DataSchema;
+
+
+/// Receives streams ordered by the logical plan and preserves that order with
a k-way merge.
+/// This distinct wire node is emitted only when the broker verifies
homogeneous cluster versions.
+/// Instances belong to a single query plan and are not thread-safe.
+public class MailboxMergeReceiveNode extends MailboxReceiveNode {
+ private final int _fetch;
+ private final int _offset;
+
+ public MailboxMergeReceiveNode(int stageId, DataSchema schema, int
senderStageId,
+ PinotRelExchangeType exchangeType, RelDistribution.Type distribution,
@Nullable List<Integer> keys,
+ List<RelFieldCollation> collations, @Nullable MailboxSendNode sender) {
+ this(stageId, schema, senderStageId, exchangeType, distribution, keys,
collations, -1, -1, sender);
+ }
+
+ public MailboxMergeReceiveNode(int stageId, DataSchema schema, int
senderStageId,
+ PinotRelExchangeType exchangeType, RelDistribution.Type distribution,
@Nullable List<Integer> keys,
+ List<RelFieldCollation> collations, int fetch, int offset, @Nullable
MailboxSendNode sender) {
+ super(stageId, schema, senderStageId, exchangeType, distribution, keys,
collations, false, false, sender);
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
MailboxMergeReceiveNode is now a sibling BasePlanNode with
visitMailboxMergeReceive; the ordinary receive visitors no longer distinguish
it with instanceof or expose misleading sort flags. The legacy
sort/sortedOnSender flags and proto fields (including sender field 7) are
deprecated but retained for decoding old plans. Serde, planner, opchain, stats,
pipeline and cancellation tests cover the new node.
##########
pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java:
##########
@@ -583,6 +584,13 @@ public void start()
_accessControlFactory, _queryQuotaManager, _tableCache,
_multiStageQueryThrottler, _failureDetector,
_threadAccountant, multiClusterRoutingContext, workerManager,
multiClusterWorkerManager,
_serverRoutingStatsManager);
+ KWayMergeSupportPredicate mergeSupport = new
KWayMergeSupportPredicate(_spectatorHelixManager);
+ try {
+ _spectatorHelixManager.addInstanceConfigChangeListener(mergeSupport);
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
SNAPSHOT/unknown brokers now skip merge listener registration entirely.
Release brokers retain the incremental listeners because an explicit
windowKWayMerge=true query can override a false broker default. Config data
events read at most the changed live participant, and the option-off query path
never consults the supplier. Partial registration failure permanently stops the
tracker and leaves the feature disabled.
##########
pinot-query-planner/src/main/java/org/apache/pinot/query/planner/plannode/KWayMergeExchangeNode.java:
##########
@@ -0,0 +1,44 @@
+/**
+ * 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.pinot.query.planner.plannode;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.pinot.calcite.rel.logical.PinotRelExchangeType;
+import org.apache.pinot.common.utils.DataSchema;
+
+
+/// Planning-only exchange requiring ordered inputs and producing a k-way
merge receive and a plain send.
+/// Instances are local to a query's planning thread and are never serialized.
+public class KWayMergeExchangeNode extends ExchangeNode {
+ public KWayMergeExchangeNode(int stageId, DataSchema schema, List<PlanNode>
inputs,
+ RelDistribution.Type distribution, @Nullable List<Integer> keys, boolean
prePartitioned,
+ List<RelFieldCollation> collations, String hashFunction) {
+ super(stageId, schema, inputs, PinotRelExchangeType.STREAMING,
distribution, keys, prePartitioned, collations,
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
KWayMergeExchangeNode is now a separate BasePlanNode with
visitKWayMergeExchange, not an ExchangeNode subtype. Its own fields express the
ordered exchange contract, and fragmentation creates a plain mailbox sender
plus the sibling merge receive. Routing metadata, explain, stage equivalence
and traversal visitors handle the distinct node explicitly.
##########
pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/PlanFragmenter.java:
##########
@@ -197,8 +199,10 @@ public PlanNode visitExchange(ExchangeNode node, Context
context) {
_mailboxSendToExchangeNodeMap.put(mailboxSendNode, node);
// Return the MailboxReceiveNode as the leave node of the current
PlanFragment.
- MailboxReceiveNode mailboxReceiveNode =
- new MailboxReceiveNode(receiverPlanFragmentId,
nextPlanFragmentRoot.getDataSchema(),
+ MailboxReceiveNode mailboxReceiveNode = node instanceof
KWayMergeExchangeNode
+ ? new MailboxMergeReceiveNode(receiverPlanFragmentId,
nextPlanFragmentRoot.getDataSchema(),
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Removed the unused merge fetch/offset support from the constructor, node,
operator, explain, serde, equivalence and limit-only tests. The merge receives
complete ordered streams; limit pushdown can be added separately with a
producing rule and its tests.
##########
pinot-common/src/main/proto/plan.proto:
##########
@@ -201,6 +202,18 @@ message MailboxReceiveNode {
bool sortedOnSender = 7;
}
+// Each input stream is ordered by the logical plan. Older servers reject the
unknown PlanNode oneof tag.
+// Brokers only emit this node after verifying homogeneous supported versions.
+message MailboxMergeReceiveNode {
+ int32 senderStageId = 1;
+ ExchangeType exchangeType = 2;
+ DistributionType distributionType = 3;
+ repeated int32 keys = 4;
+ repeated Collation collations = 5;
+ int32 fetch = 6;
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Dropped fetch/offset from the merge proto and reserved tags 6 and 7. There
is no unset proto3 integer limit with empty-window semantics anymore.
Merge-node serde and the hybrid integration test pass.
##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/SortedMailboxMergeReceiveOperator.java:
##########
@@ -0,0 +1,351 @@
+/**
+ * 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.pinot.query.runtime.operator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.query.mailbox.ReceivingMailbox;
+import org.apache.pinot.query.planner.plannode.MailboxMergeReceiveNode;
+import org.apache.pinot.query.runtime.blocks.MseBlock;
+import org.apache.pinot.query.runtime.blocks.RowHeapDataBlock;
+import org.apache.pinot.query.runtime.blocks.SuccessMseBlock;
+import org.apache.pinot.query.runtime.operator.utils.AsyncStream;
+import org.apache.pinot.query.runtime.operator.utils.SortUtils;
+import org.apache.pinot.query.runtime.plan.OpChainExecutionContext;
+import org.apache.pinot.spi.query.QueryThreadContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// K-way merges streams whose ordering is guaranteed by the logical plan.
+///
+/// The broker only emits this operator's plan node in a homogeneous cluster.
Ordering is established upstream;
+/// mailbox senders preserve it without transport metadata or runtime
structural checks.
+///
+/// Reads whichever mailbox is ready to avoid cross-receiver backpressure
cycles. Multi-sender output blocks contain
+/// at most 10,000 rows; a single sender preserves its input blocks.
Read-ahead retained while another sender is starved
+/// can approach the legacy full receiver sort's memory footprint.
+///
+/// Driven by a single consumer thread; not thread-safe.
+public class SortedMailboxMergeReceiveOperator extends
BaseMailboxReceiveOperator {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SortedMailboxMergeReceiveOperator.class);
+
+ private static final String EXPLAIN_NAME = "SORTED_MAILBOX_MERGE_RECEIVE";
+ private static final String MERGE_SCOPE =
"SortedMailboxMergeReceiveOperator";
+ private final DataSchema _dataSchema;
+ private final Comparator<Object[]> _comparator;
+ private final PriorityQueue<SenderCursor> _readyCursors;
+ private final boolean _singleSortedSender;
+ private int _rowsToSkip;
+ private long _rowsToEmit;
+ /// Senders that have not finished but do not currently have a row ready.
Nothing can be emitted while this is
+ /// non-empty because any one of these senders may hold the next row.
+ private final Set<SenderCursor> _starvedCursors =
Collections.newSetFromMap(new IdentityHashMap<>());
+ private final Map<AsyncStream<ReceivingMailbox.MseBlockWithStats>,
SenderCursor> _cursorsByStream =
+ new IdentityHashMap<>();
+
+ @Nullable
+ private MseBlock _eosBlock;
+
+ public SortedMailboxMergeReceiveOperator(OpChainExecutionContext context,
MailboxMergeReceiveNode node) {
+ super(context, node);
+ Preconditions.checkState(!CollectionUtils.isEmpty(node.getCollations()),
"Field collations must be set");
+ _dataSchema = node.getDataSchema();
+ _rowsToSkip = Math.max(node.getOffset(), 0);
+ _rowsToEmit = node.getFetch() < 0 ? Long.MAX_VALUE : node.getFetch();
+ _comparator = new
SortUtils.SortComparator(List.copyOf(node.getCollations()), false);
+ List<AsyncStream<ReceivingMailbox.MseBlockWithStats>> streams =
_multiConsumer.getLiveStreamsSnapshot();
+ _readyCursors = new PriorityQueue<>(Math.max(streams.size(), 1),
+ (left, right) -> _comparator.compare(left.peek(), right.peek()));
+ _singleSortedSender = streams.size() == 1;
+ if (!_singleSortedSender) {
+ for (AsyncStream<ReceivingMailbox.MseBlockWithStats> stream : streams) {
+ SenderCursor cursor = new SenderCursor();
+ _cursorsByStream.put(stream, cursor);
+ _starvedCursors.add(cursor);
+ }
+ }
+ }
+
+ @Override
+ protected Logger logger() {
+ return LOGGER;
+ }
+
+ @Override
+ public String toExplainString() {
+ return EXPLAIN_NAME;
+ }
+
+ @Override
+ protected MseBlock getNextBlock() {
+ if (_eosBlock != null) {
+ return _eosBlock;
+ }
+ if (_isEarlyTerminated) {
+ return readUntilEos();
+ }
+ if (_rowsToEmit == 0) {
+ earlyTerminate();
+ return readUntilEos();
+ }
+ while (true) {
+ MseBlock block = _singleSortedSender ? readSingleSortedSender() :
mergeNextBlock();
+ if (block.isEos()) {
+ return block;
+ }
+ if (_rowsToSkip == 0 && _rowsToEmit == Long.MAX_VALUE) {
+ return block;
+ }
+ List<Object[]> rows = ((MseBlock.Data) block).asRowHeap().getRows();
+ int from = Math.min(_rowsToSkip, rows.size());
+ _rowsToSkip -= from;
+ int count = (int) Math.min(rows.size() - from, _rowsToEmit);
+ _rowsToEmit -= count;
+ if (_rowsToEmit == 0) {
+ earlyTerminate();
+ }
+ if (count > 0) {
+ return from == 0 && count == rows.size() ? block
+ : new RowHeapDataBlock(rows.subList(from, from + count),
_dataSchema);
+ }
+ }
+ }
+
+ /// Passes through one sorted sender without copying its rows through the
merge heap.
+ private MseBlock readSingleSortedSender() {
+ while (true) {
+ MseBlock block = _multiConsumer.readMseBlockBlocking();
+ if (block.isEos()) {
+ return terminate(block);
+ }
+ MseBlock.Data dataBlock = (MseBlock.Data) block;
+ checkActiveTerminationAndSampleUsage();
+
+ if (dataBlock.getNumRows() > 0) {
+ return dataBlock;
+ }
+ }
+ }
+
+ /// Merges the sorted senders, emitting at most
[SortOperator#DEFAULT_MAX_ROWS_PER_BLOCK] rows per call.
+ private MseBlock mergeNextBlock() {
+ List<Object[]> rows = new ArrayList<>();
+ while (rows.size() < SortOperator.DEFAULT_MAX_ROWS_PER_BLOCK) {
+ if (!_starvedCursors.isEmpty()) {
+ MseBlock.Eos error;
+ if (rows.isEmpty()) {
+ error = readOneBlock();
+ } else {
+ // Rows already removed from the heap are a globally ordered prefix.
Consume any immediately available
+ // cursor progress so blocks can still be coalesced, but return that
safe prefix instead of waiting merely
+ // to fill the output block.
+ MseBlock block = _multiConsumer.pollMseBlockOrStreamCompletion();
+ if (block == null &&
_multiConsumer.getFinishedStreamsLastRead().isEmpty()) {
+ break;
+ }
+ error = processReadBlock(block);
+ }
+ if (error != null) {
+ return terminate(error);
+ }
+
+ continue;
+ }
+ if (_readyCursors.isEmpty()) {
+ break;
+ }
+ SenderCursor cursor = _readyCursors.remove();
+ rows.add(cursor.next());
+ if (cursor.hasRow()) {
+ _readyCursors.add(cursor);
+ } else if (!cursor._finished) {
+ _starvedCursors.add(cursor);
+ }
+
QueryThreadContext.checkTerminationAndSampleUsagePeriodically(rows.size(),
MERGE_SCOPE,
+ _context.getActiveDeadlineMs());
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Removed the active-deadline checks. The merge loop now uses the inherited
termination/resource-sampling methods and therefore
BaseMailboxReceiveOperator's passive deadline. The merge/operator and focused
runtime tests pass.
##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/plan/server/ServerPlanRequestVisitor.java:
##########
@@ -282,6 +283,12 @@ public Void visitProject(ProjectNode node,
ServerPlanRequestContext context) {
@Override
public Void visitSort(SortNode node, ServerPlanRequestContext context) {
if (visit(node.getInputs().get(0), context)) {
+ if (node.getFetch() == Integer.MAX_VALUE) {
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Replaced the fetch==MAX_VALUE sentinel with the serialized requiresSingleRun
SortNode hint. PlanFragmenter attaches it only to the merge exchange's explicit
sender sort, and ServerPlanRequestVisitor retains that sort above the combined
hybrid/logical leaf input. The visitor tests verify that marked sorts remain
above the boundary while an ordinary user MAX_VALUE limit still receives V1
pushdown; the hybrid integration test exercises the combined input.
##########
pinot-segment-local/pom.xml:
##########
@@ -88,6 +88,13 @@
<artifactId>h3</artifactId>
</dependency>
+ <!-- Zstd's API annotations are required when javac resolves annotated
return types with warnings enabled. -->
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Removed the annotations dependency. pinot-segment-local/pom.xml now has no
diff against rebased master. Warning-enabled validation used freshly installed
current dependencies for the affected modules without reintroducing the
unrelated POM change.
##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotWindowExchangeNodeInsertRule.java:
##########
@@ -141,27 +158,44 @@ public void onMatch(RelOptRuleCall call) {
exchange = PinotLogicalExchange.create(input,
RelDistributions.hash(windowGroup.keys.toList()), prePartitioned);
} else {
// PARTITION BY and ORDER BY on different key(s)
- // Add a LogicalSortExchange hashed on the partition by keys and
collation based on order by keys.
- // The ordering itself is established by the Sort placed over the
exchange below, not by the receive
- // operator - see the comment at the transformTo call.
+ // Keep the receiver full-sort path for a partitioned exchange.
Sorting before the hash exchange compares rows
+ // routed to different receivers and blocks streaming; the explicit
Sort retained above the exchange establishes
+ // the required ordering after partitioning.
exchange = PinotLogicalSortExchange.create(input,
RelDistributions.hash(windowGroup.keys.toList()),
windowGroup.orderKeys, false, false, prePartitioned);
}
}
- // WindowAggregateOperator requires its input ordered on the ORDER BY keys
and does no ordering of its own, so
- // where the exchange carries a collation the ordering has to be
established above it. Place an explicit Sort
- // rather than asking the receive operator to sort: SortOperator is the
operator that knows fetch/offset, and
- // SortedMailboxReceiveOperator is deprecated. The Sort carries no fetch,
so it keeps every row - the same
- // semantics as the unbounded list the receive operator used.
+ // WindowAggregateOperator requires its input ordered on the ORDER BY keys
and does no ordering of its own. A sort
+ // exchange that sorts on the receiver establishes that contract itself;
adding another Sort above it would make
+ // old servers sort the same rows twice during a rolling upgrade. Keep an
explicit Sort only for post-exchange
+ // full-sort paths.
// PinotSortExchangeNodeInsertRule does not re-fire on it: its matches()
rejects a Sort whose input is an
- // exchange. PinotSortExchangeCopyRule does not either: it declines when
there is no fetch.
+ // exchange. Window sorts must retain every input row, independently of
the broker response limit.
RelNode windowInput = exchange instanceof PinotLogicalSortExchange
- ? LogicalSort.create(exchange, ((PinotLogicalSortExchange)
exchange).getCollation(), null, null) : exchange;
+ && !((PinotLogicalSortExchange) exchange).isSortOnReceiver()
Review Comment:
Addressed in `6f63db504b9858b45707ee13b08a30049ad55910`.
Removed the dead isSortOnReceiver guard and obsolete upgrade comment.
Ordinary sort exchanges retain the explicit receiver sort; the distinct k-way
exchange establishes its own ordered output. The unrelated receiver MAX-fetch
hardening and window JSON changes were also removed, and the full
resource-based plan suite passes.
--
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]