summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-07-15 00:00:52 +0300
committerrobot-piglet <[email protected]>2026-07-15 00:16:54 +0300
commitd87e0a30e456b719fbfa5cf86ffa3db5162430ef (patch)
tree396777377da950935e40b21f6cab85a6c4de72b4
parentcc642b36644214a7b7da9b4b416b8c4582339ae4 (diff)
Intermediate changes
commit_hash:8171ad7327107ff77cc8bbe11c24d3e4f2fd4c06
-rw-r--r--yt/yql/providers/yt/fmr/coordinator/impl/ut/ya.make1
-rw-r--r--yt/yql/providers/yt/fmr/coordinator/operation_manager/impl/map_reduce/yql_yt_map_reduce_stage_operation_manager.cpp3
-rw-r--r--yt/yql/providers/yt/fmr/coordinator/partitioner/yql_yt_reduce_partitioner.cpp16
-rw-r--r--yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.cpp18
-rw-r--r--yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.h9
-rw-r--r--yt/yql/providers/yt/fmr/proto/request_options.proto5
-rw-r--r--yt/yql/providers/yt/fmr/request_options/proto_helpers/yql_yt_request_proto_helpers.cpp2
-rw-r--r--yt/yql/providers/yt/fmr/request_options/yql_yt_request_options.h5
-rw-r--r--yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/ya.make17
-rw-r--r--yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/yql_yt_yson_tds_block_iterator_ut.cpp88
-rw-r--r--yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ya.make4
-rw-r--r--yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.cpp40
-rw-r--r--yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h11
13 files changed, 196 insertions, 23 deletions
diff --git a/yt/yql/providers/yt/fmr/coordinator/impl/ut/ya.make b/yt/yql/providers/yt/fmr/coordinator/impl/ut/ya.make
index 8ca30f57288..6deb56f58d7 100644
--- a/yt/yql/providers/yt/fmr/coordinator/impl/ut/ya.make
+++ b/yt/yql/providers/yt/fmr/coordinator/impl/ut/ya.make
@@ -23,7 +23,6 @@ PEERDIR(
yt/yql/providers/yt/fmr/test_tools/mock_time_provider
yt/yql/providers/yt/fmr/test_tools/sorted_partitioner
yt/yql/providers/yt/fmr/test_tools/table_data_service
- yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl
yt/yql/providers/yt/gateway/fmr
yt/yql/providers/yt/gateway/file
yt/yql/providers/yt/codec/codegen/llvm16
diff --git a/yt/yql/providers/yt/fmr/coordinator/operation_manager/impl/map_reduce/yql_yt_map_reduce_stage_operation_manager.cpp b/yt/yql/providers/yt/fmr/coordinator/operation_manager/impl/map_reduce/yql_yt_map_reduce_stage_operation_manager.cpp
index b919e13dea8..bb0c896898a 100644
--- a/yt/yql/providers/yt/fmr/coordinator/operation_manager/impl/map_reduce/yql_yt_map_reduce_stage_operation_manager.cpp
+++ b/yt/yql/providers/yt/fmr/coordinator/operation_manager/impl/map_reduce/yql_yt_map_reduce_stage_operation_manager.cpp
@@ -255,6 +255,9 @@ private:
reduceTaskParams.Input = taskInput;
reduceTaskParams.SerializedReduceJobState = operationParams.SerializedReduceJobState;
reduceTaskParams.ReduceOperationSpec = operationParams.ReduceOperationSpec;
+ // The map stage above always shuffles via a synthetic _yql_key_hash prefix (see
+ // GenerateIntermediateTable), unlike a plain Reduce operation's real YT-sorted input.
+ reduceTaskParams.SortByHasKeyHashPrefix = true;
std::vector<TFmrTableOutputRef> outputRefs;
std::transform(
diff --git a/yt/yql/providers/yt/fmr/coordinator/partitioner/yql_yt_reduce_partitioner.cpp b/yt/yql/providers/yt/fmr/coordinator/partitioner/yql_yt_reduce_partitioner.cpp
index 8fd368852d8..69b9a40dc92 100644
--- a/yt/yql/providers/yt/fmr/coordinator/partitioner/yql_yt_reduce_partitioner.cpp
+++ b/yt/yql/providers/yt/fmr/coordinator/partitioner/yql_yt_reduce_partitioner.cpp
@@ -3,9 +3,19 @@
#include "yql_yt_sorted_partitioner_base.h"
#include <yt/yql/providers/yt/fmr/utils/comparator/yql_yt_binary_yson_compare_impl.h>
+#include <library/cpp/yson/node/node_io.h>
namespace NYql::NFmr {
+namespace {
+
+// Binary YSON key rows may contain arbitrary non-UTF8 bytes; text YSON escapes them, keeping error messages valid UTF-8.
+TString FormatKeyRow(const TString& binaryYsonRow) {
+ return NYT::NodeToYsonString(NYT::NodeFromYsonString(binaryYsonRow));
+}
+
+} // namespace
+
TReducePartitioner::TReducePartitioner(
const std::unordered_map<TFmrTableId, std::vector<TString>>& partIdsForTables,
const std::unordered_map<TString, std::vector<TChunkStats>>& partIdStats,
@@ -47,7 +57,7 @@ TFmrTableKeysRange TReducePartitioner::GetReadRangeFromSlices(const std::vector<
YQL_ENSURE(taskRangeLeftBorderInclusive, " task range left border should always be true for reduce paritioner");
if (taskRangeLeftBorder == taskRangeRightBorder && !taskRangeRightBorderInclusive) {
- ythrow yexception() << "Key " << taskRangeLeftBorder.Row << " takes all slice and is too large, cannot partition";
+ ythrow yexception() << "Key " << FormatKeyRow(taskRangeLeftBorder.Row) << " takes all slice and is too large, cannot partition";
}
taskRange.SetFirstKeysBound(taskRangeLeftBorder, taskRangeLeftBorderInclusive);
@@ -93,7 +103,7 @@ void TReducePartitioner::CheckMaxKeySizePerSlices(const std::vector<TSlice>& sli
if (*curLeftBorder != curRightBorder) {
if (currentFilledSlicesWeight > maxKeySizePerPart) {
- ythrow yexception() << "Key " << curLeftBorder->Row << " weighs at least " << currentFilledSlicesWeight << " bytes which is larget than maxKeySizePerPart - " << maxKeySizePerPart << " - cannot partition";
+ ythrow yexception() << "Key " << FormatKeyRow(curLeftBorder->Row) << " weighs at least " << currentFilledSlicesWeight << " bytes which is larget than maxKeySizePerPart - " << maxKeySizePerPart << " - cannot partition";
}
curLeftBorder = Nothing();
currentFilledSlicesWeight = 0;
@@ -102,7 +112,7 @@ void TReducePartitioner::CheckMaxKeySizePerSlices(const std::vector<TSlice>& sli
}
}
if (currentFilledSlicesWeight > maxKeySizePerPart) {
- ythrow yexception() << "Key " << curLeftBorder->Row << " weighs at least " << currentFilledSlicesWeight << " bytes which is larget than maxKeySizePerPart - " << maxKeySizePerPart;
+ ythrow yexception() << "Key " << FormatKeyRow(curLeftBorder->Row) << " weighs at least " << currentFilledSlicesWeight << " bytes which is larget than maxKeySizePerPart - " << maxKeySizePerPart;
}
CurrentLastKeyWeight_ = currentFilledSlicesWeight;
}
diff --git a/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.cpp b/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.cpp
index 6f4bc3e7d50..7d9c7a57b7f 100644
--- a/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.cpp
+++ b/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.cpp
@@ -113,6 +113,7 @@ void TFmrUserJob::Save(IOutputStream& s) const {
ReduceOperationSpec_,
IsMapReduceReducer_,
IsMapReduceMap_,
+ SortByHasKeyHashPrefix_,
ForceTableIndexMarking_
);
}
@@ -132,6 +133,7 @@ void TFmrUserJob::Load(IInputStream& s) {
ReduceOperationSpec_,
IsMapReduceReducer_,
IsMapReduceMap_,
+ SortByHasKeyHashPrefix_,
ForceTableIndexMarking_
);
}
@@ -283,6 +285,15 @@ void TFmrUserJob::FillQueueFromReduceInput() {
YQL_ENSURE(ReduceOperationSpec_.Defined());
auto reduceBy = ReduceOperationSpec_->ReduceBy;
auto sortBy = ReduceOperationSpec_->SortBy;
+ // sortBy is [...reduceBy, ...tiebreaker] (e.g. a join's "_yql_sort"), with a leading
+ // _yql_key_hash only for MapReduce's hash-shuffled intermediate tables (SortByHasKeyHashPrefix_,
+ // set by the coordinator - see TReduceTaskParams::SortByHasKeyHashPrefix) - a plain Reduce
+ // reads its input pre-sorted by the real reduceBy columns, with no hash involved. Either
+ // way, task boundaries must only constrain on the reduce-key prefix (hash-if-present +
+ // reduceBy) - see the comment on CompareRowToBoundaryPrefix for why comparing the tiebreaker
+ // too would silently split a reduce group across tasks whenever a task cut lands between
+ // rows that share a reduce key but differ in tiebreaker.
+ const size_t numBoundaryKeyColumns = (SortByHasKeyHashPrefix_ ? 1 : 0) + reduceBy.Columns.size();
for (const auto& inputTableRef : InputTables_.Inputs) {
if (auto fmrInput = std::get_if<TFmrTableInputRef>(&inputTableRef)) {
blockIterators.push_back(MakeIntrusive<TTableDataServiceBlockIterator>(
@@ -296,7 +307,9 @@ void TFmrUserJob::FillQueueFromReduceInput() {
fmrInput->IsFirstRowInclusive,
fmrInput->IsLastRowInclusive,
fmrInput->FirstRowKeys,
- fmrInput->LastRowKeys
+ fmrInput->LastRowKeys,
+ /*readAheadChunks*/ ui64{4},
+ numBoundaryKeyColumns
));
} else {
ythrow TFmrNonRetryableJobException() << "YtTables unsupported inside Reduce task for now";
@@ -486,7 +499,8 @@ TStatistics TFmrUserJob::DoFmrJob(const TFmrUserJobOptions& options) {
sortedWriter->NotifyRowEnd();
}
sortedWriter->Flush();
- TStatistics result({{OutputTables_[0], sortedWriter->GetStats()}});
+ auto sortedWriterStats = sortedWriter->GetStats();
+ TStatistics result({{OutputTables_[0], sortedWriterStats}});
// Direct (map-bypass) outputs at indices 1..K were already written to the real TDS via
// their normal writers (see InitializeFmrUserJob) and flushed generically by
diff --git a/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.h b/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.h
index 54a8ff4ffde..a0bf4c70c07 100644
--- a/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.h
+++ b/yt/yql/providers/yt/fmr/process/yql_yt_job_fmr.h
@@ -116,6 +116,14 @@ public:
IsMapReduceMap_ = value;
}
+ // Whether this Reduce job's input is sorted by [_yql_key_hash, ...ReduceOperationSpec's
+ // SortBy] (a MapReduce operation's reduce stage) or by SortBy alone with no hash column at
+ // all (a plain Reduce operation reading real YT-sorted input) - see
+ // TReduceTaskParams::SortByHasKeyHashPrefix, which this is set from.
+ void SetSortByHasKeyHashPrefix(bool value) {
+ SortByHasKeyHashPrefix_ = value;
+ }
+
// Set by the gateway (from execCtx->InputTables_, before this job's state is serialized to
// workers) whenever the ORIGINAL OPERATION has more than one distinct input position (e.g. a
// JOIN's mapper reading the same or different tables via multiple sections). This must be a
@@ -180,6 +188,7 @@ private:
TMaybe<TReduceOperationSpec> ReduceOperationSpec_;
bool IsMapReduceReducer_ = false;
bool IsMapReduceMap_ = false;
+ bool SortByHasKeyHashPrefix_ = false;
bool ForceTableIndexMarking_ = false;
// End of serializable part
diff --git a/yt/yql/providers/yt/fmr/proto/request_options.proto b/yt/yql/providers/yt/fmr/proto/request_options.proto
index aacb3822b15..aafb3f2a32a 100644
--- a/yt/yql/providers/yt/fmr/proto/request_options.proto
+++ b/yt/yql/providers/yt/fmr/proto/request_options.proto
@@ -300,6 +300,11 @@ message TReduceTaskParams {
repeated TFmrTableOutputRef Output = 2;
bytes SerializedReduceJobState = 3;
TReduceOperationSpec ReduceOperationSpec = 4;
+ // Whether Input's rows are sorted by [_yql_key_hash, ...ReduceOperationSpec.SortBy] (true for
+ // a MapReduce operation's reduce stage, which shuffles via a synthetic hash column) or by
+ // ReduceOperationSpec.SortBy alone (a plain Reduce operation, which reads real YT-sorted
+ // input with no hash column at all).
+ bool SortByHasKeyHashPrefix = 5;
}
message TSortOperationParams {
diff --git a/yt/yql/providers/yt/fmr/request_options/proto_helpers/yql_yt_request_proto_helpers.cpp b/yt/yql/providers/yt/fmr/request_options/proto_helpers/yql_yt_request_proto_helpers.cpp
index 855b572b486..c8f992255b6 100644
--- a/yt/yql/providers/yt/fmr/request_options/proto_helpers/yql_yt_request_proto_helpers.cpp
+++ b/yt/yql/providers/yt/fmr/request_options/proto_helpers/yql_yt_request_proto_helpers.cpp
@@ -717,6 +717,7 @@ NProto::TReduceTaskParams ReduceTaskParamsToProto(const TReduceTaskParams& reduc
protoReduceTaskParams.SetSerializedReduceJobState(reduceTaskParams.SerializedReduceJobState);
auto protoReduceOperationSpec = ReduceOperationSpecToProto(reduceTaskParams.ReduceOperationSpec);
protoReduceTaskParams.MutableReduceOperationSpec()->Swap(&protoReduceOperationSpec);
+ protoReduceTaskParams.SetSortByHasKeyHashPrefix(reduceTaskParams.SortByHasKeyHashPrefix);
return protoReduceTaskParams;
}
@@ -730,6 +731,7 @@ TReduceTaskParams ReduceTaskParamsFromProto(const NProto::TReduceTaskParams& pro
reduceTaskParams.Output = outputTables;
reduceTaskParams.SerializedReduceJobState = protoReduceTaskParams.GetSerializedReduceJobState();
reduceTaskParams.ReduceOperationSpec = ReduceOperationSpecFromProto(protoReduceTaskParams.GetReduceOperationSpec());
+ reduceTaskParams.SortByHasKeyHashPrefix = protoReduceTaskParams.GetSortByHasKeyHashPrefix();
return reduceTaskParams;
}
diff --git a/yt/yql/providers/yt/fmr/request_options/yql_yt_request_options.h b/yt/yql/providers/yt/fmr/request_options/yql_yt_request_options.h
index a32e521ba36..a2694941ae3 100644
--- a/yt/yql/providers/yt/fmr/request_options/yql_yt_request_options.h
+++ b/yt/yql/providers/yt/fmr/request_options/yql_yt_request_options.h
@@ -538,6 +538,11 @@ struct TReduceTaskParams {
std::vector<TFmrTableOutputRef> Output;
TString SerializedReduceJobState;
TReduceOperationSpec ReduceOperationSpec;
+ // Whether Input's rows are sorted by [_yql_key_hash, ...ReduceOperationSpec.SortBy] (true for
+ // a MapReduce operation's reduce stage, which shuffles via a synthetic hash column) or by
+ // ReduceOperationSpec.SortBy alone (a plain Reduce operation, which reads real YT-sorted
+ // input with no hash column at all).
+ bool SortByHasKeyHashPrefix = false;
};
// Service column added by map stage to hash-route rows to the correct reducer.
diff --git a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/ya.make b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/ya.make
new file mode 100644
index 00000000000..61a5d2ff9f6
--- /dev/null
+++ b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/ya.make
@@ -0,0 +1,17 @@
+UNITTEST()
+
+SRCS(
+ yql_yt_yson_tds_block_iterator_ut.cpp
+)
+
+PEERDIR(
+ yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl
+ yt/yql/providers/yt/fmr/request_options
+ yt/yql/providers/yt/fmr/table_data_service/local/impl
+ yt/yql/providers/yt/fmr/test_tools/yson
+ yt/yql/providers/yt/fmr/utils
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/yql_yt_yson_tds_block_iterator_ut.cpp b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/yql_yt_yson_tds_block_iterator_ut.cpp
new file mode 100644
index 00000000000..e2f92d30156
--- /dev/null
+++ b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ut/yql_yt_yson_tds_block_iterator_ut.cpp
@@ -0,0 +1,88 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h>
+#include <yt/yql/providers/yt/fmr/test_tools/yson/yql_yt_yson_helpers.h>
+#include <yt/yql/providers/yt/fmr/table_data_service/local/impl/yql_yt_table_data_service_local.h>
+#include <yt/yql/providers/yt/fmr/utils/yql_yt_table_data_service_key.h>
+
+namespace NYql::NFmr {
+
+namespace {
+
+// Two rows sharing the same reduce key (_yql_key_hash=1, k=5) but differing only in the
+// tiebreaker column _yql_sort - e.g. a join's build-side row (sort=0) and one of its probe-side
+// rows (sort=1). Mirrors the real production row shape from yt/yql/providers/yt/fmr/job/impl/
+// yql_yt_reduce_reader.cpp's dumps.
+const TVector<TString> KeyColumns = {"_yql_key_hash", "k", "_yql_sort"};
+
+std::vector<TIndexedBlock> ReadAllBlocks(TTableDataServiceBlockIterator::TPtr iter) {
+ std::vector<TIndexedBlock> blocks;
+ TIndexedBlock block;
+ while (iter->NextBlock(block)) {
+ blocks.push_back(block);
+ }
+ return blocks;
+}
+
+ui64 CountRows(const std::vector<TIndexedBlock>& blocks) {
+ ui64 total = 0;
+ for (const auto& b : blocks) {
+ total += b.Rows.size();
+ }
+ return total;
+}
+
+} // namespace
+
+Y_UNIT_TEST_SUITE(TableDataServiceBlockIteratorBoundaryTests) {
+ Y_UNIT_TEST(TieBreakerColumnMustNotSplitReduceGroupAcrossBoundary) {
+ auto tds = MakeLocalTableDataService();
+ const TString tableId = "t";
+ const TString partId = "p0";
+ const TString group = GetTableDataServiceGroup(tableId, partId);
+ const TString chunkId = GetTableDataServiceChunkId(0, TString());
+
+ const TString chunkText =
+ "{\"_yql_key_hash\"=1u;\"k\"=5u;\"_yql_sort\"=0u};\n"
+ "{\"_yql_key_hash\"=1u;\"k\"=5u;\"_yql_sort\"=1u};\n";
+ tds->Put(group, chunkId, GetBinaryYson(chunkText)).GetValueSync();
+
+ // The task boundary itself is built from the sort=0 row's real key - exactly what
+ // happens in production when that row is the last physical row before a task cut.
+ const TString boundaryRow = GetBinaryYson(
+ "{\"_yql_key_hash\"=1u;\"k\"=5u;\"_yql_sort\"=0u}",
+ NYson::EYsonType::Node
+ );
+
+ std::vector<TTableRange> tableRanges = {TTableRange{.PartId = partId, .MinChunk = 0, .MaxChunk = 1}};
+ std::vector<ESortOrder> sortOrders(KeyColumns.size(), ESortOrder::Ascending);
+
+ // Without a cap (numBoundaryKeyColumns defaults to comparing every key column, including
+ // the tiebreaker): reproduces the bug - the sort=1 row looks like it's past the boundary
+ // and gets dropped, even though it shares the exact same reduce key.
+ {
+ auto iter = MakeIntrusive<TTableDataServiceBlockIterator>(
+ tableId, tableRanges, tds, KeyColumns, sortOrders,
+ /*neededColumns*/ TVector<TString>{}, /*serializedColumnGroupsSpec*/ TString(),
+ /*isFirstRowKeysInclusive*/ Nothing(), /*isLastRowKeysInclusive*/ true,
+ /*firstRowKeys*/ Nothing(), /*lastRowKeys*/ boundaryRow
+ );
+ UNIT_ASSERT_VALUES_EQUAL(CountRows(ReadAllBlocks(iter)), 1);
+ }
+
+ // With the fix - numBoundaryKeyColumns=2 caps comparison to [_yql_key_hash, k], excluding
+ // _yql_sort: both rows of the reduce group survive the same boundary.
+ {
+ auto iter = MakeIntrusive<TTableDataServiceBlockIterator>(
+ tableId, tableRanges, tds, KeyColumns, sortOrders,
+ /*neededColumns*/ TVector<TString>{}, /*serializedColumnGroupsSpec*/ TString(),
+ /*isFirstRowKeysInclusive*/ Nothing(), /*isLastRowKeysInclusive*/ true,
+ /*firstRowKeys*/ Nothing(), /*lastRowKeys*/ boundaryRow,
+ /*readAheadChunks*/ ui64{4}, /*numBoundaryKeyColumns*/ size_t{2}
+ );
+ UNIT_ASSERT_VALUES_EQUAL(CountRows(ReadAllBlocks(iter)), 2);
+ }
+ }
+}
+
+} // namespace NYql::NFmr
diff --git a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ya.make b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ya.make
index afb3020f720..7f68fea4899 100644
--- a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ya.make
+++ b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/ya.make
@@ -21,3 +21,7 @@ PEERDIR(
YQL_LAST_ABI_VERSION()
END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.cpp b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.cpp
index 584614844d9..e87a9791cbb 100644
--- a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.cpp
+++ b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.cpp
@@ -21,7 +21,8 @@ TTableDataServiceBlockIterator::TTableDataServiceBlockIterator(
TMaybe<bool> isLastRowKeysInclusive,
TMaybe<TString> firstRowKeys,
TMaybe<TString> lastRowKeys,
- ui64 readAheadChunks
+ ui64 readAheadChunks,
+ TMaybe<size_t> numBoundaryKeyColumns
)
: TableId_(std::move(tableId))
, TableRanges_(std::move(tableRanges))
@@ -31,6 +32,7 @@ TTableDataServiceBlockIterator::TTableDataServiceBlockIterator(
, NeededColumns_(std::move(neededColumns))
, SerializedColumnGroupsSpec_(std::move(serializedColumnGroupsSpec))
, ReadAheadChunks_(readAheadChunks)
+ , NumBoundaryKeyColumns_(numBoundaryKeyColumns.GetOrElse(KeyColumns_.size()))
{
if (SortOrders_.empty()) {
SortOrders_.assign(KeyColumns_.size(), ESortOrder::Ascending);
@@ -38,6 +40,7 @@ TTableDataServiceBlockIterator::TTableDataServiceBlockIterator(
if (SortOrders_.size() != KeyColumns_.size()) {
ythrow yexception() << "SortOrders and KeyColumns sizes are different";
}
+ Y_ENSURE(NumBoundaryKeyColumns_ <= KeyColumns_.size(), "numBoundaryKeyColumns must not exceed keyColumns size");
if (firstRowKeys) {
FirstBoundary_ = TFmrTableKeysBoundary(*firstRowKeys, KeyColumns_, SortOrders_);
@@ -144,24 +147,29 @@ bool IsNullColumnValue(TStringBuf blob, const TColumnOffsetRange& range) {
return SliceRange(blob, range) == TStringBuf(&EntitySymbol, 1);
}
-// FirstRowKeys/LastRowKeys boundaries are built by the coordinator from a task's chunk-stats
-// key columns (e.g. [_yql_key_hash, ...ReduceBy] for MapReduce reduce tasks), which can be a
-// strict PREFIX of the fuller key column set (e.g. [_yql_key_hash, ...SortBy]) this iterator
-// parses actual rows with. TFmrTableKeysBoundary parses its own Row bytes with the SAME
-// (fuller) key columns as the row markup, so any column beyond the boundary's real prefix comes
-// out with an IsValid()==false range - not because the value is legitimately null, but because
-// the boundary blob simply never encoded it. Comparing that as "boundary < row" (the usual
-// null-handling rule) would incorrectly exclude real rows that land exactly on the boundary, so
-// this comparison stops at the boundary's own valid prefix instead of delegating to the
-// generic null-aware CompareKeyRowsAcrossYsonBlocks.
+// FirstRowKeys/LastRowKeys boundaries are built by the coordinator from a task's chunk-stats,
+// i.e. from an actual physical row's real key - for MapReduce reduce tasks over joins that key
+// is [_yql_key_hash, ...ReduceBy, ...SortBy tiebreaker] in full, tiebreaker included, because
+// that's what TReducePartitioner sorts intermediate data by (see
+// yql_yt_map_reduce_stage_operation_manager.cpp). It is NOT reliably a strict prefix that omits
+// the tiebreaker: whichever physical row happens to land on a task cut contributes its own real
+// tiebreaker value to the boundary blob. So this can't stop at "wherever the boundary blob runs
+// out of valid columns" (that already-invalid range doesn't reliably appear here) - it must be
+// told explicitly, via numBoundaryKeyColumns, how many leading columns actually define a reduce
+// group, and ignore everything past that. Comparing the tiebreaker as a real bound column would
+// silently split a reduce group in half whenever a task cut lands between two physical rows that
+// share a reduce key but differ in tiebreaker (e.g. a join's build-side row vs. its probe-side
+// rows) - each side would look like it's on the "wrong side" of the other's boundary.
int CompareRowToBoundaryPrefix(
TStringBuf rowBlob,
const TRowIndexMarkup& row,
TStringBuf boundaryBlob,
const TRowIndexMarkup& boundaryMarkup,
- const std::vector<ESortOrder>& sortOrders
+ const std::vector<ESortOrder>& sortOrders,
+ size_t numBoundaryKeyColumns
) {
- for (ui64 colIdx = 0; colIdx + 1 < boundaryMarkup.size(); ++colIdx) {
+ const size_t limit = std::min<size_t>(numBoundaryKeyColumns, boundaryMarkup.empty() ? 0 : boundaryMarkup.size() - 1);
+ for (ui64 colIdx = 0; colIdx < limit; ++colIdx) {
const auto& boundaryRange = boundaryMarkup[colIdx];
if (!boundaryRange.IsValid()) {
// Boundary doesn't encode this column (or any further one, since it's a prefix) -
@@ -204,7 +212,8 @@ bool TTableDataServiceBlockIterator::RowInKeyBounds(const TString& blob, const T
row,
FirstBoundary_->Row,
FirstBoundary_->Markup,
- SortOrders_
+ SortOrders_,
+ NumBoundaryKeyColumns_
);
if (c < 0) { // if row < first boundary
return false;
@@ -218,7 +227,8 @@ bool TTableDataServiceBlockIterator::RowInKeyBounds(const TString& blob, const T
row,
LastBoundary_->Row,
LastBoundary_->Markup,
- SortOrders_
+ SortOrders_,
+ NumBoundaryKeyColumns_
);
if (c > 0) { // if row > last boundary
return false;
diff --git a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h
index 4ab202869fc..8f661e5ec01 100644
--- a/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h
+++ b/yt/yql/providers/yt/fmr/utils/yson_block_iterator/impl/yql_yt_yson_tds_block_iterator.h
@@ -27,8 +27,14 @@ public:
TMaybe<bool> isLastRowKeysInclusive = Nothing(),
TMaybe<TString> firstRowKeys = Nothing(),
TMaybe<TString> lastRowKeys = Nothing(),
- ui64 readAheadChunks = 4
-
+ ui64 readAheadChunks = 4,
+ // How many leading keyColumns actually define a reduce group (e.g. _yql_key_hash +
+ // reduceBy, excluding a join's _yql_sort tiebreaker). Boundary comparisons stop after
+ // this many columns instead of running to the end of keyColumns - see the comment on
+ // CompareRowToBoundaryPrefix for why relying on the boundary blob happening to omit
+ // trailing columns is not safe. Defaults to comparing every column in keyColumns, which
+ // is correct whenever keyColumns has no trailing tiebreaker (e.g. non-reduce contexts).
+ TMaybe<size_t> numBoundaryKeyColumns = Nothing()
);
~TTableDataServiceBlockIterator() final;
@@ -73,6 +79,7 @@ private:
std::vector<TString> GroupNamesToRead_;
ui64 ReadAheadChunks_ = 4;
+ const size_t NumBoundaryKeyColumns_;
std::deque<TPrefetchEntry> PrefetchQueue_;
};