morrySnow commented on code in PR #65644:
URL: https://github.com/apache/doris/pull/65644#discussion_r3604596263


##########
be/src/information_schema/schema_plugins_scanner.cpp:
##########
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "information_schema/schema_plugins_scanner.h"
+
+#include <utility>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/string_ref.h"
+#include "runtime/exec_env.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_state.h"
+#include "util/client_cache.h"
+#include "util/thrift_rpc_helper.h"
+
+namespace doris {
+
+std::vector<SchemaScanner::ColumnDesc> SchemaPluginsScanner::_s_tbls_columns = 
{
+        {"PLUGIN_NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_TYPE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_VERSION", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"SOURCE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"DESCRIPTION", TYPE_STRING, sizeof(StringRef), true},
+};
+
+SchemaPluginsScanner::SchemaPluginsScanner()
+        : SchemaScanner(_s_tbls_columns, TSchemaTableType::SCH_PLUGINS) {}
+
+Status SchemaPluginsScanner::start(RuntimeState* state) {
+    if (!_is_init) {
+        return Status::InternalError("used before initialized.");
+    }
+    _block_rows_limit = state->batch_size();
+    _rpc_timeout_ms = state->execution_timeout() * 1000;
+    // Plugins are per-FE local state: ask the FE this session is connected to.
+    _fe_addr = state->get_query_ctx()->current_connect_fe;

Review Comment:
   **Suggestion (defensive hardening):** `state->get_query_ctx()` can return 
null. Dereferencing it directly (`->current_connect_fe`) would crash with a 
null pointer dereference. While this follows the same pattern as the existing 
`SchemaCatalogMetaCacheStatsScanner::start()` (line 66), consider adding a null 
check:
   
   ```cpp
   auto* query_ctx = state->get_query_ctx();
   if (query_ctx == nullptr) {
       return Status::InternalError("query context is null");
   }
   _fe_addr = query_ctx->current_connect_fe;
   ```



##########
be/src/information_schema/schema_plugins_scanner.cpp:
##########
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "information_schema/schema_plugins_scanner.h"
+
+#include <utility>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/string_ref.h"
+#include "runtime/exec_env.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_state.h"
+#include "util/client_cache.h"
+#include "util/thrift_rpc_helper.h"
+
+namespace doris {
+
+std::vector<SchemaScanner::ColumnDesc> SchemaPluginsScanner::_s_tbls_columns = 
{
+        {"PLUGIN_NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_TYPE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_VERSION", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"SOURCE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"DESCRIPTION", TYPE_STRING, sizeof(StringRef), true},
+};
+
+SchemaPluginsScanner::SchemaPluginsScanner()
+        : SchemaScanner(_s_tbls_columns, TSchemaTableType::SCH_PLUGINS) {}
+
+Status SchemaPluginsScanner::start(RuntimeState* state) {
+    if (!_is_init) {
+        return Status::InternalError("used before initialized.");
+    }
+    _block_rows_limit = state->batch_size();
+    _rpc_timeout_ms = state->execution_timeout() * 1000;
+    // Plugins are per-FE local state: ask the FE this session is connected to.
+    _fe_addr = state->get_query_ctx()->current_connect_fe;
+    return Status::OK();
+}
+
+Status SchemaPluginsScanner::_get_plugins_block_from_fe() {
+    TSchemaTableRequestParams schema_table_request_params;
+    for (int i = 0; i < _s_tbls_columns.size(); i++) {
+        schema_table_request_params.__isset.columns_name = true;
+        
schema_table_request_params.columns_name.emplace_back(_s_tbls_columns[i].name);
+    }
+    
schema_table_request_params.__set_current_user_ident(*_param->common_param->current_user_ident);
+
+    TFetchSchemaTableDataRequest request;
+    request.__set_schema_table_name(TSchemaTableName::PLUGINS);
+    request.__set_schema_table_params(schema_table_request_params);
+
+    TFetchSchemaTableDataResult result;
+    RETURN_IF_ERROR(ThriftRpcHelper::rpc<FrontendServiceClient>(
+            _fe_addr.hostname, _fe_addr.port,
+            [&request, &result](FrontendServiceConnection& client) {
+                client->fetchSchemaTableData(result, request);
+            },
+            _rpc_timeout_ms));
+
+    Status status(Status::create(result.status));
+    if (!status.ok()) {
+        LOG(WARNING) << "fetch plugins from FE(" << _fe_addr.hostname
+                     << ") failed, errmsg=" << status;
+        return status;
+    }
+
+    _plugins_block = Block::create_unique();
+    for (int i = 0; i < _s_tbls_columns.size(); ++i) {
+        auto data_type =
+                
DataTypeFactory::instance().create_data_type(_s_tbls_columns[i].type, true);
+        
_plugins_block->insert(ColumnWithTypeAndName(data_type->create_column(), 
data_type,
+                                                     _s_tbls_columns[i].name));
+    }
+    _plugins_block->reserve(_block_rows_limit);
+
+    std::vector<TRow> result_data = std::move(result.data_batch);
+    if (!result_data.empty()) {
+        auto col_size = result_data[0].column_value.size();
+        if (col_size != _s_tbls_columns.size()) {
+            return Status::InternalError<false>("plugins schema is not match 
for FE and BE");
+        }
+    }
+
+    for (int i = 0; i < result_data.size(); i++) {
+        const TRow& row = result_data[i];
+        for (int j = 0; j < _s_tbls_columns.size(); j++) {
+            const TCell& cell = row.column_value[j];
+            // An unset string cell means SQL NULL (e.g. unknown 
PLUGIN_VERSION);
+            // insert_block_column would materialize it as an empty string.
+            if (!cell.__isset.stringVal) {
+                MutableColumnPtr mutable_col_ptr =
+                        
IColumn::mutate(std::move(_plugins_block->get_by_position(j).column));

Review Comment:
   **Suggestion (code clarity):** `std::move` on a const reference is a no-op: 
`get_by_position()` returns `const ColumnWithTypeAndName&`, so `.column` is 
`const ColumnPtr&`. `std::move` on a const lvalue-reference falls back to a 
copy, incrementing the refcount. The subsequent `IColumn::mutate()` will then 
always deep-copy since the refcount is at least 2.
   
   The intent is clear (get a mutable column to insert NULL into), but the 
`std::move` is misleading -- it suggests ownership transfer that doesn't 
actually happen. Consider either removing `std::move` or extracting the column 
first:
   
   ```cpp
   // Option A: drop std::move (same behavior, less misleading)
   MutableColumnPtr mutable_col_ptr =
       IColumn::mutate(_plugins_block->get_by_position(j).column);
   
   // Option B: extract first so std::move actually moves
   auto col = _plugins_block->get_by_position(j).column;
   MutableColumnPtr mutable_col_ptr = IColumn::mutate(std::move(col));
   ```
   
   (Note: the same pattern exists in `SchemaScanner::insert_block_column()` at 
schema_scanner.cpp:476, so this is a pre-existing codebase pattern rather than 
a new issue.)



##########
be/src/information_schema/schema_plugins_scanner.cpp:
##########
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "information_schema/schema_plugins_scanner.h"
+
+#include <utility>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/string_ref.h"
+#include "runtime/exec_env.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_state.h"
+#include "util/client_cache.h"
+#include "util/thrift_rpc_helper.h"
+
+namespace doris {
+
+std::vector<SchemaScanner::ColumnDesc> SchemaPluginsScanner::_s_tbls_columns = 
{
+        {"PLUGIN_NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_TYPE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_VERSION", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"SOURCE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"DESCRIPTION", TYPE_STRING, sizeof(StringRef), true},
+};
+
+SchemaPluginsScanner::SchemaPluginsScanner()
+        : SchemaScanner(_s_tbls_columns, TSchemaTableType::SCH_PLUGINS) {}
+
+Status SchemaPluginsScanner::start(RuntimeState* state) {
+    if (!_is_init) {
+        return Status::InternalError("used before initialized.");
+    }
+    _block_rows_limit = state->batch_size();
+    _rpc_timeout_ms = state->execution_timeout() * 1000;
+    // Plugins are per-FE local state: ask the FE this session is connected to.
+    _fe_addr = state->get_query_ctx()->current_connect_fe;
+    return Status::OK();
+}
+
+Status SchemaPluginsScanner::_get_plugins_block_from_fe() {
+    TSchemaTableRequestParams schema_table_request_params;
+    for (int i = 0; i < _s_tbls_columns.size(); i++) {
+        schema_table_request_params.__isset.columns_name = true;
+        
schema_table_request_params.columns_name.emplace_back(_s_tbls_columns[i].name);
+    }
+    
schema_table_request_params.__set_current_user_ident(*_param->common_param->current_user_ident);
+
+    TFetchSchemaTableDataRequest request;
+    request.__set_schema_table_name(TSchemaTableName::PLUGINS);
+    request.__set_schema_table_params(schema_table_request_params);
+
+    TFetchSchemaTableDataResult result;
+    RETURN_IF_ERROR(ThriftRpcHelper::rpc<FrontendServiceClient>(
+            _fe_addr.hostname, _fe_addr.port,
+            [&request, &result](FrontendServiceConnection& client) {
+                client->fetchSchemaTableData(result, request);
+            },
+            _rpc_timeout_ms));
+
+    Status status(Status::create(result.status));
+    if (!status.ok()) {
+        LOG(WARNING) << "fetch plugins from FE(" << _fe_addr.hostname
+                     << ") failed, errmsg=" << status;
+        return status;
+    }
+
+    _plugins_block = Block::create_unique();
+    for (int i = 0; i < _s_tbls_columns.size(); ++i) {
+        auto data_type =
+                
DataTypeFactory::instance().create_data_type(_s_tbls_columns[i].type, true);
+        
_plugins_block->insert(ColumnWithTypeAndName(data_type->create_column(), 
data_type,
+                                                     _s_tbls_columns[i].name));
+    }
+    _plugins_block->reserve(_block_rows_limit);
+
+    std::vector<TRow> result_data = std::move(result.data_batch);
+    if (!result_data.empty()) {
+        auto col_size = result_data[0].column_value.size();
+        if (col_size != _s_tbls_columns.size()) {
+            return Status::InternalError<false>("plugins schema is not match 
for FE and BE");
+        }
+    }
+
+    for (int i = 0; i < result_data.size(); i++) {
+        const TRow& row = result_data[i];
+        for (int j = 0; j < _s_tbls_columns.size(); j++) {
+            const TCell& cell = row.column_value[j];
+            // An unset string cell means SQL NULL (e.g. unknown 
PLUGIN_VERSION);
+            // insert_block_column would materialize it as an empty string.
+            if (!cell.__isset.stringVal) {
+                MutableColumnPtr mutable_col_ptr =
+                        
IColumn::mutate(std::move(_plugins_block->get_by_position(j).column));
+                
assert_cast<ColumnNullable*>(mutable_col_ptr.get())->insert_data(nullptr, 0);

Review Comment:
   **Nit (documentation):** The `assert_cast<ColumnNullable*>` here relies on 
the invariant that all schema columns are defined with `is_nullable=true` in 
`_s_tbls_columns`. While this is guaranteed by the static column definitions 
above, consider adding a brief comment explaining this invariant for future 
maintainers:
   
   ```cpp
   // All _s_tbls_columns are defined with is_nullable=true, so the column
   // is always a ColumnNullable wrapping a ColumnString.
   assert_cast<ColumnNullable*>(mutable_col_ptr.get())->insert_data(nullptr, 0);
   ```



##########
be/src/information_schema/schema_plugins_scanner.cpp:
##########
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "information_schema/schema_plugins_scanner.h"
+
+#include <utility>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/string_ref.h"
+#include "runtime/exec_env.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_state.h"
+#include "util/client_cache.h"
+#include "util/thrift_rpc_helper.h"
+
+namespace doris {
+
+std::vector<SchemaScanner::ColumnDesc> SchemaPluginsScanner::_s_tbls_columns = 
{
+        {"PLUGIN_NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_TYPE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"PLUGIN_VERSION", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"SOURCE", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"DESCRIPTION", TYPE_STRING, sizeof(StringRef), true},
+};
+
+SchemaPluginsScanner::SchemaPluginsScanner()
+        : SchemaScanner(_s_tbls_columns, TSchemaTableType::SCH_PLUGINS) {}
+
+Status SchemaPluginsScanner::start(RuntimeState* state) {
+    if (!_is_init) {
+        return Status::InternalError("used before initialized.");
+    }
+    _block_rows_limit = state->batch_size();
+    _rpc_timeout_ms = state->execution_timeout() * 1000;
+    // Plugins are per-FE local state: ask the FE this session is connected to.
+    _fe_addr = state->get_query_ctx()->current_connect_fe;
+    return Status::OK();
+}
+
+Status SchemaPluginsScanner::_get_plugins_block_from_fe() {
+    TSchemaTableRequestParams schema_table_request_params;
+    for (int i = 0; i < _s_tbls_columns.size(); i++) {
+        schema_table_request_params.__isset.columns_name = true;
+        
schema_table_request_params.columns_name.emplace_back(_s_tbls_columns[i].name);
+    }
+    
schema_table_request_params.__set_current_user_ident(*_param->common_param->current_user_ident);
+
+    TFetchSchemaTableDataRequest request;
+    request.__set_schema_table_name(TSchemaTableName::PLUGINS);
+    request.__set_schema_table_params(schema_table_request_params);
+
+    TFetchSchemaTableDataResult result;
+    RETURN_IF_ERROR(ThriftRpcHelper::rpc<FrontendServiceClient>(
+            _fe_addr.hostname, _fe_addr.port,
+            [&request, &result](FrontendServiceConnection& client) {
+                client->fetchSchemaTableData(result, request);
+            },
+            _rpc_timeout_ms));
+
+    Status status(Status::create(result.status));
+    if (!status.ok()) {
+        LOG(WARNING) << "fetch plugins from FE(" << _fe_addr.hostname
+                     << ") failed, errmsg=" << status;
+        return status;
+    }
+
+    _plugins_block = Block::create_unique();

Review Comment:
   **Nit (minor efficiency):** The block is allocated (lines 84-91) before the 
column count validation check (lines 94-99). If the column count mismatch check 
fails, the allocated block is discarded because `_plugins_block` is a 
`unique_ptr` and the method returns early. This is not a leak, but moving the 
column count check before block allocation would be slightly more efficient -- 
avoiding unnecessary allocations when the FE/BE schema is mismatched.
   
   That said, this matches the pattern in the existing 
`SchemaAuthenticationIntegrationsScanner`, so it's consistent with the codebase.



##########
regression-test/suites/query_p0/schema_table/test_plugins_schema.groovy:
##########
@@ -0,0 +1,76 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.junit.Assert
+
+suite("test_plugins_schema", "p0") {
+    // Schema check: fixed five columns.
+    def schema = sql "DESC information_schema.plugins"
+    def columnNames = schema.collect { it[0] }
+    Assert.assertEquals(
+            ["PLUGIN_NAME", "PLUGIN_TYPE", "PLUGIN_VERSION", "SOURCE", 
"DESCRIPTION"], columnNames)
+
+    // As an admin user: loaded plugins must be listed. Do not require BUILTIN
+    // rows: the packaged cluster deploys filesystem/connector providers as
+    // directory plugins (source = EXTERNAL), so a correct inventory may 
contain
+    // no BUILTIN row at all.
+    def rows = sql """
+        SELECT PLUGIN_NAME, PLUGIN_TYPE, SOURCE, PLUGIN_VERSION
+        FROM information_schema.plugins
+        ORDER BY PLUGIN_TYPE, PLUGIN_NAME
+    """
+    Assert.assertTrue("expect at least one plugin row", rows.size() > 0)
+    rows.each { row ->
+        Assert.assertTrue(row[2] == "BUILTIN" || row[2] == "EXTERNAL")
+        // Unknown versions must surface as SQL NULL, never as an empty string.
+        Assert.assertTrue("PLUGIN_VERSION must be NULL or non-empty, got ''",
+                row[3] == null || row[3].toString().length() > 0)
+    }
+
+    // (type, name) is the primary key: no duplicates may appear.
+    def keys = rows.collect { "${it[1]}|${it[0]}".toString() }
+    Assert.assertEquals(keys.size(), keys.unique(false).size())
+
+    // Filter by family works.
+    def fsRows = sql """
+        SELECT PLUGIN_NAME FROM information_schema.plugins WHERE PLUGIN_TYPE = 
'FILESYSTEM'
+    """
+    Assert.assertTrue("expect built-in filesystem providers", fsRows.size() > 
0)
+
+    // Non-admin users see an empty inventory (ADMIN-level metadata).
+    String user = "test_plugins_schema_user"
+    String pwd = "C123_567p"
+    try_sql("DROP USER IF EXISTS ${user}")
+    sql "CREATE USER ${user} IDENTIFIED BY '${pwd}'"
+    // The JDBC URL carries a default database; without a grant on it the
+    // connection itself is refused before the query runs.
+    sql "GRANT SELECT_PRIV ON regression_test TO ${user}"
+    if (isCloudMode()) {
+        def clusters = sql " SHOW CLUSTERS; "
+        Assert.assertTrue(!clusters.isEmpty())
+        def validCluster = clusters[0][0]
+        sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}"""
+    }
+    try {
+        connect(user, pwd, context.config.jdbcUrl) {

Review Comment:
   **Suggestion (robustness):** If `connect(user, pwd, ...)` succeeds but the 
inner `sql "SELECT ..."` throws, the connection is not explicitly closed. In 
some Groovy/JDBC setups this can leave a dangling connection until GC or 
timeout. Most Doris regression test `connect()` blocks handle cleanup 
implicitly (the framework closes them after the block), but it's worth 
confirming this applies here too with the nested try-catch structure.



##########
fe/fe-extension-loader/src/main/java/org/apache/doris/extension/loader/PluginRegistry.java:
##########
@@ -0,0 +1,197 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.extension.loader;
+
+import org.apache.doris.extension.spi.PluginFactory;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Process-wide registry of loaded plugins across all plugin families
+ * (filesystem, connector, authentication, lineage, ...).
+ *
+ * <p>This is the single fact source behind {@code information_schema.plugins}.
+ * It only stores load-time snapshots (plain strings); listing the registry
+ * never executes plugin code.
+ *
+ * <p>Rules:
+ * <ul>
+ *   <li>Primary key is {@code (type, name)}. The first registration wins;
+ *       later registrations with the same key are rejected, never silently
+ *       overridden. Family managers register built-ins before external 
plugins,
+ *       so a directory jar can never displace a built-in row.</li>
+ *   <li>Rows only exist for successfully loaded plugins. Load failures are
+ *       reported via logs by the loading side and never enter the 
registry.</li>
+ * </ul>
+ */
+public final class PluginRegistry {
+
+    /** Where a plugin was loaded from. */
+    public enum PluginSource {
+        /** Bundled on the FE classpath, discovered via ServiceLoader. */
+        BUILTIN,
+        /** Loaded from a plugin directory jar. */
+        EXTERNAL
+    }
+
+    /** Immutable load-time snapshot of one plugin. */
+    public static final class PluginRecord {
+        private final String type;
+        private final String name;
+        private final String version;
+        private final String description;
+        private final PluginSource source;
+        private final Instant loadTime;
+
+        public PluginRecord(String type, String name, String version, String 
description,
+                PluginSource source, Instant loadTime) {
+            this.type = Objects.requireNonNull(type, "type");
+            this.name = Objects.requireNonNull(name, "name");
+            this.version = version;
+            this.description = description == null ? "" : description;
+            this.source = Objects.requireNonNull(source, "source");
+            this.loadTime = Objects.requireNonNull(loadTime, "loadTime");
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        /** Plugin release version from jar MANIFEST Implementation-Version, 
may be null. */
+        public String getVersion() {
+            return version;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public PluginSource getSource() {
+            return source;
+        }
+
+        public Instant getLoadTime() {
+            return loadTime;
+        }
+    }
+
+    private static final Logger LOG = 
LogManager.getLogger(PluginRegistry.class);
+
+    private static final PluginRegistry INSTANCE = new PluginRegistry();
+
+    private final ConcurrentMap<String, PluginRecord> recordsByKey = new 
ConcurrentHashMap<>();
+
+    private PluginRegistry() {
+    }
+
+    public static PluginRegistry getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Registers a load-time snapshot for one plugin.
+     *
+     * <p>Rejects (returns false) when the name is invalid or the
+     * {@code (type, name)} key is already taken. Rejection never throws so a
+     * bad self-reported name cannot break FE startup paths that register
+     * built-ins; callers decide how to react.
+     *
+     * @param type plugin family label, e.g. "FILESYSTEM", "AUTHENTICATION"
+     * @param name plugin identity within the family
+     * @param version plugin release version, may be null when unknown
+     * @param description one-line description, may be null
+     * @param source BUILTIN or EXTERNAL
+     * @return true if the record was inserted, false if rejected
+     */
+    public boolean register(String type, String name, String version, String 
description, PluginSource source) {

Review Comment:
   **Suggestion (defensive hardening):** The `register()` method validates 
`name` via `PluginNames.validate()` but does not validate `type`. If a caller 
passes `null` for `type`, it reaches `new PluginRecord(type, name.trim(), ...)` 
which calls `Objects.requireNonNull(type, "type")` and throws 
`NullPointerException`. Since `register()` is a public method, consider adding 
explicit null validation for `type` for a cleaner error message, consistent 
with how `name` is validated:
   
   ```java
   if (type == null || type.trim().isEmpty()) {
       LOG.warn("Reject plugin registration with null or blank type");
       return false;
   }
   ```
   
   In practice all callers hardcode type constants (e.g., `"FILESYSTEM"`, 
`"AUTHENTICATION"`), so this is a defensive improvement rather than a bug fix.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to