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 |