be/src/exprs/aggregate/aggregate_function_python_udaf.cpp
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 | | #include "exprs/aggregate/aggregate_function_python_udaf.h" |
19 | | |
20 | | #include <arrow/array.h> |
21 | | #include <arrow/memory_pool.h> |
22 | | #include <arrow/record_batch.h> |
23 | | #include <arrow/type.h> |
24 | | #include <fmt/format.h> |
25 | | |
26 | | #include "common/exception.h" |
27 | | #include "common/logging.h" |
28 | | #include "core/block/block.h" |
29 | | #include "core/column/column_nullable.h" |
30 | | #include "core/column/column_vector.h" |
31 | | #include "core/data_type/data_type_factory.hpp" |
32 | | #include "core/data_type/data_type_nullable.h" |
33 | | #include "core/data_type/define_primitive_type.h" |
34 | | #include "format/arrow/arrow_block_convertor.h" |
35 | | #include "format/arrow/arrow_row_batch.h" |
36 | | #include "runtime/exec_env.h" |
37 | | #include "runtime/user_function_cache.h" |
38 | | #include "udf/python/python_env.h" |
39 | | #include "udf/python/python_server.h" |
40 | | #include "util/timezone_utils.h" |
41 | | |
42 | | namespace doris { |
43 | | |
44 | 0 | Status AggregatePythonUDAFData::create(int64_t place) { |
45 | 0 | DCHECK(client) << "Client must be set before calling create"; |
46 | 0 | RETURN_IF_ERROR(client->create(place)); |
47 | 0 | return Status::OK(); |
48 | 0 | } |
49 | | |
50 | | Status AggregatePythonUDAFData::add(int64_t place_id, const IColumn** columns, |
51 | | int64_t row_num_start, int64_t row_num_end, |
52 | 0 | const DataTypes& argument_types) { |
53 | 0 | DCHECK(client) << "Client must be set before calling add"; |
54 | | |
55 | | // Zero-copy: Use full columns with range specification |
56 | 0 | Block input_block; |
57 | 0 | for (size_t i = 0; i < argument_types.size(); ++i) { |
58 | 0 | input_block.insert( |
59 | 0 | ColumnWithTypeAndName(columns[i]->get_ptr(), argument_types[i], std::to_string(i))); |
60 | 0 | } |
61 | |
|
62 | 0 | std::shared_ptr<arrow::Schema> schema; |
63 | 0 | RETURN_IF_ERROR( |
64 | 0 | get_arrow_schema_from_block(input_block, &schema, TimezoneUtils::default_time_zone)); |
65 | 0 | cctz::time_zone timezone_obj; |
66 | 0 | TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj); |
67 | |
|
68 | 0 | std::shared_ptr<arrow::RecordBatch> batch; |
69 | | // Zero-copy: convert only the specified range |
70 | 0 | RETURN_IF_ERROR(convert_to_arrow_batch(input_block, schema, |
71 | 0 | ExecEnv::GetInstance()->arrow_memory_pool(), &batch, |
72 | 0 | timezone_obj, row_num_start, row_num_end)); |
73 | | // Send the batch (already sliced in convert_to_arrow_batch) |
74 | | // Single place mode: no places column needed |
75 | 0 | RETURN_IF_ERROR(client->accumulate(place_id, true, *batch, 0, batch->num_rows())); |
76 | 0 | return Status::OK(); |
77 | 0 | } |
78 | | |
79 | | Status AggregatePythonUDAFData::add_batch(AggregateDataPtr* places, size_t place_offset, |
80 | | size_t num_rows, const IColumn** columns, |
81 | | const DataTypes& argument_types, size_t start, |
82 | 0 | size_t end) { |
83 | 0 | DCHECK(client) << "Client must be set before calling add_batch"; |
84 | 0 | DCHECK(end > start) << "end must be greater than start"; |
85 | 0 | DCHECK(end <= num_rows) << "end must not exceed num_rows"; |
86 | |
|
87 | 0 | size_t slice_rows = end - start; |
88 | 0 | Block input_block; |
89 | 0 | for (size_t i = 0; i < argument_types.size(); ++i) { |
90 | 0 | DCHECK(columns[i]->size() == num_rows) << "Column size must match num_rows"; |
91 | 0 | input_block.insert( |
92 | 0 | ColumnWithTypeAndName(columns[i]->get_ptr(), argument_types[i], std::to_string(i))); |
93 | 0 | } |
94 | |
|
95 | 0 | auto places_col = ColumnInt64::create(num_rows); |
96 | 0 | auto& places_data = places_col->get_data(); |
97 | | |
98 | | // Fill places column with place IDs for the slice [start, end) |
99 | 0 | for (size_t i = start; i < end; ++i) { |
100 | 0 | places_data[i] = reinterpret_cast<int64_t>(places[i] + place_offset); |
101 | 0 | } |
102 | |
|
103 | 0 | static DataTypePtr places_type = |
104 | 0 | DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, false); |
105 | 0 | input_block.insert(ColumnWithTypeAndName(std::move(places_col), places_type, "places")); |
106 | |
|
107 | 0 | std::shared_ptr<arrow::Schema> schema; |
108 | 0 | RETURN_IF_ERROR( |
109 | 0 | get_arrow_schema_from_block(input_block, &schema, TimezoneUtils::default_time_zone)); |
110 | 0 | cctz::time_zone timezone_obj; |
111 | 0 | TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj); |
112 | |
|
113 | 0 | std::shared_ptr<arrow::RecordBatch> batch; |
114 | | // Zero-copy: convert only the [start, end) range |
115 | | // This slice includes the places column automatically |
116 | 0 | RETURN_IF_ERROR(convert_to_arrow_batch(input_block, schema, |
117 | 0 | ExecEnv::GetInstance()->arrow_memory_pool(), &batch, |
118 | 0 | timezone_obj, start, end)); |
119 | | // Send entire batch (already contains places column) to Python |
120 | | // place_id=0 is ignored when is_single_place=false |
121 | 0 | RETURN_IF_ERROR(client->accumulate(0, false, *batch, 0, slice_rows)); |
122 | 0 | return Status::OK(); |
123 | 0 | } |
124 | | |
125 | 0 | Status AggregatePythonUDAFData::merge(const AggregatePythonUDAFData& rhs, int64_t place) { |
126 | 0 | DCHECK(client) << "Client must be set before calling merge"; |
127 | | |
128 | | // Get serialized state from rhs (already stored in serialize_data by read()) |
129 | 0 | auto serialized_state = arrow::Buffer::Wrap( |
130 | 0 | reinterpret_cast<const uint8_t*>(rhs.serialize_data.data()), rhs.serialize_data.size()); |
131 | 0 | RETURN_IF_ERROR(client->merge(place, serialized_state)); |
132 | 0 | return Status::OK(); |
133 | 0 | } |
134 | | |
135 | 0 | Status AggregatePythonUDAFData::write(BufferWritable& buf, int64_t place) const { |
136 | 0 | DCHECK(client) << "Client must be set before calling write"; |
137 | | |
138 | | // Serialize state from Python server |
139 | 0 | std::shared_ptr<arrow::Buffer> serialized_state; |
140 | 0 | RETURN_IF_ERROR(client->serialize(place, &serialized_state)); |
141 | 0 | const char* data = reinterpret_cast<const char*>(serialized_state->data()); |
142 | 0 | size_t size = serialized_state->size(); |
143 | 0 | buf.write_binary(StringRef {data, size}); |
144 | 0 | return Status::OK(); |
145 | 0 | } |
146 | | |
147 | 0 | void AggregatePythonUDAFData::read(BufferReadable& buf) { |
148 | | // Read serialized state from buffer into serialize_data |
149 | | // This will be used later by merge() in deserialize_and_merge() |
150 | 0 | buf.read_binary(serialize_data); |
151 | 0 | } |
152 | | |
153 | 0 | Status AggregatePythonUDAFData::reset(int64_t place) { |
154 | 0 | DCHECK(client) << "Client must be set before calling reset"; |
155 | 0 | RETURN_IF_ERROR(client->reset(place)); |
156 | | // After reset, state still exists but is back to initial state |
157 | 0 | return Status::OK(); |
158 | 0 | } |
159 | | |
160 | 0 | Status AggregatePythonUDAFData::destroy(int64_t place) { |
161 | 0 | DCHECK(client) << "Client must be set before calling destroy"; |
162 | 0 | RETURN_IF_ERROR(client->destroy(place)); |
163 | 0 | return Status::OK(); |
164 | 0 | } |
165 | | |
166 | | Status AggregatePythonUDAFData::get(IColumn& to, const DataTypePtr& result_type, |
167 | 0 | int64_t place) const { |
168 | 0 | DCHECK(client) << "Client must be set before calling get"; |
169 | | |
170 | | // Get final result from Python server |
171 | 0 | std::shared_ptr<arrow::RecordBatch> result; |
172 | 0 | RETURN_IF_ERROR(client->finalize(place, &result)); |
173 | | |
174 | | // Convert Arrow RecordBatch to Block |
175 | 0 | Block result_block; |
176 | 0 | DataTypes types = {result_type}; |
177 | 0 | cctz::time_zone timezone_obj; |
178 | 0 | TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj); |
179 | 0 | RETURN_IF_ERROR(convert_from_arrow_batch(result, types, &result_block, timezone_obj)); |
180 | | |
181 | | // Insert the result value into output column |
182 | 0 | if (result_block.rows() != 1) { |
183 | 0 | return Status::InternalError("Expected 1 row in result block, got {}", result_block.rows()); |
184 | 0 | } |
185 | | |
186 | 0 | auto& result_column = result_block.get_by_position(0).column; |
187 | 0 | to.insert_from(*result_column, 0); |
188 | 0 | return Status::OK(); |
189 | 0 | } |
190 | | |
191 | 0 | Status AggregatePythonUDAF::open() { |
192 | | // Build function metadata from TFunction |
193 | 0 | _func_meta.id = _fn.id; |
194 | 0 | _func_meta.name = _fn.name.function_name; |
195 | | |
196 | | // For UDAF, symbol is in aggregate_fn |
197 | 0 | if (_fn.__isset.aggregate_fn && _fn.aggregate_fn.__isset.symbol) { |
198 | 0 | _func_meta.symbol = _fn.aggregate_fn.symbol; |
199 | 0 | } else { |
200 | 0 | return Status::InvalidArgument("Python UDAF symbol is not set"); |
201 | 0 | } |
202 | | |
203 | | // Determine load type (inline code or module) |
204 | 0 | if (!_fn.function_code.empty()) { |
205 | 0 | _func_meta.type = PythonUDFLoadType::INLINE; |
206 | 0 | _func_meta.location = "inline"; |
207 | 0 | _func_meta.inline_code = _fn.function_code; |
208 | 0 | } else if (!_fn.hdfs_location.empty()) { |
209 | 0 | _func_meta.type = PythonUDFLoadType::MODULE; |
210 | 0 | _func_meta.location = _fn.hdfs_location; |
211 | 0 | _func_meta.checksum = _fn.checksum; |
212 | 0 | } else { |
213 | 0 | _func_meta.type = PythonUDFLoadType::UNKNOWN; |
214 | 0 | _func_meta.location = "unknown"; |
215 | 0 | } |
216 | |
|
217 | 0 | _func_meta.input_types = argument_types; |
218 | 0 | _func_meta.return_type = _return_type; |
219 | 0 | _func_meta.client_type = PythonClientType::UDAF; |
220 | | |
221 | | // Get Python version |
222 | 0 | if (_fn.__isset.runtime_version && !_fn.runtime_version.empty()) { |
223 | 0 | RETURN_IF_ERROR(PythonVersionManager::instance().get_version(_fn.runtime_version, |
224 | 0 | &_python_version)); |
225 | 0 | } else { |
226 | 0 | return Status::InvalidArgument("Python UDAF runtime version is not set"); |
227 | 0 | } |
228 | | |
229 | 0 | _func_meta.runtime_version = _python_version.full_version; |
230 | 0 | RETURN_IF_ERROR(_func_meta.check()); |
231 | 0 | _func_meta.always_nullable = _return_type->is_nullable(); |
232 | |
|
233 | 0 | LOG(INFO) << fmt::format("Creating Python UDAF: {}, runtime_version: {}, func_meta: {}", |
234 | 0 | _fn.name.function_name, _python_version.to_string(), |
235 | 0 | _func_meta.to_string()); |
236 | |
|
237 | 0 | if (_func_meta.type == PythonUDFLoadType::MODULE) { |
238 | 0 | RETURN_IF_ERROR(UserFunctionCache::instance()->get_pypath( |
239 | 0 | _func_meta.id, _func_meta.location, _func_meta.checksum, &_func_meta.location)); |
240 | 0 | } |
241 | | |
242 | 0 | return Status::OK(); |
243 | 0 | } |
244 | | |
245 | 0 | void AggregatePythonUDAF::create(AggregateDataPtr __restrict place) const { |
246 | 0 | std::call_once(_schema_init_flag, [this]() { |
247 | 0 | std::vector<std::shared_ptr<arrow::Field>> fields; |
248 | |
|
249 | 0 | std::string timezone = TimezoneUtils::default_time_zone; |
250 | 0 | for (size_t i = 0; i < argument_types.size(); ++i) { |
251 | 0 | std::shared_ptr<arrow::DataType> arrow_type; |
252 | 0 | Status st = convert_to_arrow_type(argument_types[i], &arrow_type, timezone); |
253 | 0 | if (!st.ok()) { |
254 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, |
255 | 0 | "Failed to convert argument type {} to Arrow type: {}", i, |
256 | 0 | st.to_string()); |
257 | 0 | } |
258 | 0 | fields.push_back(create_arrow_field_with_metadata( |
259 | 0 | std::to_string(i), arrow_type, argument_types[i]->is_nullable(), |
260 | 0 | argument_types[i]->get_primitive_type())); |
261 | 0 | } |
262 | | |
263 | | // Add places column for GROUP BY aggregation (always included, NULL in single-place mode) |
264 | 0 | fields.push_back(arrow::field("places", arrow::int64())); |
265 | | // Add binary_data column for merge operations |
266 | 0 | fields.push_back(arrow::field("binary_data", arrow::binary())); |
267 | 0 | _schema = arrow::schema(fields); |
268 | 0 | }); |
269 | | |
270 | | // Initialize the data structure |
271 | 0 | new (place) Data(); |
272 | 0 | DCHECK(reinterpret_cast<Data*>(place)) << "Place must not be null"; |
273 | |
|
274 | 0 | if (Status st = PythonServerManager::instance().get_client( |
275 | 0 | _func_meta, _python_version, &(this->data(place).client), _schema); |
276 | 0 | UNLIKELY(!st.ok())) { |
277 | 0 | this->data(place).~Data(); |
278 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, "Failed to get Python UDAF client: {}", |
279 | 0 | st.to_string()); |
280 | 0 | } |
281 | | |
282 | | // Initialize UDAF state in Python server |
283 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
284 | 0 | if (Status st = this->data(place).create(place_id); UNLIKELY(!st.ok())) { |
285 | 0 | this->data(place).~Data(); |
286 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
287 | 0 | } |
288 | 0 | } |
289 | | |
290 | 0 | void AggregatePythonUDAF::destroy(AggregateDataPtr __restrict place) const noexcept { |
291 | 0 | try { |
292 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
293 | | |
294 | | // Destroy state in Python server |
295 | 0 | if (this->data(place).client) { |
296 | 0 | Status st = this->data(place).destroy(place_id); |
297 | 0 | if (UNLIKELY(!st.ok())) { |
298 | 0 | LOG(WARNING) << "Failed to destroy Python UDAF state for place_id=" << place_id |
299 | 0 | << ", function=" << _func_meta.name << ": " << st.to_string(); |
300 | 0 | } |
301 | |
|
302 | 0 | this->data(place).client.reset(); |
303 | 0 | } |
304 | |
|
305 | 0 | this->data(place).~Data(); |
306 | 0 | } catch (const std::exception& e) { |
307 | 0 | LOG(ERROR) << "Exception in AggregatePythonUDAF::destroy: " << e.what(); |
308 | 0 | } catch (...) { |
309 | 0 | LOG(ERROR) << "Unknown exception in AggregatePythonUDAF::destroy"; |
310 | 0 | } |
311 | 0 | } |
312 | | |
313 | | void AggregatePythonUDAF::add(AggregateDataPtr __restrict place, const IColumn** columns, |
314 | 0 | ssize_t row_num, Arena&) const { |
315 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
316 | 0 | Status st = this->data(place).add(place_id, columns, row_num, row_num + 1, argument_types); |
317 | 0 | if (UNLIKELY(!st.ok())) { |
318 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
319 | 0 | } |
320 | 0 | } |
321 | | |
322 | | void AggregatePythonUDAF::add_batch(size_t batch_size, AggregateDataPtr* places, |
323 | | size_t place_offset, const IColumn** columns, Arena&, |
324 | 0 | bool /*agg_many*/) const { |
325 | 0 | if (batch_size == 0) return; |
326 | | |
327 | 0 | size_t start = 0; |
328 | 0 | while (start < batch_size) { |
329 | | // Get the starting place for this segment |
330 | 0 | AggregateDataPtr start_place = places[start] + place_offset; |
331 | 0 | auto& start_place_data = this->data(start_place); |
332 | | // Get the process for this segment |
333 | 0 | const auto* current_process = start_place_data.client->get_process().get(); |
334 | | |
335 | | // Scan forward to find the end of this consecutive segment (same process) |
336 | 0 | size_t end = start + 1; |
337 | 0 | while (end < batch_size) { |
338 | 0 | AggregateDataPtr end_place = places[end] + place_offset; |
339 | 0 | auto& end_place_data = this->data(end_place); |
340 | 0 | const auto* next_process = end_place_data.client->get_process().get(); |
341 | | // If different process, end the current segment |
342 | 0 | if (*next_process != *current_process) break; |
343 | 0 | ++end; |
344 | 0 | } |
345 | | |
346 | | // Send this segment to Python with zero-copy |
347 | | // Pass places array and let add_batch construct place_ids on-demand |
348 | 0 | Status st = start_place_data.add_batch(places, place_offset, batch_size, columns, |
349 | 0 | argument_types, start, end); |
350 | |
|
351 | 0 | if (UNLIKELY(!st.ok())) { |
352 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, |
353 | 0 | "Failed to send segment to Python: " + st.to_string()); |
354 | 0 | } |
355 | | |
356 | 0 | start = end; |
357 | 0 | } |
358 | 0 | } |
359 | | |
360 | | void AggregatePythonUDAF::add_batch_single_place(size_t batch_size, AggregateDataPtr place, |
361 | 0 | const IColumn** columns, Arena&) const { |
362 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
363 | 0 | Status st = this->data(place).add(place_id, columns, 0, batch_size, argument_types); |
364 | 0 | if (UNLIKELY(!st.ok())) { |
365 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
366 | 0 | } |
367 | 0 | } |
368 | | |
369 | | void AggregatePythonUDAF::add_range_single_place(int64_t partition_start, int64_t partition_end, |
370 | | int64_t frame_start, int64_t frame_end, |
371 | | AggregateDataPtr place, const IColumn** columns, |
372 | | Arena& arena, UInt8* current_window_empty, |
373 | 0 | UInt8* current_window_has_inited) const { |
374 | | // Calculate actual frame range |
375 | 0 | frame_start = std::max<int64_t>(frame_start, partition_start); |
376 | 0 | frame_end = std::min<int64_t>(frame_end, partition_end); |
377 | |
|
378 | 0 | if (frame_start >= frame_end) { |
379 | 0 | if (!*current_window_has_inited) { |
380 | 0 | *current_window_empty = true; |
381 | 0 | } |
382 | 0 | return; |
383 | 0 | } |
384 | | |
385 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
386 | 0 | Status st = this->data(place).add(place_id, columns, frame_start, frame_end, argument_types); |
387 | 0 | if (UNLIKELY(!st.ok())) { |
388 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
389 | 0 | } |
390 | | |
391 | 0 | *current_window_empty = false; |
392 | 0 | *current_window_has_inited = true; |
393 | 0 | } |
394 | | |
395 | 0 | void AggregatePythonUDAF::reset(AggregateDataPtr place) const { |
396 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
397 | 0 | Status st = this->data(place).reset(place_id); |
398 | 0 | if (UNLIKELY(!st.ok())) { |
399 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
400 | 0 | } |
401 | 0 | } |
402 | | |
403 | | void AggregatePythonUDAF::merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs, |
404 | 0 | Arena&) const { |
405 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
406 | 0 | Status st = this->data(place).merge(this->data(rhs), place_id); |
407 | 0 | if (UNLIKELY(!st.ok())) { |
408 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
409 | 0 | } |
410 | 0 | } |
411 | | |
412 | | void AggregatePythonUDAF::serialize(ConstAggregateDataPtr __restrict place, |
413 | 0 | BufferWritable& buf) const { |
414 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
415 | 0 | Status st = this->data(place).write(buf, place_id); |
416 | 0 | if (UNLIKELY(!st.ok())) { |
417 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
418 | 0 | } |
419 | 0 | } |
420 | | |
421 | | void AggregatePythonUDAF::deserialize(AggregateDataPtr __restrict place, BufferReadable& buf, |
422 | 0 | Arena&) const { |
423 | 0 | this->data(place).read(buf); |
424 | 0 | } |
425 | | |
426 | | void AggregatePythonUDAF::insert_result_into(ConstAggregateDataPtr __restrict place, |
427 | 0 | IColumn& to) const { |
428 | 0 | int64_t place_id = reinterpret_cast<int64_t>(place); |
429 | 0 | Status st = this->data(place).get(to, _return_type, place_id); |
430 | 0 | if (UNLIKELY(!st.ok())) { |
431 | 0 | throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string()); |
432 | 0 | } |
433 | 0 | } |
434 | | |
435 | | } // namespace doris |