This is an automated email from the ASF dual-hosted git repository. zhangstar333 pushed a commit to branch doris_master_new in repository https://gitbox.apache.org/repos/asf/doris.git
commit 05f2248c44621e04d179f4ddb3c8a1f94de6ada4 Author: zhangstar333 <[email protected]> AuthorDate: Wed May 27 23:57:14 2026 +0800 add session enableGracefulShutdown avoid cancel_worker --- be/src/agent/heartbeat_server.cpp | 17 +++++ be/src/common/config.cpp | 3 - be/src/common/config.h | 9 --- be/src/runtime/exec_env.h | 7 +++ be/src/runtime/fragment_mgr.cpp | 17 ++++- be/src/util/brpc_client_cache.h | 17 +++-- be/src/util/thrift_rpc_helper.cpp | 45 ++++++------- .../main/java/org/apache/doris/qe/Coordinator.java | 26 ++++++++ .../java/org/apache/doris/qe/SessionVariable.java | 18 ++++++ .../org/apache/doris/rpc/BackendServiceProxy.java | 73 +++++++++++++++++++++- .../java/org/apache/doris/system/HeartbeatMgr.java | 10 +++ gensrc/thrift/HeartbeatService.thrift | 7 +++ 12 files changed, 201 insertions(+), 48 deletions(-) diff --git a/be/src/agent/heartbeat_server.cpp b/be/src/agent/heartbeat_server.cpp index 2253e096b17..390f46f0f8d 100644 --- a/be/src/agent/heartbeat_server.cpp +++ b/be/src/agent/heartbeat_server.cpp @@ -251,6 +251,23 @@ Status HeartbeatServer::_heartbeat(const TMasterInfo& master_info) { master_info.network_address.hostname, master_info.network_address.port); } + // Propagate the cluster-level "graceful shutdown" flag pushed from FE master + // (set via SET GLOBAL enable_graceful_shutdown = true). When true, BE + // FragmentMgr::cancel_worker will skip cancelling queries for reasons related + // to coord FE process_uuid change / missing, so rolling restart does not kill + // in-flight stream load / select. + { + const bool new_flag = + master_info.__isset.in_graceful_shutdown ? master_info.in_graceful_shutdown : false; + if (new_flag != doris::k_in_graceful_shutdown) { + LOG(INFO) << "cluster in_graceful_shutdown flag changed: " + << doris::k_in_graceful_shutdown << " -> " << new_flag + << " (master=" << master_info.network_address.hostname << ":" + << master_info.network_address.port << ")"; + doris::k_in_graceful_shutdown = new_flag; + } + } + if (master_info.__isset.meta_service_endpoint != config::is_cloud_mode()) { LOG(WARNING) << "Detected mismatch in cloud mode configuration between FE and BE. " << "FE cloud mode: " diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp index 5c193ec603c..b2d51ecbb6d 100644 --- a/be/src/common/config.cpp +++ b/be/src/common/config.cpp @@ -885,9 +885,6 @@ DEFINE_Int64(brpc_max_body_size, "3147483648"); DEFINE_Int64(brpc_socket_max_unwritten_bytes, "-1"); DEFINE_mBool(brpc_usercode_in_pthread, "false"); -DEFINE_mBool(enable_brpc_get_client_handshake, "true"); // test it -DEFINE_mInt32(brpc_get_client_handshake_max_retries, "3"); - // TODO(zxy): expect to be true in v1.3 // Whether to embed the ProtoBuf Request serialized string together with Tuple/Block data into // Controller Attachment and send it through http brpc when the length of the Tuple/Block data diff --git a/be/src/common/config.h b/be/src/common/config.h index 670f0284a83..ad8b0e4f1fb 100644 --- a/be/src/common/config.h +++ b/be/src/common/config.h @@ -944,15 +944,6 @@ DECLARE_Int64(brpc_socket_max_unwritten_bytes); // Whether to set FLAGS_usercode_in_pthread to true in brpc DECLARE_mBool(brpc_usercode_in_pthread); -// Whether BrpcClientCache::get_client() should handshake-validate a freshly -// rebuilt stub before caching/returning it, and retry on failure. Costs 1 RTT -// per cache-miss / rebuild (the warm-cache fast path is untouched). Default -// false to preserve current behavior; flip on (or wire a session variable into -// get_client) to harden rolling-restart scenarios. -DECLARE_mBool(enable_brpc_get_client_handshake); -// Max attempts in get_client when handshake validation is enabled. Each -// failed attempt evicts the stub and marks DNSCache dirty for re-resolution. -DECLARE_mInt32(brpc_get_client_handshake_max_retries); // TODO(zxy): expect to be true in v1.3 // Whether to embed the ProtoBuf Request serialized string together with Tuple/Block data into // Controller Attachment and send it through http brpc when the length of the Tuple/Block data diff --git a/be/src/runtime/exec_env.h b/be/src/runtime/exec_env.h index dfbd1eee0dd..8a67fccd42d 100644 --- a/be/src/runtime/exec_env.h +++ b/be/src/runtime/exec_env.h @@ -143,6 +143,13 @@ inline bool k_doris_exit = false; // set to true once FE Master has pulled a heartbeat carrying is_shutdown=true. // Used by Phase A of graceful shutdown to know FE has been informed. inline bool k_shutdown_fe_known = false; +// Cluster-level "graceful shutdown" flag pushed by FE master via +// TMasterInfo.in_graceful_shutdown (operator does `SET GLOBAL +// enable_graceful_shutdown=true` before rolling restart). When true, +// FragmentMgr::cancel_worker will skip cancelling queries for reasons related to +// coord FE process_uuid change / coord-missing. Timeout / pipeline_task_leak +// cancels are NOT affected. Updated in HeartbeatServer::_heartbeat. +inline bool k_in_graceful_shutdown = false; // set to true after BE start ready inline bool k_is_server_ready = false; diff --git a/be/src/runtime/fragment_mgr.cpp b/be/src/runtime/fragment_mgr.cpp index e56fe738fa0..8b915da8cc7 100644 --- a/be/src/runtime/fragment_mgr.cpp +++ b/be/src/runtime/fragment_mgr.cpp @@ -907,16 +907,31 @@ void FragmentMgr::_collect_invalid_queries( auto itr = running_fes.find(q_ctx->coord_addr); if (itr != running_fes.end()) { - if (fe_process_uuid >= itr->second.info.process_uuid || + if (fe_process_uuid == itr->second.info.process_uuid || itr->second.info.process_uuid == 0) { continue; } else { + if (doris::k_in_graceful_shutdown) { + LOG_EVERY_N(INFO, 50) + << "Skip cancel coord-restarted query " + << print_id(q_ctx->query_id()) + << " because cluster is in graceful shutdown. coord=" + << q_ctx->coord_addr.hostname << ":" << q_ctx->coord_addr.port; + continue; + } LOG_WARNING( "Coordinator of query {} restarted, going to cancel " "it.", print_id(q_ctx->query_id())); } } else { + if (doris::k_in_graceful_shutdown) { + LOG_EVERY_N(INFO, 50) + << "Skip cancel coord-missing query " << print_id(q_ctx->query_id()) + << " because cluster is in graceful shutdown. coord=" + << q_ctx->coord_addr.hostname << ":" << q_ctx->coord_addr.port; + continue; + } // In some rear cases, the rpc port of follower is not updated in time, // then the port of this follower will be zero, but acutally it is still running, // and be has already received the query from follower. diff --git a/be/src/util/brpc_client_cache.h b/be/src/util/brpc_client_cache.h index b4ebb10acea..9daffd7d69e 100644 --- a/be/src/util/brpc_client_cache.h +++ b/be/src/util/brpc_client_cache.h @@ -217,21 +217,18 @@ public: } std::shared_ptr<T> get_client(const std::string& host, int port) { - // Default: read the global config switch. Callers with session context - // (e.g. a RuntimeState query option) should call the 3-arg overload - // below and pass an explicit bool — that's the integration point for a - // future session-level "enable handshake on get_client" variable. - return get_client(host, port, config::enable_brpc_get_client_handshake); + // The handshake-and-retry path is only enabled while the cluster-level + // graceful shutdown flag is on (operator did + // `SET GLOBAL enable_graceful_shutdown=true` before rolling restart). + // Outside of rolling restart, get_client stays one-shot. + return get_client(host, port, doris::k_in_graceful_shutdown); } // Explicit-override entry point. Pass `enable_handshake=true` to force the // handshake-and-retry path (paid only on cache miss / rebuild — warm cache - // hits stay free). Pass `false` to disable. Max attempts is governed by - // config::brpc_get_client_handshake_max_retries (default 3). + // hits stay free). Pass `false` to disable. Max attempts is 3. std::shared_ptr<T> get_client(const std::string& host, int port, bool enable_handshake) { - const int max_attempts = enable_handshake - ? std::max(1, config::brpc_get_client_handshake_max_retries) - : 1; + const int max_attempts = enable_handshake ? 3 : 1; std::string host_port; for (int attempt = 1; attempt <= max_attempts; ++attempt) { std::string realhost = host; diff --git a/be/src/util/thrift_rpc_helper.cpp b/be/src/util/thrift_rpc_helper.cpp index f7881ca755c..63b3cd54125 100644 --- a/be/src/util/thrift_rpc_helper.cpp +++ b/be/src/util/thrift_rpc_helper.cpp @@ -48,6 +48,8 @@ class TTransport; namespace doris { +using namespace std::chrono_literals; + using apache::thrift::protocol::TProtocol; using apache::thrift::protocol::TBinaryProtocol; using apache::thrift::transport::TSocket; @@ -225,6 +227,17 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( // rpc<FrontendServiceClient>. Status st = rpc<FrontendServiceClient>(address_provider, callback, timeout_ms); + // Outside of a graceful rolling restart (operator-controlled + // `SET GLOBAL enable_graceful_shutdown=true`), keep strictly one-shot + // behavior: return the first-attempt result as-is, without probing + // followers or refreshing the cached master. The caller will see the + // raw TStatus (including NOT_MASTER) and decide what to do. This is + // equivalent to the legacy behavior before rpc_fe_with_master_refresh + // was introduced. + if (!doris::k_in_graceful_shutdown) { + return st; + } + bool transport_failed = !st.ok(); bool not_master = false; TNetworkAddress new_master; @@ -259,12 +272,6 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( // Scenario A: transport failure; or NOT_MASTER without master_address // (old FE without thrift change). // - // Replace the previous "sleep & re-read heartbeat" fallback with a - // bounded follower-probe loop: enumerate live FEs from - // get_running_frontends() (heartbeat-fresh, not just configured), skip - // the failed master, and try up to kMaxProbes candidates within a - // small time budget. - // // * candidate RPC returns ok -> candidate IS current master, // cache it and return OK // (the response was filled by @@ -274,13 +281,7 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( // retry against new master. // * candidate transport-failed -> try the next one. // - // We do NOT sleep, do NOT scan the whole FE list (max 2 probes), and - // give up to the caller if no candidate yields recovery within the - // budget. - constexpr int kMaxProbes = 2; - const auto deadline = std::chrono::steady_clock::now() - + std::chrono::milliseconds(config::thrift_client_retry_interval_ms * 4); - + constexpr int kMaxProbes = 3; auto running = _s_exec_env->get_running_frontends(); int probed = 0; bool master_refreshed = false; @@ -289,9 +290,6 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( if (probed >= kMaxProbes) { break; } - if (std::chrono::steady_clock::now() >= deadline) { - break; - } const TNetworkAddress& candidate_addr = kv.first; if (candidate_addr.hostname == old_master.hostname && candidate_addr.port == old_master.port) { @@ -302,12 +300,6 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( } ++probed; - // Each probe goes through the existing rpc<T>() helper. Its - // built-in one-shot transport retry is acceptable because it - // only re-reads address_provider() which here returns the - // constant candidate address; the worst case is one extra - // sleep of thrift_client_retry_interval_ms, still bounded by - // our deadline. auto candidate_provider = [candidate_addr]() { return candidate_addr; }; Status probe_st = rpc<FrontendServiceClient>( candidate_provider, callback, timeout_ms); @@ -315,6 +307,9 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( LOG(INFO) << "fe RPC probe to " << candidate_addr << " transport-failed after master " << old_master << " was unreachable: " << probe_st; + if (probed < kMaxProbes) { + std::this_thread::sleep_for(1000ms); // Back off a bit before the next probe. + } continue; } @@ -329,6 +324,9 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( << " returned NOT_MASTER with stale master_address " << *hinted << ", same as failed master " << old_master << "; trying next"; + if (probed < kMaxProbes) { + std::this_thread::sleep_for(1000ms); // Back off a bit before the next probe. + } continue; } _s_exec_env->cluster_info()->master_fe_addr = *hinted; @@ -341,6 +339,9 @@ Status ThriftRpcHelper::rpc_fe_with_master_refresh( // NOT_MASTER without master_address (old FE). Try next candidate. LOG(INFO) << "fe RPC probe to " << candidate_addr << " returned NOT_MASTER without master_address, trying next"; + if (probed < kMaxProbes) { + std::this_thread::sleep_for(2000ms); // Back off a bit before the next probe. + } continue; } diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java b/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java index 74e0128e8da..ab8a318094b 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java @@ -1347,17 +1347,37 @@ public class Coordinator implements CoordInterface { // We use a very conservative cancel strategy. // 0. If backends has zero process epoch, do not cancel. Zero process epoch usually arises in cluster upgrading. // 1. If process epoch is same, do not cancel. Means backends does not restart or die. + // 2. If process epoch is different, but cluster-level "graceful shutdown" flag is set, do not cancel. + // Means backends may restart, but we want to defer cancel until + // the backend actually restarts and becomes unavailable. public Status shouldCancel(List<Backend> currentBackends) { Map<Long, Backend> curBeMap = Maps.newHashMap(); for (Backend be : currentBackends) { curBeMap.put(be.getId(), be); } + // Cluster-level "graceful shutdown" flag set via `SET GLOBAL enable_graceful_shutdown`. + // When true, fully skip cancel for BE-death / epoch-change reasons cluster-wide + // so a rolling restart does not kill in-flight queries / stream loads. + // Timeout / explicit KILL paths are NOT affected by this flag. + boolean clusterGraceful = false; + try { + clusterGraceful = VariableMgr.getDefaultSessionVariable().enableGracefulShutdown; + } catch (Throwable t) { + // Defensive: ignore. + } + try { lock(); for (PipelineExecContext pipelineExecContext : pipelineExecContexts.values()) { Backend be = curBeMap.get(pipelineExecContext.backend.getId()); if (be == null || !be.isAlive()) { + if (clusterGraceful) { + LOG.info("enable_graceful_shutdown=true, defer cancel for query {} " + + "(BE {} not alive)", DebugUtil.printId(queryId), + pipelineExecContext.backend.toString()); + continue; + } Status errorStatus = new Status(TStatusCode.CANCELLED, "Backend {} not exists or dead, query {} should be cancelled", pipelineExecContext.backend.toString(), DebugUtil.printId(queryId)); @@ -1369,6 +1389,12 @@ public class Coordinator implements CoordInterface { // Check zero since during upgrading, older version oplog will not persistent be start time // so newer version follower will get zero epoch when replaying oplog or snapshot if (pipelineExecContext.beProcessEpoch != be.getProcessEpoch() && be.getProcessEpoch() != 0) { + if (clusterGraceful) { + LOG.info("enable_graceful_shutdown=true, defer cancel for query {} " + + "(BE {} epoch {} -> {})", DebugUtil.printId(queryId), + be.toString(), pipelineExecContext.beProcessEpoch, be.getProcessEpoch()); + continue; + } Status errorStatus = new Status(TStatusCode.CANCELLED, "Backend process epoch changed, previous {} now {}, " + "means this be has already restarted, should cancel this coordinator," diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java index aa70a4f6ea6..1dcb8562dc3 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java @@ -188,6 +188,7 @@ public class SessionVariable implements Serializable, Writable { public static final String ENABLE_SERVER_SIDE_PREPARED_STATEMENT = "enable_server_side_prepared_statement"; public static final String MAX_PREPARED_STMT_COUNT = "max_prepared_stmt_count"; + public static final String ENABLE_GRACEFUL_SHUTDOWN = "enable_graceful_shutdown"; public static final String ENABLE_GROUP_COMMIT_FULL_PREPARE = "enable_group_commit_full_prepare"; public static final String PREFER_JOIN_METHOD = "prefer_join_method"; @@ -2312,6 +2313,23 @@ public class SessionVariable implements Serializable, Writable { "服务端 prepared statement 最大个数", "the maximum prepared statements server holds."}) public int maxPreparedStmtCount = 100000; + @VarAttrDef.VarAttr(name = ENABLE_GRACEFUL_SHUTDOWN, flag = VarAttrDef.GLOBAL, needForward = true, + description = { + "集群级别开关:当 FE 或 BE 准备做滚动重启 / 优雅退出时,由运维 SET GLOBAL 打开。" + + "打开后:(1) FE QueryCancelWorker / Coordinator.shouldCancel 不再因 BE alive=false " + + "或 process epoch 变化 cancel in-flight query;(2) FE master 通过心跳把该标记下发" + + "给所有 BE,BE 端 FragmentMgr::cancel_worker 也跳过 'Coordinator restarted' / " + + "'Source frontend is not running' cancel。timeout / 用户主动 KILL / pipeline task " + + "leak 等其他 cancel 不受影响。滚动结束后必须 SET GLOBAL ... = false 复位。", + "Cluster-level switch operators flip on via SET GLOBAL before performing a rolling restart " + + "or graceful shutdown. When true: (1) FE QueryCancelWorker / Coordinator.shouldCancel " + + "skip cancelling in-flight queries on BEs whose alive=false or process_epoch changed; " + + "(2) FE master propagates the flag to all BEs via heartbeat so that BE " + + "FragmentMgr::cancel_worker also skips 'Coordinator restarted' / 'Source frontend is " + + "not running' cancel. Timeout / explicit KILL / pipeline-task-leak cancels are not " + + "affected. Operators MUST flip it back to false after rolling restart finishes."}) + public boolean enableGracefulShutdown = false; + @VarAttrDef.VarAttr(name = ENABLE_GROUP_COMMIT_FULL_PREPARE) public boolean enableGroupCommitFullPrepare = true; diff --git a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java index 4fa6f346f10..48fae0674b6 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java +++ b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java @@ -51,7 +51,10 @@ import java.net.UnknownHostException; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; @@ -65,6 +68,21 @@ public class BackendServiceProxy { Config.grpc_threadmgr_threads_nums, "grpc_thread_pool", true); + // Single-threaded scheduler used to *deferred-shutdown* gRPC channels that were + // invalidated by removeProxy / removeProxyForAll on BE restart. We MUST NOT call + // BackendServiceClient.shutdown() inline because it triggers channel.shutdownNow() + // (after a 5s grace) and instantly kills any in-flight RPC on the old channel with + // "UNAVAILABLE: Channel shutdownNow invoked". Instead we drop the client from the + // serviceMap (so new RPCs go to a freshly-created channel pointing at the new BE IP) + // and asynchronously close the old channel after a grace period long enough for + // in-flight RPCs to finish or fail naturally. + private static final ScheduledExecutorService deferredShutdownExecutor = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "grpc-deferred-shutdown"); + t.setDaemon(true); + return t; + }); + private final Map<TNetworkAddress, BackendServiceClientExtIp> serviceMap; public BackendServiceProxy() { @@ -115,7 +133,7 @@ public class BackendServiceProxy { if (serviceClientExtIp != null) { LOG.info("remove gRPC proxy on shard, address={}, cachedRealIp={}, channelState={}", address, serviceClientExtIp.realIp, serviceClientExtIp.client.getConnectivityState()); - serviceClientExtIp.client.shutdown(); + scheduleDeferredShutdown(address, serviceClientExtIp.client, "removeProxy"); } else { LOG.info("remove gRPC proxy on shard, address={}, no cached channel", address); } @@ -140,7 +158,7 @@ public class BackendServiceProxy { LOG.info("removeProxyForAll shard={}, address={}, cachedRealIp={}, channelState={}", i, address, serviceClientExtIp.realIp, serviceClientExtIp.client.getConnectivityState()); - serviceClientExtIp.client.shutdown(); + scheduleDeferredShutdown(address, serviceClientExtIp.client, "removeProxyForAll"); removed++; } } @@ -148,6 +166,52 @@ public class BackendServiceProxy { address, removed, shardCount); } + // Schedule a deferred shutdown of the given gRPC client. The client has already been + // removed from serviceMap, so new RPCs will create a fresh client (pointing at the + // possibly-new BE IP). The old channel is kept alive for a grace window so that + // in-flight RPCs can either complete naturally or fail with the real underlying + // brpc error (e.g. "Network closed" when the BE socket RSTs) instead of being killed + // by a synchronous channel.shutdownNow(). + private static void scheduleDeferredShutdown(TNetworkAddress address, + BackendServiceClient client, String cause) { + // 120 seconds, should be long enough for in-flight RPCs to finish or fail naturally. + long delayMs = 120 * 1000L; + if (delayMs <= 0) { + // Disabled: fall back to legacy synchronous shutdown. + LOG.info("deferred shutdown disabled (delayMs={}), shutdown inline. address={}, cause={}", + delayMs, address, cause); + try { + client.shutdown(); + } catch (Throwable t) { + LOG.warn("inline client shutdown failed, address={}, err={}", address, t.getMessage(), t); + } + return; + } + LOG.info("schedule deferred gRPC channel shutdown, address={}, delayMs={}, cause={}, " + + "channelState={}", address, delayMs, cause, client.getConnectivityState()); + try { + deferredShutdownExecutor.schedule(() -> { + try { + LOG.info("deferred gRPC channel shutdown firing, address={}, cause={}, " + + "channelState={}", address, cause, client.getConnectivityState()); + client.shutdown(); + } catch (Throwable t) { + LOG.warn("deferred client shutdown failed, address={}, err={}", + address, t.getMessage(), t); + } + }, delayMs, TimeUnit.MILLISECONDS); + } catch (Throwable t) { + LOG.warn("failed to schedule deferred shutdown, fallback to inline. address={}, err={}", + address, t.getMessage(), t); + try { + client.shutdown(); + } catch (Throwable inner) { + LOG.warn("inline fallback shutdown failed, address={}, err={}", + address, inner.getMessage(), inner); + } + } + } + private BackendServiceClient getProxy(TNetworkAddress address) throws UnknownHostException { String realIp = Env.getCurrentEnv().getDnsCache().get(address.hostname); @@ -215,7 +279,10 @@ public class BackendServiceProxy { } finally { lock.unlock(); if (removedClient != null) { - removedClient.shutdown(); + // Use deferred shutdown to avoid killing in-flight RPCs on the old channel + // (same rationale as removeProxy/removeProxyForAll). + scheduleDeferredShutdown(address, removedClient, + removedCause == null ? "getProxy-replace" : "getProxy-" + removedCause); } } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/system/HeartbeatMgr.java b/fe/fe-core/src/main/java/org/apache/doris/system/HeartbeatMgr.java index dbd36715d53..b168c111c0c 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/system/HeartbeatMgr.java +++ b/fe/fe-core/src/main/java/org/apache/doris/system/HeartbeatMgr.java @@ -302,6 +302,16 @@ public class HeartbeatMgr extends MasterDaemon { copiedMasterInfo.setBackendId(backendId); copiedMasterInfo.setFrontendInfos(feInfos); copiedMasterInfo.setAuthToken(Env.getCurrentEnv().getTokenManager().acquireToken()); + // Propagate cluster-level "graceful shutdown" flag (SessionVariable set GLOBAL) + // to BE so that BE cancel_worker knows to skip coord-restart cancellation. + try { + copiedMasterInfo.setInGracefulShutdown( + org.apache.doris.qe.VariableMgr.getDefaultSessionVariable() + .enableGracefulShutdown); + } catch (Throwable t) { + // Defensive: VariableMgr might not be initialized in unit tests. + copiedMasterInfo.setInGracefulShutdown(false); + } if (Config.isCloudMode()) { String cloudUniqueId = backend.getTagMap().get(Tag.CLOUD_UNIQUE_ID); copiedMasterInfo.setCloudUniqueId(cloudUniqueId); diff --git a/gensrc/thrift/HeartbeatService.thrift b/gensrc/thrift/HeartbeatService.thrift index 3fde7fdf2df..4ed8ab280da 100644 --- a/gensrc/thrift/HeartbeatService.thrift +++ b/gensrc/thrift/HeartbeatService.thrift @@ -50,6 +50,13 @@ struct TMasterInfo { 12: optional i64 tablet_report_inactive_duration_ms; 13: optional string auth_token; 14: optional TCloudClusterInfo cloud_cluster_info; + // True when the FE master cluster is in "graceful shutdown" mode + // (SessionVariable.enable_graceful_shutdown set to true via SET GLOBAL). + // When true, BE.fragment_mgr cancel_worker MUST skip cancelling queries + // due to "Coordinator restarted" / "Source frontend is not running", + // so that in-flight stream load / select can finish or be retried via + // master-failover instead of being killed within 1s of FE restart. + 15: optional bool in_graceful_shutdown } struct TBackendInfo { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
