Coverage Report

Created: 2026-09-16 19:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/aggregate/aggregate_function_java_udaf.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 <jni.h>
21
#include <unistd.h>
22
23
#include <cstdint>
24
#include <memory>
25
26
#include "absl/strings/substitute.h"
27
#include "common/cast_set.h"
28
#include "common/compiler_util.h"
29
#include "common/exception.h"
30
#include "common/logging.h"
31
#include "common/status.h"
32
#include "core/column/column_array.h"
33
#include "core/column/column_map.h"
34
#include "core/column/column_string.h"
35
#include "core/field.h"
36
#include "core/string_ref.h"
37
#include "core/types.h"
38
#include "exprs/aggregate/aggregate_function.h"
39
#include "format/jni/jni_data_bridge.h"
40
#include "runtime/user_function_cache.h"
41
#include "util/jni-util.h"
42
#include "util/jni_plugin_registry.h"
43
44
namespace doris {
45
46
const char* UDAF_EXECUTOR_CLOSE_SIGNATURE = "()V";
47
const char* UDAF_EXECUTOR_DESTROY_SIGNATURE = "()V";
48
const char* UDAF_EXECUTOR_ADD_SIGNATURE = "(ZIIJILjava/util/Map;)V";
49
const char* UDAF_EXECUTOR_SERIALIZE_SIGNATURE = "(J)[B";
50
const char* UDAF_EXECUTOR_MERGE_SIGNATURE = "(J[B)V";
51
const char* UDAF_EXECUTOR_GET_SIGNATURE = "(JLjava/util/Map;)J";
52
const char* UDAF_EXECUTOR_RESET_SIGNATURE = "(J)V";
53
// Calling Java method about those signature means: "(argument-types)return-type"
54
// https://www.iitk.ac.in/esc101/05Aug/tutorial/native1.1/implementing/method.html
55
56
struct AggregateJavaUdafData {
57
public:
58
0
    AggregateJavaUdafData() = default;
59
0
    AggregateJavaUdafData(int64_t num_args) { cast_set(argument_size, num_args); }
60
61
0
    ~AggregateJavaUdafData() = default;
62
63
0
    Status close_and_delete_object() {
64
0
        if (!can_call(executor_close_id) || executor_closed) {
65
0
            return Status::OK();
66
0
        }
67
0
        JNIEnv* env = nullptr;
68
69
0
        RETURN_IF_ERROR(Jni::Env::Get(&env));
70
71
        // Raised before the call, not after: UdafExecutor.close() drops its state map, so a
72
        // close that threw halfway is still a close as far as everything below is concerned.
73
0
        executor_closed = true;
74
0
        auto st = executor_obj.call_nonvirtual_void_method(env, executor_cl, executor_close_id)
75
0
                          .call();
76
0
        if (!st.ok()) {
77
0
            LOG(WARNING) << "Failed to close JAVA UDAF: " << st.to_string();
78
0
            return st;
79
0
        }
80
0
        return Status::OK();
81
0
    }
82
83
0
    Status init_udaf(const TFunction& fn, const std::string& local_location) {
84
0
        JNIEnv* env = nullptr;
85
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf init_udaf function");
86
87
0
        TJavaUdfExecutorCtorParams ctor_params;
88
0
        ctor_params.__set_fn(fn);
89
0
        if (!fn.hdfs_location.empty() && !fn.checksum.empty()) {
90
0
            ctor_params.__set_location(local_location);
91
0
        }
92
93
0
        Jni::LocalArray ctor_params_bytes;
94
0
        RETURN_IF_ERROR(Jni::Util::SerializeThriftMsg(env, &ctor_params, &ctor_params_bytes));
95
0
        RETURN_IF_ERROR(Jni::PluginRegistry::create_udf_executor(
96
0
                env, Jni::plugin::JAVA_UDF_AGGREGATE, ctor_params_bytes, &executor_obj,
97
0
                &executor_cl));
98
        // From here the Java executor is alive, so a failure below has to close it: the
99
        // caller's cleanup path cannot, since it is exactly the method ids resolved here that
100
        // it would need to do so.
101
0
        if (Status status = register_func_id(env); !status.ok()) {
102
0
            LOG(WARNING) << "Java-Udaf register_func_id function failed: " << status.to_string();
103
            // Logged, not discarded: this is the failure that says the Java executor object was
104
            // leaked, and it is the only place it can be seen. register_func_id's own failure is
105
            // the one returned, since that is what the caller asked for.
106
0
            WARN_IF_ERROR(close_and_delete_object(),
107
0
                          "failed to close the Java UDAF executor after register_func_id failed");
108
0
            return status;
109
0
        }
110
0
        return Status::OK();
111
0
    }
112
113
    Status add(int64_t places_address, bool is_single_place, const IColumn** columns,
114
               int64_t row_num_start, int64_t row_num_end, const DataTypes& argument_types,
115
0
               int64_t place_offset) {
116
0
        JNIEnv* env = nullptr;
117
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf add function");
118
119
0
        Block input_block;
120
0
        for (size_t i = 0; i < argument_size; ++i) {
121
0
            input_block.insert(ColumnWithTypeAndName(columns[i]->get_ptr(), argument_types[i],
122
0
                                                     std::to_string(i)));
123
0
        }
124
0
        std::unique_ptr<long[]> input_table;
125
0
        RETURN_IF_ERROR(JniDataBridge::to_java_table(&input_block, input_table));
126
0
        auto input_table_schema = JniDataBridge::parse_table_schema(&input_block);
127
0
        std::map<String, String> input_params = {
128
0
                {"meta_address", std::to_string((long)input_table.get())},
129
0
                {"required_fields", input_table_schema.first},
130
0
                {"columns_types", input_table_schema.second}};
131
132
0
        Jni::LocalObject input_map;
133
0
        RETURN_IF_ERROR(Jni::Util::convert_to_java_map(env, input_params, &input_map));
134
        // invoke add batch
135
        // Keep consistent with the function signature of executor_add_batch_id.
136
137
0
        return executor_obj.call_void_method(env, executor_add_batch_id)
138
0
                .with_arg((jboolean)is_single_place)
139
0
                .with_arg(cast_set<jint>(row_num_start))
140
0
                .with_arg(cast_set<jint>(row_num_end))
141
0
                .with_arg(cast_set<jlong>(places_address))
142
0
                .with_arg(cast_set<jint>(place_offset))
143
0
                .with_arg(input_map)
144
0
                .call();
145
0
    }
146
147
0
    Status merge(const AggregateJavaUdafData& rhs, int64_t place) {
148
0
        JNIEnv* env = nullptr;
149
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf merge function");
150
0
        serialize_data = rhs.serialize_data;
151
0
        Jni::LocalArray byte_arr;
152
0
        RETURN_IF_ERROR(Jni::Util::WriteBufferToByteArray(env, (jbyte*)serialize_data.data(),
153
0
                                                          cast_set<jsize>(serialize_data.length()),
154
0
                                                          &byte_arr));
155
156
0
        return executor_obj.call_nonvirtual_void_method(env, executor_cl, executor_merge_id)
157
0
                .with_arg((jlong)place)
158
0
                .with_arg(byte_arr)
159
0
                .call();
160
0
    }
161
162
0
    Status write(BufferWritable& buf, int64_t place) {
163
0
        JNIEnv* env = nullptr;
164
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf write function");
165
        // TODO: Here get a byte[] from FE serialize, and then allocate the same length bytes to
166
        // save it in BE, Because i'm not sure there is a way to use the byte[] not allocate again.
167
0
        Jni::LocalArray arr;
168
0
        RETURN_IF_ERROR(
169
0
                executor_obj.call_nonvirtual_object_method(env, executor_cl, executor_serialize_id)
170
0
                        .with_arg((jlong)place)
171
0
                        .call(&arr));
172
173
0
        jsize len = 0;
174
0
        RETURN_IF_ERROR(arr.get_length(env, &len));
175
0
        serialize_data.resize(len);
176
0
        RETURN_IF_ERROR(arr.get_byte_elements(env, 0, len,
177
0
                                              reinterpret_cast<jbyte*>(serialize_data.data())));
178
0
        buf.write_binary(serialize_data);
179
0
        return Status::OK();
180
0
    }
181
182
0
    Status reset(int64_t place) {
183
0
        JNIEnv* env = nullptr;
184
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf reset function");
185
0
        return executor_obj.call_nonvirtual_void_method(env, executor_cl, executor_reset_id)
186
0
                .with_arg(cast_set<jlong>(place))
187
0
                .call();
188
0
    }
189
190
0
    void read(BufferReadable& buf) { buf.read_binary(serialize_data); }
191
192
0
    Status destroy() {
193
        // Also once the executor has been closed: UdafExecutor.close() sets its state map to
194
        // null and destroy() walks that map. AggregateJavaUdaf::create() calls this right after
195
        // a failed init_udaf(), which closes - so without this the tail of the id resolution
196
        // (a failure on the last id, with close and destroy both bound already) would run
197
        // destroy() on a closed executor and swallow the NPE that comes back.
198
0
        if (!can_call(executor_destroy_id) || executor_closed) {
199
0
            return Status::OK();
200
0
        }
201
0
        JNIEnv* env = nullptr;
202
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf destroy function");
203
0
        return executor_obj.call_nonvirtual_void_method(env, executor_cl, executor_destroy_id)
204
0
                .call();
205
0
    }
206
207
0
    Status get(IColumn& to, const DataTypePtr& result_type, int64_t place) const {
208
0
        JNIEnv* env = nullptr;
209
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Jni::Env::Get(&env), "Java-Udaf get value function");
210
211
0
        Block output_block;
212
0
        output_block.insert(
213
0
                ColumnWithTypeAndName(result_type->create_column(), result_type, "_result_"));
214
0
        auto output_table_schema = JniDataBridge::parse_table_schema(&output_block);
215
0
        std::string output_nullable = result_type->is_nullable() ? "true" : "false";
216
0
        std::map<String, String> output_params = {{"is_nullable", output_nullable},
217
0
                                                  {"required_fields", output_table_schema.first},
218
0
                                                  {"columns_types", output_table_schema.second}};
219
220
0
        Jni::LocalObject output_map;
221
0
        RETURN_IF_ERROR(Jni::Util::convert_to_java_map(env, output_params, &output_map));
222
0
        long output_address;
223
224
0
        RETURN_IF_ERROR(executor_obj.call_long_method(env, executor_get_value_id)
225
0
                                .with_arg(cast_set<jlong>(place))
226
0
                                .with_arg(output_map)
227
0
                                .call(&output_address));
228
229
0
        RETURN_IF_ERROR(JniDataBridge::fill_block(&output_block, {0}, output_address));
230
0
        const auto& result_column = output_block.get_by_position(0).column;
231
0
        DORIS_CHECK(result_column->size() == 1);
232
0
        to.insert_from(*result_column, 0);
233
0
        return Status::OK();
234
0
    }
235
236
private:
237
    /**
238
     * Whether a JNI call through this method id can be made at all.
239
     *
240
     * The clean-up path is reached with nothing bound: init_udaf() creates the executor before it
241
     * resolves any method id, and AggregateJavaUdaf::create() calls destroy() when init_udaf()
242
     * fails - through a null receiver, a null class and a null method id if the Java factory was
243
     * what threw. That is undefined behaviour rather than an error, because the two DCHECKs in
244
     * the JNI wrappers that would catch it are compiled out of a release build. The scalar and
245
     * UDTF paths guard the same window with JniContext::open_successes.
246
     */
247
0
    bool can_call(const Jni::MethodId& method_id) const {
248
0
        return !executor_obj.uninitialized() && !executor_cl.uninitialized() &&
249
0
               !method_id.uninitialized();
250
0
    }
251
252
    // Whether close() has already been called on the Java executor. Nothing that touches its
253
    // state may run afterwards.
254
    bool executor_closed = false;
255
256
0
    Status register_func_id(JNIEnv* env) {
257
        // close first, and deliberately: the executor object already exists by the time this
258
        // runs, so every resolution below is a failure that has to close it - and
259
        // close_and_delete_object() is itself gated on can_call(executor_close_id). Resolving
260
        // any other id before this one leaves a window where the executor is alive and there
261
        // is no way left to close it.
262
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "close", UDAF_EXECUTOR_CLOSE_SIGNATURE,
263
0
                                               &executor_close_id));
