| // 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 "udf/python/python_server.h" |
| |
| #include <arrow/type_fwd.h> |
| #include <butil/fd_utility.h> |
| #include <dirent.h> |
| #include <fmt/core.h> |
| #include <signal.h> |
| #include <sys/poll.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <boost/asio.hpp> |
| #include <boost/process.hpp> |
| #include <chrono> |
| #include <fstream> |
| #include <thread> |
| |
| #include "arrow/flight/client.h" |
| #include "common/config.h" |
| #include "common/status.h" |
| #include "runtime/thread_context.h" |
| #include "udf/python/python_udaf_client.h" |
| #include "udf/python/python_udf_client.h" |
| #include "udf/python/python_udtf_client.h" |
| #include "util/cpu_info.h" |
| |
| namespace doris { |
| |
| Result<std::shared_ptr<PythonServerManager::VersionedProcessPool>> |
| PythonServerManager::_get_or_create_process_pool(const PythonVersion& version) { |
| std::lock_guard<std::mutex> lock(_pools_mutex); |
| // shutdown() owns the manager lifecycle. Once it starts, creating a new pool would let detached |
| // init workers publish Python processes that the manager no longer tracks. |
| if (_shutdown_flag.load(std::memory_order_acquire)) { |
| return ResultError(Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python server manager is shutting down")); |
| } |
| auto& pool = _process_pools[version]; |
| if (!pool) { |
| pool = std::make_shared<VersionedProcessPool>(); |
| } |
| return pool; |
| } |
| |
| std::vector<std::pair<PythonVersion, std::shared_ptr<PythonServerManager::VersionedProcessPool>>> |
| PythonServerManager::_snapshot_process_pools() { |
| std::lock_guard<std::mutex> lock(_pools_mutex); |
| std::vector<std::pair<PythonVersion, std::shared_ptr<VersionedProcessPool>>> snapshot; |
| snapshot.reserve(_process_pools.size()); |
| for (const auto& [version, pool] : _process_pools) { |
| snapshot.emplace_back(version, pool); |
| } |
| return snapshot; |
| } |
| |
| #ifdef BE_TEST |
| void PythonServerManager::set_process_pool_for_test(const PythonVersion& version, |
| std::vector<ProcessPtr> processes, |
| bool initialized) { |
| auto versioned_pool = _get_or_create_process_pool(version).value(); |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| versioned_pool->processes = std::move(processes); |
| versioned_pool->state = initialized ? PoolState::INITIALIZED : PoolState::UNINITIALIZED; |
| versioned_pool->has_available_process = |
| std::any_of(versioned_pool->processes.begin(), versioned_pool->processes.end(), |
| [](const ProcessPtr& process) { return process && process->is_alive(); }); |
| } |
| |
| std::vector<ProcessPtr> PythonServerManager::process_pool_snapshot_for_test( |
| const PythonVersion& version) { |
| auto versioned_pool = _get_or_create_process_pool(version).value(); |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| return versioned_pool->processes; |
| } |
| |
| bool PythonServerManager::process_pool_is_initializing_for_test(const PythonVersion& version) { |
| auto versioned_pool = _get_or_create_process_pool(version).value(); |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| return versioned_pool->state == PoolState::INITIALIZING; |
| } |
| |
| bool PythonServerManager::process_pool_is_initialized_for_test(const PythonVersion& version) { |
| auto versioned_pool = _get_or_create_process_pool(version).value(); |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| return versioned_pool->state == PoolState::INITIALIZED; |
| } |
| #endif |
| |
| bool PythonServerManager::_select_alive_process_from_pool(const std::vector<ProcessPtr>& pool, |
| ProcessPtr* process) { |
| auto alive_iter = std::min_element(pool.begin(), pool.end(), |
| [](const ProcessPtr& a, const ProcessPtr& b) { |
| const bool a_alive = a && a->is_alive(); |
| const bool b_alive = b && b->is_alive(); |
| if (a_alive != b_alive) { |
| return a_alive > b_alive; |
| } |
| return a.use_count() < b.use_count(); |
| }); |
| if (alive_iter == pool.end() || !*alive_iter || !(*alive_iter)->is_alive()) { |
| return false; |
| } |
| *process = *alive_iter; |
| return true; |
| } |
| |
| template <typename ClientType> |
| Status PythonServerManager::get_client(const PythonUDFMeta& func_meta, const PythonVersion& version, |
| std::shared_ptr<ClientType>* client, |
| const std::shared_ptr<arrow::Schema>& data_schema) { |
| std::shared_ptr<VersionedProcessPool> versioned_pool = |
| DORIS_TRY(_ensure_pool_initialized(version)); |
| |
| ProcessPtr process; |
| RETURN_IF_ERROR(_get_process(version, versioned_pool, &process)); |
| |
| if constexpr (std::is_same_v<ClientType, PythonUDAFClient>) { |
| RETURN_IF_ERROR(ClientType::create(func_meta, std::move(process), data_schema, client)); |
| } else { |
| RETURN_IF_ERROR(ClientType::create(func_meta, std::move(process), client)); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Result<std::shared_ptr<PythonServerManager::VersionedProcessPool>> |
| PythonServerManager::_ensure_pool_initialized(const PythonVersion& version) { |
| auto versioned_pool_result = _get_or_create_process_pool(version); |
| if (!versioned_pool_result.has_value()) { |
| return ResultError(versioned_pool_result.error()); |
| } |
| auto versioned_pool = versioned_pool_result.value(); |
| const int max_pool_size = config::max_python_process_num > 0 ? config::max_python_process_num |
| : CpuInfo::num_cores(); |
| |
| std::unique_lock<std::mutex> lock(versioned_pool->mutex); |
| if (versioned_pool->state == PoolState::INITIALIZED) { |
| return versioned_pool; |
| } |
| |
| if (versioned_pool->state != PoolState::STOPPED) { |
| if (versioned_pool->state != PoolState::INITIALIZING) { |
| versioned_pool->state = PoolState::INITIALIZING; |
| versioned_pool->has_available_process = false; |
| versioned_pool->processes.resize(max_pool_size); |
| auto init_finished_count = std::make_shared<std::atomic<int>>(0); |
| |
| LOG(INFO) << "Initializing Python process pool for version " << version.to_string() |
| << " with " << max_pool_size << " processes (config::max_python_process_num=" |
| << config::max_python_process_num << ", CPU cores=" << CpuInfo::num_cores() |
| << ")"; |
| |
| std::thread([this, versioned_pool, init_finished_count, max_pool_size]() { |
| SCOPED_INIT_THREAD_CONTEXT(); |
| std::unique_lock<std::mutex> lock(versioned_pool->mutex); |
| versioned_pool->cv.wait_for( |
| lock, PROCESS_POOL_INIT_TIMEOUT, |
| [&versioned_pool, init_finished_count, max_pool_size]() { |
| return versioned_pool->state != PoolState::INITIALIZING || |
| init_finished_count->load(std::memory_order_acquire) >= |
| max_pool_size; |
| }); |
| if (versioned_pool->state == PoolState::INITIALIZING) { |
| if (versioned_pool->has_available_process) { |
| // Keep this under the pool lock. shutdown() must acquire the same lock |
| // before it can destroy manager-owned health-check state. |
| _start_health_check_thread(); |
| versioned_pool->state = PoolState::INITIALIZED; |
| } else { |
| versioned_pool->state = PoolState::UNINITIALIZED; |
| } |
| } |
| versioned_pool->cv.notify_all(); |
| }).detach(); |
| |
| for (int i = 0; i < max_pool_size; ++i) { |
| std::thread([version, versioned_pool, i, max_pool_size, init_finished_count]() { |
| SCOPED_INIT_THREAD_CONTEXT(); |
| ProcessPtr process; |
| Status status = PythonServerManager::fork(version, &process); |
| const bool ok = status.ok() && process; |
| ProcessPtr process_to_shutdown; |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| // shutdown() and repair can race with detached init workers after timeout. |
| // Late successful forks only fill slots that are still empty or dead. |
| if (ok && |
| (versioned_pool->state == PoolState::INITIALIZING || |
| versioned_pool->state == PoolState::INITIALIZED) && |
| i < versioned_pool->processes.size() && |
| (!versioned_pool->processes[i] || |
| !versioned_pool->processes[i]->is_alive())) { |
| versioned_pool->processes[i] = std::move(process); |
| versioned_pool->has_available_process = true; |
| } else if (ok) { |
| process_to_shutdown = std::move(process); |
| } else [[unlikely]] { |
| LOG(WARNING) << "Failed to create Python process " << (i + 1) << "/" |
| << max_pool_size << " for version " << version.to_string() |
| << ": " << status.to_string(); |
| } |
| } |
| init_finished_count->fetch_add(1, std::memory_order_acq_rel); |
| versioned_pool->cv.notify_all(); |
| if (process_to_shutdown) { |
| process_to_shutdown->shutdown(); |
| } |
| }).detach(); |
| } |
| } |
| |
| // Wait only for the first usable process. INITIALIZED is set later by the last init worker |
| // after every slot has attempted initialization. |
| versioned_pool->cv.wait_for(lock, PROCESS_POOL_INIT_TIMEOUT, [&versioned_pool]() { |
| return versioned_pool->has_available_process || |
| versioned_pool->state == PoolState::STOPPED || |
| versioned_pool->state != PoolState::INITIALIZING; |
| }); |
| if (versioned_pool->has_available_process) { |
| return versioned_pool; |
| } |
| versioned_pool->cv.notify_all(); |
| } |
| |
| return ResultError(Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Failed to initialize Python process pool for version {}: no process became available " |
| "within {} ms", |
| version.to_string(), PROCESS_POOL_INIT_TIMEOUT.count())); |
| } |
| |
| Status PythonServerManager::_get_process( |
| const PythonVersion& version, const std::shared_ptr<VersionedProcessPool>& versioned_pool, |
| ProcessPtr* process) { |
| { |
| std::unique_lock<std::mutex> lock(versioned_pool->mutex); |
| std::vector<ProcessPtr>& pool = versioned_pool->processes; |
| |
| if (versioned_pool->state == PoolState::STOPPED) { |
| versioned_pool->has_available_process = false; |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool has stopped for version {}", version.to_string()); |
| } |
| |
| if (_select_alive_process_from_pool(pool, process)) [[likely]] { |
| versioned_pool->has_available_process = true; |
| return Status::OK(); |
| } |
| versioned_pool->has_available_process = false; |
| |
| if (versioned_pool->state == PoolState::INITIALIZING) { |
| versioned_pool->cv.wait_for(lock, PROCESS_REPAIR_WAIT_TIMEOUT, [&versioned_pool]() { |
| return std::any_of(versioned_pool->processes.begin(), |
| versioned_pool->processes.end(), |
| [](const ProcessPtr& p) { return p && p->is_alive(); }) || |
| versioned_pool->state != PoolState::INITIALIZING; |
| }); |
| if (versioned_pool->state == PoolState::STOPPED) { |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool has stopped for version {}", version.to_string()); |
| } |
| if (_select_alive_process_from_pool(pool, process)) { |
| versioned_pool->has_available_process = true; |
| return Status::OK(); |
| } |
| versioned_pool->has_available_process = false; |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool is initializing but has no available process for version " |
| "{} after waiting {} ms", |
| version.to_string(), PROCESS_REPAIR_WAIT_TIMEOUT.count()); |
| } |
| |
| if (versioned_pool->state != PoolState::INITIALIZED) { |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool is not initialized for version {}", version.to_string()); |
| } |
| |
| if (!versioned_pool->repairing) { |
| versioned_pool->repairing = true; |
| // Repair is done in the background because fork can be slow. The current request still |
| // waits briefly below so a transient all-dead pool can recover without failing. |
| std::thread([version, versioned_pool]() { |
| SCOPED_INIT_THREAD_CONTEXT(); |
| int recreated = PythonServerManager::_repair_process_pool(version, versioned_pool); |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| versioned_pool->repairing = false; |
| } |
| versioned_pool->cv.notify_all(); |
| if (recreated > 0) { |
| LOG(INFO) << "Repaired Python process pool for version " << version.to_string() |
| << ": recreated=" << recreated; |
| } |
| }).detach(); |
| } |
| |
| // Keep the request recoverable in the common case where the Python runtime can fork |
| // normally and only the existing pool entries died. The wait is short so a wedged fork path |
| // still returns SERVICE_UNAVAILABLE promptly. |
| versioned_pool->cv.wait_for(lock, PROCESS_REPAIR_WAIT_TIMEOUT, [&versioned_pool]() { |
| return std::any_of(versioned_pool->processes.begin(), versioned_pool->processes.end(), |
| [](const ProcessPtr& p) { return p && p->is_alive(); }) || |
| versioned_pool->state == PoolState::STOPPED; |
| }); |
| if (versioned_pool->state == PoolState::STOPPED) { |
| versioned_pool->has_available_process = false; |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool has stopped for version {}", version.to_string()); |
| } |
| |
| if (_select_alive_process_from_pool(pool, process)) { |
| versioned_pool->has_available_process = true; |
| return Status::OK(); |
| } |
| versioned_pool->has_available_process = false; |
| } |
| |
| return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>( |
| "Python process pool has no available process for version {} after waiting repair for " |
| "{} ms", |
| version.to_string(), PROCESS_REPAIR_WAIT_TIMEOUT.count()); |
| } |
| |
| Status PythonServerManager::fork(const PythonVersion& version, ProcessPtr* process) { |
| std::string python_executable_path = version.get_executable_path(); |
| std::string fight_server_path = get_fight_server_path(); |
| std::string base_unix_socket_path = get_base_unix_socket_path(); |
| std::vector<std::string> args = {"-u", fight_server_path, base_unix_socket_path}; |
| boost::process::environment env = boost::this_process::environment(); |
| boost::process::ipstream child_output; |
| |
| try { |
| boost::process::child c( |
| python_executable_path, args, boost::process::std_out > child_output, |
| boost::process::env = env, |
| boost::process::on_exit([](int exit_code, const std::error_code& ec) { |
| if (ec) { |
| LOG(WARNING) << "Python UDF server exited with error: " << ec.message(); |
| } |
| })); |
| |
| // Bound socket readiness: a child process can start but never create the Flight socket. |
| // Without this, pool initialization can block until FE reports send-fragments RPC timeout. |
| pid_t child_pid = c.id(); |
| std::string expected_socket_path = get_unix_socket_file_path(child_pid); |
| bool started_successfully = false; |
| std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); |
| |
| while (std::chrono::steady_clock::now() - start < PROCESS_START_TIMEOUT) { |
| struct stat buffer; |
| if (stat(expected_socket_path.c_str(), &buffer) == 0) { |
| started_successfully = true; |
| break; |
| } |
| |
| if (!c.running()) { |
| break; |
| } |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| } |
| |
| if (!started_successfully) { |
| int exit_status = 0; |
| if (c.running()) { |
| // Don't use the `wait` of boost::process, but use the operating system signal and waitpid with timeout instead. |
| // Because boost::process may block the initialization/repair thread for a long time, |
| // exceeding the timeout limit expected by the process pool. |
| ::kill(child_pid, SIGTERM); |
| auto wait_result = PythonUDFProcess::wait_child_exit( |
| child_pid, PROCESS_TERMINATE_TIMEOUT, &exit_status); |
| if (wait_result == PythonUDFProcess::ChildExitWaitResult::TIMEOUT || |
| wait_result == PythonUDFProcess::ChildExitWaitResult::ERROR) { |
| LOG(WARNING) << "Python server start timeout and terminate timeout exceeded," |
| << " sending SIGKILL to pid=" << child_pid; |
| ::kill(child_pid, SIGKILL); |
| wait_result = PythonUDFProcess::wait_child_exit( |
| child_pid, PROCESS_TERMINATE_TIMEOUT, &exit_status); |
| if (wait_result == PythonUDFProcess::ChildExitWaitResult::TIMEOUT || |
| wait_result == PythonUDFProcess::ChildExitWaitResult::ERROR) [[unlikely]] { |
| // The child was SIGKILLed but not reaped within the bounded wait. Do not |
| // drop waitpid ownership after detach; otherwise a later exit can leave a |
| // zombie Python process under BE. |
| PythonUDFProcess::enqueue_child_for_reap(child_pid); |
| c.detach(); |
| return Status::InternalError( |
| "Python server start failed: process did not exit after SIGKILL, " |
| "pid={}", |
| child_pid); |
| } |
| } |
| } else { |
| PythonUDFProcess::wait_child_exit(child_pid, std::chrono::milliseconds(0), |
| &exit_status); |
| } |
| c.detach(); |
| return Status::InternalError("Python server start failed: socket file not found at {}", |
| expected_socket_path); |
| } |
| |
| *process = std::make_shared<PythonUDFProcess>(std::move(c), std::move(child_output)); |
| |
| } catch (const std::exception& e) { |
| return Status::InternalError("Failed to start Python UDF server: {}", e.what()); |
| } |
| |
| return Status::OK(); |
| } |
| |
| void PythonServerManager::_start_health_check_thread() { |
| std::lock_guard<std::mutex> lock(_health_check_mutex); |
| if (_health_check_thread || _shutdown_flag.load(std::memory_order_acquire)) return; |
| |
| LOG(INFO) << "Starting Python process health check thread (interval: 30 seconds)"; |
| |
| _health_check_thread = std::make_unique<std::thread>([this]() { |
| // Health check loop |
| while (!_shutdown_flag.load(std::memory_order_acquire)) { |
| // Wait for interval or shutdown signal |
| { |
| std::unique_lock<std::mutex> lock(_health_check_mutex); |
| _health_check_cv.wait_for(lock, std::chrono::seconds(30), [this]() { |
| return _shutdown_flag.load(std::memory_order_acquire); |
| }); |
| } |
| |
| if (_shutdown_flag.load(std::memory_order_acquire)) break; |
| |
| _check_and_recreate_processes(); |
| _refresh_memory_stats(); |
| } |
| |
| LOG(INFO) << "Python process health check thread exiting"; |
| }); |
| } |
| |
| void PythonServerManager::_check_and_recreate_processes() { |
| int total_recreated = 0; |
| for (auto& [version, versioned_pool] : _snapshot_process_pools()) { |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| if (versioned_pool->state != PoolState::INITIALIZED || versioned_pool->repairing) { |
| continue; |
| } |
| // Share the same repair guard with foreground requests. Otherwise health check and |
| // _get_process() can fork the same empty/dead slots at the same time under failures. |
| versioned_pool->repairing = true; |
| } |
| int recreated = _repair_process_pool(version, versioned_pool); |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| versioned_pool->repairing = false; |
| } |
| versioned_pool->cv.notify_all(); |
| total_recreated += recreated; |
| } |
| |
| if (total_recreated > 0) { |
| LOG(INFO) << "Health check completed: recreated=" << total_recreated; |
| } |
| } |
| |
| int PythonServerManager::_repair_process_pool( |
| const PythonVersion& version, const std::shared_ptr<VersionedProcessPool>& versioned_pool) { |
| const int max_pool_size = config::max_python_process_num > 0 ? config::max_python_process_num |
| : CpuInfo::num_cores(); |
| std::vector<size_t> died_process_indices; |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| if (versioned_pool->state != PoolState::INITIALIZED) { |
| return 0; |
| } |
| |
| auto& pool = versioned_pool->processes; |
| died_process_indices.reserve(std::max<size_t>(pool.size(), max_pool_size)); |
| // Need to fix the following two cases |
| // 1. Existing processes have died |
| // 2. Process pool not filled due to fork timeouts and other issues |
| for (size_t i = 0; i < pool.size(); ++i) { |
| const auto& process = pool[i]; |
| if (!process || !process->is_alive()) { |
| died_process_indices.push_back(i); |
| } |
| } |
| for (size_t i = pool.size(); i < static_cast<size_t>(max_pool_size); ++i) { |
| died_process_indices.push_back(i); |
| } |
| } |
| |
| if (died_process_indices.empty()) [[likely]] { |
| return 0; |
| } |
| |
| int recreated = 0; |
| std::vector<ProcessPtr> processes_to_shutdown; |
| for (size_t index : died_process_indices) { |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| if (versioned_pool->state != PoolState::INITIALIZED) { |
| break; |
| } |
| } |
| |
| ProcessPtr new_process; |
| Status status = fork(version, &new_process); |
| if (status.ok() && new_process) { |
| bool published = false; |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| auto& pool = versioned_pool->processes; |
| if (versioned_pool->state != PoolState::INITIALIZED) { |
| processes_to_shutdown.emplace_back(std::move(new_process)); |
| } else if (index < pool.size()) { |
| if (!pool[index] || !pool[index]->is_alive()) [[likely]] { |
| pool[index] = std::move(new_process); |
| versioned_pool->has_available_process = true; |
| recreated++; |
| published = true; |
| } else { |
| processes_to_shutdown.emplace_back(std::move(new_process)); |
| } |
| } else if (pool.size() < static_cast<size_t>(max_pool_size)) { |
| pool.emplace_back(std::move(new_process)); |
| versioned_pool->has_available_process = true; |
| recreated++; |
| published = true; |
| } else { |
| processes_to_shutdown.emplace_back(std::move(new_process)); |
| } |
| } |
| if (published) { |
| versioned_pool->cv.notify_all(); |
| } |
| } else { |
| LOG(ERROR) << "Failed to recreate Python process for version " << version.to_string() |
| << ": " << status.to_string(); |
| } |
| } |
| |
| { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| auto& pool = versioned_pool->processes; |
| |
| if (versioned_pool->state != PoolState::INITIALIZED) { |
| versioned_pool->has_available_process = false; |
| } else { |
| // Keep empty/dead slots instead of shrinking the vector. Init workers are detached and |
| // publish by original slot index; shrinking here would make a late successful init look |
| // out-of-range and discard a usable process. |
| versioned_pool->has_available_process = std::any_of( |
| pool.begin(), pool.end(), |
| [](const ProcessPtr& process) { return process && process->is_alive(); }); |
| } |
| } |
| versioned_pool->cv.notify_all(); |
| for (auto& process : processes_to_shutdown) { |
| process->shutdown(); |
| } |
| return recreated; |
| } |
| |
| void PythonServerManager::shutdown() { |
| std::vector<std::pair<PythonVersion, std::shared_ptr<VersionedProcessPool>>> pools_to_shutdown; |
| { |
| std::lock_guard<std::mutex> lock(_pools_mutex); |
| _shutdown_flag.store(true, std::memory_order_release); |
| pools_to_shutdown.reserve(_process_pools.size()); |
| for (auto& [version, versioned_pool] : _process_pools) { |
| pools_to_shutdown.emplace_back(version, std::move(versioned_pool)); |
| } |
| _process_pools.clear(); |
| } |
| |
| for (auto& [version, versioned_pool] : pools_to_shutdown) { |
| if (!versioned_pool) { |
| continue; |
| } |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| versioned_pool->state = PoolState::STOPPED; |
| versioned_pool->has_available_process = false; |
| versioned_pool->repairing = false; |
| versioned_pool->cv.notify_all(); |
| } |
| |
| std::unique_ptr<std::thread> health_check_thread; |
| { |
| std::lock_guard<std::mutex> lock(_health_check_mutex); |
| health_check_thread = std::move(_health_check_thread); |
| } |
| _health_check_cv.notify_one(); |
| if (health_check_thread && health_check_thread->joinable()) { |
| health_check_thread->join(); |
| } |
| |
| // Shutdown all processes |
| std::vector<ProcessPtr> processes_to_shutdown; |
| for (auto& [version, versioned_pool] : pools_to_shutdown) { |
| if (!versioned_pool) { |
| continue; |
| } |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| auto& pool = versioned_pool->processes; |
| for (auto& process : pool) { |
| if (process) { |
| processes_to_shutdown.emplace_back(std::move(process)); |
| } |
| } |
| pool.clear(); |
| versioned_pool->cv.notify_all(); |
| } |
| for (auto& process : processes_to_shutdown) { |
| process->shutdown(); |
| } |
| } |
| |
| Status PythonServerManager::_read_process_memory(pid_t pid, size_t* rss_bytes) { |
| // Read from /proc/{pid}/statm |
| // Format: size resident shared text lib data dt |
| std::string statm_path = fmt::format("/proc/{}/statm", pid); |
| std::ifstream statm_file(statm_path); |
| |
| if (!statm_file.is_open()) { |
| return Status::InternalError("Cannot open {}", statm_path); |
| } |
| |
| size_t size_pages = 0, rss_pages = 0; |
| // we only care about RSS, read and ignore the total size field |
| statm_file >> size_pages >> rss_pages; |
| |
| if (statm_file.fail()) { |
| return Status::InternalError("Failed to read {}", statm_path); |
| } |
| |
| // Convert pages to bytes |
| long page_size = sysconf(_SC_PAGESIZE); |
| *rss_bytes = rss_pages * page_size; |
| |
| return Status::OK(); |
| } |
| |
| void PythonServerManager::_refresh_memory_stats() { |
| int64_t total_rss = 0; |
| |
| for (const auto& [version, versioned_pool] : _snapshot_process_pools()) { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| const auto& pool = versioned_pool->processes; |
| for (const auto& process : pool) { |
| if (!process || !process->is_alive()) continue; |
| |
| size_t rss_bytes = 0; |
| Status s = _read_process_memory(process->get_child_pid(), &rss_bytes); |
| |
| if (s.ok()) { |
| total_rss += rss_bytes; |
| } else [[unlikely]] { |
| LOG(WARNING) << "Failed to read memory info for Python process (pid=" |
| << process->get_child_pid() << "): " << s.to_string(); |
| } |
| } |
| } |
| _mem_tracker.set_consumption(total_rss); |
| LOG(INFO) << _mem_tracker.log_usage(); |
| |
| if (config::python_udf_processes_memory_limit_bytes > 0 && |
| total_rss > config::python_udf_processes_memory_limit_bytes) { |
| LOG(WARNING) << "Python UDF process memory usage exceeds limit: rss_bytes=" << total_rss |
| << ", limit_bytes=" << config::python_udf_processes_memory_limit_bytes; |
| } |
| } |
| |
| Status PythonServerManager::clear_module_cache(const std::string& location) { |
| if (location.empty()) { |
| return Status::InvalidArgument("Empty location for clear_module_cache"); |
| } |
| |
| std::string body = fmt::format(R"({{"location": "{}"}})", location); |
| return _broadcast_action_to_processes("clear_module_cache", body, |
| fmt::format("location={}", location)); |
| } |
| |
| void PythonServerManager::clear_udaf_state_cache(int64_t function_id) { |
| std::string body = fmt::format(R"({{"function_id": {}}})", function_id); |
| WARN_IF_ERROR(_broadcast_action_to_processes("clear_udaf_state_cache", body, |
| fmt::format("function_id={}", function_id)), |
| "failed to clear Python UDAF state cache"); |
| } |
| |
| Status PythonServerManager::_broadcast_action_to_processes(const std::string& action_type, |
| const std::string& body, |
| const std::string& log_name) { |
| int success_count = 0; |
| int fail_count = 0; |
| bool has_active_process = false; |
| |
| for (auto& [version, versioned_pool] : _snapshot_process_pools()) { |
| std::lock_guard<std::mutex> lock(versioned_pool->mutex); |
| auto& pool = versioned_pool->processes; |
| for (auto& process : pool) { |
| if (!process || !process->is_alive()) { |
| continue; |
| } |
| has_active_process = true; |
| try { |
| auto loc_result = arrow::flight::Location::Parse(process->get_uri()); |
| if (!loc_result.ok()) [[unlikely]] { |
| fail_count++; |
| continue; |
| } |
| |
| auto client_result = arrow::flight::FlightClient::Connect(*loc_result); |
| if (!client_result.ok()) [[unlikely]] { |
| fail_count++; |
| continue; |
| } |
| auto client = std::move(*client_result); |
| |
| arrow::flight::Action action; |
| action.type = action_type; |
| action.body = arrow::Buffer::FromString(body); |
| |
| auto result_stream = client->DoAction(action); |
| if (!result_stream.ok()) { |
| fail_count++; |
| continue; |
| } |
| |
| auto result = (*result_stream)->Next(); |
| if (result.ok() && *result) { |
| success_count++; |
| } else { |
| fail_count++; |
| } |
| |
| } catch (...) { |
| fail_count++; |
| } |
| } |
| } |
| |
| if (!has_active_process) { |
| return Status::OK(); |
| } |
| |
| LOG(INFO) << action_type << " completed for " << log_name << ", success=" << success_count |
| << ", failed=" << fail_count; |
| |
| if (fail_count > 0) { |
| return Status::InternalError("{} failed for {}, success={}, failed={}", action_type, |
| log_name, success_count, fail_count); |
| } |
| |
| return Status::OK(); |
| } |
| |
| // Explicit template instantiation for UDF, UDAF and UDTF clients |
| template Status PythonServerManager::get_client<PythonUDFClient>( |
| const PythonUDFMeta& func_meta, const PythonVersion& version, |
| std::shared_ptr<PythonUDFClient>* client, |
| const std::shared_ptr<arrow::Schema>& data_schema); |
| |
| template Status PythonServerManager::get_client<PythonUDAFClient>( |
| const PythonUDFMeta& func_meta, const PythonVersion& version, |
| std::shared_ptr<PythonUDAFClient>* client, |
| const std::shared_ptr<arrow::Schema>& data_schema); |
| |
| template Status PythonServerManager::get_client<PythonUDTFClient>( |
| const PythonUDFMeta& func_meta, const PythonVersion& version, |
| std::shared_ptr<PythonUDTFClient>* client, |
| const std::shared_ptr<arrow::Schema>& data_schema); |
| |
| } // namespace doris |