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 | | |
22 | | #include <map> |
23 | | #include <string> |
24 | | #include <string_view> |
25 | | |
26 | | #include "common/status.h" |
27 | | #include "util/jni-util.h" |
28 | | |
29 | | namespace doris::Jni { |
30 | | |
31 | | // How BE names one Java factory: the plugin's directory under plugins/jni/, and the |
32 | | // factory's own name within that plugin. It replaces the class name BE used to reflect on - |
33 | | // a concrete class is private to its plugin's classloader, so naming one from C++ stopped |
34 | | // being possible the moment plugins were isolated. |
35 | | // |
36 | | // Both halves are compile-time constants; nothing builds a PluginRef out of a temporary. |
37 | | struct PluginRef { |
38 | | std::string_view plugin; |
39 | | std::string_view factory; |
40 | | }; |
41 | | |
42 | | // Every plugin and factory BE addresses, in one place because the plugin half is also a |
43 | | // deployment directory name: it appears in plugins/jni/, in build.sh, in the layout check |
44 | | // build.sh runs over the deployed tree, and in the "is not deployed" a user sees. It is named here |
45 | | // once so renaming a plugin is one edit rather than a hunt through the readers - the same |
46 | | // connector is reached from both the v1 and the v2 scan paths, and two independent string |
47 | | // literals for one directory is how they would drift apart. |
48 | | // |
49 | | // The factory half must be unique within its plugin across all kinds of factory, because the |
50 | | // pair below is everything BE sends: PluginRegistry receives a plugin name and a factory name |
51 | | // and nothing that says which kind was wanted, so a name meaning two things would resolve by |
52 | | // whichever list happens to be searched first. |
53 | | // |
54 | | // So a factory is named for what it does within its plugin, not for the connector - the plugin |
55 | | // half already says which connector this is. A connector's ordinary read side is "reader" and |
56 | | // its write side "writer"; anything else says what it is, as "connection-tester" does. |
57 | | // |
58 | | // The entry still repeating its plugin's name predates that rule and is renamed to "reader" by |
59 | | // the change that turns it into a plugin, together with the getName() of the factory that change |
60 | | // writes. Renaming it earlier would name a factory nothing implements. |
61 | | namespace plugin { |
62 | | |
63 | | inline constexpr PluginRef PAIMON_SCANNER {"paimon", "reader"}; |
64 | | inline constexpr PluginRef HUDI_SCANNER {"hudi", "reader"}; |
65 | | // Only the Iceberg system tables come through Java; Iceberg data files are read natively - so |
66 | | // this one keeps its own name when it is migrated. It is not the connector's reader. |
67 | | inline constexpr PluginRef ICEBERG_SYS_TABLE_SCANNER {"iceberg", "sys-table"}; |
68 | | inline constexpr PluginRef MAX_COMPUTE_SCANNER {"max-compute", "reader"}; |
69 | | inline constexpr PluginRef MAX_COMPUTE_WRITER {"max-compute", "writer"}; |
70 | | inline constexpr PluginRef JDBC_SCANNER {"jdbc", "reader"}; |
71 | | inline constexpr PluginRef JDBC_WRITER {"jdbc", "writer"}; |
72 | | // Testing a JDBC connection is a scan of zero rows, so it is a scanner factory of its own |
73 | | // rather than a second entry point. |
74 | | inline constexpr PluginRef JDBC_CONNECTION_TESTER {"jdbc", "connection-tester"}; |
75 | | inline constexpr PluginRef TRINO_CONNECTOR_SCANNER {"trino-connector", "reader"}; |
76 | | // A table function's executor is the scalar one: the same evaluate() runs per row and the |
77 | | // serialized parameters carry the flag that makes its return type an array. Hence two entries |
78 | | // for three kinds of function. |
79 | | inline constexpr PluginRef JAVA_UDF_SCALAR {"java-udf", "scalar"}; |
80 | | inline constexpr PluginRef JAVA_UDF_AGGREGATE {"java-udf", "aggregate"}; |
81 | | |
82 | | } // namespace plugin |
83 | | |
84 | | // The methods BE calls on a scanner, resolved on the SPI's JniScanner rather than on the |
85 | | // concrete class. Two reasons, and the first is what forces the design: the concrete class |
86 | | // is loaded by its plugin's classloader, so its jclass cannot be shared between plugins, |
87 | | // while the abstract base is loaded by the shared parent and is one class for the whole |
88 | | // process. The second is that every method here is final on the base class, so a plugin |
89 | | // cannot shadow one and change what BE calls. |
90 | | // |
91 | | // The names and descriptors are fixed by PROTOCOL.md section 1. Changing one means changing |
92 | | // both sides in the same commit. |
93 | | // |
94 | | // The base class is kept alongside the ids resolved on it, for two reasons: a jmethodID is only |
95 | | // valid while its class is reachable, and checking that an object really is a scanner before |
96 | | // calling these on it needs that same class. |
97 | | struct ScannerApi { |
98 | | GlobalClass cls; |
99 | | MethodId open; |
100 | | MethodId get_next_batch_meta; |
101 | | MethodId get_statistics; |
102 | | MethodId get_append_data_time; |
103 | | MethodId get_create_vector_table_time; |
104 | | MethodId set_batch_size; |
105 | | MethodId release_column; |
106 | | MethodId release_table; |
107 | | MethodId close; |
108 | | }; |
109 | | |
110 | | // The writer half of the same contract, resolved on the SPI's JniWriter. |
111 | | struct WriterApi { |
112 | | GlobalClass cls; |
113 | | MethodId open; |
114 | | MethodId write; |
115 | | MethodId get_statistics; |
116 | | MethodId close; |
117 | | }; |
118 | | |
119 | | // The single Java entry point BE calls into, wrapping |
120 | | // org.apache.doris.jni.bootstrap.PluginRegistry. |
121 | | // |
122 | | // There is deliberately no fallback anywhere below. If the registry cannot answer - the |
123 | | // plugin is not deployed, its jars are broken, the factory name is wrong - that is the |
124 | | // result, and the Java message says which of those it was. Nothing here retries against |
125 | | // another classloader or degrades to a built-in reader: doing so would report a plugin's |
126 | | // deployment problem as a query producing no rows. |
127 | | class PluginRegistry { |
128 | | public: |
129 | | // Creates a scanner and hands back the shared method ids for it. They come together |
130 | | // because an instance is useless without them and resolving them anywhere but on the |
131 | | // SPI base class is the mistake this API exists to prevent. |
132 | | static Status create_scanner(JNIEnv* env, const PluginRef& ref, int batch_size, |
133 | | const std::map<std::string, std::string>& params, |
134 | | GlobalObject* scanner, const ScannerApi** api); |
135 | | |
136 | | static Status create_writer(JNIEnv* env, const PluginRef& ref, int batch_size, |
137 | | const std::map<std::string, std::string>& params, |
138 | | GlobalObject* writer, const WriterApi** api); |
139 | | |
140 | | // Creates the executor behind one Java function. The parameters stay a serialized thrift |
141 | | // struct all the way into the plugin, which owns the only copy of thrift that can read |
142 | | // them; the executor comes back as a plain object because the three function kinds do |
143 | | // not share a method shape. See UdfExecutorFactory. |
144 | | // |
145 | | // Its class comes back with it, and it is the only way the caller can get one: the |
146 | | // executor's class lives in the plugin's classloader, so FindClass by name - which searches |
147 | | // BE's loader - would not find it. The caller resolves the methods of its own kind on this. |
148 | | static Status create_udf_executor(JNIEnv* env, const PluginRef& ref, |
149 | | const LocalArray& thrift_params, GlobalObject* executor, |
150 | | GlobalClass* executor_class); |
151 | | |
152 | | // Forwarded to every loaded plugin on DROP FUNCTION, so whichever one compiled that |
153 | | // function can drop what it cached for it. The id is the identity a plugin caches by; the |
154 | | // signature is for its logs, and for the case where FE sent no id. |
155 | | // |
156 | | // Returns without touching Java when no plugin has ever been loaded: dropping a |
157 | | // function must not be the thing that starts a JVM on a BE that runs no Java at all. |
158 | | static Status clean_udf_cache(int64_t function_id, const std::string& function_signature); |
159 | | |
160 | | // State of every plugin loaded so far, as JSON. |
161 | | static Status plugin_status_json(std::string* status); |
162 | | |
163 | | // Loads every deployed plugin now rather than on first use, so a broken deployment is |
164 | | // in the log before a user query finds it. Never fails for a single plugin's sake. |
165 | | // |
166 | | // Does nothing, and creates no JVM, when no plugin is deployed. |
167 | | static Status warmup(); |
168 | | |
169 | | // Whether the plugin directory holds at least one plugin. Public because it is the |
170 | | // reason warmup is safe to turn on, and something has to be able to check it. |
171 | | static bool any_plugin_deployed(); |
172 | | |
173 | | // Whether anything has reached the Java registry yet. The status endpoint asks first: |
174 | | // there is nothing to report before that, and asking Java would create the JVM. |
175 | 0 | static bool registry_initialized() { return _registry_ready; } |
176 | | |
177 | | private: |
178 | | // The bootstrap class and its static method ids, resolved once. |
179 | | struct Registry { |
180 | | GlobalClass cls; |
181 | | MethodId create_instance; |
182 | | MethodId create_udf_executor; |
183 | | MethodId clean_udf_cache; |
184 | | MethodId plugin_status_json; |
185 | | MethodId warmup; |
186 | | }; |
187 | | |
188 | | static Status _ensure_registry(const Registry** registry); |
189 | | static Status _init_registry(); |
190 | | static Status _init_scanner_api(); |
191 | | static Status _init_writer_api(); |
192 | | |
193 | | // createInstance() backs both scanners and writers: what a plugin hands back is decided |
194 | | // by which factory list the name was looked up in, not by a separate Java entry point. |
195 | | static Status _create_instance(JNIEnv* env, const PluginRef& ref, int batch_size, |
196 | | const std::map<std::string, std::string>& params, |
197 | | GlobalObject* instance); |
198 | | |
199 | | // Rejects an instance of the wrong kind before its method ids are handed out, because a |
200 | | // factory name can come from user SQL. See the comment at the definition. |
201 | | static Status _check_kind(JNIEnv* env, const PluginRef& ref, const GlobalObject& instance, |
202 | | const GlobalClass& expected, const char* expected_kind); |
203 | | |
204 | | static Registry _registry; |
205 | | static ScannerApi _scanner_api; |
206 | | static WriterApi _writer_api; |
207 | | |
208 | | // Whether _init_registry() has ever succeeded, so that dropping a function can tell "no |
209 | | // plugin has ever been loaded" from "the registry is unreachable". |
210 | | static std::atomic<bool> _registry_ready; |
211 | | }; |
212 | | |
213 | | } // namespace doris::Jni |