264
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "reset", UDAF_EXECUTOR_RESET_SIGNATURE,
265
0
                                               &executor_reset_id));
266
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "merge", UDAF_EXECUTOR_MERGE_SIGNATURE,
267
0
                                               &executor_merge_id));
268
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "serialize", UDAF_EXECUTOR_SERIALIZE_SIGNATURE,
269
0
                                               &executor_serialize_id));
270
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "getValue", UDAF_EXECUTOR_GET_SIGNATURE,
271
0
                                               &executor_get_value_id));
272
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "destroy", UDAF_EXECUTOR_DESTROY_SIGNATURE,
273
0
                                               &executor_destroy_id));
274
0
        RETURN_IF_ERROR(executor_cl.get_method(env, "addBatch", UDAF_EXECUTOR_ADD_SIGNATURE,
275
0
                                               &executor_add_batch_id));
276
277
0
        return Status::OK();
278
0
    }
279
280
private:
281
    // TODO: too many variables are hold, it's causing a lot of memory waste
282
    // it's time to refactor it.
283
    Jni::GlobalClass executor_cl;
284
    Jni::GlobalObject executor_obj;
285
286
    Jni::MethodId executor_add_batch_id;
287
    Jni::MethodId executor_merge_id;
288
    Jni::MethodId executor_serialize_id;
