blob: 077127957bafbcc4c5792788b29a24f5d9dd2a49 [file]
/*
* Copyright 2026-present Alibaba Inc.
*
* Licensed 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.
*/
#pragma once
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include "paimon/logging.h"
#include "paimon/result.h"
#include "paimon/status.h"
namespace paimon {
/// HTTP status codes used by the REST catalog.
struct HttpStatus {
static constexpr int64_t kOk = 200;
static constexpr int64_t kAccepted = 202;
static constexpr int64_t kNoContent = 204;
static constexpr int64_t kBadRequest = 400;
static constexpr int64_t kUnauthorized = 401;
static constexpr int64_t kForbidden = 403;
static constexpr int64_t kNotFound = 404;
static constexpr int64_t kConflict = 409;
static constexpr int64_t kTooManyRequests = 429;
static constexpr int64_t kInternalServerError = 500;
static constexpr int64_t kNotImplemented = 501;
static constexpr int64_t kServiceUnavailable = 503;
};
/// A blocking HTTP client for the REST catalog based on libcurl. HTTP 429/503
/// responses are retried for all methods, transient transport errors only for
/// idempotent ones, with exponential backoff honoring a positive `Retry-After`
/// response header (delta-seconds or HTTP-date form). A backoff sleep is bounded by
/// `retry_max_delay_ms` and the whole request by `retry_timeout_ms`; a `Retry-After`
/// beyond the remaining budget stops retrying rather than shortening the sleep.
/// Redirects to http(s) targets are followed transparently, keeping the method and
/// body of POST/DELETE requests.
class RestHttpClient {
public:
struct Config {
int64_t connect_timeout_ms = 180 * 1000;
int64_t request_timeout_ms = 180 * 1000;
int32_t max_retries = 5;
int64_t retry_base_delay_ms = 1000;
/// Upper bound for a single backoff sleep; a longer `Retry-After` is still
/// honored, bounded only by `retry_timeout_ms`.
int64_t retry_max_delay_ms = 64 * 1000;
/// Overall budget for one `Execute` call, covering all attempts and retry
/// sleeps; a retry whose delay does not fit the remaining budget is not
/// attempted.
int64_t retry_timeout_ms = 5 * 60 * 1000;
};
struct Response {
int64_t code = 0;
std::string body;
/// Response headers with lower-cased names.
std::map<std::string, std::string> headers;
bool IsSuccessful() const {
return code == HttpStatus::kOk || code == HttpStatus::kAccepted ||
code == HttpStatus::kNoContent;
}
};
/// Creates a client against `base_uri`. The uri is normalized: surrounding
/// whitespace and all trailing '/' are stripped and "http://" is prepended when no
/// scheme is present.
static Result<std::unique_ptr<RestHttpClient>> Create(const std::string& base_uri);
static Result<std::unique_ptr<RestHttpClient>> Create(const std::string& base_uri,
const Config& config);
~RestHttpClient();
/// Executes `method` ("GET", "POST" or "DELETE") on `path` (already url-encoded,
/// starting with '/'); query parameter keys and values are url-encoded internally.
/// Header names must be valid HTTP tokens and values must not contain CR, LF or
/// NUL; a violating header fails the request before anything is sent. Returns the
/// final response, which may carry a non-2xx code, or an error status when the
/// request could not be transported at all. Only transient transport errors (an
/// established connection breaking mid-request or a truncated response body) are
/// retried; every other transport failure fails immediately.
Result<Response> Execute(const std::string& method, const std::string& path,
const std::map<std::string, std::string>& query_params,
const std::map<std::string, std::string>& headers,
const std::string& body) const;
const std::string& GetBaseUri() const {
return base_uri_;
}
static std::string NormalizeUri(const std::string& uri);
/// Builds "k1=v1&k2=v2" with url-encoded keys and values.
static std::string BuildQueryString(const std::map<std::string, std::string>& query_params);
/// Computes the delay before the retry following the `execution_count`-th attempt
/// (counted from 1), in ms, or `nullopt` when retrying should stop. A positive
/// `Retry-After` header of `response` (delta-seconds, or HTTP-date resolved against
/// `now_epoch_seconds`) wins and is never shortened: beyond `remaining_budget_ms`
/// it yields `nullopt`. Otherwise exponential backoff with up to 10% jitter
/// applies, clamped to `retry_max_delay_ms`, yielding `nullopt` when even the
/// clamped delay exceeds `remaining_budget_ms`. `response` is null when the attempt
/// failed with a transport error.
static std::optional<int64_t> ComputeRetryDelayMs(const Config& config, int32_t execution_count,
const Response* response,
int64_t now_epoch_seconds,
int64_t remaining_budget_ms);
private:
RestHttpClient(const std::string& base_uri, const Config& config);
/// Pool of the libcurl easy handles used by `ExecuteOnce`, also keeping libcurl's
/// global state alive for the lifetime of the client.
class HandlePool;
/// On a transport failure, `transport_retriable` reports whether the failure kind
/// may be retried; see `Execute` for which kinds are not.
Result<Response> ExecuteOnce(const std::string& method, const std::string& url,
const std::map<std::string, std::string>& headers,
const std::string& body, bool* transport_retriable) const;
std::optional<int64_t> GetRetryDelayMs(int32_t execution_count, const Response* response,
int64_t remaining_budget_ms) const;
std::unique_ptr<HandlePool> handle_pool_;
std::string base_uri_;
Config config_;
std::shared_ptr<Logger> logger_;
};
} // namespace paimon