This is an automated email from the ASF dual-hosted git repository. dataroaring pushed a commit to branch branch-2.0 in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-2.0 by this push: new f1a9d803fd2 [branch-2.0](publish version) fix publish version failed but return ok #28425 (#28791) f1a9d803fd2 is described below commit f1a9d803fd29889b571ff360a9b484f526993c53 Author: yujun <yu.jun.re...@gmail.com> AuthorDate: Tue Dec 26 17:38:40 2023 +0800 [branch-2.0](publish version) fix publish version failed but return ok #28425 (#28791) Co-authored-by: Kang <kxiao.ti...@gmail.com> --- be/src/http/action/report_action.cpp | 41 ++++++++++++ be/src/http/action/report_action.h | 38 +++++++++++ be/src/olap/storage_engine.cpp | 5 +- be/src/olap/storage_engine.h | 2 +- be/src/olap/task/engine_publish_version_task.cpp | 34 ++++++++-- be/src/olap/task/engine_publish_version_task.h | 2 + be/src/service/http_service.cpp | 16 +++++ .../insert_p0/test_be_inject_publish_txn_fail.out | 13 ++++ .../apache/doris/regression/util/DebugPoint.groovy | 28 +++----- .../org/apache/doris/regression/util/Http.groovy | 27 +++++++- .../plugins/plugin_curl_requester.groovy | 45 +++++++++++++ .../test_be_inject_publish_txn_fail.groovy | 77 ++++++++++++++++++++++ .../plugin_p0/test_plugin_curl_requester.groovy | 30 +++++++++ 13 files changed, 330 insertions(+), 28 deletions(-) diff --git a/be/src/http/action/report_action.cpp b/be/src/http/action/report_action.cpp new file mode 100644 index 00000000000..cf5621f5791 --- /dev/null +++ b/be/src/http/action/report_action.cpp @@ -0,0 +1,41 @@ +// 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. + +#include "http/action/report_action.h" + +#include "common/status.h" +#include "http/http_channel.h" +#include "olap/storage_engine.h" + +namespace doris { + +ReportAction::ReportAction(ExecEnv* exec_env, TPrivilegeHier::type hier, TPrivilegeType::type type, + TaskWorkerPool::TaskWorkerType report_type) + : HttpHandlerWithAuth(exec_env, hier, type), _report_type(report_type) {} + +void ReportAction::handle(HttpRequest* req) { + if (StorageEngine::instance()->notify_listener(_report_type)) { + HttpChannel::send_reply(req, HttpStatus::OK, Status::OK().to_json()); + } else { + HttpChannel::send_reply( + req, HttpStatus::INTERNAL_SERVER_ERROR, + Status::InternalError("unknown reporter with name: " + std::to_string(_report_type)) + .to_json()); + } +} + +} // namespace doris diff --git a/be/src/http/action/report_action.h b/be/src/http/action/report_action.h new file mode 100644 index 00000000000..e4a3b9e5f98 --- /dev/null +++ b/be/src/http/action/report_action.h @@ -0,0 +1,38 @@ +// 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. + +#pragma once + +#include "agent/task_worker_pool.h" +#include "http/http_handler_with_auth.h" +#include "http/http_request.h" + +namespace doris { + +// use in regression test, just for test. + +class ReportAction : public HttpHandlerWithAuth { +public: + ReportAction(ExecEnv* exec_env, TPrivilegeHier::type hier, TPrivilegeType::type type, + TaskWorkerPool::TaskWorkerType report_type); + void handle(HttpRequest* req) override; + +private: + const TaskWorkerPool::TaskWorkerType _report_type; +}; + +} // namespace doris diff --git a/be/src/olap/storage_engine.cpp b/be/src/olap/storage_engine.cpp index a4ecf33a0bb..e22cb94040f 100644 --- a/be/src/olap/storage_engine.cpp +++ b/be/src/olap/storage_engine.cpp @@ -1232,13 +1232,16 @@ void StorageEngine::notify_listeners() { } } -void StorageEngine::notify_listener(TaskWorkerPool::TaskWorkerType task_worker_type) { +bool StorageEngine::notify_listener(TaskWorkerPool::TaskWorkerType task_worker_type) { + bool found = false; std::lock_guard<std::mutex> l(_report_mtx); for (auto& listener : _report_listeners) { if (listener->task_worker_type() == task_worker_type) { listener->notify_thread(); + found = true; } } + return found; } Status StorageEngine::execute_task(EngineTask* task) { diff --git a/be/src/olap/storage_engine.h b/be/src/olap/storage_engine.h index 7215e2bb484..244e134b512 100644 --- a/be/src/olap/storage_engine.h +++ b/be/src/olap/storage_engine.h @@ -139,7 +139,7 @@ public: void register_report_listener(TaskWorkerPool* listener); void deregister_report_listener(TaskWorkerPool* listener); void notify_listeners(); - void notify_listener(TaskWorkerPool::TaskWorkerType task_worker_type); + bool notify_listener(TaskWorkerPool::TaskWorkerType task_worker_type); Status execute_task(EngineTask* task); diff --git a/be/src/olap/task/engine_publish_version_task.cpp b/be/src/olap/task/engine_publish_version_task.cpp index e1a151c1973..d70877c4c48 100644 --- a/be/src/olap/task/engine_publish_version_task.cpp +++ b/be/src/olap/task/engine_publish_version_task.cpp @@ -108,6 +108,14 @@ Status EnginePublishVersionTask::finish() { StorageEngine::instance()->tablet_publish_txn_thread_pool()->new_token( ThreadPool::ExecutionMode::CONCURRENT); std::unordered_map<int64_t, int64_t> tablet_id_to_num_delta_rows; + +#ifndef NDEBUG + if (UNLIKELY(_publish_version_req.partition_version_infos.empty())) { + LOG(WARNING) << "transaction_id: " << transaction_id << " empty partition_version_infos"; + } +#endif + + std::vector<std::shared_ptr<TabletPublishTxnTask>> tablet_tasks; // each partition for (auto& par_ver_info : _publish_version_req.partition_version_infos) { int64_t partition_id = par_ver_info.partition_id; @@ -230,12 +238,22 @@ Status EnginePublishVersionTask::finish() { auto tablet_publish_txn_ptr = std::make_shared<TabletPublishTxnTask>( this, tablet, rowset, partition_id, transaction_id, version, tablet_info); + tablet_tasks.push_back(tablet_publish_txn_ptr); auto submit_st = token->submit_func([=]() { tablet_publish_txn_ptr->handle(); }); CHECK(submit_st.ok()) << submit_st; } } token->wait(); + if (res.ok()) { + for (const auto& tablet_task : tablet_tasks) { + res = tablet_task->result(); + if (!res.ok()) { + break; + } + } + } + _succ_tablets->clear(); // check if the related tablet remained all have the version for (auto& par_ver_info : _publish_version_req.partition_version_infos) { @@ -324,24 +342,24 @@ void TabletPublishTxnTask::handle() { rowset_update_lock.lock(); } _stats.schedule_time_us = MonotonicMicros() - _stats.submit_time_us; - auto publish_status = StorageEngine::instance()->txn_manager()->publish_txn( + _result = StorageEngine::instance()->txn_manager()->publish_txn( _partition_id, _tablet, _transaction_id, _version, &_stats); - if (publish_status != Status::OK()) { + if (!_result.ok()) { LOG(WARNING) << "failed to publish version. rowset_id=" << _rowset->rowset_id() << ", tablet_id=" << _tablet_info.tablet_id << ", txn_id=" << _transaction_id - << ", res=" << publish_status; + << ", res=" << _result; _engine_publish_version_task->add_error_tablet_id(_tablet_info.tablet_id); return; } // add visible rowset to tablet int64_t t1 = MonotonicMicros(); - publish_status = _tablet->add_inc_rowset(_rowset); + _result = _tablet->add_inc_rowset(_rowset); _stats.add_inc_rowset_us = MonotonicMicros() - t1; - if (publish_status != Status::OK() && !publish_status.is<PUSH_VERSION_ALREADY_EXIST>()) { + if (!_result.ok() && !_result.is<PUSH_VERSION_ALREADY_EXIST>()) { LOG(WARNING) << "fail to add visible rowset to tablet. rowset_id=" << _rowset->rowset_id() << ", tablet_id=" << _tablet_info.tablet_id << ", txn_id=" << _transaction_id - << ", res=" << publish_status; + << ", res=" << _result; _engine_publish_version_task->add_error_tablet_id(_tablet_info.tablet_id); return; } @@ -351,9 +369,11 @@ void TabletPublishTxnTask::handle() { LOG(INFO) << "publish version successfully on tablet" << ", table_id=" << _tablet->table_id() << ", tablet=" << _tablet->full_name() << ", transaction_id=" << _transaction_id << ", version=" << _version.first - << ", num_rows=" << _rowset->num_rows() << ", res=" << publish_status + << ", num_rows=" << _rowset->num_rows() << ", res=" << _result << ", cost: " << cost_us << "(us) " << (cost_us > 500 * 1000 ? _stats.to_string() : ""); + + _result = Status::OK(); } void AsyncTabletPublishTask::handle() { diff --git a/be/src/olap/task/engine_publish_version_task.h b/be/src/olap/task/engine_publish_version_task.h index 8f3790574a2..dc84dd73923 100644 --- a/be/src/olap/task/engine_publish_version_task.h +++ b/be/src/olap/task/engine_publish_version_task.h @@ -69,6 +69,7 @@ public: ~TabletPublishTxnTask() = default; void handle(); + Status result() { return _result; } private: EnginePublishVersionTask* _engine_publish_version_task; @@ -80,6 +81,7 @@ private: Version _version; TabletInfo _tablet_info; TabletPublishStatistics _stats; + Status _result; }; class EnginePublishVersionTask : public EngineTask { diff --git a/be/src/service/http_service.cpp b/be/src/service/http_service.cpp index 556d101b05c..547b88878a1 100644 --- a/be/src/service/http_service.cpp +++ b/be/src/service/http_service.cpp @@ -41,6 +41,7 @@ #include "http/action/pad_rowset_action.h" #include "http/action/pprof_actions.h" #include "http/action/reload_tablet_action.h" +#include "http/action/report_action.h" #include "http/action/reset_rpc_channel_action.h" #include "http/action/restore_tablet_action.h" #include "http/action/snapshot_action.h" @@ -276,6 +277,21 @@ Status HttpService::start() { _ev_http_server->register_handler(HttpMethod::POST, "/api/debug_point/clear", clear_debug_points_action); + ReportAction* report_tablet_action = + _pool.add(new ReportAction(_env, TPrivilegeHier::GLOBAL, TPrivilegeType::ADMIN, + TaskWorkerPool::TaskWorkerType::REPORT_OLAP_TABLE)); + _ev_http_server->register_handler(HttpMethod::GET, "/api/report/tablet", report_tablet_action); + + ReportAction* report_disk_action = + _pool.add(new ReportAction(_env, TPrivilegeHier::GLOBAL, TPrivilegeType::ADMIN, + TaskWorkerPool::TaskWorkerType::REPORT_DISK_STATE)); + _ev_http_server->register_handler(HttpMethod::GET, "/api/report/disk", report_disk_action); + + ReportAction* report_task_action = + _pool.add(new ReportAction(_env, TPrivilegeHier::GLOBAL, TPrivilegeType::ADMIN, + TaskWorkerPool::TaskWorkerType::REPORT_TASK)); + _ev_http_server->register_handler(HttpMethod::GET, "/api/report/task", report_task_action); + _ev_http_server->start(); return Status::OK(); } diff --git a/regression-test/data/insert_p0/test_be_inject_publish_txn_fail.out b/regression-test/data/insert_p0/test_be_inject_publish_txn_fail.out new file mode 100644 index 00000000000..35378c9914e --- /dev/null +++ b/regression-test/data/insert_p0/test_be_inject_publish_txn_fail.out @@ -0,0 +1,13 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !select_1 -- + +-- !select_2 -- +100 + +-- !select_1 -- +100 + +-- !select_2 -- +100 +200 + diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy index c30f7fbbcbe..65e96cc41ba 100644 --- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy +++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy @@ -40,13 +40,13 @@ class DebugPoint { * name: debug point name * params: timeout, execute, or other customized input params */ - static def enableDebugPoint(String host, String httpPort, NodeType type, String name, Map<String, String> params = null) { + static def enableDebugPoint(String host, int httpPort, NodeType type, String name, Map<String, String> params = null) { def url = 'http://' + host + ':' + httpPort + '/api/debug_point/add/' + name if (params != null && params.size() > 0) { url += '?' + params.collect((k, v) -> k + '=' + v).join('&') } - def result = Http.http_post(url, null, true) - checkHttpResult(result, type) + def result = Http.POST(url, null, true) + Http.checkHttpResult(result, type) } /* Disable debug point in regression @@ -56,10 +56,10 @@ class DebugPoint { * type: NodeType.BE or NodeType.FE * name: debug point name */ - static def disableDebugPoint(String host, String httpPort, NodeType type, String name) { + static def disableDebugPoint(String host, int httpPort, NodeType type, String name) { def url = 'http://' + host + ':' + httpPort + '/api/debug_point/remove/' + name - def result = Http.http_post(url, null, true) - checkHttpResult(result, type) + def result = Http.POST(url, null, true) + Http.checkHttpResult(result, type) } /* Disable all debug points in regression @@ -68,10 +68,10 @@ class DebugPoint { * httpPort: http port of target node * type: NodeType.BE or NodeType.FE */ - static def clearDebugPoints(String host, String httpPort, NodeType type) { + static def clearDebugPoints(String host, int httpPort, NodeType type) { def url = 'http://' + host + ':' + httpPort + '/api/debug_point/clear' - def result = Http.http_post(url, null, true) - checkHttpResult(result, type) + def result = Http.POST(url, null, true) + Http.checkHttpResult(result, type) } def operateDebugPointForAllBEs(Closure closure) { @@ -79,7 +79,7 @@ class DebugPoint { def portList = [:] (ipList, portList) = getBEHostAndHTTPPort() ipList.each { beid, ip -> - closure.call(ip, portList[beid]) + closure.call(ip, portList[beid] as int) } } @@ -127,13 +127,5 @@ class DebugPoint { def clearDebugPointsForAllFEs() { assert false : 'not implemented yet' } - - static void checkHttpResult(Object result, NodeType type) { - if (type == NodeType.FE) { - assert result.code == 0 : result.toString() - } else if (type == NodeType.BE) { - assert result.status == 'OK' : result.toString() - } - } } diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy index 1daabf469b5..cd688a1fcfc 100644 --- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy +++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy @@ -39,7 +39,32 @@ class Http { final static Logger logger = LoggerFactory.getLogger(this.class) - static Object http_post(url, data = null, isJson = false) { + static void checkHttpResult(Object result, NodeType type) { + if (type == NodeType.FE) { + assert result.code == 0 : result.toString() + } else if (type == NodeType.BE) { + assert result.status == 'OK' : result.toString() + } + } + + static Object GET(url, isJson = false) { + def conn = new URL(url).openConnection() + conn.setRequestMethod('GET') + conn.setRequestProperty('Authorization', 'Basic cm9vdDo=') //token for root + def code = conn.responseCode + def text = conn.content.text + logger.info("http post url=${url}, isJson=${isJson}, response code=${code}, text=${text}") + Assert.assertEquals(200, code) + if (isJson) { + def json = new JsonSlurper() + def result = json.parseText(text) + return result + } else { + return text + } + } + + static Object POST(url, data = null, isJson = false) { def conn = new URL(url).openConnection() conn.setRequestMethod('POST') conn.setRequestProperty('Authorization', 'Basic cm9vdDo=') //token for root diff --git a/regression-test/plugins/plugin_curl_requester.groovy b/regression-test/plugins/plugin_curl_requester.groovy index aed5e518efe..51e1a893796 100644 --- a/regression-test/plugins/plugin_curl_requester.groovy +++ b/regression-test/plugins/plugin_curl_requester.groovy @@ -16,6 +16,8 @@ // under the License. import org.apache.doris.regression.suite.Suite +import org.apache.doris.regression.util.Http +import org.apache.doris.regression.util.NodeType import org.codehaus.groovy.runtime.IOGroovyMethods Suite.metaClass.curl = { String method, String url /* param */-> @@ -76,3 +78,46 @@ Suite.metaClass.update_be_config = { String ip, String port, String key, String } logger.info("Added 'update_be_config' function to Suite") + +Suite.metaClass.update_all_be_config = { String key, Object value -> + def backendId_to_backendIP = [:] + def backendId_to_backendHttpPort = [:] + getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort); + backendId_to_backendIP.each { beId, beIp -> + def port = backendId_to_backendHttpPort.get(beId) + def url = "http://${beIp}:${port}/api/update_config?${key}=${value}" + def result = Http.POST(url, null, true) + assert result.size() == 1, result.toString() + assert result[0].status == "OK", result.toString() + } +} + +logger.info("Added 'update_all_be_config' function to Suite") + + +Suite.metaClass._be_report = { String ip, int port, String reportName -> + def url = "http://${ip}:${port}/api/report/${reportName}" + def result = Http.GET(url, true) + Http.checkHttpResult(result, NodeType.BE) +} + +// before report, be need random sleep 5s +Suite.metaClass.be_report_disk = { String ip, int port -> + _be_report(ip, port, "disk") +} + +logger.info("Added 'be_report_disk' function to Suite") + +// before report, be need random sleep 5s +Suite.metaClass.be_report_tablet = { String ip, int port -> + _be_report(ip, port, "tablet") +} + +logger.info("Added 'be_report_tablet' function to Suite") + +// before report, be need random sleep 5s +Suite.metaClass.be_report_task = { String ip, int port -> + _be_report(ip, port, "task") +} + +logger.info("Added 'be_report_task' function to Suite") diff --git a/regression-test/suites/insert_p0/test_be_inject_publish_txn_fail.groovy b/regression-test/suites/insert_p0/test_be_inject_publish_txn_fail.groovy new file mode 100644 index 00000000000..bd7fe9d8a2f --- /dev/null +++ b/regression-test/suites/insert_p0/test_be_inject_publish_txn_fail.groovy @@ -0,0 +1,77 @@ +// 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_be_inject_publish_txn_fail', 'nonConcurrent') { + def tbl = 'test_be_inject_publish_txn_fail_tbl' + def dbug1 = 'TxnManager.publish_txn.random_failed_before_save_rs_meta' + def dbug2 = 'TxnManager.publish_txn.random_failed_after_save_rs_meta' + + def allBeReportTask = { -> + def backendId_to_backendIP = [:] + def backendId_to_backendHttpPort = [:] + getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort) + backendId_to_backendIP.each { beId, beIp -> + def port = backendId_to_backendHttpPort.get(beId) as int + be_report_task(beIp, port) + } + } + + def testInsertValue = { dbug, value -> + // insert succ but not visible + GetDebugPoint().enableDebugPointForAllBEs(dbug, [percent : 1.0]) + sql "INSERT INTO ${tbl} VALUES (${value})" + sleep(6000) + order_qt_select_1 "SELECT * FROM ${tbl}" + + GetDebugPoint().disableDebugPointForAllBEs(dbug) + + // be report publish fail to fe, then fe will not remove its task. + // after be report its tasks, fe will resend publish version task to be. + // the txn will visible + allBeReportTask() + sleep(8000) + order_qt_select_2 "SELECT * FROM ${tbl}" + } + + try { + sql "DROP TABLE IF EXISTS ${tbl} FORCE" + sql "CREATE TABLE ${tbl} (k INT) DISTRIBUTED BY HASH(k) BUCKETS 5 PROPERTIES ('replication_num' = '1')" + + sql "ADMIN SET FRONTEND CONFIG ('agent_task_resend_wait_time_ms' = '1000')" + sql 'SET insert_visible_timeout_ms = 2000' + + testInsertValue dbug1, 100 + testInsertValue dbug2, 200 + } finally { + try { + sql "ADMIN SET FRONTEND CONFIG ('agent_task_resend_wait_time_ms' = '5000')" + } catch (Throwable e) { + } + + try { + GetDebugPoint().disableDebugPointForAllBEs(dbug1) + } catch (Throwable e) { + } + + try { + GetDebugPoint().disableDebugPointForAllBEs(dbug2) + } catch (Throwable e) { + } + + sql "DROP TABLE IF EXISTS ${tbl} FORCE" + } +} diff --git a/regression-test/suites/plugin_p0/test_plugin_curl_requester.groovy b/regression-test/suites/plugin_p0/test_plugin_curl_requester.groovy new file mode 100644 index 00000000000..31d5bc36671 --- /dev/null +++ b/regression-test/suites/plugin_p0/test_plugin_curl_requester.groovy @@ -0,0 +1,30 @@ +// 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_plugin_curl_requester') { + def backendId_to_backendIP = [:] + def backendId_to_backendHttpPort = [:] + getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort) + + backendId_to_backendIP.each { beId, beIp -> + def port = backendId_to_backendHttpPort.get(beId) as int + be_report_disk(beIp, port) + be_report_task(beIp, port) + be_report_tablet(beIp, port) + } +} + --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org For additional commands, e-mail: commits-h...@doris.apache.org