289
    Jni::MethodId executor_get_value_id;
290
    Jni::MethodId executor_reset_id;
291
    Jni::MethodId executor_close_id;
292
    Jni::MethodId executor_destroy_id;
293
    int argument_size = 0;
294
    std::string serialize_data;
295
};
296
297
class AggregateJavaUdaf final
298
        : public IAggregateFunctionDataHelper<AggregateJavaUdafData, AggregateJavaUdaf>,
299
          VarargsExpression,
300
          NullableAggregateFunction {
301
public:
302
    ENABLE_FACTORY_CREATOR(AggregateJavaUdaf);
303
    AggregateJavaUdaf(const TFunction& fn, const DataTypes& argument_types_,
304
                      const DataTypePtr& return_type)
305
0
            : IAggregateFunctionDataHelper(argument_types_),
306
0
              _fn(fn),
307
0
              _return_type(return_type),
308
0
              _first_created(true),
309
0
              _exec_place(nullptr) {}
310
0
    ~AggregateJavaUdaf() override = default;
311
312
    static AggregateFunctionPtr create(const TFunction& fn, const DataTypes& argument_types_,
313
0
                                       const DataTypePtr& return_type) {
314
0
        return std::make_shared<AggregateJavaUdaf>(fn, argument_types_, return_type);
315
0
    }
316
    //Note: The condition is added because maybe the BE can't find java-udaf impl jar
317
    //So need to check as soon as possible, before call Data function
318
0
    Status check_udaf(const TFunction& fn) {
319
0
        auto function_cache = UserFunctionCache::instance();
320
        // get jar path if both file path location and checksum are null
321
0
        if (!fn.hdfs_location.empty() && !fn.checksum.empty()) {
322
0
            return function_cache->get_jarpath(fn.id, fn.hdfs_location, fn.checksum,
323
0
                                               &_local_location);
324
0
        } else {
325
0
            return Status::OK();
326
0
        }
327
0
    }
328
329
0
    void create(AggregateDataPtr __restrict place) const override {
330
0
        new (place) Data(argument_types.size());
331
0
        if (_first_created) {
332
0
            Status status = this->data(place).init_udaf(_fn, _local_location);
333
0
            _first_created = false;
334
0
            _exec_place = place;
335
0
            if (UNLIKELY(!status.ok())) {
336
0
                static_cast<void>(this->data(place).destroy());
337
0
                this->data(place).~Data();
338
0
                throw doris::Exception(ErrorCode::INTERNAL_ERROR, status.to_string());
339
0
            }
340
0
        }
341
0
    }
342
343
    // To avoid multiple times JNI call, Here will destroy all data at once
344
0
    void destroy(AggregateDataPtr __restrict place) const noexcept override {
345
0
        if (place == _exec_place) {
346
0
            Status status = Status::OK();
347
0
            status = this->data(_exec_place).destroy();
348
0
            status = this->data(_exec_place).close_and_delete_object();
349
0
            _first_created = true;
350
0
            if (UNLIKELY(!status.ok())) {
351
0
                LOG(WARNING) << "Failed to destroy function: " << status.to_string();
352
0
            }
353
0
        }
354
0
        this->data(place).~Data();
355
0
    }
356
357
0
    String get_name() const override { return _fn.name.function_name; }
358
359
0
    DataTypePtr get_return_type() const override { return _return_type; }
360
361
    void add(AggregateDataPtr __restrict place, const IColumn** columns, ssize_t row_num,
362
0
             Arena&) const override {
363
0
        int64_t places_address = reinterpret_cast<int64_t>(place);
364
0
        Status st = this->data(_exec_place)
365
0
                            .add(places_address, true, columns, row_num, row_num + 1,
366
0
                                 argument_types, 0);
367
0
        if (UNLIKELY(!st.ok())) {
368
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
369
0
        }
370
0
    }
371
372
    void add_batch(size_t batch_size, AggregateDataPtr* places, size_t place_offset,
373
0
                   const IColumn** columns, Arena&, bool /*agg_many*/) const override {
374
0
        int64_t places_address = reinterpret_cast<int64_t>(places);
375
0
        Status st = this->data(_exec_place)
376
0
                            .add(places_address, false, columns, 0, batch_size, argument_types,
377
0
                                 place_offset);
378
0
        if (UNLIKELY(!st.ok())) {
379
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
380
0
        }
381
0
    }
382
383
    void add_batch_single_place(size_t batch_size, AggregateDataPtr place, const IColumn** columns,
384
0
                                Arena&) const override {
385
0
        int64_t places_address = reinterpret_cast<int64_t>(place);
386
0
        Status st = this->data(_exec_place)
387
0
                            .add(places_address, true, columns, 0, batch_size, argument_types, 0);
388
0
        if (UNLIKELY(!st.ok())) {
389
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
390
0
        }
391
0
    }
392
393
    void add_range_single_place(int64_t partition_start, int64_t partition_end, int64_t frame_start,
394
                                int64_t frame_end, AggregateDataPtr place, const IColumn** columns,
395
                                Arena&, UInt8* current_window_empty,
396
0
                                UInt8* current_window_has_inited) const override {
397
0
        frame_start = std::max<int64_t>(frame_start, partition_start);
398
0
        frame_end = std::min<int64_t>(frame_end, partition_end);
399
0
        int64_t places_address = reinterpret_cast<int64_t>(place);
400
0
        Status st = this->data(_exec_place)
401
0
                            .add(places_address, true, columns, frame_start, frame_end,
402
0
                                 argument_types, 0);
403
0
        if (frame_start >= frame_end) {
404
0
            if (!*current_window_has_inited) {
405
0
                *current_window_empty = true;
406
0
            }
407
0
        } else {
408
0
            *current_window_empty = false;
409
0
            *current_window_has_inited = true;
410
0
        }
411
0
        if (UNLIKELY(!st.ok())) {
412
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
413
0
        }
414
0
    }
415
416
0
    void reset(AggregateDataPtr place) const override {
417
0
        Status st = this->data(_exec_place).reset(reinterpret_cast<int64_t>(place));
418
0
        if (UNLIKELY(!st.ok())) {
419
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
420
0
        }
421
0
    }
422
423
    void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs,
424
0
               Arena&) const override {
425
0
        Status st =
426
0
                this->data(_exec_place).merge(this->data(rhs), reinterpret_cast<int64_t>(place));
427
0
        if (UNLIKELY(!st.ok())) {
428
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
429
0
        }
430
0
    }
