JingsongLi commented on code in PR #274:
URL: https://github.com/apache/paimon-cpp/pull/274#discussion_r3974583504


##########
src/paimon/rest/rest_messages.cpp:
##########
@@ -353,6 +355,22 @@ void GetTableResponse::FromJson(const rapidjson::Value& 
obj) noexcept(false) {
     audit_.ParseFrom(obj);
 }
 
+rapidjson::Value 
GetTableTokenResponse::ToJson(rapidjson::Document::AllocatorType* allocator) 
const
+    noexcept(false) {
+    rapidjson::Value obj(rapidjson::kObjectType);
+    obj.AddMember(rapidjson::StringRef(kFieldToken),
+                  RapidJsonUtil::SerializeValue(token_, allocator).Move(), 
*allocator);
+    obj.AddMember(rapidjson::StringRef(kFieldExpiresAtMillis),
+                  RapidJsonUtil::SerializeValue(expires_at_millis_, 
allocator).Move(), *allocator);
+    return obj;
+}
+
+void GetTableTokenResponse::FromJson(const rapidjson::Value& obj) 
noexcept(false) {
+    token_ = RapidJsonUtil::DeserializeKeyValue<std::map<std::string, 
std::string>>(
+        obj, kFieldToken, {});

Review Comment:
   **[P2] Reject a missing or null token instead of falling back to catalog 
credentials**
   
   This default makes a missing or `null` `token` indistinguishable from an 
explicitly empty object. With existing catalog storage credentials, a 
successful HTTP response that omits the token can therefore continue into 
delegate construction using those credentials. Java retains a null response 
field and fails when 
[mergeTokenWithCatalogOptions()](https://github.com/apache/paimon/blob/d77efe0d2e3d15e33df0eb8211f833035e0959b8/paimon-common/src/main/java/org/apache/paimon/rest/RESTTokenFileIO.java#L281-L293)
 copies it, so it does not turn this malformed response into fallback data 
access. Please require a non-null token object and test missing/null separately 
from an explicitly empty `{}`.



##########
src/paimon/rest/rest_catalog.cpp:
##########
@@ -414,6 +432,23 @@ std::shared_ptr<FileSystem> RestCatalog::GetFileSystem() 
const {
     return fs_;
 }
 
+Result<std::shared_ptr<FileSystem>> RestCatalog::GetTableFileSystem(
+    const Identifier& identifier) const {
+    if (!data_token_enabled_) {
+        return fs_;
+    }
+    // The credentials are issued for the data table, so a system table shares 
those of
+    // the table it belongs to.
+    PAIMON_ASSIGN_OR_RAISE(Identifier load_identifier, 
ToLoadIdentifier(identifier));
+    return table_fs_cache_->Get(
+        load_identifier.GetFullName(),
+        [this, &load_identifier](const std::string&) -> 
Result<std::shared_ptr<FileSystem>> {
+            std::shared_ptr<FileSystem> fs = 
std::make_shared<RestTokenFileSystem>(
+                api_, api_->GetMergedOptions(), load_identifier);

Review Comment:
   **[P2] Preserve Java's token-keyed FileIO cache semantics**
   
   Java's 
[RESTTokenFileIO](https://github.com/apache/paimon/blob/d77efe0d2e3d15e33df0eb8211f833035e0959b8/paimon-common/src/main/java/org/apache/paimon/rest/RESTTokenFileIO.java#L71-L82)
 caches backend FileIO instances by credential map and expiration, with idle 
expiration. This instead caches entire table-bound wrappers by their printable 
name. For example, `Identifier("a.b", "c")` and `Identifier("a", "b.c")` both 
use `a.b.c` and receive the same wrapper, which remains bound to the first 
table's token endpoint. Also, after dropping and recreating a table at a 
different storage location, a new lookup still receives the old wrapper and its 
unexpired credentials because DDL never invalidates this cache. Please preserve 
token-keyed backend reuse; if wrappers are additionally cached, use the 
complete Identifier and invalidate them on table changes, as Java's 
[CachingCatalog](https://github.com/apache/paimon/blob/d77efe0d2e3d15e33df0eb8211f833035e0959b8/paimon-core/src/main/java/org/apa
 che/paimon/catalog/CachingCatalog.java) does.



##########
src/paimon/rest/rest_token_file_system.cpp:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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 "paimon/rest/rest_token_file_system.h"
+
+#include <mutex>
+#include <utility>
+
+#include "paimon/catalog_options.h"
+#include "paimon/core/core_options.h"
+
+namespace paimon {
+
+namespace {
+/// Declared here rather than taken from the OSS file system, which is an 
optional build
+/// component this module must not depend on.
+constexpr const char kOssEndpointOption[] = "fs.oss.endpoint";
+}  // namespace
+
+RestTokenFileSystem::RestTokenFileSystem(const std::shared_ptr<RestApi>& api,
+                                         const std::map<std::string, 
std::string>& catalog_options,
+                                         const Identifier& identifier, Clock 
clock)
+    : api_(api),
+      catalog_options_(catalog_options),
+      identifier_(identifier),
+      clock_(std::move(clock)),
+      logger_(Logger::GetLogger("RestTokenFileSystem")) {}
+
+bool RestTokenFileSystem::ShouldRefresh() const {
+    if (!token_) {
+        return true;
+    }
+    int64_t now_millis =
+        
std::chrono::duration_cast<std::chrono::milliseconds>(clock_().time_since_epoch()).count();
+    return token_->expires_at_millis - now_millis < 
RestApi::kTokenExpirationSafeTimeMillis;
+}
+
+std::map<std::string, std::string> RestTokenFileSystem::MergeTokenOptions(
+    const std::map<std::string, std::string>& token) const {
+    std::map<std::string, std::string> merged = token;
+    // The DLF OSS endpoint overrides the standard one, since the credentials 
are issued
+    // for the DLF endpoint rather than for the endpoint the catalog was 
configured with.
+    auto dlf_oss_endpoint = 
catalog_options_.find(CatalogOptions::DLF_OSS_ENDPOINT);
+    if (dlf_oss_endpoint != catalog_options_.end() && 
!dlf_oss_endpoint->second.empty()) {
+        merged[kOssEndpointOption] = dlf_oss_endpoint->second;
+    }
+    return merged;
+}
+
+Status RestTokenFileSystem::Refresh() const {
+    PAIMON_LOG_INFO(logger_, "begin refresh data token for identifier [%s]",
+                    identifier_.ToString().c_str());
+    PAIMON_ASSIGN_OR_RAISE(GetTableTokenResponse response, 
api_->LoadTableToken(identifier_));
+    PAIMON_LOG_INFO(logger_, "end refresh data token for identifier [%s] 
expiresAtMillis [%ld]",
+                    identifier_.ToString().c_str(),
+                    static_cast<int64_t>(response.GetExpiresAtMillis()));
+
+    RestToken token{MergeTokenOptions(response.GetToken()), 
response.GetExpiresAtMillis()};
+    // The credentials are the only file system options that change, so the 
file system is
+    // rebuilt from the catalog options with the credentials merged over them.
+    std::map<std::string, std::string> fs_options = catalog_options_;
+    for (const auto& [key, value] : token.token) {
+        fs_options[key] = value;
+    }
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+                           CoreOptions::FromMap(fs_options, 
/*specified_file_system=*/nullptr));

Review Comment:
   **[P2] Keep ValidToken independent of delegate initialization**
   
   Java's 
[validToken()](https://github.com/apache/paimon/blob/d77efe0d2e3d15e33df0eb8211f833035e0959b8/paimon-common/src/main/java/org/apache/paimon/rest/RESTTokenFileIO.java#L296-L303)
 only refreshes and returns credentials, specifically so native engines can use 
their own file system. Here `ValidToken()` calls `Refresh()`, which runs the 
full `CoreOptions::FromMap()` before saving the token. That also initializes 
data/manifest format factories, so a native integration that only needs 
credentials and omits Avro can fail on the default `manifest.format=avro` even 
when the REST response is valid. Please separate token refresh from delegate 
construction, and let `ValidToken()` perform only the former. A token-only test 
without registered format factories would cover this distinction.



-- 
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]

Reply via email to