be/src/udf/python/python_udaf_client.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 "udf/python/python_udaf_client.h" |
19 | | |
20 | | #include <arrow/array/builder_base.h> |
21 | | #include <arrow/array/builder_binary.h> |
22 | | #include <arrow/array/builder_primitive.h> |
23 | | #include <arrow/flight/client.h> |
24 | | #include <arrow/flight/server.h> |
25 | | #include <arrow/io/memory.h> |
26 | | #include <arrow/ipc/writer.h> |
27 | | #include <arrow/record_batch.h> |
28 | | #include <arrow/type.h> |
29 | | |
30 | | #include "common/compiler_util.h" |
31 | | #include "common/status.h" |
32 | | #include "format/arrow/arrow_utils.h" |
33 | | #include "runtime/exec_env.h" |
34 | | #include "udf/python/python_udf_meta.h" |
35 | | #include "udf/python/python_udf_runtime.h" |
36 | | #include "util/unaligned.h" |
37 | | |
38 | | namespace doris { |
39 | | |
40 | | // Unified response structure for UDAF operations |
41 | | // Arrow Schema: [success: bool, rows_processed: int64, data: binary] |
42 | | // Different operations use different fields: |
43 | | // - CREATE/MERGE/RESET/DESTROY: use success only |
44 | | // - ACCUMULATE: use success + rows_processed (number of rows processed) |
45 | | // - SERIALIZE: use success + data (serialized_state) |
46 | | // - FINALIZE: use success + data (serialized result, may be null) |
47 | | // - Any failed operation: use success=false + data (UTF-8 error message) |
48 | | // |
49 | | // This unified schema allows all operations to return consistent format, |
50 | | // solving Arrow Flight's limitation that all responses must have the same schema. |
51 | | static const std::shared_ptr<arrow::Schema> kUnifiedUDAFResponseSchema = arrow::schema({ |
52 | | arrow::field("success", arrow::boolean()), |
53 | | arrow::field("rows_processed", arrow::int64()), |
54 | | arrow::field("serialized_data", arrow::binary()), |
55 | | }); |
56 | | |
57 | | Status PythonUDAFClient::make_udaf_failure_status( |
58 | | const std::shared_ptr<arrow::RecordBatch>& response, const char* operation, |
59 | 11 | int64_t place_id) { |
60 | 11 | if (response == nullptr || response->num_rows() != 1 || |
61 | 11 | response->num_columns() != kUnifiedUDAFResponseSchema->num_fields()) [[unlikely]] { |
62 | 3 | return Status::InternalError("Invalid {} failure response for place_id={}", operation, |
63 | 3 | place_id); |
64 | 3 | } |
65 | | |
66 | 8 | auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response->column(2)); |
67 | 8 | if (data_array->IsNull(0)) { |
68 | 1 | return Status::InternalError("{} operation failed for place_id={}", operation, place_id); |
69 | 1 | } |
70 | | |
71 | 7 | const auto* offsets = data_array->raw_value_offsets(); |
72 | 7 | if (offsets == nullptr) [[unlikely]] { |
73 | 0 | return Status::InternalError("Invalid {} failure response for place_id={}: null offsets", |
74 | 0 | operation, place_id); |
75 | 0 | } |
76 | | // Arrow Flight buffers may be unaligned after IPC deserialization |
77 | 7 | int32_t offset_start = unaligned_load<int32_t>(offsets); |
78 | 7 | int32_t offset_end = unaligned_load<int32_t>(offsets + 1); |
79 | | |
80 | 7 | int32_t length = offset_end - offset_start; |
81 | 7 | if (length <= 0) { |
82 | 1 | return Status::InternalError("{} operation failed for place_id={}", operation, place_id); |
83 | 1 | } |
84 | 6 | const uint8_t* data = data_array->value_data()->data() + offset_start; |
85 | 6 | std::string error_message(reinterpret_cast<const char*>(data), length); |
86 | 6 | return Status::InternalError("{} operation failed for place_id={}: {}", operation, place_id, |
87 | 6 | error_message); |
88 | 7 | } |
89 | | |
90 | | #ifdef BE_TEST |
91 | | Status PythonUDAFClient::make_udaf_failure_status_for_test( |
92 | | const std::shared_ptr<arrow::RecordBatch>& response, const char* operation, |
93 | | int64_t place_id) { |
94 | | return make_udaf_failure_status(response, operation, place_id); |
95 | | } |
96 | | #endif |
97 | | |
98 | | Status PythonUDAFClient::create(const PythonUDFMeta& func_meta, ProcessPtr process, |
99 | | const std::shared_ptr<arrow::Schema>& data_schema, |
100 | 2.68k | PythonUDAFClientPtr* client) { |
101 | 2.68k | PythonUDAFClientPtr python_udaf_client = std::make_shared<PythonUDAFClient>(); |
102 | 2.68k | RETURN_IF_ERROR(python_udaf_client->init(func_meta, std::move(process), data_schema)); |
103 | 2.68k | *client = std::move(python_udaf_client); |
104 | 2.68k | return Status::OK(); |
105 | 2.68k | } |
106 | | |
107 | | Status PythonUDAFClient::init(const PythonUDFMeta& func_meta, ProcessPtr process, |
108 | 2.68k | const std::shared_ptr<arrow::Schema>& data_schema) { |
109 | 2.68k | _schema = data_schema; |
110 | 2.68k | return PythonClient::init(func_meta, std::move(process)); |
111 | 2.68k | } |
112 | | |
113 | 2.68k | Status PythonUDAFClient::create(int64_t place_id) { |
114 | 2.68k | std::shared_ptr<arrow::RecordBatch> request_batch; |
115 | 2.68k | RETURN_IF_ERROR(_get_empty_request_batch(&request_batch)); |
116 | | |
117 | 2.68k | UDAFMetadata metadata { |
118 | 2.68k | .meta_version = UDAF_METADATA_VERSION, |
119 | 2.68k | .operation = static_cast<uint8_t>(UDAFOperation::CREATE), |
120 | 2.68k | .is_single_place = 0, |
121 | 2.68k | .place_id = place_id, |
122 | 2.68k | .row_start = 0, |
123 | 2.68k | .row_end = 0, |
124 | 2.68k | }; |
125 | | |
126 | 2.68k | std::shared_ptr<arrow::RecordBatch> response_batch; |
127 | 2.68k | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response_batch)); |
128 | | |
129 | | // Parse unified response_batch: [success: bool, rows_processed: int64, serialized_data: binary] |
130 | 2.65k | if (response_batch->num_rows() != 1) { |
131 | 0 | return Status::InternalError("Invalid CREATE response_batch: expected 1 row"); |
132 | 0 | } |
133 | | |
134 | 2.65k | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response_batch->column(0)); |
135 | 2.65k | if (!success_array->Value(0)) { |
136 | 0 | return make_udaf_failure_status(response_batch, "CREATE", place_id); |
137 | 0 | } |
138 | | |
139 | 2.65k | _created_place_id = place_id; |
140 | 2.65k | return Status::OK(); |
141 | 2.65k | } |
142 | | |
143 | | Status PythonUDAFClient::accumulate(int64_t place_id, bool is_single_place, |
144 | | const arrow::RecordBatch& input, int64_t row_start, |
145 | 2.63k | int64_t row_end) { |
146 | | // Validate input parameters |
147 | 2.63k | if (UNLIKELY(row_start < 0 || row_end < row_start || row_end > input.num_rows())) { |
148 | 0 | return Status::InvalidArgument( |
149 | 0 | "Invalid row range: row_start={}, row_end={}, input.num_rows={}", row_start, |
150 | 0 | row_end, input.num_rows()); |
151 | 0 | } |
152 | | |
153 | | // In multi-place mode, input RecordBatch must contain "places" column as last column |
154 | 2.63k | if (UNLIKELY(!is_single_place && |
155 | 2.63k | (input.num_columns() == 0 || |
156 | 2.63k | input.schema()->field(input.num_columns() - 1)->name() != "places"))) { |
157 | 0 | return Status::InternalError( |
158 | 0 | "In multi-place mode, input RecordBatch must contain 'places' column as the " |
159 | 0 | "last column"); |
160 | 0 | } |
161 | | |
162 | | // Create request batch: input data + NULL binary_data column |
163 | 2.63k | std::shared_ptr<arrow::RecordBatch> request_batch; |
164 | 2.63k | RETURN_IF_ERROR(_create_data_request_batch(input, &request_batch)); |
165 | | |
166 | | // Create metadata structure |
167 | 2.63k | UDAFMetadata metadata { |
168 | 2.63k | .meta_version = UDAF_METADATA_VERSION, |
169 | 2.63k | .operation = static_cast<uint8_t>(UDAFOperation::ACCUMULATE), |
170 | 2.63k | .is_single_place = static_cast<uint8_t>(is_single_place ? 1 : 0), |
171 | 2.63k | .place_id = place_id, |
172 | 2.63k | .row_start = row_start, |
173 | 2.63k | .row_end = row_end, |
174 | 2.63k | }; |
175 | | |
176 | | // Send to server with metadata in app_metadata |
177 | 2.63k | std::shared_ptr<arrow::RecordBatch> response; |
178 | 2.63k | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response)); |
179 | | |
180 | | // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
181 | 2.63k | if (response->num_rows() != 1) { |
182 | 0 | return Status::InternalError("Invalid ACCUMULATE response: expected 1 row"); |
183 | 0 | } |
184 | | |
185 | 2.63k | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0)); |
186 | 2.63k | auto rows_processed_array = std::static_pointer_cast<arrow::Int64Array>(response->column(1)); |
187 | | |
188 | 2.63k | if (!success_array->Value(0)) { |
189 | 1 | return make_udaf_failure_status(response, "ACCUMULATE", place_id); |
190 | 1 | } |
191 | | |
192 | | // Arrow Flight buffers may be unaligned after IPC deserialization. |
193 | 2.63k | const auto* raw_ptr = rows_processed_array->raw_values(); |
194 | 2.63k | if (raw_ptr == nullptr) { |
195 | 0 | return Status::InternalError("ACCUMULATE response has null rows_processed array"); |
196 | 0 | } |
197 | 2.63k | int64_t rows_processed = unaligned_load<int64_t>(raw_ptr); |
198 | | |
199 | 2.63k | int64_t expected_rows = row_end - row_start; |
200 | | |
201 | 2.63k | if (rows_processed < expected_rows) { |
202 | 0 | return Status::InternalError( |
203 | 0 | "ACCUMULATE operation only processed {} out of {} rows for place_id={}", |
204 | 0 | rows_processed, expected_rows, place_id); |
205 | 0 | } |
206 | 2.63k | return Status::OK(); |
207 | 2.63k | } |
208 | | |
209 | | Status PythonUDAFClient::serialize(int64_t place_id, |
210 | 579 | std::shared_ptr<arrow::Buffer>* serialized_state) { |
211 | 579 | std::shared_ptr<arrow::RecordBatch> request_batch; |
212 | 579 | RETURN_IF_ERROR(_get_empty_request_batch(&request_batch)); |
213 | | |
214 | 579 | UDAFMetadata metadata { |
215 | 579 | .meta_version = UDAF_METADATA_VERSION, |
216 | 579 | .operation = static_cast<uint8_t>(UDAFOperation::SERIALIZE), |
217 | 579 | .is_single_place = 0, |
218 | 579 | .place_id = place_id, |
219 | 579 | .row_start = 0, |
220 | 579 | .row_end = 0, |
221 | 579 | }; |
222 | | |
223 | 579 | std::shared_ptr<arrow::RecordBatch> response; |
224 | 579 | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response)); |
225 | | |
226 | | // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
227 | 579 | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0)); |
228 | 579 | auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response->column(2)); |
229 | | |
230 | 579 | if (!success_array->Value(0)) { |
231 | 0 | return make_udaf_failure_status(response, "SERIALIZE", place_id); |
232 | 0 | } |
233 | | |
234 | | // Arrow Flight buffers may be unaligned after IPC deserialization. |
235 | 579 | const auto* offsets = data_array->raw_value_offsets(); |
236 | 579 | if (offsets == nullptr) { |
237 | 0 | return Status::InternalError("SERIALIZE response has null offsets"); |
238 | 0 | } |
239 | 579 | int32_t offset_start = unaligned_load<int32_t>(offsets); |
240 | 579 | int32_t offset_end = unaligned_load<int32_t>(offsets + 1); |
241 | | |
242 | 579 | int32_t length = offset_end - offset_start; |
243 | | |
244 | 579 | if (length == 0) { |
245 | 0 | return Status::InternalError("SERIALIZE operation returned empty data for place_id={}", |
246 | 0 | place_id); |
247 | 0 | } |
248 | | |
249 | 579 | const uint8_t* data = data_array->value_data()->data() + offset_start; |
250 | 579 | *serialized_state = arrow::Buffer::Wrap(data, length); |
251 | 579 | return Status::OK(); |
252 | 579 | } |
253 | | |
254 | | Status PythonUDAFClient::merge(int64_t place_id, |
255 | 579 | const std::shared_ptr<arrow::Buffer>& serialized_state) { |
256 | 579 | std::shared_ptr<arrow::RecordBatch> request_batch; |
257 | 579 | RETURN_IF_ERROR(_create_binary_request_batch(serialized_state, &request_batch)); |
258 | | |
259 | 579 | UDAFMetadata metadata { |
260 | 579 | .meta_version = UDAF_METADATA_VERSION, |
261 | 579 | .operation = static_cast<uint8_t>(UDAFOperation::MERGE), |
262 | 579 | .is_single_place = 0, |
263 | 579 | .place_id = place_id, |
264 | 579 | .row_start = 0, |
265 | 579 | .row_end = 0, |
266 | 579 | }; |
267 | | |
268 | 579 | std::shared_ptr<arrow::RecordBatch> response; |
269 | 579 | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response)); |
270 | | |
271 | | // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
272 | 579 | if (response->num_rows() != 1) { |
273 | 0 | return Status::InternalError("Invalid MERGE response: expected 1 row"); |
274 | 0 | } |
275 | | |
276 | 579 | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0)); |
277 | 579 | if (!success_array->Value(0)) { |
278 | 1 | return make_udaf_failure_status(response, "MERGE", place_id); |
279 | 1 | } |
280 | | |
281 | 578 | return Status::OK(); |
282 | 579 | } |
283 | | |
284 | 2.28k | Status PythonUDAFClient::finalize(int64_t place_id, std::shared_ptr<arrow::RecordBatch>* output) { |
285 | 2.28k | std::shared_ptr<arrow::RecordBatch> request_batch; |
286 | 2.28k | RETURN_IF_ERROR(_get_empty_request_batch(&request_batch)); |
287 | | |
288 | 2.28k | UDAFMetadata metadata { |
289 | 2.28k | .meta_version = UDAF_METADATA_VERSION, |
290 | 2.28k | .operation = static_cast<uint8_t>(UDAFOperation::FINALIZE), |
291 | 2.28k | .is_single_place = 0, |
292 | 2.28k | .place_id = place_id, |
293 | 2.28k | .row_start = 0, |
294 | 2.28k | .row_end = 0, |
295 | 2.28k | }; |
296 | | |
297 | 2.28k | std::shared_ptr<arrow::RecordBatch> response_batch; |
298 | 2.28k | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response_batch)); |
299 | | |
300 | | // Parse unified response_batch: [success: bool, rows_processed: int64, serialized_data: binary] |
301 | 2.28k | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response_batch->column(0)); |
302 | 2.28k | auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response_batch->column(2)); |
303 | | |
304 | 2.28k | if (!success_array->Value(0)) { |
305 | 2 | return make_udaf_failure_status(response_batch, "FINALIZE", place_id); |
306 | 2 | } |
307 | | |
308 | | // Arrow Flight buffers may be unaligned after IPC deserialization. |
309 | 2.28k | const auto* offsets = data_array->raw_value_offsets(); |
310 | 2.28k | if (offsets == nullptr) { |
311 | 0 | return Status::InternalError("FINALIZE response has null offsets"); |
312 | 0 | } |
313 | 2.28k | int32_t offset_start = unaligned_load<int32_t>(offsets); |
314 | 2.28k | int32_t offset_end = unaligned_load<int32_t>(offsets + 1); |
315 | | |
316 | 2.28k | int32_t length = offset_end - offset_start; |
317 | | |
318 | 2.28k | if (length == 0) { |
319 | 0 | return Status::InternalError("FINALIZE operation returned empty data for place_id={}", |
320 | 0 | place_id); |
321 | 0 | } |
322 | | |
323 | 2.28k | const uint8_t* data = data_array->value_data()->data() + offset_start; |
324 | 2.28k | auto buffer = arrow::Buffer::Wrap(data, length); |
325 | 2.28k | auto input_stream = std::make_shared<arrow::io::BufferReader>(buffer); |
326 | | |
327 | 2.28k | auto reader_result = arrow::ipc::RecordBatchStreamReader::Open(input_stream); |
328 | 2.28k | if (UNLIKELY(!reader_result.ok())) { |
329 | 0 | return Status::InternalError("Failed to deserialize FINALIZE result: {}", |
330 | 0 | reader_result.status().message()); |
331 | 0 | } |
332 | 2.28k | auto reader = std::move(reader_result).ValueOrDie(); |
333 | | |
334 | 2.28k | auto batch_result = reader->Next(); |
335 | 2.28k | if (UNLIKELY(!batch_result.ok())) { |
336 | 0 | return Status::InternalError("Failed to read FINALIZE result: {}", |
337 | 0 | batch_result.status().message()); |
338 | 0 | } |
339 | | |
340 | 2.28k | *output = std::move(batch_result).ValueOrDie(); |
341 | | |
342 | 2.28k | return Status::OK(); |
343 | 2.28k | } |
344 | | |
345 | 670 | Status PythonUDAFClient::reset(int64_t place_id) { |
346 | 670 | std::shared_ptr<arrow::RecordBatch> request_batch; |
347 | 670 | RETURN_IF_ERROR(_get_empty_request_batch(&request_batch)); |
348 | | |
349 | 670 | UDAFMetadata metadata { |
350 | 670 | .meta_version = UDAF_METADATA_VERSION, |
351 | 670 | .operation = static_cast<uint8_t>(UDAFOperation::RESET), |
352 | 670 | .is_single_place = 0, |
353 | 670 | .place_id = place_id, |
354 | 670 | .row_start = 0, |
355 | 670 | .row_end = 0, |
356 | 670 | }; |
357 | | |
358 | 670 | std::shared_ptr<arrow::RecordBatch> response; |
359 | 670 | RETURN_IF_ERROR(_send_request(metadata, request_batch, &response)); |
360 | | |
361 | | // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
362 | 670 | if (response->num_rows() != 1) { |
363 | 0 | return Status::InternalError("Invalid RESET response: expected 1 row"); |
364 | 0 | } |
365 | | |
366 | 670 | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0)); |
367 | 670 | if (!success_array->Value(0)) { |
368 | 0 | return make_udaf_failure_status(response, "RESET", place_id); |
369 | 0 | } |
370 | | |
371 | 670 | return Status::OK(); |
372 | 670 | } |
373 | | |
374 | 2.65k | Status PythonUDAFClient::destroy(int64_t place_id) { |
375 | 2.65k | std::shared_ptr<arrow::RecordBatch> request_batch; |
376 | 2.65k | RETURN_IF_ERROR(_get_empty_request_batch(&request_batch)); |
377 | | |
378 | 2.65k | UDAFMetadata metadata { |
379 | 2.65k | .meta_version = UDAF_METADATA_VERSION, |
380 | 2.65k | .operation = static_cast<uint8_t>(UDAFOperation::DESTROY), |
381 | 2.65k | .is_single_place = 0, |
382 | 2.65k | .place_id = place_id, |
383 | 2.65k | .row_start = 0, |
384 | 2.65k | .row_end = 0, |
385 | 2.65k | }; |
386 | | |
387 | 2.65k | std::shared_ptr<arrow::RecordBatch> response; |
388 | 2.65k | Status st = _send_request(metadata, request_batch, &response); |
389 | | |
390 | | // Always clear tracking, even if RPC failed |
391 | 2.65k | _created_place_id.reset(); |
392 | | |
393 | 2.65k | if (!st.ok()) { |
394 | 0 | LOG(WARNING) << "Failed to destroy place_id=" << place_id << ": " << st.to_string(); |
395 | 0 | return st; |
396 | 0 | } |
397 | | |
398 | | // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
399 | 2.65k | if (response->num_rows() != 1) { |
400 | 0 | return Status::InternalError("Invalid DESTROY response: expected 1 row"); |
401 | 0 | } |
402 | | |
403 | 2.65k | auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0)); |
404 | | |
405 | 2.65k | if (!success_array->Value(0)) { |
406 | 0 | LOG(WARNING) << "DESTROY operation failed for place_id=" << place_id; |
407 | 0 | return make_udaf_failure_status(response, "DESTROY", place_id); |
408 | 0 | } |
409 | | |
410 | 2.65k | return Status::OK(); |
411 | 2.65k | } |
412 | | |
413 | 2.68k | Status PythonUDAFClient::close() { |
414 | 2.68k | if (!_inited || !_writer) return Status::OK(); |
415 | | |
416 | | // Destroy the place if it exists (cleanup on client destruction) |
417 | 2.65k | if (_created_place_id.has_value()) { |
418 | 0 | int64_t place_id = _created_place_id.value(); |
419 | 0 | Status st = destroy(place_id); |
420 | 0 | if (!st.ok()) { |
421 | 0 | LOG(WARNING) << "Failed to destroy place_id=" << place_id |
422 | 0 | << " during close: " << st.to_string(); |
423 | | // Clear tracking even on failure to prevent issues in base class close |
424 | 0 | _created_place_id.reset(); |
425 | 0 | } |
426 | 0 | } |
427 | | |
428 | 2.65k | return PythonClient::close(); |
429 | 2.68k | } |
430 | | |
431 | | Status PythonUDAFClient::_send_request(const UDAFMetadata& metadata, |
432 | | const std::shared_ptr<arrow::RecordBatch>& request_batch, |
433 | 12.0k | std::shared_ptr<arrow::RecordBatch>* response_batch) { |
434 | 12.0k | DCHECK(response_batch != nullptr); |
435 | | |
436 | | // Create app_metadata buffer from metadata struct |
437 | 12.0k | auto app_metadata = |
438 | 12.0k | arrow::Buffer::Wrap(reinterpret_cast<const uint8_t*>(&metadata), sizeof(metadata)); |
439 | | |
440 | 12.0k | std::lock_guard<std::mutex> lock(_operation_mutex); |
441 | | |
442 | | // Check if writer/reader are still valid (they could be reset by handle_error) |
443 | 12.0k | if (UNLIKELY(!_writer || !_reader)) { |
444 | 0 | return Status::InternalError("{} writer/reader have been closed due to previous error", |
445 | 0 | _operation_name); |
446 | 0 | } |
447 | | |
448 | | // Begin stream on first call (using data schema: argument_types + places + binary_data) |
449 | 12.0k | if (UNLIKELY(!_begin)) { |
450 | 2.68k | auto begin_res = _writer->Begin(_schema); |
451 | 2.68k | if (!begin_res.ok()) { |
452 | 28 | return handle_error(begin_res); |
453 | 28 | } |
454 | 2.65k | _begin = true; |
455 | 2.65k | } |
456 | | |
457 | | // Write batch with metadata in app_metadata |
458 | 12.0k | auto write_res = _writer->WriteWithMetadata(*request_batch, app_metadata); |
459 | 12.0k | if (!write_res.ok()) { |
460 | 0 | return handle_error(write_res); |
461 | 0 | } |
462 | | |
463 | | // Read unified response: [success: bool, rows_processed: int64, serialized_data: binary] |
464 | 12.0k | auto read_res = _reader->Next(); |
465 | 12.0k | if (!read_res.ok()) { |
466 | 0 | return handle_error(read_res.status()); |
467 | 0 | } |
468 | | |
469 | 12.0k | arrow::flight::FlightStreamChunk chunk = std::move(*read_res); |
470 | 12.0k | if (!chunk.data) { |
471 | 0 | return Status::InternalError("Received empty RecordBatch from {} server", _operation_name); |
472 | 0 | } |
473 | | |
474 | | // Validate unified response schema |
475 | 12.0k | if (!chunk.data->schema()->Equals(kUnifiedUDAFResponseSchema)) { |
476 | 0 | return Status::InternalError( |
477 | 0 | "Invalid response schema: expected [success: bool, rows_processed: int64, " |
478 | 0 | "serialized_data: binary], got {}", |
479 | 0 | chunk.data->schema()->ToString()); |
480 | 0 | } |
481 | | |
482 | 12.0k | *response_batch = std::move(chunk.data); |
483 | 12.0k | return Status::OK(); |
484 | 12.0k | } |
485 | | |
486 | | Status PythonUDAFClient::_create_data_request_batch(const arrow::RecordBatch& input_data, |
487 | 2.64k | std::shared_ptr<arrow::RecordBatch>* out) { |
488 | | // Determine if input has places column |
489 | 2.64k | int num_input_columns = input_data.num_columns(); |
490 | 2.64k | bool has_places = false; |
491 | 2.64k | if (num_input_columns > 0 && |
492 | 2.64k | input_data.schema()->field(num_input_columns - 1)->name() == "places") { |
493 | 778 | has_places = true; |
494 | 778 | } |
495 | | |
496 | | // Expected schema structure: [argument_types..., places, binary_data] |
497 | | // - Input in single-place mode: [argument_types...] |
498 | | // - Input in multi-place mode: [argument_types..., places] |
499 | 2.64k | std::vector<std::shared_ptr<arrow::Array>> columns; |
500 | | // Copy argument_types columns |
501 | 2.64k | int num_arg_columns = has_places ? (num_input_columns - 1) : num_input_columns; |
502 | | |
503 | 5.89k | for (int i = 0; i < num_arg_columns; ++i) { |
504 | 3.25k | columns.push_back(input_data.column(i)); |
505 | 3.25k | } |
506 | | |
507 | | // Add places column |
508 | 2.64k | if (has_places) { |
509 | | // Use existing places column from input |
510 | 778 | columns.push_back(input_data.column(num_input_columns - 1)); |
511 | 1.86k | } else { |
512 | | // Create NULL places column for single-place mode |
513 | 1.86k | arrow::Int64Builder places_builder(ExecEnv::GetInstance()->arrow_memory_pool()); |
514 | 1.86k | std::shared_ptr<arrow::Array> places_array; |
515 | 1.86k | RETURN_DORIS_STATUS_IF_ERROR(places_builder.AppendNulls(input_data.num_rows())); |
516 | 1.86k | RETURN_DORIS_STATUS_IF_ERROR(places_builder.Finish(&places_array)); |
517 | 1.86k | columns.push_back(places_array); |
518 | 1.86k | } |
519 | | |
520 | | // Add NULL binary_data column |
521 | 2.64k | arrow::BinaryBuilder binary_builder(ExecEnv::GetInstance()->arrow_memory_pool()); |
522 | 2.64k | std::shared_ptr<arrow::Array> binary_array; |
523 | 2.64k | RETURN_DORIS_STATUS_IF_ERROR(binary_builder.AppendNulls(input_data.num_rows())); |
524 | 2.64k | RETURN_DORIS_STATUS_IF_ERROR(binary_builder.Finish(&binary_array)); |
525 | 2.64k | columns.push_back(binary_array); |
526 | | |
527 | 2.64k | *out = arrow::RecordBatch::Make(_schema, input_data.num_rows(), columns); |
528 | 2.64k | return Status::OK(); |
529 | 2.64k | } |
530 | | |
531 | | Status PythonUDAFClient::_create_binary_request_batch( |
532 | | const std::shared_ptr<arrow::Buffer>& binary_data, |
533 | 579 | std::shared_ptr<arrow::RecordBatch>* out) { |
534 | 579 | std::vector<std::shared_ptr<arrow::Array>> columns; |
535 | | |
536 | | // Create NULL arrays for data columns (all columns except the last binary_data column) |
537 | | // Schema: [argument_types..., places, binary_data] |
538 | 579 | int num_data_columns = _schema->num_fields() - 1; |
539 | 1.91k | for (int i = 0; i < num_data_columns; ++i) { |
540 | 1.33k | std::unique_ptr<arrow::ArrayBuilder> builder; |
541 | 1.33k | std::shared_ptr<arrow::Array> null_array; |
542 | 1.33k | RETURN_DORIS_STATUS_IF_ERROR(arrow::MakeBuilder(ExecEnv::GetInstance()->arrow_memory_pool(), |
543 | 1.33k | _schema->field(i)->type(), &builder)); |
544 | 1.33k | RETURN_DORIS_STATUS_IF_ERROR(builder->AppendNull()); |
545 | 1.33k | RETURN_DORIS_STATUS_IF_ERROR(builder->Finish(&null_array)); |
546 | 1.33k | columns.push_back(null_array); |
547 | 1.33k | } |
548 | | |
549 | | // Create binary_data column |
550 | 579 | arrow::BinaryBuilder binary_builder(ExecEnv::GetInstance()->arrow_memory_pool()); |
551 | 579 | std::shared_ptr<arrow::Array> binary_array; |
552 | 579 | RETURN_DORIS_STATUS_IF_ERROR( |
553 | 579 | binary_builder.Append(binary_data->data(), static_cast<int32_t>(binary_data->size()))); |
554 | 579 | RETURN_DORIS_STATUS_IF_ERROR(binary_builder.Finish(&binary_array)); |
555 | 579 | columns.push_back(binary_array); |
556 | | |
557 | 579 | *out = arrow::RecordBatch::Make(_schema, 1, columns); |
558 | 579 | return Status::OK(); |
559 | 579 | } |
560 | | |
561 | 8.86k | Status PythonUDAFClient::_get_empty_request_batch(std::shared_ptr<arrow::RecordBatch>* out) { |
562 | | // Return cached batch if already created |
563 | 8.86k | if (_empty_request_batch) { |
564 | 6.18k | *out = _empty_request_batch; |
565 | 6.18k | return Status::OK(); |
566 | 6.18k | } |
567 | | |
568 | | // Create empty batch on first use (all columns NULL, 1 row) |
569 | 2.67k | std::vector<std::shared_ptr<arrow::Array>> columns; |
570 | | |
571 | 11.4k | for (int i = 0; i < _schema->num_fields(); ++i) { |
572 | 8.81k | auto field = _schema->field(i); |
573 | 8.81k | std::unique_ptr<arrow::ArrayBuilder> builder; |
574 | 8.81k | std::shared_ptr<arrow::Array> null_array; |
575 | 8.81k | RETURN_DORIS_STATUS_IF_ERROR(arrow::MakeBuilder(ExecEnv::GetInstance()->arrow_memory_pool(), |
576 | 8.81k | field->type(), &builder)); |
577 | 8.81k | RETURN_DORIS_STATUS_IF_ERROR(builder->AppendNull()); |
578 | 8.81k | RETURN_DORIS_STATUS_IF_ERROR(builder->Finish(&null_array)); |
579 | 8.81k | columns.push_back(null_array); |
580 | 8.81k | } |
581 | | |
582 | 2.67k | _empty_request_batch = arrow::RecordBatch::Make(_schema, 1, columns); |
583 | 2.67k | *out = _empty_request_batch; |
584 | 2.67k | return Status::OK(); |
585 | 2.67k | } |
586 | | |
587 | | } // namespace doris |