431
432
0
    void serialize(ConstAggregateDataPtr __restrict place, BufferWritable& buf) const override {
433
0
        Status st = this->data(_exec_place).write(buf, reinterpret_cast<int64_t>(place));
434
0
        if (UNLIKELY(!st.ok())) {
435
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
436
0
        }
437
0
    }
438
439
    // during merge-finalized phase, for deserialize and merge firstly,
440
    // will call create --- deserialize --- merge --- destory for each rows ,
441
    // so need doing new (place), to create Data and read to buf, then call merge ,
442
    // and during destory about deserialize, because haven't done init_udaf,
443
    // so it's can't call ~Data, only to change _destory_deserialize flag.
444
    void deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
445
0
                     Arena&) const override {
446
0
        this->data(place).read(buf);
447
0
    }
448
449
0
    void insert_result_into(ConstAggregateDataPtr __restrict place, IColumn& to) const override {
450
0
        Status st = this->data(_exec_place).get(to, _return_type, reinterpret_cast<int64_t>(place));
451
0
        if (UNLIKELY(!st.ok())) {
452
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
453
0
        }
454
0
    }
455
456
private:
457
    TFunction _fn;
458
    DataTypePtr _return_type;
459
    mutable bool _first_created;
460
    mutable AggregateDataPtr _exec_place;
461
    std::string _local_location;
462
};
463
464
} // namespace doris