Coverage Report

Created: 2026-10-28 06:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/udf/python/python_server.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <atomic>
21
#include <chrono>
22
#include <condition_variable>
23
#include <memory>
24
#include <mutex>
25
#include <thread>
26
#include <unordered_map>
27
#include <vector>
28
29
#include "common/status.h"
30
#include "runtime/memory/mem_tracker.h"
31
#include "udf/python/python_udf_meta.h"
32
#include "udf/python/python_udf_runtime.h"
33
34
namespace doris {
35
struct UserFunctionCacheEntry;
36
class PythonUDFPoolLease;
37
class PythonServerManager {
38
public:
39
57
    PythonServerManager() = default;
40
41
56
    ~PythonServerManager() { shutdown(); }
42
43
8.11k
    static PythonServerManager& instance() {
44
8.11k
        static PythonServerManager instance;
45
8.11k
        return instance;
46
8.11k
    }
47
48
    template <typename T>
49
    Status get_client(const PythonUDFMeta& func_meta, const PythonVersion& version,
50
                      std::shared_ptr<T>* client,
51
                      const std::shared_ptr<arrow::Schema>& data_schema = nullptr,
52
                      std::shared_ptr<UserFunctionCacheEntry> package_cache_entry = nullptr);
53
54
    static Status fork(const PythonVersion& version, ProcessPtr* process);
55
56
    // Stop accepting new clients. Health check removes the pool after its last client closes.
57
    void drop_function(int64_t function_id);
58
59
    void shutdown();
60
61
#ifdef BE_TEST
62
    // For unit testing only.
63
    void check_and_recreate_processes_for_test() { _check_and_recreate_processes(); }
64
65
    void set_process_pool_for_test(const PythonUDFMeta& meta, const PythonVersion& version,
66
                                   std::vector<ProcessPtr> processes, bool initialized = true);
67
68
    std::vector<ProcessPtr> process_pool_snapshot_for_test(int64_t function_id);
69
70
    bool process_pool_is_initialized_for_test(int64_t function_id);
71
72
    bool wait_for_process_pool_initialized_for_test(int64_t function_id,
73
                                                    std::chrono::milliseconds timeout);
74
75
    bool wait_for_process_pool_initialization_finished_for_test(int64_t function_id,
76
                                                                std::chrono::milliseconds timeout);
77
78
#endif
79
80
private:
81
    friend class PythonUDFPoolLease;
82
83
    enum class PoolState {
84
        UNINITIALIZED,
85
        INITIALIZING,
86
        // `Initialized` means:
87
        // 1. All process slots have attempted initialization.
88
        // 2. At least one live process is available for requests.
89
        INITIALIZED,
90
        // Reject new clients and late fork publication after DROP, shutdown or idle reclamation.
91
        STOPPED,
92
    };
93
94
    // Includes processes held by clients after DROP, without extending their lifetime.
95
    struct ProcessRegistry {
96
        void add(const ProcessPtr& process);
97
        std::vector<ProcessPtr> snapshot();
98
99
        std::mutex mutex;
100
        std::vector<std::weak_ptr<PythonUDFProcess>> processes;
101
    };
102
103
    struct FunctionProcessPool {
104
        // Interpreter information needed to replace dead processes in this function pool.
105
        PythonVersion version;
106
        // Detached fork/repair workers can outlive the manager.
107
        std::shared_ptr<ProcessRegistry> process_registry;
108
        int64_t function_id = 0;
109
        int32_t pool_size = 3;
110
        int64_t auto_suspend = PythonUDFMeta::DEFAULT_AUTO_SUSPEND_SECONDS;
111
        size_t active_clients = 0;
112
        std::chrono::steady_clock::time_point idle_since = std::chrono::steady_clock::now();
113
        std::shared_ptr<UserFunctionCacheEntry> package_cache_entry;
114
        std::mutex mutex;
115
        // Coordinates initialization and repair workers with foreground requests.
116
        std::condition_variable cv;
117
        std::vector<ProcessPtr> processes;
118
        PoolState state = PoolState::UNINITIALIZED;
119
        // True when at least one process in the pool can serve requests.
120
        bool has_available_process = false;
121
        // True while a background repair is recreating dead or missing processes.
122
        bool repairing = false;
123
    };
124
125
    /**
126
     * Lazily initialize and return the process pool for specific function.
127
     */
128
    Result<std::shared_ptr<FunctionProcessPool>> _ensure_pool_initialized(
129
            const PythonUDFMeta& meta, const PythonVersion& version,
130
            std::shared_ptr<UserFunctionCacheEntry> package_cache_entry = nullptr,
131
            std::shared_ptr<PythonUDFPoolLease>* lease = nullptr);
132
133
    /**
134
     * Pick an available process from specific pool, recreating one on demand if needed.
135
     */
136
    static void _start_pool_initialization(
137
            const std::shared_ptr<FunctionProcessPool>& function_pool, const PythonVersion& version,
138
            std::chrono::milliseconds process_pool_init_timeout);
139
140
    static void _start_pool_repair(const PythonVersion& version,
141
                                   const std::shared_ptr<FunctionProcessPool>& function_pool);
142
143
    Status _get_process(const PythonVersion& version,
144
                        const std::shared_ptr<FunctionProcessPool>& function_pool,
145
                        ProcessPtr* process);
146
147
    /**
148
     * Start health check background thread when the first function pool is created
149
     * Thread periodically reclaims pools, checks process health and refreshes memory stats.
150
     */
151
    void _start_health_check_thread();
152
153
    // Only reclaim pools with no active clients: STOPPED pools on the next scan; other pools
154
    // after auto_suspend seconds without a client (-1 disables idle reclamation).
155
    void _reclaim_idle_pools(std::chrono::steady_clock::time_point now);
156
157
    /**
158
     * Check process health and recreate dead processes
159
     */
160
    void _check_and_recreate_processes();
161
162
#ifdef BE_TEST
163
    static constexpr std::chrono::milliseconds PROCESS_START_TIMEOUT {500};
164
    static constexpr std::chrono::milliseconds PROCESS_TERMINATE_TIMEOUT {100};
165
    static constexpr std::chrono::milliseconds PROCESS_POOL_INIT_TIMEOUT {1000};
166
    static constexpr std::chrono::milliseconds PROCESS_REPAIR_WAIT_TIMEOUT {200};
167
#else
168
    static constexpr std::chrono::milliseconds PROCESS_START_TIMEOUT {5000};
169
    static constexpr std::chrono::milliseconds PROCESS_TERMINATE_TIMEOUT {1000};
170
    // FE's default send-fragments RPC timeout is 30s. Keep BE's Python pool wait below it so the
171
    // caller sees SERVICE_UNAVAILABLE with Python context instead of a generic RPC deadline error.
172
    static constexpr std::chrono::milliseconds PROCESS_POOL_INIT_TIMEOUT {20000};
173
    static constexpr std::chrono::milliseconds PROCESS_REPAIR_WAIT_TIMEOUT {1000};
174
#endif
175
    static int _repair_process_pool(const PythonVersion& version,
176
                                    const std::shared_ptr<FunctionProcessPool>& function_pool);
177
178
    /**
179
     * Read resident set size (RSS) for a single process from /proc/{pid}/statm
180
     */
181
    Status _read_process_memory(pid_t pid, size_t* rss_bytes);
182
183
    /**
184
     * Refresh memory statistics for all Python processes
185
     */
186
    void _refresh_memory_stats();
187
188
    Result<std::shared_ptr<FunctionProcessPool>> _get_or_create_process_pool(
189
            const PythonUDFMeta& meta, const PythonVersion& version,
190
            std::shared_ptr<UserFunctionCacheEntry> package_cache_entry,
191
            std::shared_ptr<PythonUDFPoolLease>* lease = nullptr);
192
    std::vector<std::shared_ptr<FunctionProcessPool>> _snapshot_process_pools();
193
    static bool _select_alive_process_from_pool(const std::vector<ProcessPtr>& pool,
194
                                                ProcessPtr* process);
195
196
    // Weak references keep dropped-but-active processes visible without keeping them alive.
197
    std::shared_ptr<ProcessRegistry> _process_registry = std::make_shared<ProcessRegistry>();
198
199
    // function_id -> process_pool
200
    std::unordered_map<int64_t, std::shared_ptr<FunctionProcessPool>> _process_pools;
201
    // Protects the function id -> pool map. DROP also stops the pool under this lock;
202
    // when taking both locks, acquire this before FunctionProcessPool::mutex.
203
    std::mutex _pools_mutex;
204
    // Health check background thread
205
    std::unique_ptr<std::thread> _health_check_thread;
206
    std::atomic<bool> _shutdown_flag {false};
207
    std::condition_variable _health_check_cv;
208
    std::mutex _health_check_mutex;
209
    MemTracker _mem_tracker {"PythonUDFProcesses"};
210
};
211
212
} // namespace doris