| /* | |
| * 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/common/utils/http_client.h" | |
| #include <curl/curl.h> | |
| #include <chrono> | |
| #include <mutex> | |
| #include <thread> | |
| #include <utility> | |
| #include <vector> | |
| #include "fmt/format.h" | |
| #include "paimon/common/utils/scope_guard.h" | |
| #include "paimon/common/utils/string_utils.h" | |
| namespace paimon { | |
| namespace { | |
| constexpr int32_t kMaxAttempts = 3; | |
| class CurlGlobalGuard { | |
| public: | |
| CurlGlobalGuard() { | |
| curl_global_init(CURL_GLOBAL_DEFAULT); | |
| } | |
| ~CurlGlobalGuard() { | |
| curl_global_cleanup(); | |
| } | |
| }; | |
| void TrimHttpWhitespace(std::string* value) { | |
| constexpr char kHttpWhitespace[] = " \t\r\n"; | |
| size_t begin = value->find_first_not_of(kHttpWhitespace); | |
| if (begin == std::string::npos) { | |
| value->clear(); | |
| return; | |
| } | |
| size_t end = value->find_last_not_of(kHttpWhitespace); | |
| value->erase(end + 1); | |
| value->erase(0, begin); | |
| } | |
| struct TransferContext { | |
| CURL* handle; | |
| const HttpBodyConsumer* consumer; | |
| HttpResponse response; | |
| Status status = Status::OK(); | |
| }; | |
| size_t WriteCallback(char* data, size_t size, size_t count, void* user_data) { | |
| auto* context = static_cast<TransferContext*>(user_data); | |
| size_t bytes = size * count; | |
| if (!context->status.ok()) { | |
| return 0; | |
| } | |
| long status_code = 0; // NOLINT(runtime/int, google-runtime-int): required by curl. | |
| curl_easy_getinfo(context->handle, CURLINFO_RESPONSE_CODE, &status_code); | |
| context->response.status_code = static_cast<int32_t>(status_code); | |
| if (status_code >= 300) { | |
| context->response.body_size += static_cast<int64_t>(bytes); | |
| return bytes; | |
| } | |
| context->status = (*context->consumer)(data, static_cast<int64_t>(bytes)); | |
| if (!context->status.ok()) { | |
| return 0; | |
| } | |
| context->response.body_size += static_cast<int64_t>(bytes); | |
| return bytes; | |
| } | |
| size_t HeaderCallback(char* data, size_t size, size_t count, void* user_data) { | |
| auto* context = static_cast<TransferContext*>(user_data); | |
| size_t bytes = size * count; | |
| ParseHttpHeaderLine(data, bytes, &context->response.headers); | |
| return bytes; | |
| } | |
| bool IsRetryable(CURLcode code, int64_t status_code) { | |
| if (code == CURLE_COULDNT_RESOLVE_HOST || code == CURLE_COULDNT_CONNECT || | |
| code == CURLE_OPERATION_TIMEDOUT || code == CURLE_SEND_ERROR || code == CURLE_RECV_ERROR || | |
| code == CURLE_PARTIAL_FILE) { | |
| return true; | |
| } | |
| return status_code == 429 || status_code >= 500; | |
| } | |
| } // namespace | |
| std::shared_ptr<void> EnsureCurlGlobalInit() { | |
| static auto guard = std::make_shared<CurlGlobalGuard>(); | |
| return guard; | |
| } | |
| void ParseHttpHeaderLine(const char* data, size_t size, HttpHeaders* headers) { | |
| std::string line(data, size); | |
| size_t colon = line.find(':'); | |
| if (colon == std::string::npos) { | |
| return; | |
| } | |
| std::string name = line.substr(0, colon); | |
| TrimHttpWhitespace(&name); | |
| if (name.empty()) { | |
| return; | |
| } | |
| name = StringUtils::ToLowerCase(name); | |
| std::string value = line.substr(colon + 1); | |
| TrimHttpWhitespace(&value); | |
| (*headers)[name] = std::move(value); | |
| } | |
| class CurlHttpClient::Impl { | |
| public: | |
| Impl() : guard_(EnsureCurlGlobalInit()) {} | |
| ~Impl() { | |
| for (CURL* handle : handles_) { | |
| curl_easy_cleanup(handle); | |
| } | |
| } | |
| CURL* Acquire() const { | |
| std::scoped_lock lock(mutex_); | |
| if (handles_.empty()) { | |
| return curl_easy_init(); | |
| } | |
| CURL* handle = handles_.back(); | |
| handles_.pop_back(); | |
| return handle; | |
| } | |
| void Release(CURL* handle) const { | |
| curl_easy_reset(handle); | |
| std::scoped_lock lock(mutex_); | |
| handles_.push_back(handle); | |
| } | |
| private: | |
| std::shared_ptr<void> guard_; | |
| mutable std::mutex mutex_; | |
| mutable std::vector<CURL*> handles_; | |
| }; | |
| CurlHttpClient::CurlHttpClient() : impl_(std::make_unique<Impl>()) {} | |
| CurlHttpClient::~CurlHttpClient() = default; | |
| Result<HttpResponse> CurlHttpClient::Execute(const HttpRequest& request, | |
| const HttpBodyConsumer& consumer) const { | |
| if (request.request_timeout_ms < 0) { | |
| return Status::Invalid("HTTP request timeout must not be negative"); | |
| } | |
| for (int32_t attempt = 0; attempt < kMaxAttempts; ++attempt) { | |
| CURL* handle = impl_->Acquire(); | |
| if (handle == nullptr) { | |
| return Status::IOError("failed to create curl easy handle"); | |
| } | |
| ScopeGuard release_handle([this, handle] { impl_->Release(handle); }); | |
| TransferContext context{handle, &consumer, {}, Status::OK()}; | |
| curl_slist* headers = nullptr; | |
| ScopeGuard release_headers([&headers] { curl_slist_free_all(headers); }); | |
| for (const auto& [name, value] : request.headers) { | |
| curl_slist* updated_headers = curl_slist_append(headers, (name + ": " + value).c_str()); | |
| if (updated_headers == nullptr) { | |
| return Status::IOError("failed to create HTTP headers"); | |
| } | |
| headers = updated_headers; | |
| } | |
| curl_easy_setopt(handle, CURLOPT_URL, request.url.c_str()); | |
| curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); | |
| curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L); | |
| curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT_MS, 30000L); | |
| curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, | |
| static_cast<long>(request.request_timeout_ms)); // NOLINT(runtime/int) | |
| curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, WriteCallback); | |
| curl_easy_setopt(handle, CURLOPT_WRITEDATA, &context); | |
| curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, HeaderCallback); | |
| curl_easy_setopt(handle, CURLOPT_HEADERDATA, &context); | |
| curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 0L); | |
| if (request.method == HttpMethod::HEAD) { | |
| curl_easy_setopt(handle, CURLOPT_NOBODY, 1L); | |
| } | |
| CURLcode code = curl_easy_perform(handle); | |
| long response_code = 0; // NOLINT(runtime/int, google-runtime-int): required by curl. | |
| curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &response_code); | |
| context.response.status_code = static_cast<int32_t>(response_code); | |
| if (!context.status.ok()) { | |
| return context.status; | |
| } | |
| if (code == CURLE_OK && !IsRetryable(code, response_code)) { | |
| return context.response; | |
| } | |
| if (!IsRetryable(code, response_code) || | |
| (context.response.body_size > 0 && response_code < 300) || | |
| attempt + 1 == kMaxAttempts) { | |
| if (code == CURLE_OK) { | |
| return Status::IOError(fmt::format("HTTP request to {} returned status {}", | |
| request.url, response_code)); | |
| } | |
| return Status::IOError(fmt::format("HTTP request to {} failed: {} (status {})", | |
| request.url, curl_easy_strerror(code), | |
| response_code)); | |
| } | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100 * (1 << attempt))); | |
| } | |
| return Status::IOError("HTTP request failed"); | |
| } | |
| } // namespace paimon |