github-actions[bot] commented on code in PR #65190:
URL: https://github.com/apache/doris/pull/65190#discussion_r3667766360


##########
be/src/common/config.cpp:
##########
@@ -1819,6 +1819,7 @@ DEFINE_Validator(concurrency_stats_dump_interval_ms,
 DEFINE_mBool(cloud_mow_sync_rowsets_when_load_txn_begin, "true");
 
 DEFINE_mBool(enable_cloud_make_rs_visible_on_be, "false");
+DEFINE_mBool(enable_cloud_random_segment_id, "true");

Review Comment:
   [P1] Gate incompatible rowsets during rolling upgrades
   
   With this default, the first upgraded BE that compacts can publish (for 
example) `num_segments=1, segment_ids=[437]` and only `<rowset>_437.dat` to the 
shared meta service. A pre-PR BE can sync that visible rowset, but its 
CloudPB-to-RowsetMeta conversion cannot copy field 200 and its readers still 
enumerate `[0, num_segments)`, so it opens `<rowset>_0.dat` and 
queries/compactions fail during a rolling upgrade. Please keep production of 
nonzero IDs disabled by default until every reader is capable, or gate it on a 
cluster-wide minimum BE version/capability, with a mixed-version test.



##########
be/src/storage/rowset/rowset.cpp:
##########
@@ -108,10 +110,15 @@ bool Rowset::check_rowset_segment() {
 std::string Rowset::get_rowset_info_str() {
     std::string disk_size = PrettyPrinter::print(
             static_cast<uint64_t>(_rowset_meta->total_disk_size()), 
TUnit::BYTES);
-    return fmt::format("[{}-{}] {} {} {} {} {}", start_version(), 
end_version(), num_segments(),
-                       _rowset_meta->has_delete_predicate() ? "DELETE" : 
"DATA",
-                       
SegmentsOverlapPB_Name(_rowset_meta->segments_overlap()),
-                       rowset_id().to_string(), disk_size);
+    std::string rowset_info =
+            fmt::format("[{}-{}] {} {} {} {} {}", start_version(), 
end_version(), num_segments(),
+                        _rowset_meta->has_delete_predicate() ? "DELETE" : 
"DATA",
+                        
SegmentsOverlapPB_Name(_rowset_meta->segments_overlap()),
+                        rowset_id().to_string(), disk_size);
+    if (_rowset_meta->has_segment_ids()) {
+        rowset_info += fmt::format(" [{}]", 
fmt::join(_rowset_meta->segment_ids(), ","));

Review Comment:
   [P1] Keep Cloud show-data size accounting compatible
   
   This turns an explicit-ID rowset status into eight whitespace-separated 
tokens, but `regression-test/plugins/cloud_show_data_plugin.groovy:215-222` 
counts a rowset only when `fields.length == 7`. The show-data suites trigger 
compaction and then compare this API total with MySQL/backend storage, so the 
surviving randomized-ID rowset is silently omitted and those checks 
under-report or fail. Please preserve the existing representation or update the 
parser to handle the optional segment list (and add an explicit-ID parser case).



##########
regression-test/suites/compaction/test_mow_cumulative_compaction_multi_output_segments.groovy:
##########
@@ -0,0 +1,424 @@
+// 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.
+
+suite("test_mow_cumulative_compaction_multi_output_segments", "nonConcurrent") 
{
+    if (!isCloudMode()) {
+        logger.info("skip test_mow_cumulative_compaction_multi_output_segments 
in non-cloud mode")
+        return
+    }
+
+    def backendIdToHost = [:]
+    def backendIdToHttpPort = [:]
+    getBackendIpHttpPort(backendIdToHost, backendIdToHttpPort)
+
+    def configNames = [
+            "compaction_batch_size",
+            "doris_scanner_row_bytes",
+            "enable_rowid_conversion_correctness_check",
+            "enable_vertical_compaction",
+            "inverted_index_compaction_enable",
+            "vertical_compaction_max_segment_size"
+    ]
+    def originalConfigs = [:]
+
+    def readBeConfig = { String backendId, String configName ->
+        def host = backendIdToHost[backendId]
+        def port = backendIdToHttpPort[backendId]
+        def (code, out, err) = curl(
+                "GET", 
"http://${host}:${port}/api/show_config?conf_item=${configName}";)
+        assertEquals(0, code)
+        def configs = parseJson(out)
+        assertEquals(1, configs.size())
+        assertEquals(configName, configs[0][0])
+        return configs[0][2].toString()
+    }
+
+    def updateBeConfig = { String configName, String value ->
+        backendIdToHost.keySet().each { String backendId ->
+            def host = backendIdToHost[backendId]
+            def port = backendIdToHttpPort[backendId]
+            def (code, out, err) = curl(
+                    "POST", 
"http://${host}:${port}/api/update_config?${configName}=${value}";)
+            assertEquals(0, code)
+            assertTrue(out.contains("OK"), "failed to set 
${configName}=${value}: ${out}, ${err}")
+        }
+    }
+
+    backendIdToHost.keySet().each { String backendId ->
+        originalConfigs[backendId] = [:]
+        configNames.each { String configName ->
+            originalConfigs[backendId][configName] = readBeConfig(backendId, 
configName)
+        }
+    }
+
+    def resetBeConfigs = {
+        originalConfigs.each { String backendId, Map configs ->
+            def host = backendIdToHost[backendId]
+            def port = backendIdToHttpPort[backendId]
+            configs.each { String configName, String value ->
+                def (code, out, err) = curl(
+                        "POST", 
"http://${host}:${port}/api/update_config?${configName}=${value}";)
+                assertEquals(0, code)
+                assertTrue(out.contains("OK"),
+                        "failed to reset ${configName}=${value}: ${out}, 
${err}")
+            }
+        }
+    }
+
+    def showTablet = { def backend, String tabletId ->
+        def (code, out, err) =
+                be_show_tablet_status(backend.Host, backend.HttpPort, tabletId)
+        logger.info("Show tablet status: code=${code}, out=${out}, err=${err}")
+        assertEquals(0, code)
+        return parseJson(out.trim())
+    }
+
+    def findRowset = { def tabletStatus, int startVersion, int endVersion ->
+        def rowset =
+                tabletStatus.rowsets.find { 
it.startsWith("[${startVersion}-${endVersion}] ") }
+        assertNotNull(rowset,
+                "cannot find rowset [${startVersion}-${endVersion}]: 
${tabletStatus.rowsets}")
+        return rowset
+    }
+
+    def parseRowset = { String rowset ->
+        def matcher = rowset =~
+                /\[[0-9]+-[0-9]+\]\s+([0-9]+)\s+DATA\s+([A-Z_]+)\s+([0-9a-f]+)/
+        assertTrue(matcher.find(), "unexpected rowset format: ${rowset}")
+        def segmentIds = []
+        def segmentIdsMatcher = rowset =~ /\s\[([0-9]+(?:,[0-9]+)*)\]$/
+        if (segmentIdsMatcher.find()) {
+            segmentIds = segmentIdsMatcher.group(1).split(",").collect { 
it.toInteger() }
+        }
+        return [
+                segmentNum: matcher.group(1).toInteger(),
+                overlap: matcher.group(2),
+                rowsetId: matcher.group(3),
+                segmentIds: segmentIds
+        ]
+    }
+
+    def waitForCompaction = { def backend, String tabletId ->
+        long timeoutMs = 120000
+        long deadline = System.currentTimeMillis() + timeoutMs
+        def lastStatus = null
+        while (System.currentTimeMillis() < deadline) {
+            def (code, out, err) =
+                    be_get_compaction_status(backend.Host, backend.HttpPort, 
tabletId)
+            logger.info("Get compaction status: code=${code}, out=${out}, 
err=${err}")
+            assertEquals(0, code)
+            lastStatus = parseJson(out.trim())
+            assertEquals("success", lastStatus.status.toLowerCase())
+            if (!lastStatus.run_status) {
+                return
+            }
+            Thread.sleep(1000)
+        }
+        assertTrue(false,
+                "compaction timeout: tablet=${tabletId}, 
timeoutMs=${timeoutMs}, last=${lastStatus}")
+    }
+
+    def waitForRowset = { backend, tabletId, startVersion, endVersion ->
+        long timeoutMs = 30000
+        long deadline = System.currentTimeMillis() + timeoutMs
+        def lastStatus = null
+        while (System.currentTimeMillis() < deadline) {
+            lastStatus = showTablet(backend, tabletId)
+            if (lastStatus.rowsets.any {
+                it.startsWith("[${startVersion}-${endVersion}] ")
+            }) {
+                return lastStatus
+            }
+            Thread.sleep(200)
+        }
+        assertTrue(false,
+                "rowset [${startVersion}-${endVersion}] is not visible: " +
+                        "tablet=${tabletId}, timeoutMs=${timeoutMs}, 
last=${lastStatus}")
+    }
+
+    def loadRows = { String tableName, int startKey, int endKey, int valueBase 
->
+        def batchTags = [
+                100000: "batchone",
+                200000: "batchtwo",
+                300000: "batchthree",
+                400000: "batchfour",
+                500000: "batchfive"
+        ]
+        def batchTag = batchTags[valueBase]
+        assertNotNull(batchTag)
+        StringBuilder content = new StringBuilder()
+        (startKey..endKey).each { int key ->
+            String suffix = key.toString().padLeft(32, "0")
+            String payload =
+                    "${batchTag} payload ${suffix} ${(key * 
17L).toString().padLeft(32, "0")}"
+            content.append("${key},${valueBase + key},${payload}\n")
+        }
+        streamLoad {
+            table tableName
+            set "column_separator", ","
+            inputStream new ByteArrayInputStream(content.toString().getBytes())
+            time 120000
+            check { result, exception, startTime, endTime ->
+                if (exception != null) {
+                    throw exception
+                }
+                def json = parseJson(result)
+                assertEquals("success", json.Status.toLowerCase())
+                assertEquals(endKey - startKey + 1, json.NumberTotalRows)
+                assertEquals(0, json.NumberFilteredRows)
+            }
+        }
+        sql "sync"
+    }
+
+    def readRows = { String tableName ->
+        return sql("""
+            SELECT k, v
+            FROM ${tableName}
+            WHERE k IN (1, 16384, 16385, 32768, 32769, 49152, 49153, 65536, 
65537, 81920)
+            ORDER BY k
+        """)
+    }
+
+    def readRowsByIndex = { String tableName ->
+        def batchTags = ["batchone", "batchtwo", "batchthree", "batchfour", 
"batchfive"]
+        return batchTags.collect { String batchTag ->
+            def result = sql """
+                SELECT /*+ SET_VAR(enable_match_without_inverted_index = 
false) */ COUNT(*)
+                FROM ${tableName}
+                WHERE payload MATCH '${batchTag}'
+            """
+            return result[0][0]
+        }
+    }
+
+    def getLocalDeleteBitmap = { def backend, String tabletId ->
+        def (code, out, err) = curl(
+                "GET",
+                "http://${backend.Host}:${backend.HttpPort}"; +
+                        
"/api/delete_bitmap/count_local?verbose=true&tablet_id=${tabletId}")
+        logger.info("Get local delete bitmap: code=${code}, out=${out}, 
err=${err}")
+        assertEquals(0, code)
+        return parseJson(out.trim())
+    }
+
+    def getDeleteBitmapAtVersion = { def deleteBitmap, String rowsetId, int 
version ->
+        def segmentIds = []
+        long cardinality = 0
+        deleteBitmap.delete_bitmap.each { String key, bitmapVersions ->
+            if (!key.contains("rowset: ${rowsetId},")) {
+                return
+            }
+            def segmentMatcher = key =~ /segment:\s+([0-9]+)/
+            assertTrue(segmentMatcher.find(), "unexpected delete bitmap key: 
${key}")
+            bitmapVersions.each { String bitmapVersion ->
+                def versionMatcher = bitmapVersion =~ 
/v:\s+([0-9]+),\s+c:\s+([0-9]+)/
+                assertTrue(versionMatcher.find(),
+                        "unexpected delete bitmap value: ${bitmapVersion}")
+                if (versionMatcher.group(1).toInteger() == version) {
+                    segmentIds.add(segmentMatcher.group(1).toInteger())
+                    cardinality += versionMatcher.group(2).toLong()
+                }
+            }
+        }
+        return [segmentIds: segmentIds.unique(), cardinality: cardinality]
+    }
+
+    GetDebugPoint().clearDebugPointsForAllBEs()
+    try {
+        updateBeConfig("compaction_batch_size", "512")
+        updateBeConfig("doris_scanner_row_bytes", "1")
+        updateBeConfig("enable_rowid_conversion_correctness_check", "true")
+        updateBeConfig("inverted_index_compaction_enable", "true")
+        updateBeConfig("vertical_compaction_max_segment_size", "8192")
+        updateBeConfig("enable_cloud_random_segment_id", "true")

Review Comment:
   [P2] Restore this backend config after the suite
   
   `enable_cloud_random_segment_id` is not included in `configNames`, so its 
original value is never captured and `resetBeConfigs()` in the `finally` block 
cannot restore it. A run that starts with this disabled is left enabled for 
later suites. Please add it to the snapshotted config list before changing it 
here.



##########
be/src/storage/compaction/compaction.cpp:
##########
@@ -1962,7 +1976,28 @@ Status 
CloudCompactionMixin::construct_output_rowset_writer(RowsetWriterContext&
     ctx.tablet = _tablet;
     ctx.job_id = _uuid;
 
+    if (!_is_vertical) {
+        DBUG_EXECUTE_IF(
+                
"CloudCompactionMixin.construct_output_rowset_writer.max_rows_per_segment", {
+                    ctx.max_rows_per_segment =
+                            dp->param<uint32_t>("max_rows_per_segment", 
ctx.max_rows_per_segment);
+                    DORIS_CHECK_GT(ctx.max_rows_per_segment, 0);
+                });
+    }
     _output_rs_writer = DORIS_TRY(_tablet->create_rowset_writer(ctx, 
_is_vertical));
+    if (config::enable_cloud_random_segment_id) {

Review Comment:
   [P1] Update the encryption action before producing nonzero IDs
   
   Both helpers in `be/src/service/http/action/check_encryption_action.cpp` 
still call `rs->segment_path(0)` (lines 81 and 132). Once this block assigns a 
start above zero, a compacted rowset has no `_0.dat`; the Cloud-enabled 
`/api/check_tablet_encryption` action then fails `open_file` and returns HTTP 
500 (including `get_footer=true` when that rowset is newest). Please select the 
first segment by position, e.g. `rs->segment(0).path()`, in both helpers and 
cover a rowset whose only physical ID is nonzero.



-- 
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]

Reply via email to