Coverage Report

Created: 2026-09-12 01:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/jvm_launcher.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 <pthread.h>
22
23
#include <string>
24
#include <vector>
25
26
#include "common/status.h"
27
28
namespace doris::Jni {
29
30
// Owns the JVM of this process: creates it and attaches threads to it.
31
//
32
// The JVM used to be bootstrapped by hadoop's libhdfs. Its getJNIEnv() created the VM out
33
// of the CLASSPATH and LIBHDFS_OPTS environment variables on first use, and registered a
34
// thread-local destructor that detached the thread again. Every Java feature of the BE -
35
// JNI table formats, Java UDFs, the plugin registry - therefore only worked because a
36
// file system client happened to be linked in, and the JVM was configured by that client
37
// rather than by the BE. This class takes the job over so that the two are independent:
38
// builds without libhdfs keep their Java features, and the BE decides how the JVM looks.
39
//
40
// The JVM is created on first use, never at startup: a BE that touches no Java feature
41
// pays for no JVM at all.
42
class JvmLauncher {
43
public:
44
    // Makes sure this process has a JVM, creating one if needed. Succeeding means exactly that
45
    // and nothing more: the jvm_* metrics are published along the way, because they describe the
46
    // JVM itself and a BE that reached one only through libhdfs must still export them, but
47
    // resolving the plugin SPI is NOT part of the answer. That comes out of doris-jni-spi.jar,
48
    // and a deployment missing it has a Java problem, not an HDFS one - both hdfs_file_system.cpp
49
    // and hdfs_mgr.cpp gate on this call. Java callers get that second failure from
50
    // Jni::Env::Get(), the door they all come through.
51
    //
52
    // Thread-safe; the JVM is created at most once and the outcome of that single
53
    // attempt is what every later call returns. Fails with a clear message when Java support
54
    // is turned off, which is what every Java entry point of the BE reports to the user.
55
    //
56
    // Safe to call from a bthread: creating the JVM is JNI code, which cannot run on one, so
57
    // this switches to a pthread itself when it has to. Callers need no switch of their own.
58
    static Status ensure_jvm();
59
60
    // Attaches the calling thread to the JVM and hands out its JNIEnv, arranging for the
61
    // thread to be detached again when it exits. Implies ensure_jvm(), and like it says nothing
62
    // about the plugin SPI - Jni::Env::Get() is the entry point that checks that too.
63
    static Status attach_current_thread(JNIEnv** env);
64
65
    // A JNIEnv on a thread that must NOT ask for a JVM to be created, for the whole of one
66
    // scope. Two things, and the one caller this exists for - JvmStats, the jvm_* metrics -
67
    // needs both:
68
    //
69
    //  * it attaches WITHOUT ensure_jvm(). JvmStats::init() is reached from _bootstrap(), which
70
    //    runs inside ensure_jvm()'s own call_once, so an ensure_jvm() there re-enters that once
71
    //    flag and the process deadlocks on the very first JVM it creates.
72
    //  * it primes Jni::Env::Get()'s thread-local cache for the life of the guard, putting back
73
    //    whatever was there when it is destroyed. Everything the caller allocates is released by
74
    //    a RAII wrapper that asks Env::Get() for an env of its own, and off the fast path that is
75
    //    the plugin-SPI gate: on a BE whose doris-jni-spi.jar did not resolve it refuses, the
76
    //    destructor logs and returns WITHOUT deleting the reference, and every metrics tick then
77
    //    leaks one JNI local ref per object it touched.
78
    //
79
    // Deliberately not a general-purpose way around that gate. It is sound here because the
80
    // jvm_* metrics reach only for java.lang.management, which every JVM has and no plugin
81
    // supplies, and they are published whenever a JVM exists - base or no base. Anything that
82
    // runs Doris's own Java code must keep coming through Jni::Env::Get().
83
    class ScopedVmEnv {
84
    public:
85
0
        ScopedVmEnv() = default;
86
        ~ScopedVmEnv();
87
        ScopedVmEnv(const ScopedVmEnv&) = delete;
88
        ScopedVmEnv& operator=(const ScopedVmEnv&) = delete;
89
90
        // Fails when this process has no JVM yet: this guard looks at one, it never asks for one
91
        // to be made. Every call site is reached only after a JVM exists by construction.
92
        Status attach(JNIEnv** env);
93
94
    private:
95
        JNIEnv* _previous = nullptr;
96
        bool _primed = false;
97
    };
98
99
    // The VM of this process, nullptr until this process has one. Deliberately not "until
100
    // ensure_jvm() has succeeded": _bootstrap() has two error paths after JNI_CreateJavaVM
101
    // returns, so a failed ensure_jvm() can leave a live VM behind. "Does a JVM exist" is both
102
    // what this can actually answer and what its callers - the tests asserting that a code path
103
    // creates none - are asking.
104
3
    static JavaVM* vm() { return _vm; }
105
106
private:
107
    static Status _bootstrap_on_pthread();
108
    // _bootstrap() with the directory-walk exceptions turned into a Status, for both branches
109
    // of _bootstrap_on_pthread().
110
    static Status _bootstrap_guarded();
111
    static Status _bootstrap();
112
    // attach_current_thread() without the ensure_jvm(), for the bootstrap itself: it runs
113
    // inside that call_once and would deadlock on it.
114
    static Status _attach_current_thread(JNIEnv** env);
115
    // Jni::Env's thread-local env cache, reached from here because JvmLauncher is its friend
116
    // and ScopedVmEnv - a member of this class - inherits that access. Defined in the .cpp,
117
    // where Env is a complete type.
118
    static JNIEnv* _tls_env();
119
    static void _set_tls_env(JNIEnv* env);
120
    static Status _create_jvm();
121
    static std::vector<std::string> _build_options();
122
    static std::string _class_path_option();
123
    static void _load_file_systems(JNIEnv* env);
124
    static void _detach_current_thread(void* attached_env);
125
126
    static JavaVM* _vm;
127
    // Key whose only purpose is to get _detach_current_thread() called on thread exit.
128
    static pthread_key_t _detach_key;
129
};
130
131
} // namespace doris::Jni